语义向量搜索及其他赢得朋友与爱人的话题
完整搜索全景:精确、模糊、语义、混合——以及何时将它们全部叠加。
搜索并非单一概念,语义搜索也不是其他搜索方式的替代品。
“查找邮箱为 dan@example.com 的用户”和“查找关于新手工程师调试的文章”都被称为搜索,但作为工程问题,它们几乎毫无共同点。前者有正确答案,且可通过 O(log n) 索引查找完成。后者没有正确答案——只有相关性——并且需要理解语言、意图和含义。
在搜索决策中最有说服力的工程师——那些能赢得争论并交付正确系统的人——理解整个技术版图。他们知道该用哪种工具以及为什么,并且能清晰解释。
本文涵盖语义层:向量搜索实际做什么、何时适用、以及哪里应该让路。有用的版本不是“嵌入一切”,而是知道在混合架构中,向量何时应与词法搜索、模糊搜索和精确匹配搜索并存。
词法和模糊搜索的部分——tsvector、pg_trgm、pg_search——详见 Postgres 文本搜索指南 2026。
术语速览
嵌入(Embedding) — 由模型生成的一组密集浮点数,将一段文本(或图像、音频等)表示为高维空间中的一个点。语义相关的内容彼此靠近;无关的内容相距甚远。
词法搜索(Lexical search) — 基于精确词和令牌匹配的搜索。快速、确定、对已知术语正确。不理解同义词、释义或跨语言等价物。
语义搜索(Semantic search) — 基于含义而非令牌的搜索。查询“如何处理超时”可以匹配标题为“配置重试策略”的文档,即使没有共享词汇,因为它们的嵌入在几何上接近。
向量(Vector) — 一组数字。在搜索上下文中,指嵌入模型的输出。“向量搜索”通过几何距离找到最接近查询向量的向量。
全文搜索(FTS) — Postgres 内置的词法搜索,由 tsvector / tsquery 驱动。对文本进行分词、词干提取和索引,用于关键词查询。适用于散文和精确术语查找;对含义无感知。
BM25 — 词法搜索的排序算法(Elasticsearch、Qdrant 等使用)。根据词频与词在整个语料库中的稀有程度加权评分。优于原始关键词匹配,但仍属词法搜索。
HNSW(分层可导航小世界) — 向量搜索的标准近似最近邻索引。构建分层邻近图,实现快速、高召回率的相似性查询。pgvector、Qdrant、Weaviate 等大多使用它。
RRF(倒数排名融合) — 一种合并多个检索系统排序结果列表的算法。仅使用排名位置——无需分数归一化。在全文搜索和向量列表中均排名靠前的结果,其综合得分高于仅在一个系统中占优的结果。
语义搜索的实际作用
向量嵌入将文本(或图像、音频等)转换为一组数字——高维空间中的一个点。嵌入模型经过训练,使语义相关的文本在该空间中彼此靠近。“狗”和“犬科”最终接近。“跑马拉松”和“运行 Python 脚本”尽管共享一个词,却相距甚远。
在该空间中进行相似性搜索,可以找到与查询含义最接近的文档,而不考虑精确的词重叠。
这意味着:
- “如何配置请求超时?”可以匹配一篇标题为“设置连接限制和重试策略”的文章——没有重叠的关键词,但概念相关性高
- “适合夏夜的小酌”可以匹配一款葡萄酒推荐,而产品描述中不出现任何关键词
- 如果嵌入模型经过多语言训练,英文查询可以匹配法语、西班牙语或日语的相关文档
词法搜索(tsvector、pg_trgm)无法做到这些。它基于单词和字符运作,而非含义。这些工具不可互换——它们解决不同的问题。
pgvector 的优势场景
构建 RAG。 检索增强生成(RAG)会检索与用户问题含义最接近的文档块,然后将它们作为上下文传递给语言模型。这个检索步骤是向量操作。全文搜索(FTS)会遗漏相关块可能以不同方式表达的改写、同义词和概念匹配。pgvector 相对于独立向量存储的优势在于:它运行在你现有的 Postgres 实例内部——无需部署、操作或同步数据到单独的服务。
用户描述他们想要什么,而不是搜索什么。 “关于新经理建立自信的文章”没有可靠出现在相关帖子中的关键词。“一个轻量级的副作用处理框架”可能在文档中并不使用这些确切的词语。向量搜索匹配的是意图,而不是拼写。
查找相似项。 相关产品、相似的支持工单、重复的 bug 报告、你可能也喜欢的文章。“查找与此类似的问题”是一种最近邻搜索——对项目进行嵌入,找到其几何邻居。一个重要警告:向量搜索总是返回结果,即使没有真正相似的内容。对于去重和推荐用例,应通过最小相似度阈值(例如余弦相似度 ≥ 0.80)进行过滤,避免将低置信度的匹配当作有意义的结果展示出来。
语义去重。 在为 RAG 或搜索索引内容之前,你通常需要识别语料库中的近似重复项——多次修订的文章、重复提交的支持工单、重叠严重的知识库条目。对文档进行嵌入,并通过余弦相似度阈值过滤,在它们污染索引之前标记或合并近似重复项。这可以防止检索返回多个几乎相同的块,从而稀释上下文窗口。
多语言搜索。 多语言嵌入模型将跨语言的语义等价内容映射到邻近的向量。西班牙语查询“perder peso”可以匹配一篇关于“可持续减肥习惯”的英文文章——没有共享的 token,但底层含义相同。全文搜索(FTS)需要针对每种语言配置词典,并且处理跨语言查询效果不佳。pg_trgm 与语言无关,但它是正字法的,而非语义的。
设置 pgvector
从扩展安装到相似性查询,只需几条 SQL 语句即可完成设置:
CREATE EXTENSION IF NOT EXISTS vector;
ALTER TABLE documents ADD COLUMN embedding vector(1536);
-- HNSW is usually the first index to try for moderate-size datasetsCREATE INDEX documents_embedding_idx ON documents USING hnsw (embedding vector_cosine_ops);
-- Semantic search querySELECT id, title, 1 - (embedding <=> $1::vector) AS similarityFROM documentsORDER BY embedding <=> $1::vectorLIMIT 10;<=> 是余弦距离。1 - cosine_distance 给出余弦相似度(1.0 = 完全相同,0.0 = 正交)。对于 ivfflat(更旧、构建更快的替代方案),使用 lists = sqrt(row_count) 作为起点。
pgvector 不擅长的场景
- 精确 token 匹配——产品 SKU、错误代码、函数名。
ORD-12345在语义上与任何内容都不相似。基于嵌入的搜索可能返回ORD-12344或无关内容。应使用 FTS 或 B-tree 索引。 - 名称和专有名词。嵌入空间按含义组织,而非拼写。用户记录中的“Micheal Jordan”不一定在向量空间中靠近“Michael Jordan”。
- 短字符串,其中字符级相似性比含义更重要。
pg_trgm处理这种情况。 - 查询中必须出现确切术语的情况。对于已知术语匹配,BM25 和 FTS 更可靠。
混合搜索:两者皆需的场景
技术文档是最清晰的例子,说明单独使用任何一种工具都不够。
搜索”如何配置超时”的用户需要概念匹配:一篇题为”设置重试策略和连接限制”的文章虽然没有重叠的关键词,但正是他们需要的。
同一批用户也会搜索 withRetry()、ECONNRESET 和 ERR_SOCKET_TIMEOUT。这些精确字符串必须出现——语义匹配可能无法可靠地找到它们,而一个假阳性(概念相似但并非正确的 API)会严重误导。
向量搜索处理概念查询。FTS 处理精确术语。单独任何一种都无法很好地处理两者。
解决方案是混合搜索:同时运行两者并融合结果。
互惠排名融合
互惠排名融合(RRF) 是组合来自不同检索系统的排名列表的标准算法。它不需要跨系统归一化分数——仅使用排名位置。在两个列表中都排名靠前的结果,其组合得分会强于仅在一个列表中占优的结果。
WITH fts_results AS ( SELECT id, ROW_NUMBER() OVER (ORDER BY ts_rank(search_vector, query) DESC) AS rank FROM documents, to_tsquery('english', $1) query WHERE search_vector @@ query LIMIT 50),vector_results AS ( SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $2::vector) AS rank FROM documents ORDER BY embedding <=> $2::vector LIMIT 50),rrf AS ( SELECT COALESCE(f.id, v.id) AS id, COALESCE(1.0 / (60 + f.rank), 0) + COALESCE(1.0 / (60 + v.rank), 0) AS rrf_score FROM fts_results f FULL OUTER JOIN vector_results v ON f.id = v.id)SELECT d.id, d.title, rrf.rrf_scoreFROM rrfJOIN documents d ON d.id = rrf.idORDER BY rrf_score DESCLIMIT 10;分母中的 60 是 RRF 常数。值越大,排名位置差异的影响越小;值越小,影响越大。默认值 60 在大多数内容类型上效果良好。
RRF 避免了归一化 ts_rank(对数频率分数)与余弦距离(几何度量)这个更困难的问题。它们不可比。RRF 只问:“这个结果在每个列表中排得多高?“
同时使用三元组进行混合搜索
对于面向用户的混合内容搜索——用户可能在同一会话中搜索人名、概念或精确术语——三路融合可以处理所有情况:
WITH trgm_results AS ( SELECT id, ROW_NUMBER() OVER (ORDER BY similarity(title, $1) DESC) AS rank FROM documents WHERE title % $1 LIMIT 50),fts_results AS ( SELECT id, ROW_NUMBER() OVER (ORDER BY ts_rank(search_vector, to_tsquery('english', $1)) DESC) AS rank FROM documents WHERE search_vector @@ to_tsquery('english', $1) LIMIT 50),vector_results AS ( SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $2::vector) AS rank FROM documents ORDER BY embedding <=> $2::vector LIMIT 50),rrf AS ( SELECT COALESCE(t.id, f.id, v.id) AS id, COALESCE(1.0 / (60 + t.rank), 0) + COALESCE(1.0 / (60 + f.rank), 0) + COALESCE(1.0 / (60 + v.rank), 0) AS rrf_score FROM trgm_results t FULL OUTER JOIN fts_results f ON t.id = f.id FULL OUTER JOIN vector_results v ON COALESCE(t.id, f.id) = v.id)SELECT d.id, d.title, rrf.rrf_scoreFROM rrfJOIN documents d ON d.id = rrf.idORDER BY rrf_score DESCLIMIT 10;这处理了:模糊名称匹配(三元组)、精确关键词匹配(FTS)和概念查询(向量)。一个搜索框即可服务三种用户意图。
多层混合架构
实际应用很少只有一个搜索面。它们有多个,每个都有不同的需求:
| 搜索面 | 用户查询内容 | 推荐层 |
|---|---|---|
| 博客/文档搜索 | 关键词 + 概念 | FTS + pgvector(RRF) |
| 用户/客户名称查找 | 带拼写错误的名字 | pg_trgm |
| 产品搜索 | 名称、描述、“类似” | pg_trgm + FTS + pgvector |
| 支持工单去重 | ”与此类似的问题” | 仅 pgvector |
| 内部 SKU/订单搜索 | 精确标识符 | B-tree 索引 |
| 基于大型知识库的 RAG | 自然语言问题 | pgvector(分块文档) |
| 电商”你可能还喜欢” | 行为 + 语义相似性 | pgvector |
| 自动补全 | 前缀、容忍拼写错误 | pg_trgm |
这些并非假设。大多数内容密集型应用至少需要两个不同的搜索面,且查询形态各异。诱惑在于选择一种方法并在所有地方使用——现在通常是向量搜索,因为它很时髦。这会导致为那些本可以用三元组索引更快、更便宜、更正确的问题,却花费昂贵的嵌入成本。
经验法则
当出现当前层无法修复的故障模式时,就添加一层:
- 用户抱怨拼写错误不匹配 → 添加
pg_trgm - 用户按概念搜索却漏掉相关结果 → 添加 pgvector
- 用户搜索精确符号或代码却得到概念结果 → 添加 FTS 或检查是否过度依赖向量搜索
- 延迟成为问题 → 评估预过滤、近似索引或专用存储
如果你确实需要专用向量存储
pgvector 能处理大量应用搜索,远在需要另一个数据库之前。粗略的界限取决于向量数量、索引设置、写入速率、过滤器、硬件和并发量,因此任何”低于 1000 万向量”的规则都应视为基准测试的起点假设,而非产品限制。当你真正超出其能力时——极高并发、极低 p99 延迟要求、数十亿向量或严重的多租户隔离需求——专用向量数据库的版图广阔且值得理解。
矩阵列的实际含义
混合搜索 意味着 BM25 关键词搜索和向量相似性在同一个查询中运行,通过 RRF 合并。没有它,你要么选择一种搜索模式,要么自己融合两个查询。
稀疏向量 比 BM25 更进一步。SPLADE 稀疏向量约有 30,000 维(每个词汇项一维),约 98% 为零。非零位置告诉你哪些项重要以及重要程度。对”狗”的查询也会加权”犬科”和”宠物”——BM25 级别的精度加上向量索引内的词项扩展。如果此列为 false,你需要一个单独的 FTS 层来处理精确词项查询。
# SPLADE: ~30,000 维,~60 非零——只有相关的词汇位置被激活def encode_splade(text: str) -> dict: tokens = tokenizer(text, return_tensors="pt", truncation=True, max_length=512) with torch.no_grad(): output = model(**tokens) vec = torch.log1p(torch.relu(output.logits)).max(dim=1).values.squeeze() return {"indices": vec.nonzero().squeeze().tolist(), "values": vec[vec != 0].tolist()}SQL / 类 SQL 实际上关乎过滤。没有过滤的向量搜索只是演示。你仍然需要租户范围、日期范围、权限和类别过滤器。完整的 SQL(pgvector、LanceDB)可以在现有连接旁表达这些。专用数据库使用 JSON 过滤器对象(Qdrant、Pinecone)、查询 DSL(Elasticsearch、Milvus)或 GraphQL(Weaviate)。它们都能工作;当过滤器逻辑变得复杂时,SQL 更具吸引力。
-- pgvector:向量相似性只是另一个表达式SELECT id, title, 1 - (embedding <=> $1) AS scoreFROM documentsWHERE tenant_id = $2 AND category = ANY($3::text[]) AND created_at > NOW() - INTERVAL '90 days'ORDER BY embedding <=> $1LIMIT 10;# Qdrant:等效过滤器作为 Python 对象——相同结果,更多仪式results = client.query_points( collection_name="documents", query=query_embedding, query_filter=models.Filter(must=[ models.FieldCondition(key="tenant_id", match=models.MatchValue(value=tenant_id)), models.FieldCondition(key="category", match=models.MatchAny(any=categories)), models.FieldCondition(key="created_at", range=models.DatetimeRange(gte=cutoff)), ]), limit=10,)多模态原生 意味着数据库自带非文本内容的嵌入模型。你给它一个原始图像 URL;它处理向量化。大多数数据库与嵌入无关——你拥有嵌入管道。Marqo 和 Weaviate(通过 CLIP/ImageBind 模块)闭环了这一点。
# Marqo:POST 原始图像,用文本查询——无需外部嵌入步骤mq.index("products").add_documents( [{"id": "shoe-001", "image": "https://cdn.example.com/shoes/001.jpg"}], tensor_fields=["image"])results = mq.index("products").search(q="lightweight shoes for summer")# 返回 shoe-001,尽管关键词零重叠——CLIP 处理跨模态匹配基于磁盘的索引 是一个成本杠杆。内存驻留的 HNSW 索引每百万个 1536 维向量可能需要数 GB 内存,包括原始向量、图开销和元数据。磁盘原生替代方案(Milvus DiskANN、Elasticsearch DiskBBQ、LanceDB 的 Lance 格式、Turbopuffer 的对象存储层)通常以部分查询延迟换取更低的基础设施成本。对于模型延迟已经占主导的 RAG 工作负载,这种权衡通常值得基准测试。
最大维度 是隐藏在你架构中的迁移。text-embedding-3-large 使用 3072 维,Jina v3 可以生成更大的嵌入,研究模型还在继续推高。一些托管服务发布硬性维度上限;其他则记录高上限或对典型嵌入模型没有实际限制。在承诺之前检查当前文档。选择有扩展空间的;因为达到维度上限而迁移向量索引是一场痛苦的冲刺。
上次验证于 2026 年 5 月 8 日,对照公开项目文档和产品页面。将下表视为决策辅助工具,而非替代检查当前限制、定价和托管服务功能标志。
版图
| 数据库 | 部署方式 | 许可证 | 混合搜索 | 稀疏向量 | SQL / 类 SQL | 多模态 | 磁盘索引 | 最大维度 | 最佳场景 |
|---|---|---|---|---|---|---|---|---|---|
| pgvector | 自托管 / 托管(Supabase、Neon、RDS) | OSS(PostgreSQL) | 手动(通过 SQL 的 RRF) | ❌ | ✅ 完整 SQL | ❌ | ✅ HNSW 磁盘 | 16,000 存储;2,000 索引 vector | 已在 Postgres 上;中等向量数量 |
| Qdrant | 自托管 / 云 | Apache 2.0 | ✅ 原生 BM25 | ✅ 成熟支持 | ❌(REST/gRPC) | ❌ | ✅ | 65,535 | 大规模过滤查询;复杂元数据 |
| Weaviate | 自托管 / 云 | BSD 3 | ✅ 原生 BM25 + RRF | ✅ | ❌(GraphQL / gRPC) | ✅ 通过模块 | ✅ | 65,535 | GraphQL 访问模式;内置向量化 |
| Pinecone | 仅云 | 专有 | ✅(2024 年新增) | ✅ | ❌ | ❌ | ✅(无服务器) | 20,000 | 托管简洁性;无运维团队 |
| Milvus / Zilliz | 自托管 / 云(Zilliz) | Apache 2.0 | ✅ 原生 | ✅ | ✅ 类 SQL(Milvus 查询语言) | ✅ | ✅ DiskANN | 32,768 | 十亿级;企业本地部署 |
| Chroma | 嵌入式 / 自托管 | Apache 2.0 | ❌ | ❌ | ❌ | ❌ | ❌ | 65,535 | 仅限本地开发和原型 |
| LanceDB | 嵌入式 / 云 | Apache 2.0 | ✅ | ❌ | ✅ 通过 DataFusion 的 SQL | ✅ 原生 | ✅(Lance 格式) | 无限制 | 边缘 / 无服务器;多模态湖仓 |
| Orama | 嵌入式 / 云 | Apache 2.0 | ✅ 全文 + 向量 | ❌ | ❌ | ❌ | ❌ | 可变 | JS/边缘应用;轻量级站点/应用搜索 |
| Turbopuffer | 仅云(无服务器) | 专有 | ✅ BM25 + 向量 | ❌ | ❌ | ❌ | ✅(对象存储) | 16,000 | 多租户 SaaS;数百万命名空间 |
| Elasticsearch | 自托管 / Elastic Cloud | SSPL / AGPLv3 | ✅ RRF + ELSER 稀疏 | ✅(ELSER) | ✅ 查询 DSL | ❌ | ✅ DiskBBQ | 4,096 | 已在 Elastic 栈上;混合企业搜索 |
| OpenSearch | 自托管 / AWS 托管 | Apache 2.0 | ✅ RRF + 神经搜索 | ✅ | ✅ 查询 DSL | ❌ | ✅ FAISS + HNSW | 16,000 | AWS 原生;开源 Elastic 替代 |
| Vespa | 自托管 / 云 | Apache 2.0 | ✅ 原生 | ✅ 张量 / 词汇排序 | ✅ YQL | ✅ 张量 | ✅ | 实际上无限制 | 搜索 + 排序 + 推荐系统 |
| ClickHouse | 自托管 / 云 | Apache 2.0 | 手动 | ❌ | ✅ 完整 SQL | ❌ | ✅ 列式 + HNSW | 可变 | 分析 / 日志,向量搜索与 OLAP 并存 |
| MongoDB Atlas | 云 / 自托管 | SSPL | ✅ 内置 | ❌ | ✅ MQL + 聚合 | ❌ | ✅ HNSW | 8,192 | 已在 MongoDB 上;文档 + 向量合一 |
| Redis (VSS) | 自托管 / Redis Cloud | RSALv2 / SSPL | ✅(RediSearch) | ✅ | ❌ | ❌ | ❌ 仅内存 | 32,768 | 超低延迟;缓存层向量搜索 |
| Marqo | 云 / 自托管 | Apache 2.0 | ✅ | ❌ | ❌ | ✅ 原生重点 | ✅ | 可变 | 端到端多模态:图像 + 文本 + 视频 |
表格中放不下的几件事
Turbopuffer 的多租户 围绕极高的命名空间计数构建。其公开定位和客户案例强调像 Notion 这样的大型、命名空间密集的语料库。如果每个用户或组织需要隔离的向量搜索,这种架构可以改变经济性,但仍需针对自己的租户形态进行基准测试。
LanceDB 嵌入式模式 是最接近”向量搜索的 SQLite”的东西。它在进程内运行,无需服务器,适用于 Lambda、Cloudflare Workers 和边缘环境。Lance 列式格式使嵌入式操作在真实规模下变得实用。
Chroma 在开发/测试和小型应用部署中最强。 如果你的目标是极大语料库、高可用、磁盘密集型操作或一流的混合搜索,在将原型推广到基础设施之前,请评估面向生产的存储。
Vespa 是当检索只占产品一半时你会选用的工具。 它融合了词法检索、近邻搜索、张量、排序表达式、分组和在线服务。这种能力是真实的,但操作和建模的复杂性也同样真实。它更适合搜索/推荐团队,而不是“给我的 CRUD 应用加上语义搜索”。
当搜索与分析绑定在一起时,ClickHouse 就值得纳入讨论。 如果你的数据源是事件、日志、追踪或指标,ClickHouse 将向量距离、过滤、聚合和严肃的全文索引整合在同一个 SQL 引擎中。它不是专门构建的向量数据库,但对于分析型检索来说,往往是“无聊但正确”的选择。
稀疏向量让你能在向量索引内部获得 BM25 级别的关键词匹配——无需运行独立的全文检索引擎。Qdrant 和 Elasticsearch 在这方面有特别成熟的实现。如果混合搜索至关重要,而双系统架构又无法接受,那么稀疏向量支持就是你要关注的重点。
当你已经超出 pgvector 能力范围时的选择
- 需要租户隔离的 SaaS 产品 → Turbopuffer
- 大规模复杂元数据过滤 → Qdrant
- 已经在使用 Elastic/ELK 栈 → Elasticsearch + DiskBBQ
- 想要开源方案的 AWS 用户 → OpenSearch
- 有严肃排序需求的搜索/推荐平台 → Vespa
- 分析、可观测性、日志/事件搜索 → ClickHouse
- 十亿级本地/自托管 → Milvus
- 边缘 / 无服务器 / 多模态 → LanceDB
- 小型 JS 应用、文档站点或边缘原生搜索体验 → Orama
- 零运维,成本次要 → Pinecone
- 多模态优先(图像、视频、音频) → Marqo
- 已经在用 MongoDB → Atlas Vector Search
- 已经在用 Postgres,需要更多扩展空间 → Supabase Vector 或 Neon(两者都是托管 pgvector,工具链更完善)
唯一不该做的事
不要对有正确答案的事情使用向量搜索作为模糊文本搜索。
“找到邮箱为 dan@example.com 的用户”不是向量搜索问题。“找到订单 ID 为 ORD-12345 的订单”也不是。对 ORD-12345 进行嵌入并通过余弦相似度搜索会返回某个结果——但它可能是错的。标识符有正确答案。对标识符进行近似匹配是一个 bug。
向量搜索返回数据集中最相似的结果,即使没有任何东西是真正相关的。它不知道什么时候没有好的答案。这对相关文档来说没问题。但对于精确记录查找来说,这是一个严重问题,因为一个自信的错误答案比空结果更糟糕。
反过来也一样:不要对用户描述概念的查询使用全文搜索。“关于在不确定性下做出艰难决定的文章”不包含可靠的关键词。全文搜索要么返回噪声,要么返回空。根据查询形态使用正确的工具。
全景图
大多数生产级搜索系统需要不止一层:
pg_trgm用于名称、拼写错误、自动补全- FTS /
pg_search用于基于关键词的文本搜索 - pgvector 用于语义和概念查询
- RRF 融合 用于用户混合查询类型的场景
- 常规索引 用于精确标识符、过滤和排序列表
这些不是相互竞争的工具。它们是互补的。一个构建良好的搜索系统会为每种查询形态选择正确的层——当查询形态重叠时,它会运行多个层并融合结果。
那些能交付优秀搜索功能的团队理解整个技术栈。而那些做不到的团队,只会拿起一个向量数据库,把所有东西都嵌入进去,然后奇怪为什么精确查找有时会返回错误的记录。