很多开发者做搜索功能时,第一反应还是用关键词匹配——用户搜”如何退换货”,系统只匹配含”退换货”三个字的页面,搜”退款流程”就匹配不到。
语义搜索解决的就是这个问题。它不依赖关键词的精确匹配,而是理解用户问题的”意思”,找到意思相近的内容。而这一切的核心,是Embedding模型。
这篇文章不讲高大上的理论,直接给实操:怎么选模型、怎么搭向量库、怎么写检索代码、怎么部署。
Embedding模型是什么(30秒速通)
Embedding模型做的事情很简单:把一段文本转换成一串固定长度的数字列表(向量)。
“如何退换货” → [0.12, -0.34, 0.67, … 1536个数字]
“退款流程是什么” → [0.14, -0.31, 0.65, … 1536个数字]
这两个向量之间的距离很近,语义相似度就高。关键词匹配做不到这点。
向量之间的距离计算方式通常用余弦相似度(cosine similarity),值越接近1表示语义越相似。
第一步:模型选型
2026年主流的Embedding模型有几个梯队:
开源模型
BAAI/bge-small-en-v1.5(384维)
- 体积最小,速度最快
- 适合对延迟敏感的场景,比如实时搜索
- 内存占用低,CPU即可运行
- 在MTEB(Massive Text Embedding Benchmark)上的平均分数约60.2
BAAI/bge-large-en-v1.5(1024维)
- 精度更高,适合对召回率要求高的场景
- 需要GPU或较好的CPU资源
- MTEB平均分数约63.7
intfloat/e5-mistral-7b-instruct
- 目前开源Embedding的SOTA级别
- 7B参数,需要16GB以上显存
- MTEB平均分数约66.6
闭源API
OpenAI text-embedding-3-small(1536维)
- 最省心的选择,不用自己部署
- 定价:$0.02/1M tokens
- 适合原型验证和中低流量场景
OpenAI text-embedding-3-large(3072维)
- OpenAI目前最强Embedding模型
- 定价:$0.13/1M tokens
- 适合对精度要求极高的场景
选型建议:独立开发者做产品原型阶段,直接用 OpenAI text-embedding-3-small,成本极低(1万次调用才$0.20),不需要GPU。等流量大了再考虑换开源自建。
第二步:向量数据库
把文本转成向量后,需要存起来并在检索时做相似度搜索。这里有几个选择:
pgvector(PostgreSQL扩展)
- 如果你的应用已经在用PostgreSQL,这是最自然的选择
- 不用引入新的基础设施,直接在数据库里加向量列
- 支持IVFFlat和HNSW索引
- 适合数据量在100万条以内的场景
Chroma
- 专门为AI应用设计的向量数据库
- Python API非常简洁
- 支持持久化到磁盘
- 适合快速原型开发
Qdrant
- 独立的向量搜索引擎
- 支持过滤、分组、payload存储
- 性能优于Chroma
- 适合生产环境
选型建议:原型阶段用Chroma最快,生产环境如果有PostgreSQL就用pgvector,否则用Qdrant。
第三步:从零搭建语义搜索(可运行代码)
以下是一个完整的语义搜索系统,用OpenAI Embedding + Chroma实现:
import chromadb
from openai import OpenAI
import numpy as np
client = OpenAI(api_key="your-api-key")
# 初始化Chroma
chroma_client = chromadb.PersistentClient(path="./my_vectordb")
collection = chroma_client.get_or_create_collection(name="docs")
def embed_text(text: str) -> list:
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
# 添加文档
documents = [
"产品支持7天无理由退换货,但需要保持包装完整",
"退款将在收到退货后3-5个工作日内原路返回",
"会员等级分为普通、银卡、金卡、钻石四个级别",
"银卡会员享受全年包邮权益,每月可领一张满减券",
]
ids = [f"doc_{i}" for i in range(len(documents))]
embeddings = [embed_text(doc) for doc in documents]
collection.add(
documents=documents,
embeddings=embeddings,
ids=ids
)
# 搜索
def search(query: str, top_k: int = 3):
query_embedding = embed_text(query)
results = collection.query(
query_embeddings=[query_embedding],
n_results=top_k
)
return results
# 测试
results = search("怎么退款")
for i, doc in enumerate(results["documents"][0]):
print(f"结果{i+1}: {doc}")
这段代码可以直接复制运行,只需要替换OpenAI API key。它做的事情:
- 用text-embedding-3-small把文档转成向量
- 存到Chroma本地数据库
- 用户搜索时,把查询也转成向量,在库里找最近的匹配
第四步:提升检索质量
基础版本能跑,但要上线还差几步。
1. 文本分块策略
原始文档通常很长,不能整篇塞给Embedding模型。需要切成合适的块。
经验规则:
- 块大小:256-512 tokens
- 块重叠:20-50 tokens(避免切断了关键信息)
- 按段落切,不要按固定字符数硬切
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=50,
separators=["\n\n", "\n", "。", ".", " "]
)
chunks = text_splitter.split_text(long_text)
2. Hybrid Search(混合搜索)
纯语义搜索有一个常见问题:如果用户搜的是一个精确的专有名词(比如产品型号),语义搜索可能会返回意思相近但型号不同的结果。
解决办法是混合搜索:同时做关键词搜索和语义搜索,然后把结果合并排序。
def hybrid_search(query, top_k=5, alpha=0.5):
# 语义搜索
semantic_results = semantic_search(query, top_k)
# 关键词搜索(用简单的BM25)
keyword_results = bm25_search(query, top_k)
# 加权合并
combined = merge_with_rrf(semantic_results, keyword_results, alpha)
return combined
alpha控制两种搜索的权重,0.5表示各占一半。需要根据实际数据调优。
3. 元数据过滤
不是所有文档都适合返回。加上元数据过滤,让搜索更精准:
collection.add(
documents=[doc1, doc2],
embeddings=[emb1, emb2],
metadatas=[
{"category": "退货政策", "language": "zh"},
{"category": "会员权益", "language": "zh"}
],
ids=["doc_0", "doc_1"]
)
results = collection.query(
query_embeddings=[query_embedding],
n_results=5,
where={"category": {"$eq": "退货政策"}}
)
第五步:部署到生产
生产环境的语义搜索和原型有一些关键区别:
缓存:Embedding调用API需要时间,而且相同文本每次生成的向量是一样的。加一层缓存,避免重复调用:
import hashlib
import json
embedding_cache = {}
def cached_embed(text: str) -> list:
key = hashlib.md5(text.encode()).hexdigest()
if key in embedding_cache:
return embedding_cache[key]
emb = embed_text(text)
embedding_cache[key] = emb
return emb
异步:如果搜索量高,同步调用会阻塞。用异步客户端:
import asyncio
from openai import AsyncOpenAI
async_client = AsyncOpenAI(api_key="your-api-key")
async def async_embed(text: str):
response = await async_client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
索引类型:Chroma默认使用HNSW索引,适合小规模。如果数据量超过10万条,考虑用IVFFlat索引,建索引速度更快,查询精度牺牲不大。
总结
语义搜索的核心链路:Embedding模型 → 向量数据库 → 检索逻辑 → 部署优化。
对于独立开发者,最推荐的起步路径:
- 模型:OpenAI text-embedding-3-small(不折腾)
- 向量库:Chroma(快速原型)→ pgvector(生产)
- 检索:先纯语义搜索,再加Hybrid Search
- 部署:加缓存和异步,用FastAPI包装成接口
全套代码加起来不超过200行,但效果远超传统关键词搜索。特别是做AI产品的知识库问答、客服系统、文档搜索等场景,这个方案可以说是标配了。
参考来源:
- OpenAI Embeddings API文档:https://platform.openai.com/docs/guides/embeddings
- Chroma官方文档:https://docs.trychroma.com
- pgvector GitHub仓库:https://github.com/pgvector/pgvector
- MTEB Leaderboard(Hugging Face):https://huggingface.co/spaces/mteb/leaderboard
- LangChain Text Splitters文档:https://python.langchain.com/docs/how_to/#text-splitters
