💡 一句话核心概念
Chroma 的 where 过滤 + 六种相似度算法,等于给语义搜索装了"导航仪"------不仅要搜得"像",还要搜得"准"。元数据就是你的精准制导系统。
🧩 关键实操
1. 元数据过滤:让语义搜索"指哪打哪"
python
# 05_metadata_filter.py
from chromadb import Client
client = Client()
collection = client.get_or_create_collection(
name="advanced_search",
metadata={"hnsw:space": "cosine"},
)
# 塞一批带丰富元数据的文档------元数据就是你的"过滤标签"
collection.add(
documents=[
"Python 3.12 引入了更友好的错误提示信息。",
"Python 3.12 的 type 语句简化了泛型写法。",
"Python 3.11 提升了 10-60% 的运行速度。",
"Rust 的所有权系统让内存安全无需 GC。",
"Rust 的 async/await 在 1.39 版本稳定。",
"Go 的 goroutine 让并发编程变得简单。",
],
metadatas=[
{"lang": "Python", "version": "3.12", "category": "语法"},
{"lang": "Python", "version": "3.12", "category": "类型系统"},
{"lang": "Python", "version": "3.11", "category": "性能"},
{"lang": "Rust", "version": "1.0", "category": "内存管理"},
{"lang": "Rust", "version": "1.39", "category": "异步"},
{"lang": "Go", "version": "1.0", "category": "并发"},
],
ids=["py_error", "py_type", "py_speed", "rust_owner", "rust_async", "go_goroutine"],
)
# ===== 过滤运算符全家福 =====
# $eq (等于), $ne (不等于), $gt (大于), $gte (>=), $lt, $lte
# $in (在列表中), $nin (不在列表中)
# $and, $or (组合条件)
# 场景1:只要 Python 3.12 的文档
results = collection.query(
query_texts=["新特性"],
n_results=3,
where={
"$and": [
{"lang": {"$eq": "Python"}},
{"version": {"$eq": "3.12"}},
]
},
)
print("🐍 Python 3.12 文档:")
for doc, meta in zip(results["documents"][0], results["metadatas"][0]):
print(f" [{meta['category']}] {doc}")
# 场景2:要 Rust 或 Go 的内容(多语言检索)
results = collection.query(
query_texts=["并发编程"],
n_results=4,
where={"lang": {"$in": ["Rust", "Go"]}},
)
print("\n🦀 Rust 或 Go 的并发:")
for doc, meta in zip(results["documents"][0], results["metadatas"][0]):
print(f" [{meta['lang']}] {doc}")
# 场景3:排除性能类文档
results = collection.query(
query_texts=["编程语言特性"],
n_results=5,
where={"category": {"$ne": "性能"}},
)
print(f"\n🚫 排除'性能'类:找到 {len(results['ids'][0])} 条")
2. where_document:全文关键词过滤
python
# ===== where_document:直接在原始文本上做关键词匹配 =====
# 注意:这是简单的字符串包含,不是语义搜索!
# Chroma 内部用 sqlite 的 LIKE 实现
results = collection.query(
query_texts=["编程"],
n_results=3,
where_document={"$contains": "并发"}, # 文档中必须包含"并发"这个词
)
print("\n🔤 文档包含'并发'的语义搜索结果:")
for doc in results["documents"][0]:
print(f" → {doc}")
3. 距离算法深度对比:选错算法 = 搜索结果报废
python
# 05_distance_metrics.py
# Chroma 支持六种距离算法,创建 Collection 时通过 metadata 指定
# 默认是 l2(欧几里得),但 99% 的 NLP 场景你该用 cosine
from chromadb import Client
import numpy as np
client = Client()
# ===== 六种算法的通俗解释 =====
algorithms = {
"l2": "欧几里得:空间中两点直线距离。范围[0,∞),越小越像。没归一化时对长文本不友好",
"cosine": "余弦相似度:只看方向不看长度。范围[0,2],越小越像。NLP 首选,管你文本多长",
"ip": "内积:向量投影乘积。范围(-∞,∞),越大越像。Chroma 内部转成距离:dist = -ip",
}
for name, desc in algorithms.items():
print(f" {name:8s} → {desc}")
# ===== 实操:同一个查询,不同算法结果差异有多大 =====
# 用默认 embedding(all-MiniLM-L6-v2,384维)
docs = [
"我喜欢吃苹果", # 食物
"苹果公司发布了新 iPhone", # 科技
"这个苹果又大又甜", # 食物
]
# 分别用 l2 和 cosine 建两个集合
col_l2 = client.create_collection(
name="demo_l2",
metadata={"hnsw:space": "l2"},
)
col_cos = client.create_collection(
name="demo_cosine",
metadata={"hnsw:space": "cosine"},
)
col_l2.add(documents=docs, ids=["d1", "d2", "d3"])
col_cos.add(documents=docs, ids=["d1", "d2", "d3"])
query = "水果"
r_l2 = col_l2.query(query_texts=[query], n_results=3)
r_cos = col_cos.query(query_texts=[query], n_results=3)
print(f"\n🍎 查询'水果',l2 排序: {r_l2['ids'][0]} → {r_l2['distances'][0]}")
print(f"🍎 查询'水果',cosine排序:{r_cos['ids'][0]} → {r_cos['distances'][0]}")
# 注意:两种算法的距离值不可直接比较!每个算法的"近"有自己的尺度
bash
uv run python 05_metadata_filter.py
uv run python 05_distance_metrics.py
📊 六种距离算法速查表
| 算法 | hnsw:space 值 |
越小越像? | 最佳场景 | 一句话 |
|---|---|---|---|---|
| 欧几里得 | l2(默认) |
✅ | 物理空间、图像 | 像尺子量距离,长文本吃亏 |
| 余弦 | cosine |
✅ | NLP/文本(首选) | 只看方向不看长度,文本之王 |
| 内积 | ip |
❌(越大越像) | 推荐系统 | 向量投影,配合归一化用 |
还有
l2_square、linf、hamming,但它们在实际文本场景基本用不到。除非你知道自己在干什么,否则 cosine 一把梭。
🚧 避坑指南
| 坑 | 现象 | 解法 |
|---|---|---|
| where 类型不匹配 | where={"version": 3.12} 查不到,但明明有这条数据 |
元数据值类型必须和写入时一致!3.12 是 float,"3.12" 是 str。用 collection.get() 看一眼实际存的类型 |
| where_document 不是语义搜索 | 搜"goroutine"找不到,但 $contains:"并发" 能找 |
where_document 是原始字符串 LIKE 匹配,不是向量搜索。它是先语义缩小范围,再关键词精确过滤的辅助手段 |
| 改 hnsw:space 不生效 | 改完 metadata 搜索结果没变化 | Collection 创建后 hnsw:space 就锁死了!想换算法只能重建集合,重新写入所有数据。这就是为什么生产环境必须一开始就想好用什么算法 |
🎤 Chroma 面试题与通关答案
Q1:Chroma 的 where 过滤是"先过滤再搜"还是"先搜再过滤"?对性能有什么影响?
考点拆解: 向量数据库的查询优化器设计,考察对"混合查询"底层执行计划的理解。
通关答案:
Chroma 0.5.x 的执行策略是:先过滤,再搜索(Pre-filtering)。
执行流程:
1. sqlite3 解析 where 条件 → 过滤出符合条件的文档 ID 集合
2. HNSW 图索引中只在这些 ID 里做 ANN 搜索
3. 返回 top-k
为什么是 Pre-filtering?
- Chroma 定位是"小规模数据集"(几十万级),全量扫元数据的开销可控
- Pre-filtering 保证结果数量精确(先过滤后结果集不会因为 ANN 近似而漏数据)
- 反例:Post-filtering(先搜再过滤)可能出现"搜了 10 条,过滤后只剩 2 条"的情况,
n_results不可控
性能影响:
无过滤: query → HNSW → top-k → 返回 (最快)
轻量过滤: query → sqlite → HNSW(子集) → top-k (略慢,sqlite 开销小)
复杂过滤: query → sqlite(大范围扫描) → HNSW → top-k (慢!sqlite 成瓶颈)
最佳实践:
python
# ✅ 好:where 字段建立索引(Chroma 自动对元数据 key 建索引)
collection.query(query_texts=["..."], n_results=10, where={"lang": "Python"})
# ❌ 坏:用复杂嵌套条件缩小到 1-2 条文档,相当于没搜
collection.query(query_texts=["..."], n_results=10, where={
"$and": [{"lang": "Python"}, {"version": "3.12"}, {"category": "语法"}]
}) # 如果只有1条符合,搜了个寂寞
# ✅ 好:过滤适度宽松,让向量搜索有发挥空间
collection.query(query_texts=["..."], n_results=10, where={"lang": "Python"})
一句话总结: Chroma 是 Pre-filtering 策略------先缩小候选集,再语义精排。过滤太狠等于自废武功。
Q2:为什么 NLP 场景默认选 cosine 而不是 l2?从数学角度解释。
考点拆解: 向量空间模型的数学直觉,区分"距离"和"方向"在语义中的含义。
通关答案:
核心原因:文本向量的"长度"和"语义"没有半毛钱关系。
l2 距离 = sqrt(Σ(ai - bi)²) ← 受向量长度影响大
cosine 距离 = 1 - (A·B)/(|A|×|B|) ← 只看夹角,和长度无关
具体例子:
两句话:
A = "苹果好吃" → Embedding 向量 va
B = "苹果苹果苹果苹果" → Embedding 向量 vb ≈ 2 × va(重复内容向量更长)
l2(A, B) ≈ 很大 → 判为不相似 ❌
cosine(A, B) ≈ 0 → 判为极相似 ✅
为什么 Embedding 向量的长度会变化?
- 文本越长,向量各维度的绝对值通常越大(信息量累积效应)
- 不同主题的文本激活不同数量的维度,导致向量的范数差异
- 所以同一个 Embedding 模型下,"短查询"和"长文档"的 l2 距离天然很大,但它们的语义可能非常接近
源码视角:
python
# Chroma 内部计算 cosine 距离(简化版)
def cosine_distance(a, b):
dot_product = sum(ai * bi for ai, bi in zip(a, b))
norm_a = sum(ai ** 2 for ai in a) ** 0.5
norm_b = sum(bi ** 2 for bi in b) ** 0.5
return 1.0 - (dot_product / (norm_a * norm_b))
# 完全同方向 → 0.0,完全相反 → 2.0
一句话总结: l2 在意"你说了多少",cosine 在意"你说的意思"。语义搜索要的是后者------否则长文档永远排在短文档后面。
Q3:元数据(metadata)和文档内容(documents)在 Chroma 中的存储路径有什么本质区别?这对查询性能意味着什么?
考点拆解: Chroma 内部存储架构,向量索引 vs 结构化存储的职责分离。
通关答案:
documents(文本内容)
↓ EmbeddingFunction
↓
向量(存在 HNSW 图索引中)← 用于 query() 的语义搜索
metadatas(元数据)
↓ 直接存
↓
sqlite3 表 ← 用于 get() 的 where 过滤、精确查询、排序
本质区别:
| 维度 | documents(→ 向量) | metadatas |
|---|---|---|
| 存储位置 | HNSW 图索引(内存) + 原始文本(sqlite3) | sqlite3 表 |
| 查询方式 | ANN 近似搜索(有损) | B-Tree 精确查询(无损) |
| 索引结构 | 图结构(节点=向量,边=近邻关系) | B-Tree(按 key 有序) |
| 速度 | O(log n) 图遍历 | O(log n) B-Tree 查找 |
查询性能的启发:
python
# 场景1:元数据过滤 + 语义搜索(推荐)
collection.query(
query_texts=["如何优化性能"],
where={"lang": "Python"}, # sqlite 快速过滤 → HNSW 精排
)
# 执行:sqlite O(log n) → HNSW O(log n)
# 场景2:纯元数据查询(别用 Chroma!)
collection.get(where={"lang": "Python", "version": "3.12"})
# 这本质上就是把 Chroma 当 SQLite 用,暴殄天物
# 这种场景直接用 sqlite3 或 PostgreSQL 更合适
一句话总结: 向量(HNSW)管"像不像",元数据(sqlite3)管"是不是"。前者模糊但聪明,后者精确但死板------混合查询才是 Chroma 的正确打开方式。