☰
Milvus向量数据库入门:余弦相似度检索与standalone部署实操
2026/10/8 9:23:10 网站建设 项目流程

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至少要有一个向量字段,也就是真正参与相似度计算的核心字段,其余的字段都可以看成是标量属性,用来做过滤或者跟着搜索结果一起返回。

写代码的时候你会发现,如果不显式定义Schema,MilvusClient在创建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 2>&1 | 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(uri="http://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(uri="http://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_name=COLLECTION, dimension=DIM, metric_type="COSINE", ) # 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_name="vector", index_type="HNSW", metric_type="COSINE", params={"M": 8, "efConstruction": 64}, ) client.create_index(COLLECTION, index_params) # 6. 构造查询向量:取id=0的向量加一点扰动 query_vec = rows[0]["vector"].copy() query_vec[0] += 0.01 # 7. 搜索最相似的3条 res = client.search( collection_name=COLLECTION, data=[query_vec], limit=3, output_fields=["title"], search_params={"metric_type": "COSINE", "params": {"ef": 64}}, ) # 8. 打印结果 for hits in res: for hit in hits: print(f"id={hit['id']}, distance={hit['distance']:.6f}, title={hit['entity'].get('title')}")

这段代码跑完,大概率能看到id=0排在第一位,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越小,代表相似度越高,distance=0的时候,说明两个向量方向完全一致。

对应的,不同度量方式的返回含义也完全不同:

度量方式返回值的含义越小越好还是越大越好
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图结构里每个节点还要存大约M+1个邻居ID,每个ID又占4字节左右,再加上一些额外开销。粗略估算100万条128维、M=16的数据,索引内存占用大约在700MB到1GB这个量级。

如果你发现机器内存吃紧,可以从几个方向优化:降低维度、减小M值、换用压缩效果更明显的索引类型,或者按业务把数据拆分到多个Collection,而不是把所有向量塞到一个表里。我在一个图片去重项目里就是把一个大Collection拆成了按时间分区的多个Collection,内存压力小了很多,查询速度反而更快。

5.6 改字段或重来:别把向量维度当成普通列随便改

最后补一个结构设计上的坑:Collection创建后,向量字段的维度是不能Alter的。你要是发现Embedding模型换了、向量维度从768变成了1024,别试图改字段,直接drop原有Collection重新建,再把数据重新灌一遍。所以在项目早期,就应该把使用的Embedding模型固定下来,维度这个参数一旦定下来,后面改动成本可不小。

我在实际项目中还发现,很多人喜欢在Collection里塞一堆标量字段做精细过滤,但每个标量字段如果没建倒排索引,过滤时也会拖慢整体查询。建议把高频过滤字段(状态、类目、时间)显式建索引,低频字段让它走动态字段就行了。

最后分享一个我踩过几次坑之后的习惯:每次在Milvus里做查询,我都会先打印出返回的distance,确认它符合我当前用量方式的“越大越好还是越小越好”的语义,再写进业务逻辑。这种“先验证再上代码”的习惯,能帮你省下大量排查时间。Milvus整体上手难度不高,把数据模型、度量方式、索引参数这三件事吃透,绝大多数应用场景你都能拿捏得住。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询