前面 7 章我们把"为什么、是什么、选什么"讲清楚了——这一章开始真正动手。 30 行代码,把一份文档从 0 变成可检索,并第一次看到"我搜了一个问题,系统给我返回了相关文档"的全过程。
8.1 反常识:向量库的"库"没你想的那么难
很多人一听"向量数据库"就觉得是重型基础设施——分布式、高可用、运维复杂。
真相:90% 的场景用 Chroma 一个文件就能跑。等你真上亿级了再考虑 Milvus 集群。
第一次构建向量库,目标是"先跑起来",不是"上生产"。 PoC 用 Chroma,验证了再换重型,这是最稳的路径。
8.2 最小可用端到端代码(30 行)
这是你能见到的最短的、能跑通的 RAG 检索脚本:
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
# 1. 加载文档
docs = TextLoader("data.txt", encoding="utf-8").load()
# 2. 切块
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=80)
chunks = splitter.split_documents(docs)
# 3. 用 bge-m3 把块变向量
emb = HuggingFaceEmbeddings(model_name="BAAI/bge-m3")
# 4. 入库(Chroma 默认本地文件存储)
db = Chroma.from_documents(chunks, emb, persist_directory="./chroma_db")
# 5. 检索
results = db.similarity_search("杭州程序员聚集区", k=3)
for i, r in enumerate(results, 1):
print(f"--- Top {i} ---")
print(r.page_content[:200])
print("元数据:", r.metadata)
跑完这段你就有了一个能检索的知识库。 不需要服务器、不需要 Docker、不需要任何基础设施。
8.3 5 大向量库实战对比
下面用同一份数据、同一段代码对比 5 个主流向量库(只看代码差异):
8.3.1 Chroma(PoC 首选 ⭐)
from langchain_community.vectorstores import Chroma
# 本地文件存储,零部署
db = Chroma.from_documents(chunks, emb, persist_directory="./chroma_db")
results = db.similarity_search("query", k=3)
| 优势 | 劣势 |
|---|---|
| 零部署,开箱即用 | 不支持分布式 |
| API 极简 | 大规模性能下降明显 |
| 适合 < 100 万向量 | 内存占用高 |
适用:PoC、个人项目、小型企业(百万级以内)。
8.3.2 Qdrant(企业首选 ⭐)
from langchain_community.vectorstores import Qdrant
from qdrant_client import QdrantClient
# 本地嵌入式
db = Qdrant.from_documents(
chunks, emb,
location=":memory:", # 或 "localhost:6333" 走服务端
collection_name="my_docs",
)
# 服务端模式(生产推荐)
client = QdrantClient(host="localhost", port=6333)
db = Qdrant(client=client, collection_name="my_docs", embeddings=emb)
| 优势 | 劣势 |
|---|---|
| Rust 写,性能极强 | 需部署服务(Docker 一行) |
| 过滤、payload 强 | 配置项多 |
| 支持亿级 | 比 Chroma 复杂 |
适用:中型企业、千万级向量、生产环境。
8.3.3 Milvus(亿级首选)
from langchain_community.vectorstores import Milvus
db = Milvus.from_documents(
chunks, emb,
connection_args={"host": "127.0.0.1", "port": "19530"},
collection_name="my_docs",
)
| 优势 | 劣势 |
|---|---|
| 亿级向量,分布式 | 部署重(需 etcd + MinIO) |
| 索引丰富 | 资源占用大 |
| 生态成熟 | 团队需要学习成本 |
适用:大型企业、亿级、已经有 K8s 团队。
8.3.4 pgvector(复用现有 PG)
from langchain_community.vectorstores import PGVector
CONN_STR = "postgresql+psycopg2://user:pass@localhost:5432/vectordb"
db = PGVector.from_documents(
chunks, emb,
connection_string=CONN_STR,
collection_name="my_docs",
)
| 优势 | 劣势 |
|---|---|
| 复用已有 PG,零新组件 | 性能不如 Qdrant/Milvus |
| SQL 查询方便 | 需要 PG 13+ |
| 事务、备份都现成 | 千万级以上吃力 |
适用:已有 PG 中间件、不想引入新组件、百万级以内。
8.3.5 FAISS(本地+高性能)
from langchain_community.vectorstores import FAISS
db = FAISS.from_documents(chunks, emb)
db.save_local("./faiss_index")
# 加载
db = FAISS.load_local("./faiss_index", emb)
| 优势 | 劣势 |
|---|---|
| Meta 出品,性能顶级 | 只能本地 |
| 索引算法丰富 | 没有服务/集群 |
| 完全离线 | 无元数据过滤 |
适用:纯本地、单机、极致性能。
8.4 选型决策表
| 场景 | 推荐 | 理由 |
|---|---|---|
| 第一次跑通 | Chroma | 零部署,30 行代码搞定 |
| 企业生产(百万级) | Qdrant | 性能+部署难度平衡 |
| 亿级 + 分布式 | Milvus | 大规模首选 |
| 已有 PG 团队 | pgvector | 复用中间件 |
| 纯本地、极致性能 | FAISS | 离线算法之王 |
经验法则:先 Chroma 跑通 → 验证了换 Qdrant → 真亿级了再上 Milvus。 不要在 PoC 阶段就堆最重的库。
8.5 一个完整的企业级入库 pipeline(100 行)
下面这段代码可以直接改造成生产脚本——包含文档加载、切块、Embedding、入库、检索 5 步完整流程:
import os
from pathlib import Path
from langchain_community.document_loaders import (
TextLoader, PyPDFLoader, UnstructuredWordDocumentLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Qdrant
from langchain.schema import Document
from qdrant_client import QdrantClient
from qdrant_client.http import models
# ============ 配置 ============
DOCS_DIR = "./docs" # 文档目录
DB_PATH = "./qdrant_db" # 向量库路径
COLLECTION = "company_knowledge" # collection 名
CHUNK_SIZE = 500
CHUNK_OVERLAP = 80
EMBED_MODEL = "BAAI/bge-m3"
# ============ 1. 多格式文档加载 ============
def load_documents(dir_path: str) -> list[Document]:
docs = []
for fp in Path(dir_path).rglob("*"):
if fp.suffix in (".txt", ".md"):
loader = TextLoader(str(fp), encoding="utf-8")
elif fp.suffix == ".pdf":
loader = PyPDFLoader(str(fp))
elif fp.suffix in (".docx", ".doc"):
loader = UnstructuredWordDocumentLoader(str(fp))
else:
continue
try:
loaded = loader.load()
for d in loaded:
d.metadata["source"] = str(fp)
docs.extend(loaded)
except Exception as e:
print(f"跳过 {fp}: {e}")
return docs
# ============ 2. 切块 ============
def split_documents(docs: list[Document]) -> list[Document]:
splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP,
)
return splitter.split_documents(docs)
# ============ 3. Embedding & 入库 ============
def build_vectorstore(chunks: list[Document]) -> Qdrant:
emb = HuggingFaceEmbeddings(model_name=EMBED_MODEL)
# 创建 Qdrant 本地服务
client = QdrantClient(path=DB_PATH)
# 如果 collection 已存在,先删
if client.collection_exists(COLLECTION):
client.delete_collection(COLLECTION)
# 创建带向量维度的 collection
client.create_collection(
collection_name=COLLECTION,
vectors_config=models.VectorParams(
size=1024, # bge-m3 输出维度
distance=models.Distance.COSINE,
),
)
# 入库
db = Qdrant(
client=client,
collection_name=COLLECTION,
embeddings=emb,
)
db.add_documents(chunks)
return db
# ============ 4. 检索 ============
def search(db: Qdrant, query: str, k: int = 5):
results = db.similarity_search_with_score(query, k=k)
for doc, score in results:
print(f"\n[相似度 {score:.3f}] {doc.metadata.get('source')}")
print(doc.page_content[:300])
print("---")
# ============ 主流程 ============
if __name__ == "__main__":
print("1. 加载文档...")
docs = load_documents(DOCS_DIR)
print(f" 加载了 {len(docs)} 份文档")
print("2. 切块...")
chunks = split_documents(docs)
print(f" 切出 {len(chunks)} 块")
print("3. Embedding + 入库...")
db = build_vectorstore(chunks)
print(" 入库完成")
print("\n4. 试试检索:")
while True:
q = input("\n请输入问题(q 退出): ").strip()
if q == "q":
break
if q:
search(db, q)
把 docs/ 目录扔一堆 PDF/Word/TXT,运行一下, 5 分钟就有了一个企业级知识库**。
8.6 5 个真实踩坑
坑 1:第一次跑 Chroma 报 sqlite 版本错
sqlite3.OperationalError: no such module: vector
原因:Python 自带 sqlite 不支持 vector 扩展。 解法:用 chromadb 自带的 pysqlite:
__import__('pysqlite3')
import sys
sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')
或在 conda 装:conda install sqlite。
坑 2:Embedding 模型没下载完就调用
首次用 HuggingFaceEmbeddings(model_name="BAAI/bge-m3") 会下载 2.2GB,网络不好就超时。
解法:
- 手动下到本地:
git clone https://huggingface.co/BAAI/bge-m3 models/bge-m3
- 指定本地路径:
emb = HuggingFaceEmbeddings(model_name="./models/bge-m3")
坑 3:bge 查询忘加前缀 → 召回率掉 20%
bge 系列对查询和文档使用不同前缀(见第 7 章)。入库时和检索时都要加:
def add_query_prefix(text):
return "为这个句子生成表示以用于检索相关文章:" + text
# 入库时
db.add_documents([Document(page_content=add_query_prefix(c.page_content), metadata=c.metadata) for c in chunks])
# 检索时
results = db.similarity_search(add_query_prefix(query), k=3)
坑 4:检索太慢——k=20 全表扫描
向量库默认返回 top-k,但 k=20 就要扫更多向量。
解法:
- 控制
k在 3–10 - 千万级向量库加 HNSW 索引(Qdrant/Milvus 默认开启,Chroma 需要配置)
# Qdrant 显式设置 HNSW
client.update_collection(
collection_name="my_docs",
hnsw_config=models.HnswConfigDiff(m=16, ef_construct=128),
)
坑 5:检索出"语义相似但无关"的文档
向量检索的弱点——"产品"和"产品经理"相似度高,但用户问产品故障时召回产品经理文档。
解法:用 Rerank(第 10 章)过滤——粗排用向量,精排用 Cross-Encoder。
8.7 检索质量自检清单
入库后先做这 5 件事再往下走:
- 10 条 query 跑检索:覆盖 3 类(事实型 / 解释型 / 模糊型)
- top-3 肉眼判断:有没有答非所问?
- 相似度分值看分布:正常 0.6–0.85,太低或太高都可疑
- 元数据正确性:来源对不对?页码对不对?
- 慢查询日志:有没有 > 1 秒的检索?
经验值:自检 10 条 query 中有 7 条以上召回正确,才能继续往下做 Rerank、Agent。
8.8 关键判断(这一章的"立场")
第一次构建向量库,目标"先跑起来",不是"上生产"。
Chroma 跑通 → Qdrant 生产 → Milvus 亿级,这是最稳的迁移路径。
入库和检索两端都要加 bge 前缀;HNSW 索引是性能关键。
入库完必须自检 10 条 query,否则后面所有优化都是空中楼阁。
8.9 下章预告
第 9 章《检索:语义搜索 vs 关键词搜索,怎么搭配》——单一向量检索有天花板。 混合检索(BM25 + 向量)能解决 60% 的"语义相似但答错"问题。第 9 章给你一组可直接调用的混合检索代码。