8. 向量化:用 Python 构建你的第一个向量库

0 阅读7分钟

前面 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,网络不好就超时

解法

  1. 手动下到本地
git clone https://huggingface.co/BAAI/bge-m3 models/bge-m3
  1. 指定本地路径
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 章给你一组可直接调用的混合检索代码