1. 引言
向量数据库是 RAG、语义搜索、推荐系统和多模态检索的核心基础设施。相比传统数据库的精确匹配,向量数据库更擅长“按语义找最相似的内容”。但真正把它用起来,除了会存向量,还需要掌握四件事:
- Collection / Partition 管理:数据如何组织、如何分区。
- 索引类型选择:HNSW 与 IVF 各自适合什么场景。
- 相似度度量:Cosine、Inner Product 等度量如何影响排序。
- 标量过滤:如何在向量检索的同时按业务字段过滤。
本文以 Milvus 和 Chroma 为例,把上述知识点落到可直接运行的 Python 代码上。
2. 环境准备
2.1 安装依赖
pip install pymilvus chromadb
2.2 启动 Milvus
本地开发推荐使用 Milvus 官方提供的嵌入式脚本,它会同时启动 etcd、MinIO 和 Milvus Standalone:
curl -sfL https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh -o standalone_embed.sh
bash standalone_embed.sh start
默认地址为 localhost:19530。不再需要时执行:
bash standalone_embed.sh stop
2.3 启动 Chroma
Chroma 可以使用内存模式或持久化模式,不需要额外服务端。本文使用本地持久化目录:
import chromadb
client = chromadb.PersistentClient(path="./chroma_db")
3. Milvus Collection 与 Partition 管理
3.1 设计字段并创建 Collection
在 Milvus 中,Collection 类似关系型数据库的表。创建前需要先定义字段 Schema,尤其是向量字段的维度:
from pymilvus import (
connections,
Collection,
CollectionSchema,
FieldSchema,
DataType,
)
connections.connect("default", host="localhost", port="19530")
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=False),
FieldSchema(name="title", dtype=DataType.VARCHAR, max_length=512),
FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=64),
FieldSchema(name="publish_year", dtype=DataType.INT64),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768),
]
schema = CollectionSchema(fields, description="技术文档集合")
collection = Collection(name="knowledge_docs", schema=schema)
这里要特别注意两点:
id字段设置is_primary=True,必须唯一。FLOAT_VECTOR必须指定dim,且后续写入的所有向量维度必须严格一致。
3.2 数据写入
Milvus 的 insert 按列组织数据,列顺序需要与 Schema 定义顺序一致:
import numpy as np
# 生成 3 条 768 维测试向量
embeddings = np.random.rand(3, 768).tolist()
data = [
[1, 2, 3], # id
["如何优化 HNSW 索引", "Chroma 持久化实践", "Milvus 标量过滤"], # title
["AI", "DB", "DB"], # category
[2024, 2024, 2025], # publish_year
embeddings, # embedding
]
collection.insert(data)
collection.flush()
print(collection.num_entities)
3.3 Partition 分区管理
Partition 可以把一个 Collection 在物理上拆成多个逻辑区域,便于按业务隔离数据、提升写入与查询效率。例如可以按技术方向把文档拆到 ai_docs 和 db_docs:
# 创建分区
collection.create_partition("ai_docs")
collection.create_partition("db_docs")
# 指定分区写入
collection.insert(data, partition_name="ai_docs")
collection.flush()
# 查看分区
print(collection.partitions)
# 直接获取某个分区对象
ai_partition = collection.partition("ai_docs")
print(ai_partition.num_entities)
一条数据如果没有指定 partition_name,会进入默认分区 _default。
分区建议按业务含义划分,例如:按时间分月、按数据源、按租户等。分区数量不宜过度膨胀,否则查询路由会变复杂。
4. 索引类型:HNSW 与 IVF 怎么选
向量检索如果没有索引,只能进行暴力全量计算,数据量大时延迟不可接受。索引本质是在“搜索速度”和“召回精度”之间做权衡。
4.1 HNSW:图结构的近邻搜索
HNSW 通过多层图结构组织向量,查询时从高层快速定位到低层局部区域。它的优点是查询快、召回高、支持动态插入;缺点是比较吃内存,构建时间相对较长。
collection.create_index(
field_name="embedding",
index_params={
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {
"M": 16,
"efConstruction": 200
}
}
)
关键参数:
M:图中每个节点的最大连接数,通常取 4 到 64。越大越占内存,召回通常越高。efConstruction:构建时的搜索宽度,通常取 100 到 500。- 检索时的
ef:查询阶段的搜索宽度,越大越慢但召回越高。
4.2 IVF:倒排聚类索引
IVF 先对全部向量做 KMeans 聚类,把空间划分成若干桶。查询时先定位到最接近的几个聚类中心,再只在这些桶内做精确计算。
collection.create_index(
field_name="embedding",
index_params={
"index_type": "IVF_FLAT",
"metric_type": "IP",
"params": {
"nlist": 128
}
}
)
关键参数:
nlist:聚类中心数量,通常取数据量平方根附近,例如 100 万条数据可以从 128 到 1024 尝试。- 检索阶段的
nprobe:搜索最近的多少个聚类中心,越大越慢但召回越高。
4.3 选择建议
| 场景 | 推荐索引 | 原因 |
|---|---|---|
| 数据量不大、追求高召回低延迟 | HNSW | 图搜索精度高,查询稳定 |
| 百万级以上、内存受限 | IVF_FLAT | 内存占用低,通过 nprobe 控制精度 |
| 快速原型 | AUTOINDEX | Milvus 根据数据规模自动选择 |
| 数据量极大、需要磁盘支持 | DiskANN | 支持磁盘索引,降低内存压力 |
实际项目中,没有绝对最优解,建议先用小批量数据分别测试 HNSW 和 IVF,对比延迟、召回与内存占用后再上线。
5. 相似度度量:Cosine 与 IP
相似度度量决定了“什么叫相似”。同一个向量在不同的度量下,最近邻结果可能完全不同。
5.1 Cosine 余弦相似度
Cosine 计算两个向量之间的夹角余弦值,只关心方向,不关心长度。它非常适合文本 Embedding 这类长度差异较大的向量:
CosineSimilarity = (A · B) / (||A|| * ||B||)
在 Milvus 中配置:
index_params = {
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {"M": 16, "efConstruction": 200}
}
5.2 IP 内积
IP 计算两个向量的点积结果。它同时考虑方向和长度,适合两部分:
- 向量已经做过 L2 归一化的场景。此时 IP 与 Cosine 等价,但计算更轻。
- 推荐系统中,Embedding 的点积可以近似表示用户与物品的匹配分数。
index_params = {
"index_type": "IVF_FLAT",
"metric_type": "IP",
"params": {"nlist": 128}
}
5.3 重要原则
- 度量方式必须与向量生成方式一致。如果你在模型侧做了归一化,IP 和 Cosine 差异不大;如果向量模长差异明显,建议优先 Cosine。
- 索引和查询阶段必须使用相同的
metric_type。否则会出现“建索引时用 Cosine,查询时用 IP”,导致排序错误。 - 对 IP 来说,分数越大越相似;对 L2 来说,距离越小越相似。Milvus 返回的
hit.distance语义与 metric 相关,排序时不要统一按“距离小更好”理解。
6. 标量过滤 Scalar Filtering
真实业务里几乎不会只要“语义最相似的 10 条”,而是“在满足业务条件下最相似的 10 条”。例如:
查询 category 属于 AI 或 NLP,并且 publish_year 不小于 2024 的相似文档。
Milvus 使用 SQL-like 表达式完成标量过滤。
6.1 查询时通过 expr 过滤
query_vec = np.random.rand(1, 768).tolist()
results = collection.search(
data=query_vec,
anns_field="embedding",
param={"metric_type": "COSINE", "params": {"ef": 64}},
limit=10,
expr="category in ['AI', 'NLP'] and publish_year >= 2024",
output_fields=["title", "category", "publish_year"]
)
for hits in results:
for hit in hits:
print(f"id={hit.id}, score={hit.distance}")
print(hit.entity.get("title"))
表达式中可以直接使用:
==、!=、>=、>等比较操作。and、or、not逻辑组合。in、like等高级匹配。- 字符串需要使用单引号包裹。
6.2 只为标量过滤,不做向量检索
有时只想按条件查询属性,不关心向量距离,可以使用 query:
rows = collection.query(
expr="publish_year >= 2024",
output_fields=["title", "category"],
limit=20
)
for row in rows:
print(row)
6.3 给标量字段建索引
数据量大时,可以在高频过滤字段上创建倒排索引:
collection.create_index(
field_name="category",
index_params={"index_type": "INVERTED"}
)
这样 category == 'AI' 这类过滤会被大幅加速。
7. Chroma 快速实操
Chroma 的 API 更轻量,适合原型验证和中小规模应用。它把 Collection、Embedding 和元数据过滤封装在一起。
7.1 创建 Collection 并指定度量方式
Chroma 通过 metadata={"hnsw:space": ...} 指定相似度度量,可选值包括 cosine、ip 和 l2:
import chromadb
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection(
name="tech_docs",
metadata={"hnsw:space": "cosine"}
)
7.2 写入文档并携带元数据
collection.add(
ids=["doc_1", "doc_2", "doc_3"],
documents=[
"如何优化 HNSW 索引",
"Chroma 持久化与过滤",
"Milvus 标量过滤示例"
],
metadatas=[
{"category": "AI", "publish_year": 2024},
{"category": "DB", "publish_year": 2024},
{"category": "DB", "publish_year": 2025}
]
)
Chroma 默认使用内置的 MiniLM Embedding 模型自动向量化,也可以传入 embeddings 参数使用自己的向量。
7.3 带标量过滤的检索
Chroma 使用字典形式的 where 完成标量过滤:
results = collection.query(
query_texts=["向量索引怎么选"],
n_results=2,
where={"category": {"$eq": "AI"}}
)
print(results["documents"])
print(results["metadatas"])
print(results["distances"])
常见过滤操作符:
$eq:等于。$ne:不等于。$in:在给定列表内。$gte:大于等于。$lte:小于等于。
也可以对文档内容做词形过滤:
results = collection.query(
query_texts=["索引优化"],
n_results=2,
where_document={"$contains": "HNSW"}
)
Chroma 没有像 Milvus 那样显式的 Partition API,通常通过创建不同 Collection,或者在元数据中增加分区字段来模拟分区能力。
8. 检索链路总览
下面这张图概括了从写入到检索的完整链路:
flowchart LR
A["写入向量数据"] --> B["确定相似度度量"]
B --> C["构建 HNSW / IVF 索引"]
C --> D["执行 ANN 向量检索"]
D --> E["应用标量过滤"]
E --> F["返回业务结果"]
在 Milvus 中,向量检索和标量过滤可以在一次 search 调用中完成;在 Chroma 中,则在 query 的 where 参数中体现。两者核心思路一致:先保证语义召回,再通过结构化条件约束结果集。
9. 常见坑与最佳实践
- 向量维度不一致:写入前必须统一 Embedding 模型和输出维度,否则插入或检索会直接报错。
- 度量方式不匹配:索引阶段和查询阶段必须使用相同
metric_type;归一化状态下 IP 才近似 Cosine。 - 忘记 load Collection:Milvus 创建索引后需要执行
collection.load(),否则无法检索。 - String 字段未指定 max_length:
DataType.VARCHAR必须指定合理的max_length,太小会写入失败。 - 过滤条件错误:字符串必须加单引号,例如
category == 'AI',不要写成category == AI。 - 参数照搬:HNSW 的 M 和 ef、IVF 的 nlist 和 nprobe 需要根据数据规模与召回要求调整,不要统一使用默认值。
- 元数据膨胀:Chroma 的 Metadata 会被持久化,高频过滤字段应保持结构简单,避免把大段文本塞进元数据。
10. 总结
本文从 Collection / Partition 管理、索引选择、相似度度量和标量过滤四个维度,分别用 Milvus 和 Chroma 做了实操演示。总结成一句话:
- 数据组织看 Collection 和 Partition;
- 性能看 HNSW / IVF 索引;
- 排序语义看 Cosine / IP;
- 业务约束看 Scalar Filtering。
向量数据库不是“存向量”就结束了,只有把这四层配置协调好,才能在生产环境稳定地跑出高召回、低延迟的检索结果。