• 欢迎访问少将全栈,学会感恩,乐于付出,珍惜缘份,成就彼此、推荐使用最新版火狐浏览器和Chrome浏览器访问本网站。
  • 吐槽,投稿,删稿,交个朋友
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏少将全栈吧

用Embedding模型做语义搜索:从选型到部署的完整实践

AI Coding 实测 admin 1小时前 5次浏览 已收录 扫描二维码

很多开发者做搜索功能时,第一反应还是用关键词匹配——用户搜”如何退换货”,系统只匹配含”退换货”三个字的页面,搜”退款流程”就匹配不到。

语义搜索解决的就是这个问题。它不依赖关键词的精确匹配,而是理解用户问题的”意思”,找到意思相近的内容。而这一切的核心,是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。它做的事情:

  1. 用text-embedding-3-small把文档转成向量
  2. 存到Chroma本地数据库
  3. 用户搜索时,把查询也转成向量,在库里找最近的匹配

第四步:提升检索质量

基础版本能跑,但要上线还差几步。

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
喜欢 (0)
[🍬谢谢你请我吃糖果🍬🍬~]
分享 (0)
关于作者:
少将,关注Web全栈开发、项目管理,持续不断的学习、努力成为一个更棒的开发,做最好的自己,让世界因你不同。