理解 Embedding → HNSW → ChromaDB 的完整链路,用直觉而非公式
左边模拟传统数据库的字面匹配逻辑(MySQL LIKE),右边是向量数据库的语义匹配逻辑(ChromaDB)。 这两个查询词在文档中一个字都没出现,看看两边分别能搜到什么。
-- 文档里写的是"麻辣",用户搜的是"好吃的",字面完全没有交集
SELECT * FROM documents WHERE content LIKE '%好吃的麻辣菜%';
-- 返回:0 行
-- 就算精确搜"麻辣",也只能匹配到恰好包含这两个字的文档
-- "香辣""酸辣""辛辣"——都不会被匹配到
HNSW = Hierarchical Navigable Small World,分层的"导航图"。
类比:从北京去中关村某条小巷的咖啡店——
HNSW 对向量做同样的事——顶层只有几十个节点可以大步跨越,底层的密集节点保证精度。最终只访问了总向量的很小一部分 → O(log N)。
不找"精确最近的那一个",找"足够近的那几个"。用精度换速度。
| 暴力搜索 | O(N) 时间 | 100% 精确 |
| ANN 搜索 | O(log N) | ~99% 精确 |
WHERE id=123 必须精确到一行不同。
所有文档和查询都用 bert-base-chinese 生成了真实的 768 维向量,然后 PCA 降到 2 维可视化。 可以清晰看到同类文档自然聚集,查询向量靠近相关文档簇。
| MySQL | PostgreSQL + pgvector | Elasticsearch | ChromaDB | FAISS | Milvus | |
|---|---|---|---|---|---|---|
| 类型 | 关系型 | 关系型 | 搜索引擎 | 向量数据库 | 向量检索库 | 向量数据库 |
| 向量支持 | ✗ 不支持 | ✓ pgvector | ✓ 8.0+ | ✓ 原生 | ✓ 核心 | ✓ 原生 |
| 部署复杂度 | 低 | 低 | 中 | 低 | 极低(纯库) | 高 |
| 数据持久化 | ✓ | ✓ | ✓ | ✓ | ✗ 需手动 | ✓ |
| 混合查询 | N/A | ✓ | ✓ | ✓ | ✗ | ✓ |
| 分布式 | 支持 | 支持 | 原生 | ✗ 单机 | ✗ 单机 | 原生 |
| 适用量级 | N/A | 百万 | 千万 | 十万~百万 | 亿级 | 十亿级 |
| 一句话 | 关系型之王 但没向量 |
一库两用 百万以内首选 |
全文+语义 混合搜索王者 |
向量DB界的SQLite 零配置 |
极致速度 但它不是数据库 |
向量DB界的PG 生产就绪 |
要选向量数据库?
├─ 只是学习/Demo/原型?
│ └─ → ChromaDB(pip install 即可)
├─ 已有 PostgreSQL?
│ └─ 数据量 < 百万 → pgvector(零迁移成本)
├─ 需要全文 + 语义混合搜索?
│ └─ → Elasticsearch
├─ 生产环境、大规模、低延迟?
│ └─ → Milvus(分布式、十亿级)
├─ 离线分析、学术研究?
│ └─ → FAISS(需自建持久化)
└─ 纯结构化查询、不需要语义?
└─ → MySQL / PostgreSQL(传统的就够了)
class ChineseBertEmbedding(EmbeddingFunction):
"""用 bert-base-chinese 生成句向量"""
def __call__(self, texts):
for text in texts:
inputs = self.tokenizer(text, return_tensors="pt")
with torch.no_grad():
outputs = self.model(**inputs)
# Mean Pooling:所有 token 向量取平均 → 句向量
pooled = mean_pooling(outputs, inputs["attention_mask"])
# L2 归一化:让所有向量长度=1,余弦相似度等价于点积
pooled = F.normalize(pooled, p=2, dim=1)
embeddings.append(pooled[0].tolist())
return embeddings
# 创建客户端(内存模式)
client = chromadb.Client()
collection = client.create_collection(
name="knowledge_base",
embedding_function=ChineseBertEmbedding(),
metadata={"hnsw:space": "cosine"}, # 用余弦距离
)
# add() 自动调用 embedding_function 把 text → 向量 → HNSW 索引
collection.add(
documents=["麻婆豆腐是一道经典的四川名菜..."],
metadatas=[{"category": "food"}],
ids=["doc_4"],
)
# 纯语义搜索
results = collection.query(
query_texts=["适合拍照的手机"],
n_results=3,
)
# 混合查询:先过滤 category='tech',再在过滤结果中做向量检索
results = collection.query(
query_texts=["和 AI 相关的技术"],
n_results=3,
where={"category": "tech"},
)
def simulate_keyword_search(query):
# 按字符出现次数打分
query_chars = set(query) - stop_chars
for text in documents:
score = sum(1 for c in query_chars if c in text)
# → "辣的菜"搜不到"麻婆豆腐"
collection.add(embeddings=vecs, ids=ids) → 搜出来只有 ID 和一串数字。documents 参数。
uv python install 3.12 && uv venv --python 3.12
pip install -r requirements.txt 装到了 Python 3.14 而不是 venv 的 3.12。uv pip install --python .venv/bin/python -r requirements.txt(强制指定解释器)