DanLevy.net

语义向量搜索及其他赢得朋友与爱人的话题

完整搜索全景:精确、模糊、语义、混合——以及何时将它们全部叠加。

搜索并非单一概念,语义搜索也不是其他搜索方式的替代品。

“查找邮箱为 dan@example.com 的用户”和“查找关于新手工程师调试的文章”都被称为搜索,但作为工程问题,它们几乎毫无共同点。前者有正确答案,且可通过 O(log n) 索引查找完成。后者没有正确答案——只有相关性——并且需要理解语言、意图和含义。

在搜索决策中最有说服力的工程师——那些能赢得争论并交付正确系统的人——理解整个技术版图。他们知道该用哪种工具以及为什么,并且能清晰解释。

本文涵盖语义层:向量搜索实际做什么、何时适用、以及哪里应该让路。有用的版本不是“嵌入一切”,而是知道在混合架构中,向量何时应与词法搜索、模糊搜索和精确匹配搜索并存。

词法和模糊搜索的部分——tsvectorpg_trgmpg_search——详见 Postgres 文本搜索指南 2026


术语速览

嵌入(Embedding) — 由模型生成的一组密集浮点数,将一段文本(或图像、音频等)表示为高维空间中的一个点。语义相关的内容彼此靠近;无关的内容相距甚远。

词法搜索(Lexical search) — 基于精确词和令牌匹配的搜索。快速、确定、对已知术语正确。不理解同义词、释义或跨语言等价物。

语义搜索(Semantic search) — 基于含义而非令牌的搜索。查询“如何处理超时”可以匹配标题为“配置重试策略”的文档,即使没有共享词汇,因为它们的嵌入在几何上接近。

向量(Vector) — 一组数字。在搜索上下文中,指嵌入模型的输出。“向量搜索”通过几何距离找到最接近查询向量的向量。

全文搜索(FTS) — Postgres 内置的词法搜索,由 tsvector / tsquery 驱动。对文本进行分词、词干提取和索引,用于关键词查询。适用于散文和精确术语查找;对含义无感知。

BM25 — 词法搜索的排序算法(Elasticsearch、Qdrant 等使用)。根据词频与词在整个语料库中的稀有程度加权评分。优于原始关键词匹配,但仍属词法搜索。

HNSW(分层可导航小世界) — 向量搜索的标准近似最近邻索引。构建分层邻近图,实现快速、高召回率的相似性查询。pgvector、Qdrant、Weaviate 等大多使用它。

RRF(倒数排名融合) — 一种合并多个检索系统排序结果列表的算法。仅使用排名位置——无需分数归一化。在全文搜索和向量列表中均排名靠前的结果,其综合得分高于仅在一个系统中占优的结果。


语义搜索的实际作用

向量嵌入将文本(或图像、音频等)转换为一组数字——高维空间中的一个点。嵌入模型经过训练,使语义相关的文本在该空间中彼此靠近。“狗”和“犬科”最终接近。“跑马拉松”和“运行 Python 脚本”尽管共享一个词,却相距甚远。

在该空间中进行相似性搜索,可以找到与查询含义最接近的文档,而不考虑精确的词重叠。

这意味着:

词法搜索(tsvectorpg_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 datasets
CREATE INDEX documents_embedding_idx
ON documents USING hnsw (embedding vector_cosine_ops);
-- Semantic search query
SELECT id, title, 1 - (embedding <=> $1::vector) AS similarity
FROM documents
ORDER BY embedding <=> $1::vector
LIMIT 10;

<=> 是余弦距离。1 - cosine_distance 给出余弦相似度(1.0 = 完全相同,0.0 = 正交)。对于 ivfflat(更旧、构建更快的替代方案),使用 lists = sqrt(row_count) 作为起点。

pgvector 不擅长的场景


混合搜索:两者皆需的场景

技术文档是最清晰的例子,说明单独使用任何一种工具都不够。

搜索”如何配置超时”的用户需要概念匹配:一篇题为”设置重试策略和连接限制”的文章虽然没有重叠的关键词,但正是他们需要的。

同一批用户也会搜索 withRetry()ECONNRESETERR_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_score
FROM rrf
JOIN documents d ON d.id = rrf.id
ORDER BY rrf_score DESC
LIMIT 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_score
FROM rrf
JOIN documents d ON d.id = rrf.id
ORDER BY rrf_score DESC
LIMIT 10;

这处理了:模糊名称匹配(三元组)、精确关键词匹配(FTS)和概念查询(向量)。一个搜索框即可服务三种用户意图。


多层混合架构

实际应用很少只有一个搜索面。它们有多个,每个都有不同的需求:

搜索面用户查询内容推荐层
博客/文档搜索关键词 + 概念FTS + pgvector(RRF)
用户/客户名称查找带拼写错误的名字pg_trgm
产品搜索名称、描述、“类似”pg_trgm + FTS + pgvector
支持工单去重”与此类似的问题”仅 pgvector
内部 SKU/订单搜索精确标识符B-tree 索引
基于大型知识库的 RAG自然语言问题pgvector(分块文档)
电商”你可能还喜欢”行为 + 语义相似性pgvector
自动补全前缀、容忍拼写错误pg_trgm

这些并非假设。大多数内容密集型应用至少需要两个不同的搜索面,且查询形态各异。诱惑在于选择一种方法并在所有地方使用——现在通常是向量搜索,因为它很时髦。这会导致为那些本可以用三元组索引更快、更便宜、更正确的问题,却花费昂贵的嵌入成本。

经验法则

当出现当前层无法修复的故障模式时,就添加一层:


如果你确实需要专用向量存储

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 score
FROM documents
WHERE tenant_id = $2
AND category = ANY($3::text[])
AND created_at > NOW() - INTERVAL '90 days'
ORDER BY embedding <=> $1
LIMIT 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,535GraphQL 访问模式;内置向量化
Pinecone仅云专有✅(2024 年新增)✅(无服务器)20,000托管简洁性;无运维团队
Milvus / Zilliz自托管 / 云(Zilliz)Apache 2.0✅ 原生✅ 类 SQL(Milvus 查询语言)✅ DiskANN32,768十亿级;企业本地部署
Chroma嵌入式 / 自托管Apache 2.065,535仅限本地开发和原型
LanceDB嵌入式 / 云Apache 2.0✅ 通过 DataFusion 的 SQL✅ 原生✅(Lance 格式)无限制边缘 / 无服务器;多模态湖仓
Orama嵌入式 / 云Apache 2.0✅ 全文 + 向量可变JS/边缘应用;轻量级站点/应用搜索
Turbopuffer仅云(无服务器)专有✅ BM25 + 向量✅(对象存储)16,000多租户 SaaS;数百万命名空间
Elasticsearch自托管 / Elastic CloudSSPL / AGPLv3✅ RRF + ELSER 稀疏✅(ELSER)✅ 查询 DSL✅ DiskBBQ4,096已在 Elastic 栈上;混合企业搜索
OpenSearch自托管 / AWS 托管Apache 2.0✅ RRF + 神经搜索✅ 查询 DSL✅ FAISS + HNSW16,000AWS 原生;开源 Elastic 替代
Vespa自托管 / 云Apache 2.0✅ 原生✅ 张量 / 词汇排序✅ YQL✅ 张量实际上无限制搜索 + 排序 + 推荐系统
ClickHouse自托管 / 云Apache 2.0手动✅ 完整 SQL✅ 列式 + HNSW可变分析 / 日志,向量搜索与 OLAP 并存
MongoDB Atlas云 / 自托管SSPL✅ 内置✅ MQL + 聚合✅ HNSW8,192已在 MongoDB 上;文档 + 向量合一
Redis (VSS)自托管 / Redis CloudRSALv2 / 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 能力范围时的选择


唯一不该做的事

不要对有正确答案的事情使用向量搜索作为模糊文本搜索。

“找到邮箱为 dan@example.com 的用户”不是向量搜索问题。“找到订单 ID 为 ORD-12345 的订单”也不是。对 ORD-12345 进行嵌入并通过余弦相似度搜索会返回某个结果——但它可能是错的。标识符有正确答案。对标识符进行近似匹配是一个 bug。

向量搜索返回数据集中最相似的结果,即使没有任何东西是真正相关的。它不知道什么时候没有好的答案。这对相关文档来说没问题。但对于精确记录查找来说,这是一个严重问题,因为一个自信的错误答案比空结果更糟糕。

反过来也一样:不要对用户描述概念的查询使用全文搜索。“关于在不确定性下做出艰难决定的文章”不包含可靠的关键词。全文搜索要么返回噪声,要么返回空。根据查询形态使用正确的工具。


全景图

大多数生产级搜索系统需要不止一层:

这些不是相互竞争的工具。它们是互补的。一个构建良好的搜索系统会为每种查询形态选择正确的层——当查询形态重叠时,它会运行多个层并融合结果。

那些能交付优秀搜索功能的团队理解整个技术栈。而那些做不到的团队,只会拿起一个向量数据库,把所有东西都嵌入进去,然后奇怪为什么精确查找有时会返回错误的记录。