三者根本不是一类东西

选错通常是因为没分清形态:sqlite-vec 是 SQLite 的一个扩展,FAISS 是一个 C++ 库(不是服务),Chroma 是带 embedding 和元数据过滤的向量数据库。一个是"数据库里加个字段",一个是"自己造轮子的零件",一个是"开箱即用但偏原型的成品"。先把这张表看明白,再按规模和运维能力做决定。

方案形态适合规模元数据过滤持久化
sqlite-vecSQLite 扩展单机 50 万以内靠 SQL WHERE 自己拼单个 .db 文件
FAISS算法库千万级无,需自建映射write_index 手动存
Chroma向量数据库十万级原型内置 wherePersistentClient
pgvectorPostgres 扩展百万级SQL 全能力随数据库

各自的最小可用代码

sqlite-vec 的价值是"一个文件 + 事务",向量和业务数据同库,备份、加字段、按条件过滤都用 SQL,嵌入式场景几乎零运维。建虚拟表时维度必须和 embedding 模型一致,插入用 float32 的字节串。

import sqlite3, sqlite_vec, numpy as np

db = sqlite3.connect("kb.db")
db.enable_load_extension(True)
sqlite_vec.load(db)
db.execute("CREATE VIRTUAL TABLE IF NOT EXISTS docs USING vec0(embedding float[1024])")
db.execute("CREATE TABLE IF NOT EXISTS meta (rowid INTEGER PRIMARY KEY, path TEXT, tag TEXT)")

v = np.array([0.1] * 1024, dtype=np.float32)
db.execute("INSERT INTO docs(rowid, embedding) VALUES (?, ?)", (1, v.tobytes()))
db.execute("INSERT INTO meta VALUES (?, ?, ?)", (1, "ai/ollama.md", "ai"))
db.commit()

q = np.array([0.1] * 1024, dtype=np.float32)
rows = db.execute(
    "SELECT d.rowid, d.distance, m.path FROM docs d "
    "JOIN meta m ON m.rowid = d.rowid "
    "WHERE d.embedding MATCH ? AND m.tag = ? ORDER BY d.distance LIMIT 5",
    (q.tobytes(), "ai"),
).fetchall()

FAISS 只负责算最近邻,返回的是下标和距离,id 到原文的映射、过滤、并发、落盘都得自己写。好处是索引类型可选、速度上限高。用余弦时务必先把向量归一化,否则 IndexFlatL2 的 L2 距离和余弦排序不等价;HNSW 的 M 和 efConstruction 决定内存与召回的平衡。

import faiss, numpy as np

vecs = np.random.rand(10000, 1024).astype("float32")
faiss.normalize_L2(vecs)
index = faiss.IndexHNSWFlat(1024, 32)
index.hnsw.efConstruction = 200
index.add(vecs)

q = np.random.rand(1, 1024).astype("float32")
faiss.normalize_L2(q)
D, I = index.search(q, 5)   # I 是下标,自己去 meta 表里查对应文档
faiss.write_index(index, "docs.index")

Chroma 适合两天内要看到效果:自带 collection、元数据过滤和默认 embedding,代码量最少。坑也在这里,默认 all-MiniLM 是英文模型,中文不换 embedding_function 会明显退化。

import chromadb

client = chromadb.PersistentClient(path="./chroma")
col = client.get_or_create_collection("blog", metadata={"hnsw:space": "cosine"})
col.add(ids=["1"], documents=["Ollama 用 num_ctx 控制上下文"],
        embeddings=[[0.1] * 1024], metadatas=[{"tag": "ai"}])
print(col.query(query_embeddings=[[0.1] * 1024], n_results=5,
                where={"tag": "ai"}))

按场景给结论

  • 单机工具、要嵌进应用:sqlite-vec。一个 .db 文件跟着 SQLite 事务走,备份就是拷文件。
  • 原型验证、要自带过滤:Chroma,但别直接上生产核心链路,数据量和并发上去后召回和内存都不理想。
  • 百万级以上、要极致召回:FAISS 的 IVF 或 HNSW;如果已经在用 Postgres,直接 pgvector 的 HNSW 索引,省一整套组件。

踩坑:维度写错,插入时有的库直接报错、有的悄悄存进去导致结果全乱;批量插入不包事务,sqlite-vec 会慢到怀疑人生;换 embedding 模型后维度和分布都变了,必须整库重建,新旧向量绝不能混查。