Milvus向量数据库入门:余弦相似度检索与standalone部署实操

发布时间:2026/10/8 9:23:36
Milvus向量数据库入门:余弦相似度检索与standalone部署实操
Milvus这个名字第一次出现在我项目里的时候我以为是某个开源项目随手起的代号后来翻文档才发现这个定位为“云原生向量数据库”的项目目标就是把非结构化数据的检索做到开箱即用。我最早接触向量检索是在做一个素材去重需求数据量也就几十万条当时想的是用Faiss自己撸一个服务就行结果越是往下做越发现“能跑起来”和“能跑好”之间差着十万八千里Milvus就是在这个空档里补上了存储、索引、客户端SDK和部署运维这一整套环节。这篇基础介绍不打算堆概念我会把Milvus的核心定位、数据模型、standalone模式安装、余弦相似度检索实操和常见坑一次讲清楚。如果你正准备用向量数据库做RAG、图片检索、推荐去重或者只是想把手上的Embedding向量变成一个能查询的服务这篇文章可以帮你少走很多弯路。1. 为什么需要Milvus先搞清楚向量检索要解决什么问题1.1 从一张图片到一串浮点数向量检索在干什么做AI应用的同学应该都知道无论是图片、文本还是音频要想让机器理解它的语义通常得先经过一个Embedding模型把它变成一串浮点数。比如一张猫的图片经过CLIP模型可能输出一个1024维的向量一段中文句子经过bge模型可能输出一个768维的向量。这些浮点数组成的向量在数学空间里其实是一个点语义上相似的内容对应的点距离就会比较近。传统数据库是没法直接处理这种需求的。你在MySQL里查“title 猫”很容易但你要是想查“哪张图片和这张图片最像”SQL就抓瞎了。这不是换个查询语句的事而是计算模型完全不同——向量检索要做的是最近邻搜索也就是在高维空间里找离某个点最近的点。暴力遍历是方案之一也就是KNN精确最近邻搜索。我试过用Python直接算几百条数据跑起来毫无压力但数据量涨到一百万条、维度又高的时候一次查询就要做上亿次浮点乘法性能就崩了。业界为了解决这个问题提出了ANN近似最近邻索引常见的算法有HNSW、IVF、PQ等它们通过牺牲一点点的召回精度换取毫秒级的查询速度。Milvus做的事比单纯封装一个索引库要多得多。它不只是拿HNSW去算相似度而是把向量数据、标量属性、索引、元数据、持久化、客户端SDK揉成了一个完整的数据库产品。你不需要自己维护Faiss的索引文件不需要担心服务重启后索引丢失也不用自己手动处理数据落盘。它解决的是从“有一堆向量”到“能稳定查询的一整套工程问题”。1.2 Milvus 2.x的重写从依赖外部组件到开箱即用Milvus不是一上来就这么好用的。在1.x时代它的部署链路相当折腾。我记得当时要自己装MySQL用来存元数据存储层对接RocksDB索引层接Faiss装完以后还要操心各个组件之间的数据一致性。如果只是想做点相似度召回光是把环境调通就能耗掉一整天属于典型的“我为了喝杯牛奶得先养头牛”的体验。2.x版本是一次彻底的重写。它把元数据放到了etcd里把数据文件放到对象存储里单机部署用MinIO就能撑起来整体架构清晰了很多。对外暴露的API也更像数据库了有Collection的概念有类似SQL的标量过滤有标准客户端SDK写起来比1.x顺手太多了。2.x还做了一个很关键的分层standalone单机模式和cluster分布式模式。很多新手一听到分布式就兴奋上来就打算搭集群结果被一堆依赖组件搞得焦头烂额。其实对于绝大多数中小场景几百条到几百万条向量级别的数据单机standalone就完全够用先把它跑通等数据量真到了千万、亿级需要水平扩展时再考虑集群迁移也不迟。2. Milvus建模扫盲Collection、向量字段与余弦相似度2.1 Collection就像一张表数据模型一分钟看懂Milvus的数据模型拿MySQL来类比会非常直观。Collection就是一张表Entity就是一张表里的一行记录Field就是列。每个Collection至少要有一个向量字段也就是真正参与相似度计算的核心字段其余的字段都可以看成是标量属性用来做过滤或者跟着搜索结果一起返回。写代码的时候你会发现如果不显式定义SchemaMilvusClient在创建Collection时默认只会帮你建id主键和vector这两个字段其他你传进去的字段会被当作动态字段存起来。这在快速原型阶段非常好用但我也建议你到了一定规模以后还是把常用的过滤字段提前定义清楚明确字段类型和索引性能会稳很多。Partition是另一个值得提前了解的概念它是Collection内的分区类比MySQL里的分区表。举个例子我有一个商品向量库每天增量入库我按日期建partition查询时通过partition_names参数只搜当天的数据扫描量会大幅减少速度提升非常明显。注意这个优化在数据量大的时候是立竿见影的别等到搜索慢到不行了才想起分区。还有一个高频易错点Query和Search是两种完全不同的操作。Query是按标量条件精确过滤比如“把id小于100的记录全部取出来”Search才是向量相似度搜索。很多新手上来就把两者混着用结果发现自己想按ID查数据时却跑了一个毫无意义的向量搜索浪费时间不说逻辑也不对。2.2 余弦值、内积和欧氏距离到底选哪个Mivus建Collection的时候必须指定度量方式这也对应了爬虫搜索里很热的“milvus 余弦值”。先说结论文本Embedding场景绝大多数情况下选COSINE余弦相似度如果向量已经做过归一化用IP内积和余弦等价性能上也没明显差别如果是图像特征或者你明确关心“绝对距离”可以考虑L2欧氏距离。余弦相似度的公式是 cos(A, B) A·B / (|A| * |B|)结果范围在-1到1之间越接近1表示方向越一致。它对向量模长不敏感哪怕“小猫”和“猫咪”的Embedding模长相差很大只要方向接近余弦值依然很高这个特性和文本语义比较非常契合。而内积则受向量长度影响很大向量未归一化时模长大的向量即使方向差异大也可能拿到很高的分数所以如果要用IP先确保数据都归一化。Milvus里COSINE度量方式还有一个官方处理服务端会自动对向量做归一化所以你传入原始向量即可。这里要特别提醒一个新手容易看反的细节搜索返回的distance字段在COSINE度量下是余弦距离不是相似度具体关系是distance 1 - cosine_similarity。也就是说返回值越小代表越相似0意味着方向完全相同。很多人第一次看到返回结果发现越相似的记录distance越接近0还以为出了bug其实这是正常的。3. Milvus standalone模式的安装和部署实操3.1 standalone模式适合谁单机起步别急着上集群standalone这个词直译是“独立模式”在Milvus里的意思很好理解所有组件跑在一台机器上用Docker Compose一键把etcd、MinIO、Milvus服务三个容器一起拉起来数据和元数据都有落盘不是那种用完就丢的内存方案。它相比cluster模式少了Pulsar消息队列、多个worker协调这些复杂度部署门槛低了一大截。我个人的建议很明确如果你手上的向量数据量在几百万条以内、日均增量不大、对可用性没有硬性要求standalone就是首选。它单机就能提供不错的检索性能运维成本低测试环境和中小型生产环境都合适。真正需要cluster模式的场景是数据量到了千万甚至亿级、单机内存扛不住索引、需要多副本高可用的时候到那时候再迁移也不迟。下面这张对比表可以帮你做判断对比项standalone模式cluster模式部署复杂度低Docker Compose即可拉起高需要协调多组件数据规模百万级量级够用适合千万到亿级水平扩展基本不扩展支持节点扩容高可用单机存在单点可配置副本和故障恢复适用场景原型验证、中小业务、内网工具大型在线检索服务很多团队明明只有几十万条数据上来就搭三节点集群最后运维成本比业务本身还高。我建议一句话先单机跑通业务再考虑分布式别让架构焦虑绑架了你。3.2 Docker Compose安装Milvus并验证服务安装过程其实很简单前提是你已经装好了Docker和Docker Compose插件。以v2.4.x版本为例直接用官方提供的standalone编排文件就行。我一般习惯用wget下载没有wget的用curl -O也一样wget https://github.com/milvus-io/milvus/releases/download/v2.4.4/milvus-standalone-docker-compose.yml然后直接后台启动docker compose up -d启动以后检查容器状态docker compose ps正常情况下你会看到milvus-standalone、etcd、minio三个容器都在运行。首次启动会拉取几个镜像稍微需要耐心一点。确认Milvus服务真正就绪可以看日志docker logs milvus-standalone 21 | tail -50日志里出现“milvus-server started”之类的信息就说明服务起来了。Milvus默认的gRPC端口是19530后面所有的Python连接都用这个端口。这里有一个安全提醒默认编排文件里etcd的2379、MinIO的9000等端口也会映射到宿主机上。如果这台机器部署在公网一定记得在防火墙层面只放行19530别把内部管理端口裸奔到公网不然容易出问题。如果本机2379或9000端口已被其他服务占用直接改编排文件里的宿主端口映射容器内部端口不用动。连接验证也很简单先用pip安装客户端pip install pymilvus然后用Python试一下能不能连通from pymilvus import MilvusClient client MilvusClient(urihttp://localhost:19530) print(client.list_collections())如果输出是空列表或一个列表说明Milvus已经准备好接收数据了。4. 用PyMilvus跑通一次余弦相似度检索4.1 准备PyMilvus环境和测试数据上一节我们已经确认Milvus服务在跑这一步直接进入正题。为了演示我会构造一批模拟向量模拟的是商品标题通过Embedding模型生成的32维向量。实际项目里你只需要把随机数换成Embedding模型的输出就行了流程完全一致。先说为什么用模拟向量因为真实Embedding模型通常要下载参数、跑推理会加深教程复杂度。而我们这里核心要演示的是Milvus的操作流程——建表、插数据、建索引、搜索、看距离模拟向量足够说明问题。等你理解了整套流程再接入真实模型只是换一个数据来源的事。数据构造方式我准备100条向量每条32维取值在0到1之间随机初始化。查询向量就取第一条向量的数据再加上一点微小扰动这样理论上它的最近邻应该就是自己。这个设计能让你直观看出distance在余弦度量下的含义。4.2 完整可复现Demo建库、插数据、建索引、搜索先把完整代码贴出来后面逐段解释。这个脚本用PyMilvus 2.4.x的MilvusClient接口写的新版本都推荐这种写法import random from pymilvus import MilvusClient # 1. 连接Milvus服务 client MilvusClient(urihttp://localhost:19530) COLLECTION product_title_demo DIM 32 # 2. 创建Collection维度指定为32度量方式用COSINE if client.has_collection(COLLECTION): client.drop_collection(COLLECTION) client.create_collection( collection_nameCOLLECTION, dimensionDIM, metric_typeCOSINE, ) # 3. 构造并插入100条模拟向量 rows [] for i in range(100): rows.append({ id: i, vector: [random.random() for _ in range(DIM)], title: f商品标题-{i}, }) client.insert(COLLECTION, rows) # 4. flush让数据立即可见 client.flush(COLLECTION) # 5. 创建HNSW索引COSINE度量 index_params client.prepare_index_params() index_params.add_index( field_namevector, index_typeHNSW, metric_typeCOSINE, params{M: 8, efConstruction: 64}, ) client.create_index(COLLECTION, index_params) # 6. 构造查询向量取id0的向量加一点扰动 query_vec rows[0][vector].copy() query_vec[0] 0.01 # 7. 搜索最相似的3条 res client.search( collection_nameCOLLECTION, data[query_vec], limit3, output_fields[title], search_params{metric_type: COSINE, params: {ef: 64}}, ) # 8. 打印结果 for hits in res: for hit in hits: print(fid{hit[id]}, distance{hit[distance]:.6f}, title{hit[entity].get(title)})这段代码跑完大概率能看到id0排在第一位distance接近0后面两条是随机向量distance会明显大一些。这就把COSINE度量“返回值越小越相似”的规则验证了一遍。说几个细节。第二步创建Collection时我只传了dimension和metric_type没有显式指定字段所以默认只有id和vector两个字段title是作为动态字段带进去的。第五步的HNSW索引参数里M指的是每个节点的最大连接数M越大索引精度越高、图越大efConstruction是建索引时的动态候选集大小这两个参数直接影响建索引质量。第七步搜索参数里的ef表示查询时的候选集大小和建索引时的efConstruction是两码事新手容易混淆。这里还要强调一个容易被忽略的工程点插入数据后强烈建议执行flush它会把数据落盘并强制可见。虽然Milvus默认有异步落盘机制但测试阶段不flush立刻去查偶尔会碰上数据还没可见的尴尬。4.3 真实项目里Embedding模型怎么选Demo里用的是随机向量但真要上生产Embedding模型选型才是决定检索效果的上游因素。文本中文检索社区里口碑比较稳的有bge系列比如bge-large-zh-v1.5也有更轻量好部署的多模态Embedding模型。图片特征检索CLIP系列基本是标配。无论用哪种模型关键是你要把生成向量和检索这两个环节彻底解耦离线批量生成向量存好线上只管查。另外一个经验是向量维度不是越高越好。有些同学喜欢把多路模型输出拼一起搞出一个几千维的向量精度不一定提升内存和计算开销倒是翻着倍涨。做了实际对比之后我通常建议把维度控制在384到1024之间这个范围对大多数业务场景已经足够而且索引和检索的性能都能兼顾。数据规模很大的时候批量入库存成一份JSON或Parquet再分批灌入Milvus会比逐条请求快非常多。一次插入几千条分批进行跑起来很稳。5. 常见报错与排查技巧实录5.1 连不上Milvus怎么排查“连接超时”或者“Connection refused”是新手群里出现频率最高的问题。出现这个先别急着怀疑代码按顺序排查三步第一步看容器是否真的起来了。跑docker compose ps如果milvus-standalone容器不是Up状态多半是启动失败再去看日志。第二步确认端口映射。默认19530是gRPC端口如果你的编排文件把宿主端口改成了别的客户端里的uri也要跟着改。第三步检查防火墙。我就碰到过云主机安全组只放行了80和443每次本地调试都通部署到服务器就死活连不上最后发现是安全组没放开19530。还有一个细节如果你在服务器上用127.0.0.1连接只适用于本机调试跨机器访问时uri里的host要填机器的内网IP或公网IP不能照抄localhost。5.2 搜索结果的距离值为什么和直觉相反这个问题我在前面提过但值得单独拿出来再强调一遍COSINE度量下distance返回的是余弦距离等于1减去余弦相似度。所以你看到distance越小代表相似度越高distance0的时候说明两个向量方向完全一致。对应的不同度量方式的返回含义也完全不同度量方式返回值的含义越小越好还是越大越好COSINE余弦距离 1 - 余弦相似度越小越相似IP内积内积值越大越相似L2欧氏距离欧氏距离越小越相似很多教程直接说“distance值越小越好”这在L2下是对的在IP下恰恰相反。所以接业务逻辑的时候务必先看清楚你建Collection时用的度量方式再决定返回结果怎么排序。5.3 插入了数据却搜不到是怎么回事插入成功后立刻查询偶尔返回空结果或者漏数据这是Milvus默认一致性模型导致的。它默认不是强一致插入的数据不一定会立即对所有读请求可见这在分布式系统里很常见。解决办法有两个最直接的是调flush像Demo里那样插完就强制落盘并可见第二个是搜索时把Consistency Level调整为强一致不过这样会牺牲一点性能适合对一致性要求高的场景。如果是测试环境直接flush省心。还要提醒一点如果你重新建了一个同名Collection但插入用的还是旧数据记得先用drop_collection清干净再建不然新旧数据混在一起查询结果会非常迷惑。5.4 搜索慢不一定是机器差先查索引状态搜得慢第一反应通常是“机器不行”。但很多情况下是你查询的Collection压根没建索引Milvus只能暴力扫描。自己排查方法很简单查看索引状态client.describe_index(COLLECTION, vector)或者用client.list_indexes(COLLECTION)看有哪些索引。如果发现向量字段压根没有索引或者索引状态不是Loaded就要重跑一遍建索引流程。建索引本身需要一些时间数据量大的时候耐心等索引建好后查询速度会有质的提升。HNSW的查询参数ef也直接影响速度。ef设得越大召回越准但越慢。多数场景ef设64已经够用精度要求高再往上调到128或256。M和efConstruction虽然在建索引时设置但它们对查询速度和召回率的影响是长期的M建议8到16之间efConstruction建议64到128之间。5.5 估算内存别让HNSW索引把机器撑爆HNSW索引的特点是建完后基本全部驻留在内存里。你机器内存多大直接决定能装下多少条向量。估算公式不复杂假设维度为d每个浮点数4字节每条向量本身占d×4字节HNSW图结构里每个节点还要存大约M1个邻居ID每个ID又占4字节左右再加上一些额外开销。粗略估算100万条128维、M16的数据索引内存占用大约在700MB到1GB这个量级。如果你发现机器内存吃紧可以从几个方向优化降低维度、减小M值、换用压缩效果更明显的索引类型或者按业务把数据拆分到多个Collection而不是把所有向量塞到一个表里。我在一个图片去重项目里就是把一个大Collection拆成了按时间分区的多个Collection内存压力小了很多查询速度反而更快。5.6 改字段或重来别把向量维度当成普通列随便改最后补一个结构设计上的坑Collection创建后向量字段的维度是不能Alter的。你要是发现Embedding模型换了、向量维度从768变成了1024别试图改字段直接drop原有Collection重新建再把数据重新灌一遍。所以在项目早期就应该把使用的Embedding模型固定下来维度这个参数一旦定下来后面改动成本可不小。我在实际项目中还发现很多人喜欢在Collection里塞一堆标量字段做精细过滤但每个标量字段如果没建倒排索引过滤时也会拖慢整体查询。建议把高频过滤字段状态、类目、时间显式建索引低频字段让它走动态字段就行了。最后分享一个我踩过几次坑之后的习惯每次在Milvus里做查询我都会先打印出返回的distance确认它符合我当前用量方式的“越大越好还是越小越好”的语义再写进业务逻辑。这种“先验证再上代码”的习惯能帮你省下大量排查时间。Milvus整体上手难度不高把数据模型、度量方式、索引参数这三件事吃透绝大多数应用场景你都能拿捏得住。