20 分钟搭一个语义搜索引擎:Chroma + Qdrant 手把手教程

📘 AI教程 💬 🔥 Trending

🩺 摘要

上篇文章做了 Chroma vs Qdrant vs Milvus 的全面对比,这篇直接动手。...

📝 详情

上篇文章做了 Chroma vs Qdrant vs Milvus 的全面对比,这篇直接动手。

目标:用 20 分钟搭一个能理解「语义」的搜索引擎——你搜「怎么退会员费」它能找到「取消订阅操作指南」,因为两者意思相近,哪怕文字完全不同。


准备工作

你需要: - Python 3.10+ - pip(国内用清华镜像装更快)

# 安装依赖
pip install chromadb qdrant-client sentence-transformers

国内加速:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple chromadb qdrant-client sentence-transformers

第一步:准备中文 Embedding 模型

向量数据库的核心是把文字变成数字(向量),这个转化工具叫做 Embedding 模型。对于中文,我用 BAAI/bge-small-zh-v1.5——很小(33MB),中文效果不错。

from sentence_transformers import SentenceTransformer

# 加载模型(首次运行会自动下载)
# 如果下载慢,可以先用 HF 镜像
# export HF_ENDPOINT=https://hf-mirror.com
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")

# 测试一下
vec = model.encode("如何取消自动续费")
print(f"向量维度: {len(vec)}")  # 输出: 512
print(f"前5个值: {vec[:5]}")    # 向量长这样

BGE-small-zh 输出 512 维向量,对于绝大多数项目够用了。如果你需要更高精度,可以上 BAAI/bge-large-zh-v1.5(1024 维,大 3 倍,慢 2 倍)。

第二步:用 Chroma 实现(5 分钟)

Chroma 适合快速原型,不需要启动任何服务:

import chromadb
from sentence_transformers import SentenceTransformer

# 1. 初始化
client = chromadb.Client()
collection = client.create_collection("knowledge_base")

model = SentenceTransformer("BAAI/bge-small-zh-v1.5")

# 2. 准备数据(一些客服常见问题)
documents = [
    "如何取消会员自动续费",
    "退款需要多长时间到账",
    "修改绑定的手机号码",
    "忘记密码怎么办",
    "如何下载历史订单数据",
]

# 3. 生成向量并存入
embeddings = model.encode(documents).tolist()
collection.add(
    documents=documents,
    embeddings=embeddings,
    ids=[f"doc{i}" for i in range(len(documents))]
)

print(f"已存入 {len(documents)} 条数据")

# 4. 搜索测试
query = "我想退订会员"
query_vec = model.encode(query).tolist()

results = collection.query(
    query_embeddings=[query_vec],
    n_results=3
)

print("搜索结果:")
for i, (doc, score) in enumerate(zip(results["documents"][0], results["distances"][0])):
    print(f"  {i+1}. {doc} (距离: {score:.4f})")

输出:

1. 如何取消会员自动续费 (距离: 0.2341)
2. 退款需要多长时间到账 (距离: 0.4587)
3. 如何下载历史订单数据 (距离: 0.6123)

「想退订会员」和「取消会员自动续费」虽然字面不同,但语义上最近——这就是向量搜索的意义。

第三步:用 Qdrant 实现(10 分钟)

Qdrant 需要启动服务,但性能更强,适合正式项目:

# 启动 Qdrant
docker run -d --name qdrant \
  -p 6333:6333 \
  -p 6334:6334 \
  -v $(pwd)/qdrant_data:/qdrant/storage \
  qdrant/qdrant

# 验证
curl http://localhost:6333/healthz
# 返回 OK 就对了
from qdrant_client import QdrantClient
from qdrant_client.http import models
from sentence_transformers import SentenceTransformer

# 连接
client = QdrantClient("localhost", port=6333)
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")

VECTOR_SIZE = 512

# 1. 建集合
client.recreate_collection(
    collection_name="knowledge_base",
    vectors_config=models.VectorParams(
        size=VECTOR_SIZE,
        distance=models.Distance.COSINE
    )
)

# 2. 存入数据
documents = [
    "如何在 Linux 上安装 Docker",
    "Docker 容器和虚拟机的区别",
    "如何将 Spring Boot 应用容器化",
    "Docker Compose 常用命令",
    "优化 Docker 镜像体积的 5 个技巧",
]

vectors = model.encode(documents)
points = [
    models.PointStruct(
        id=i,
        vector=vectors[i].tolist(),
        payload={"text": documents[i], "category": "docker"}
    )
    for i in range(len(documents))
]

client.upsert(
    collection_name="knowledge_base",
    points=points
)

# 3. 搜索
query = "怎么搞容器化"
query_vec = model.encode(query).tolist()

results = client.search(
    collection_name="knowledge_base",
    query_vector=query_vec,
    limit=3
)

for result in results:
    print(f"  {result.payload['text']} (打分: {result.score:.4f})")

输出:

  如何将 Spring Boot 应用容器化 (打分: 0.8123)
  Docker 容器和虚拟机的区别 (打分: 0.6754)
  如何在 Linux 上安装 Docker (打分: 0.5321)

第四步:进阶——带上元数据过滤

Qdrant 的杀手级功能是可以在搜索的同时做标量字段过滤:

# 只搜索 "category" 为 "docker" 的数据
results = client.search(
    collection_name="knowledge_base",
    query_vector=query_vec,
    query_filter=models.Filter(
        must=[
            models.FieldCondition(
                key="category",
                match=models.MatchValue(value="docker")
            )
        ]
    ),
    limit=3
)

这在做多租户应用或分类搜索时非常有用。比如你有个电商知识库,用户可以同时搜「退货政策」但只看「电子产品」分类下的。

第五步:配套 LLM——完整 RAG

向量搜索只是 RAG(检索增强生成)的一半。另一半是把搜到的结果喂给 LLM,让它总结回答:

from openai import OpenAI

client_llm = OpenAI(
    api_key="sk-xxx",
    base_url="https://api.deepseek.com/v1"
)

# 向量搜索
results = qdrant_client.search(
    collection_name="knowledge_base",
    query_vector=query_vec,
    limit=3
)

# 把结果拼成上下文
context = "\n\n".join([r.payload["text"] for r in results])

# 让 LLM 基于搜索结果回答
response = client_llm.chat.completions.create(
    model="deepseek-chat",
    messages=[
        {"role": "system", "content": "基于以下资料回答问题。"},
        {"role": "user", "content": f"资料:\n{context}\n\n问题:{query}"}
    ]
)

print(response.choices[0].message.content)

一个完整的 RAG 脚本参考

我把上面所有步骤整合成了一个可以直接跑的脚本。保存为 mini_rag.py

import chromadb
from sentence_transformers import SentenceTransformer
from openai import OpenAI

# Embedding
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")

# 向量库
client = chromadb.Client()
collection = client.create_collection("wiki")

# 数据(可以用你自己的文档替代)
docs = ["文档1内容...", "文档2内容..."]
embeddings = model.encode(docs).tolist()
collection.add(documents=docs, embeddings=embeddings, ids=["1", "2"])

# 搜索
query = "你的问题"
q_vec = model.encode(query).tolist()
results = collection.query(query_embeddings=[q_vec], n_results=3)

# LLM 回答
llm = OpenAI(api_key="sk-xxx", base_url="https://api.deepseek.com/v1")
resp = llm.chat.completions.create(
    model="deepseek-chat",
    messages=[
        {"role": "user", "content": f"资料:{results['documents'][0]}\n问题:{query}"}
    ]
)
print(resp.choices[0].message.content)

什么时候用 Docker 版的 Qdrant?

很多人问「为什么 Chroma 不需要 Docker 而 Qdrant 需要」——因为 Chroma 把所有数据存在当前目录的 chroma.sqlite3 文件里,而 Qdrant 是一个独立的数据库服务。如果你的项目:

  • 只需一个人用 → Chroma 就够了
  • 需要多人同时访问 → Qdrant
  • 部署到服务器上 → Qdrant 更可靠
  • 嵌入到移动 App → Chroma 更方便

没有绝对的好坏,看场景。


🔗 相关文章: MCP 服务器实战指南:从入门到写自己的第一个 MCP Server