我用 Python 搭了一套语义搜索系统:从向量数据库到智能检索,告别关键词搜索
我用 Python 搭了一套语义搜索系统:从向量数据库到智能检索,告别关键词搜索
适合想让搜索从"关键词匹配"升级到"语义理解"的开发者。
本文用 Python + ChromaDB + Sentence Transformers 搭了一套完整的语义搜索系统,附可运行代码。
背景:关键词搜索的局限
传统搜索是关键词匹配——搜"Python 自动化",只能找到包含这几个字的内容。
但用户的搜索意图往往是语义层面的:
- 搜"怎么让电脑自动干活"→ 应该找到"Python 自动化脚本"
- 搜"AI 写文章"→ 应该找到"LLM 内容生成"
- 搜"省钱的 AI 工具"→ 应该找到"开源大模型本地部署"
关键词搜索做不到这些。语义搜索可以。
核心原理
文本 → Embedding 模型 → 向量(一组数字)→ 存入向量数据库
查询 → Embedding 模型 → 查询向量 → 在数据库中找最相似的向量
语义相近的文本,向量距离近。
技术选型
| 方案 | 优点 | 缺点 | 推荐 |
|---|---|---|---|
| ChromaDB | 纯 Python,零依赖 | 大规模性能一般 | ⭐⭐⭐⭐⭐ |
| FAISS | Facebook 出品,性能强 | 需要自己管理元数据 | ⭐⭐⭐⭐ |
| Milvus | 分布式,企业级 | 部署复杂 | ⭐⭐⭐⭐ |
| Pinecone | 全托管云服务 | 收费 | ⭐⭐⭐ |
| Qdrant | Rust 实现,高性能 | 生态较小 | ⭐⭐⭐⭐ |
我选 ChromaDB,理由:纯 Python,不需要额外部署,5 分钟上手。
模块 1:Embedding 模型
from sentence_transformers import SentenceTransformer
# 加载中文 Embedding 模型
model = SentenceTransformer("shibing624/text2vec-base-chinese")
def get_embedding(text):
"""把文本转成向量"""
return model.encode(text).tolist()
# 示例
vec1 = get_embedding("Python 自动化脚本")
vec2 = get_embedding("怎么让电脑自动干活")
vec3 = get_embedding("今天天气真好")
import numpy as np
def cosine_similarity(a, b):
"""计算余弦相似度"""
a, b = np.array(a), np.array(b)
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# "Python 自动化脚本" 和 "怎么让电脑自动干活" 语义相近
print(f"自动化 vs 自动干活: {cosine_similarity(vec1, vec2):.3f}") # 约 0.75
print(f"自动化 vs 天气: {cosine_similarity(vec1, vec3):.3f}") # 约 0.15
模块 2:向量数据库
import chromadb
# 创建数据库(持久化存储)
db = chromadb.PersistentClient(path="./vector_db")
collection = db.get_or_create_collection(
name="my_docs",
metadata={"hnsw:space": "cosine"} # 使用余弦相似度
)
def add_document(doc_id, text, metadata=None):
"""添加文档到向量数据库"""
embedding = get_embedding(text)
collection.add(
ids=[doc_id],
embeddings=[embedding],
documents=[text],
metadatas=[metadata or {}]
)
def search(query, top_k=5):
"""语义搜索"""
query_embedding = get_embedding(query)
results = collection.query(
query_embeddings=[query_embedding],
n_results=top_k
)
return results
# 添加文档
add_document("doc1", "Python 自动化脚本可以帮你自动整理文件", {"category": "Python"})
add_document("doc2", "用 AI Agent 搭建内容发布流水线", {"category": "AI"})
add_document("doc3", "Docker 部署 AI 应用的完整指南", {"category": "DevOps"})
add_document("doc4", "RAG 知识库让 AI 读完你所有笔记", {"category": "AI"})
add_document("doc5", "LLM 大语言模型的核心原理", {"category": "AI"})
# 搜索
results = search("怎么让 AI 帮我写文章")
for i, (doc, score) in enumerate(zip(results["documents"][0], results["distances"][0])):
print(f" {i+1}. [{score:.3f}] {doc}")
输出示例:
1. [0.312] 用 AI Agent 搭建内容发布流水线
2. [0.387] RAG 知识库让 AI 读完你所有笔记
3. [0.456] LLM 大语言模型的核心原理
4. [0.621] Python 自动化脚本可以帮你自动整理文件
5. [0.789] Docker 部署 AI 应用的完整指南
搜"怎么让 AI 帮我写文章",最相关的是"AI Agent 内容发布流水线"——这就是语义搜索的威力。
模块 3:批量导入
import os
from pathlib import Path
def batch_import(directory, extensions=(".md", ".txt")):
"""批量导入目录下的文档"""
files = list(Path(directory).rglob("*"))
files = [f for f in files if f.suffix in extensions]
for i, file_path in enumerate(files):
text = file_path.read_text(encoding="utf-8", errors="ignore")
if len(text.strip()) < 50: # 跳过太短的文件
continue
# 截取前 500 字作为索引内容
truncated = text[:500]
doc_id = f"doc_{i}"
add_document(
doc_id=doc_id,
text=truncated,
metadata={
"source": str(file_path),
"filename": file_path.name,
"length": len(text)
}
)
if (i + 1) % 50 == 0:
print(f"已导入 {i + 1} 个文件")
print(f"导入完成,共 {len(files)} 个文件")
# 使用
batch_import("./knowledge_base")
模块 4:语义搜索 API
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class SearchRequest(BaseModel):
query: str
top_k: int = 5
@app.post("/search")
def semantic_search(req: SearchRequest):
"""语义搜索接口"""
results = search(req.query, req.top_k)
items = []
for doc, meta, dist in zip(
results["documents"][0],
results["metadatas"][0],
results["distances"][0]
):
items.append({
"content": doc,
"source": meta.get("source", ""),
"relevance": 1 - dist # 转成相似度(0-1)
})
return {"query": req.query, "results": items}
启动:
pip install fastapi uvicorn chromadb sentence-transformers
uvicorn search_api:app --reload --port 8000
使用:
curl -X POST http://localhost:8000/search \
-H "Content-Type: application/json" \
-d '{"query": "怎么让 AI 帮我写文章", "top_k": 3}'
模块 5:混合搜索(向量 + 关键词)
纯语义搜索有时会"跑偏"。混合搜索结合两种方式:
def hybrid_search(query, top_k=5, keyword_weight=0.3):
"""混合搜索:语义 + 关键词"""
# 1. 语义搜索
semantic_results = search(query, top_k=top_k * 2)
# 2. 关键词匹配(简单实现)
keyword_scores = {}
for doc, meta, dist in zip(
semantic_results["documents"][0],
semantic_results["metadatas"][0],
semantic_results["distances"][0]
):
# 关键词匹配得分
keyword_score = sum(1 for word in query if word in doc) / len(query)
# 语义得分(距离转相似度)
semantic_score = 1 - dist
# 混合得分
final_score = (1 - keyword_weight) * semantic_score + keyword_weight * keyword_score
keyword_scores[doc] = {
"score": final_score,
"source": meta.get("source", "")
}
# 排序
sorted_results = sorted(keyword_scores.items(), key=lambda x: x[1]["score"], reverse=True)
return sorted_results[:top_k]
踩坑记录
坑 1:Embedding 模型选错
症状:搜"Python 自动化",返回的结果和 Python 无关。
原因:用的英文 Embedding 模型,中文语义理解差。
解决:用中文专用模型 shibing624/text2vec-base-chinese 或 BAAI/bge-large-zh。
坑 2:向量维度不一致
症状:添加文档时报 “dimension mismatch”。
原因:不同 Embedding 模型输出的向量维度不同。
解决:确保所有操作用同一个模型。
坑 3:文档太长,Embedding 质量下降
症状:5000 字的文档搜索结果不准确。
原因:Embedding 模型对长文本的语义压缩能力有限。
解决:长文档分段,每段 200-500 字单独索引。
坑 4:ChromaDB 持久化目录被删
症状:重启后搜索结果为空。
原因:path="./vector_db" 目录被清理了。
解决:把向量数据库目录加到 .gitignore 的排除列表,定期备份。
坑 5:首次加载模型很慢
症状:第一次运行要下载 500MB 的模型文件。
解决:提前下载模型到本地,或者用更小的模型(如 text2vec-base-chinese 约 400MB)。
性能对比
| 指标 | 关键词搜索 | 语义搜索 |
|---|---|---|
| “怎么让电脑自动干活” | ❌ 找不到 | ✅ 找到"Python 自动化" |
| “AI 写文章” | ❌ 只匹配字面 | ✅ 找到"LLM 内容生成" |
| “省钱的 AI 工具” | ❌ 找不到 | ✅ 找到"开源模型本地部署" |
| 搜索速度 | 极快(<10ms) | 较快(50-200ms) |
| 存储空间 | 小 | 大(每个文档一个向量) |
总结
3 条核心经验:
-
中文场景用中文 Embedding 模型。英文模型对中文语义理解差,搜索结果不准。推荐
text2vec-base-chinese或bge-large-zh。 -
长文档要分段索引。5000 字的文档直接索引效果差,分成 200-500 字的段落,每段单独索引。
-
混合搜索 > 纯语义搜索。纯语义搜索有时会"跑偏",结合关键词匹配(权重 20-30%)效果更好。
你有做过语义搜索吗?用什么方案?评论区交流。
更多推荐



所有评论(0)