各位同仁,各位对人工智能与搜索技术充满热情的专家学者们,大家好!
今天,我将带领大家深入探讨一个在AI搜索领域既核心又常常被误解的主题:语义向量匹配的原理及其与长尾关键词在AI搜索中的持续重要性。 随着我们从传统的基于关键词的搜索迈向更智能、更理解人类意图的语义搜索时代,很多人可能会产生一个疑问:如果AI已经能“理解”意思了,那么那些细枝末节、搜索量稀少但高度具体的长尾关键词,还有它们存在的价值吗?我的答案是:不仅有,而且它们的重要性在某种程度上反而被语义搜索的进步所强化。
本次讲座,我们将从语义搜索的基石——向量表示开始,逐步揭示语义向量匹配的工作原理,然后深入剖析长尾关键词在当前及未来AI搜索生态中不可或缺的地位。我将结合代码示例,力求将抽象的理论具象化,让大家不仅知其然,更知其所以然。
1. 搜索的进化:从字面匹配到意义理解
在深入语义向量匹配之前,我们首先回顾一下搜索技术的发展历程。早期的搜索引擎,其核心是基于字面匹配(Lexical Matching)。用户输入的查询词与文档中的词汇进行精确或近似的匹配。
1.1 词法匹配的局限性
最经典的词法匹配算法之一是 TF-IDF (Term Frequency-Inverse Document Frequency),它通过计算一个词在文档中出现的频率以及在整个语料库中出现的稀有程度来评估其重要性。
TF-IDF 核心思想:
- 词频 (TF – Term Frequency): 一个词在文档中出现的次数越多,它对该文档的重要性越大。
- 逆文档频率 (IDF – Inverse Document Frequency): 一个词在越少的文档中出现,它就越能区分该文档,因此其重要性越大。
公式表示:
$$ text{TF-IDF}(t, d, D) = text{TF}(t, d) times text{IDF}(t, D) $$
其中,
$$ text{TF}(t, d) = frac{text{count}(t text{ in } d)}{sum_{t’ in d} text{count}(t’ text{ in } d)} $$
$$ text{IDF}(t, D) = log left( frac{|D|}{|{d in D : t in d}|} right) $$
- $t$: 词项 (term)
- $d$: 文档 (document)
- $D$: 文档集合 (corpus)
TF-IDF 的优点: 简单高效,在处理大量文本时表现良好。
TF-IDF 的局限性:
- 同义词问题 (Synonymy): 用户搜索“汽车”,但文档中可能只提到了“轿车”或“车辆”。TF-IDF 无法识别这些词汇之间的语义关联。
- 多义词问题 (Polysemy): 词语有多个含义,例如“苹果”可以是水果,也可以是公司。TF-IDF 无法区分上下文。
- 缺乏上下文理解: 仅仅匹配关键词,无法理解查询的真实意图。例如,“如何冲泡咖啡”和“咖啡冲泡方法”在语义上是等价的,但字面匹配可能给予不同的权重。
- 召回率与准确率的平衡挑战: 过于精确的匹配可能导致召回率低(漏掉相关结果),过于宽松则可能导致准确率低(返回不相关结果)。
这些局限性促使我们思考,如何让搜索引擎超越字面,真正理解语言的“意义”。这正是语义搜索的核心驱动力。
2. 语义向量匹配:将意义编码为数学表达
语义搜索的核心在于将文本(词语、短语、句子乃至整个文档)转化为机器可以理解和处理的数学形式,即“向量”。这些向量捕获了文本的语义信息,使得我们能够在高维空间中通过计算向量之间的距离或相似度来衡量文本的语义相关性。
2.1 词嵌入 (Word Embeddings):意义的基石
词嵌入是语义向量匹配的起点。它的基本思想是:“一个词的意义可以从它通常出现的语境中推断出来。” (Distributional Hypothesis)。如果两个词经常出现在相似的上下文中,那么它们很可能具有相似的意义。
词嵌入将每个词映射到一个低维、稠密的实数向量空间中。在这个向量空间里,语义相似的词,它们的向量在空间中的距离会比较近。
早期代表模型:
-
Word2Vec (Mikolov et al., 2013): Google 团队提出,包含两种架构:
- CBOW (Continuous Bag-of-Words): 根据上下文词预测目标词。
- Skip-gram: 根据目标词预测上下文词(在大型语料库和稀有词上表现更好)。
- 核心思想: 通过训练神经网络来学习词的分布式表示。
-
GloVe (Global Vectors for Word Representation) (Pennington et al., 2014): 斯坦福大学提出,结合了全局矩阵分解(如LSA)和局部上下文窗口方法。它通过最小化词共现矩阵中的对数共现概率与词向量点积之间的差异来学习词向量。
词嵌入的特性:
- 语义相似性: “国王”和“女王”的向量距离会比“国王”和“香蕉”的向量距离更近。
- 语义关系: 著名的“国王 – 男人 + 女人 = 女王”的向量加减运算,展示了词向量能够捕获词之间的类比关系。
代码示例 1:使用 Gensim 库训练 Word2Vec 模型
我们将使用一个小型虚拟语料库来演示 Word2Vec 的基本用法。
import gensim
from gensim.models import Word2Vec
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
# 1. 准备语料库
# 模拟一些句子,每个句子是一个词语列表
corpus = [
["natural", "language", "processing", "is", "a", "field", "of", "artificial", "intelligence"],
["machine", "learning", "is", "a", "subset", "of", "artificial", "intelligence"],
["deep", "learning", "is", "a", "type", "of", "machine", "learning"],
["semantic", "search", "uses", "natural", "language", "understanding"],
["vector", "embeddings", "are", "used", "in", "semantic", "search"],
["king", "is", "a", "man"],
["queen", "is", "a", "woman"],
["apple", "is", "a", "fruit"],
["microsoft", "is", "a", "company"],
["banana", "is", "also", "a", "fruit"]
]
print("--- 语料库示例 ---")
for i, sentence in enumerate(corpus[:3]):
print(f"Sentence {i+1}: {' '.join(sentence)}")
print("-" * 20)
# 2. 训练 Word2Vec 模型
# vector_size: 词向量的维度
# window: 上下文窗口大小
# min_count: 忽略出现次数少于此值的词
# workers: 并行训练的线程数
# sg=0: CBOW (sg=1 for Skip-gram)
model = Word2Vec(sentences=corpus, vector_size=100, window=5, min_count=1, workers=4, sg=0)
print("--- Word2Vec 模型训练完成 ---")
# 3. 查看词向量
word_vector_nlp = model.wv['nlp']
word_vector_ai = model.wv['artificial']
print(f"nVector for 'nlp' (first 5 dimensions): {word_vector_nlp[:5]}")
print(f"Vector for 'artificial' (first 5 dimensions): {word_vector_ai[:5]}")
# 4. 计算词语相似度
similarity_nlp_ai = model.wv.similarity('nlp', 'artificial')
similarity_king_queen = model.wv.similarity('king', 'queen')
similarity_king_banana = model.wv.similarity('king', 'banana')
print(f"nSimilarity between 'nlp' and 'artificial': {similarity_nlp_ai:.4f}")
print(f"Similarity between 'king' and 'queen': {similarity_king_queen:.4f}")
print(f"Similarity between 'king' and 'banana': {similarity_king_banana:.4f}")
# 5. 词语类比 (如果语料库足够大,效果会更明显)
# 尝试 'king' - 'man' + 'woman' 应该接近 'queen'
try:
result = model.wv.most_similar(positive=['king', 'woman'], negative=['man'], topn=1)
print(f"n'king' - 'man' + 'woman' is similar to: {result[0][0]} (similarity: {result[0][1]:.4f})")
except KeyError as e:
print(f"nCould not perform analogy due to missing word in vocabulary: {e}. (Small corpus limitation)")
except Exception as e:
print(f"nAn error occurred during analogy calculation: {e}")
# 6. 找出最相似的词
print("nWords most similar to 'search':")
for word, score in model.wv.most_similar('search', topn=3):
print(f" {word}: {score:.4f}")
print("nWords most similar to 'fruit':")
for word, score in model.wv.most_similar('fruit', topn=3):
print(f" {word}: {score:.4f}")
# 7. 词向量的几何表示(概念性说明)
# 我们可以将词向量视为N维空间中的一个点。
# 相似的词在空间中距离较近。
print("n--- 词向量的几何概念 ---")
print("在100维空间中,每个词都是一个点。")
print("语义相似的词,其向量在空间中的方向和位置会比较接近。")
print("例如,'king'和'queen'的向量方向会比'king'和'banana'更接近。")
代码解释:
这个示例展示了 Word2Vec 如何将词语映射到向量空间。通过计算这些向量之间的余弦相似度,我们可以量化词语之间的语义关联。most_similar 方法进一步展示了模型捕获语义关系的能力。然而,对于如此小的语料库,类比推理的效果可能不尽如人意,因为模型没有足够的数据来学习复杂的语义模式。
2.2 从词到句、段落和文档的向量表示
仅仅有词向量是不够的。在搜索中,我们通常需要比较的是整个查询(短语或句子)与文档(段落或整个文章)的语义。如何将多个词的向量组合成一个代表更长文本的向量,是一个关键挑战。
早期简单方法:
-
平均词向量 (Averaging Word Vectors): 最简单的方法是将文本中所有词的向量平均起来,得到一个代表该文本的向量。
- 优点: 简单高效。
- 缺点: 丢失词序信息,对重要词和不重要词一视同仁,无法很好地处理否定、修饰等复杂语义。
-
加权平均词向量: 改进版,可以根据TF-IDF等权重对词向量进行加权平均。
-
Doc2Vec (Paragraph Vectors) (Le & Mikolov, 2014): Word2Vec 的扩展,旨在学习固定长度的文档表示。它引入了一个“段落ID”作为训练的一部分,可以学习每个文档的唯一向量。
革命性的突破:上下文相关的词嵌入与 Transformer 模型
上述方法都存在一个根本性问题:词向量是静态的,一个词无论在何种语境下,其向量都是相同的。这无法解决多义词问题,也无法捕获词语在不同上下文中的细微语义变化。
Transformer 架构的出现(Vaswani et al., 2017)彻底改变了这一局面。它引入了 自注意力机制 (Self-Attention),使得模型在处理序列时能够根据当前词与其他词的关系来动态调整其表示。
-
BERT (Bidirectional Encoder Representations from Transformers) (Devlin et al., 2018): Google 团队发布,是 Transformer 编码器的一个预训练模型。BERT 能够生成上下文相关的词嵌入。这意味着同一个词,在不同的句子中,会生成不同的向量,因为它考虑了整个句子的上下文。
- 训练目标: 掩码语言模型 (Masked Language Model, MLM) 和下一句预测 (Next Sentence Prediction, NSP)。
- 能力: 捕捉词语的深层语义,处理多义词,理解句子结构。
-
Sentence-BERT (SBERT) (Reimers & Gurevych, 2019): 虽然 BERT 能够生成高质量的上下文词嵌入,但直接用 BERT 计算句子相似度效率很低(需要对每对句子进行全连接)。SBERT 通过在 BERT 或 RoBERTa 等预训练模型之上添加 Siamese 和 Triplet 网络结构,使其能够高效地生成高质量的句子嵌入。
- 核心优势: 生成的句子嵌入可以直接用于余弦相似度计算,大大加速了语义相似度搜索。
- 应用: 语义搜索、聚类、文本分类等。
现在,我们可以利用 SBERT 这样的模型,将查询和文档都转化为稠密的向量,从而进行高效的语义匹配。
2.3 向量空间与相似度度量
一旦我们将文本转化为高维向量,就可以在向量空间中度量它们的相似性。
向量空间 (Vector Space): 一个数学空间,其中每个维度代表一个特征(在词嵌入中,这些维度没有直接的人类可解释含义,但它们共同编码了语义信息)。每个文本(词、句、文档)都被表示为这个空间中的一个点。
相似度度量 (Similarity Metrics): 用于量化两个向量之间相似程度的函数。
-
余弦相似度 (Cosine Similarity):
- 原理: 衡量两个向量在 N 维空间中的夹角余弦值。夹角越小,余弦值越大,表示方向越一致,语义越相似。
- 范围: [-1, 1]。1 表示完全相同,0 表示正交(无相关性),-1 表示完全相反。
- 公式:
$$ text{cosinesimilarity}(mathbf{A}, mathbf{B}) = frac{mathbf{A} cdot mathbf{B}}{||mathbf{A}|| cdot ||mathbf{B}||} = frac{sum{i=1}^{n} A_i Bi}{sqrt{sum{i=1}^{n} Ai^2} sqrt{sum{i=1}^{n} B_i^2}} $$ - 优点: 对向量的长度不敏感,只关注方向。这对于文本表示很重要,因为文档长度的变化不应直接影响其语义相似性。
- 应用: 广泛用于文本相似度、推荐系统等。
-
欧几里得距离 (Euclidean Distance):
- 原理: 衡量两个向量在 N 维空间中的直线距离。距离越近,向量越相似。
- 公式:
$$ text{euclideandistance}(mathbf{A}, mathbf{B}) = sqrt{sum{i=1}^{n} (A_i – B_i)^2} $$ - 优点: 直观。
- 缺点: 对向量的长度敏感。如果两个向量方向一致但一个比另一个长很多,欧几里得距离会很大,但它们在语义上可能非常相似。因此,通常在计算欧几里得距离之前会对向量进行归一化。
在语义搜索中,余弦相似度是更常用的选择,因为它更关注语义方向而非向量长度。
代码示例 2:使用 Sentence-BERT 进行句子嵌入和余弦相似度计算
我们将使用 sentence-transformers 库,它封装了 SBERT 模型,可以方便地生成句子嵌入。
from sentence_transformers import SentenceTransformer
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
# 1. 加载预训练的 Sentence-BERT 模型
# 'all-MiniLM-L6-v2' 是一个轻量级但效果不错的模型
model_sbert = SentenceTransformer('all-MiniLM-L6-v2')
print("--- Sentence-BERT 模型加载完成 ---")
# 2. 定义查询和一些文档
query = "如何冲泡一杯完美的咖啡?"
documents = [
"冲泡咖啡的步骤和技巧。",
"咖啡豆的种类与烘焙方法。",
"制作美味意式浓缩咖啡的指南。",
"学习如何制作冷萃咖啡。",
"汽车保养的日常小贴士。", # 不相关文档
"关于茶的冲泡艺术。" # 相关度低但有重叠概念
]
print(f"n查询: '{query}'")
print("n--- 待匹配文档 ---")
for i, doc in enumerate(documents):
print(f"文档 {i+1}: '{doc}'")
print("-" * 20)
# 3. 生成查询和文档的嵌入向量
query_embedding = model_sbert.encode(query, convert_to_tensor=True)
document_embeddings = model_sbert.encode(documents, convert_to_tensor=True)
print(f"n查询嵌入向量维度: {query_embedding.shape}")
print(f"文档嵌入向量维度: {document_embeddings.shape}")
print("-" * 20)
# 4. 计算查询与所有文档之间的余弦相似度
# sklearn.metrics.pairwise.cosine_similarity 接受二维数组
# 因此我们需要将 query_embedding 转换为 [1, D] 的形状
similarities = cosine_similarity(query_embedding.cpu().numpy().reshape(1, -1), document_embeddings.cpu().numpy())[0]
# 5. 打印结果并排序
results = []
for i, (doc, sim) in enumerate(zip(documents, similarities)):
results.append({"document": doc, "similarity": sim, "index": i + 1})
# 按相似度降序排序
results = sorted(results, key=lambda x: x['similarity'], reverse=True)
print("n--- 语义相似度匹配结果 (按相似度降序) ---")
for res in results:
print(f"文档 {res['index']} (相似度: {res['similarity']:.4f}): '{res['document']}'")
# 6. 语义匹配的几何解释(概念性说明)
print("n--- 语义匹配的几何概念 ---")
print("在嵌入空间中,查询和文档都被表示为高维向量。")
print("语义相关的文档,其向量方向会更接近查询向量,从而具有更高的余弦相似度。")
print("例如,关于咖啡冲泡的文档向量与查询向量的夹角会很小。")
print("而关于汽车保养的文档向量,其方向会与查询向量大相径庭,相似度极低。")
代码解释:
这个示例清晰地展示了语义向量匹配的核心流程:
- 加载一个预训练的 Sentence-BERT 模型。
- 将查询和所有候选文档分别编码成固定维度的向量。
- 计算查询向量与每个文档向量之间的余弦相似度。
- 根据相似度得分对文档进行排序,得分越高表示语义上越相关。
我们可以看到,“汽车保养”的相似度很低,而“咖啡冲泡”相关的文档相似度很高,即使它们没有完全相同的关键词。这就是语义理解的力量。
2.4 语义搜索的实现管道
一个典型的语义搜索系统通常包含以下步骤:
-
索引阶段 (Indexing Phase):
- 文本预处理: 清理、分词、去除停用词等。
- 嵌入生成: 使用预训练的(或微调的)Transformer 模型(如 SBERT)将所有待搜索的文档转化为稠密的向量表示。
- 向量存储: 将这些文档向量存储在专门的向量数据库(Vector Database,如 Pinecone, Weaviate, Milvus, Faiss 等)中,这些数据库针对高维向量的相似度搜索进行了优化。
-
查询阶段 (Query Phase):
- 查询预处理: 对用户输入的查询进行同样的预处理。
- 查询嵌入生成: 使用与索引阶段相同的模型将查询转化为向量。
- 相似度搜索: 在向量数据库中执行近似最近邻 (Approximate Nearest Neighbor, ANN) 搜索,快速找到与查询向量最相似的 K 个文档向量。
-
结果返回 (Retrieval & Ranking):
- 根据相似度得分对检索到的文档进行排序。
- 可选:结合其他信号(如点击率、新鲜度、权威性、用户个性化偏好)进行重新排名 (Re-ranking),以提供更优的用户体验。
- 返回最终结果。
表格 1:传统关键词搜索与语义向量搜索对比
| 特性 | 传统关键词搜索 (TF-IDF, BM25) | 语义向量搜索 (Embeddings, SBERT) |
|---|---|---|
| 匹配方式 | 字面匹配 (Exact/Partial Match) | 语义匹配 (Meaning Match) |
| 核心技术 | 统计学、倒排索引 | 神经网络、深度学习、向量数据库 |
| 理解能力 | 仅限词汇层面 | 理解词汇、短语、句子、文档的深层含义 |
| 同义词 | 无法处理或需手动同义词表 | 自动处理 (向量空间中距离近) |
| 多义词 | 无法区分上下文 | 可以区分上下文 (上下文相关词嵌入) |
| 新词/罕见词 | 表现差,需人工维护 | 相对较好 (基于词根或子词单元) |
| 查询长度 | 短查询效果相对好,长查询易稀释权重 | 长查询通常提供更多上下文,效果更好 |
| 计算开销 | 索引开销相对低,查询开销低 | 索引开销(嵌入生成)高,查询开销(ANN)高 |
| 存储开销 | 倒排索引 | 高维稠密向量 |
| 可解释性 | 关键词命中清晰可见 | 向量匹配过程抽象,解释性较差 |
3. 长尾关键词的永恒价值:语义搜索的补充与强化
既然语义搜索如此强大,能够理解用户意图,处理同义词和多义词,甚至能跨越语言障碍,那我们为什么还需要关注那些搜索量极低、高度具体、由多个词组成的“长尾关键词”呢?这似乎与语义搜索的理念相悖。然而,事实是,长尾关键词在 AI 搜索时代,其重要性非但没有减弱,反而以一种新的、更深刻的方式被重新定义和强化。
3.1 什么是长尾关键词?
长尾关键词通常指那些:
- 搜索量极低: 每月搜索次数可能只有几十甚至个位数。
- 词组较长: 通常由三个或更多个词组成。
- 高度具体: 描述了非常精确、细致的意图或信息需求。
- 转化率高: 搜索这些词的用户往往意图明确,更接近购买或完成某个特定任务。
例如,相对于“咖啡机”, “2023年家用全自动意式咖啡机推荐 静音 价格2000元以下”就是一个典型的长尾关键词。
3.2 语义搜索的优势与长尾关键词的协同作用
语义搜索通过向量匹配能更好地理解用户意图,但它并非万能。长尾关键词的价值恰恰在于弥补了语义搜索在特定场景下的不足,并与其形成强大的协同效应。
3.2.1 明确的用户意图与精确性
-
语义搜索的挑战:模糊与歧义
即使是最好的语义模型,在处理简短、模糊或多义的查询时,仍然可能面临挑战。例如,用户搜索“苹果”,语义模型可能会在水果公司、水果、地名等多个概念之间摇摆。如果用户只搜索“最好的咖啡”,模型可能需要猜测用户是想买咖啡豆,还是想找咖啡馆,还是想学习冲泡技巧。 -
长尾关键词的优势:意图的明确表达
长尾关键词正是用户在试图消除这种模糊性时的直接体现。当用户输入“如何在家制作手冲咖啡的详细教程”时,其意图是极其明确的。这个长尾查询本身就包含了丰富的语义信息,即使没有语义模型,人类也能立即理解其需求。语义模型处理这样的查询时,其任务不再是“猜测”,而是“精确匹配”到与该明确意图最相关的文档。长尾查询为语义模型提供了更清晰的“靶子”,使其能够发挥其在精确匹配上的优势。
3.2.2 覆盖长尾内容与发现利基信息
-
语义搜索的局限:平均化与主流偏向
语义模型通过学习大量文本来构建词和句子的通用表示。这意味着它们在处理主流、常见概念时表现出色。然而,对于高度专业化、极度细分或非常新颖的概念,如果训练数据中这些概念的出现频率很低,模型的嵌入可能无法足够精细地捕捉到它们的独特语义。语义模型倾向于将不常见或稀有的概念“平均化”到更广阔的语义区域中。 -
长尾关键词的优势:揭示稀有而重要的信息
长尾关键词往往代表了用户对利基(Niche)信息的需求。例如,“罕见的热带植物养护技巧 光照要求高”。这类信息可能存在于少数几个专业论坛或博客中。如果没有长尾关键词,一个泛泛的查询“植物养护”在语义搜索中可能只会返回关于常见植物的大量结果。长尾关键词直接指向了这些特定的、稀有的信息,确保了这些有价值但非主流的内容能够被发现。这对于专业领域、小众爱好或特定问题解决至关重要的场景尤为重要。
3.2.3 提高相关性与转化率
-
语义搜索的挑战:广度与深度
语义搜索旨在提高召回率(找到所有相关内容)和准确率(找到最相关内容)。但对于商业场景,仅有“相关性”是不够的,还需要“转化率”。一个语义上相关的结果,如果不能满足用户的具体需求,也无法带来转化。 -
长尾关键词的优势:高意图与高转化
搜索长尾关键词的用户,通常已经完成了初步的信息收集,进入了决策或行动阶段。他们的搜索意图非常具体,比如“购买便宜且耐用的户外登山鞋 尺码42”。如果搜索引擎能够通过长尾关键词将他们直接引导到满足这些条件的产品页面或评测文章,那么用户的转化率会显著提高。长尾关键词是用户明确表达其“需求”和“意图”的信号,使得语义搜索能够更精准地连接需求与供给。
3.2.4 数据稀疏性与模型鲁棒性
-
语义模型的挑战:对训练数据的依赖
深度学习模型(包括生成嵌入的模型)的效果严重依赖于训练数据的规模和质量。对于那些在训练语料库中出现频率极低的词语组合或概念,模型生成的嵌入可能不够鲁棒或精确。当长尾查询包含这些稀有组合时,纯粹的语义匹配可能无法达到最佳效果。 -
长尾关键词的补充:提供明确的锚点
在这种情况下,长尾关键词中的“字面”信息就成为了重要的补充。即便语义模型对某个高度专业的长尾词组合的嵌入不够完美,但如果这个词组合在文档中以字面形式出现,传统词法匹配的组件仍然可以将其识别出来。这使得即使在语义模型存在盲点或数据稀疏性的情况下,系统仍能提供有用的结果,增强了搜索系统的整体鲁棒性。
3.2.5 竞争优势与成本效益
-
主流关键词的竞争:红海市场
对于高搜索量的短尾关键词,如“智能手机”,竞争异常激烈。即使是语义搜索,也难以在海量相关内容中脱颖而出。 -
长尾关键词的优势:蓝海策略
通过精准定位长尾关键词,内容创作者和企业可以进入竞争较小的“蓝海”市场。虽然单个长尾关键词带来的流量不大,但无数个长尾关键词累积起来,可以带来可观且高质量的流量。语义搜索能够更好地理解这些长尾查询,帮助用户找到这些细分市场中的优质内容,从而为这些内容带来更高的曝光度和转化机会。
3.3 混合搜索的必要性:语义与词法的融合
鉴于上述原因,现代 AI 搜索系统普遍采用混合搜索 (Hybrid Search) 策略,即将语义向量匹配与传统的词法匹配(如 BM25)相结合。
混合搜索的常见模式:
-
分阶段检索 (Multi-stage Retrieval):
- 第一阶段(召回): 使用词法匹配(如 BM25)和/或近似最近邻(ANN)向量搜索来快速召回大量潜在相关的文档。这一阶段的目标是宁可多召回一些,也不要漏掉。
- 第二阶段(重排): 对第一阶段召回的文档,使用更复杂的语义模型(如交叉编码器 BERT-Reranker)进行精细的相似度计算和重排。交叉编码器能够更深入地理解查询和文档之间的交互,提供更精准的排名。
-
融合得分 (Score Fusion):
- 独立计算词法匹配得分和语义匹配得分。
- 通过加权平均或学习排序 (Learning to Rank, LTR) 模型将两种得分融合,得到最终的排序。
表格 2:混合搜索架构的优势
| 特性 | 优势 |
|---|---|
| 召回率 | 词法匹配确保字面命中不遗漏;语义匹配拓宽召回范围,处理同义词和意图 |
| 准确率 | 语义重排提升最终结果的精准度,特别是对于复杂和抽象的查询 |
| 鲁棒性 | 应对数据稀疏性(长尾关键词)和模型盲点,任一机制失效时仍有备用方案 |
| 效率 | 词法/ANN快速过滤,减少后续复杂语义模型处理的数据量 |
| 用户体验 | 兼顾用户直觉的关键词匹配和更深层次的意图理解,提供更全面、更相关的结果 |
| 可解释性 | 词法匹配结果提供一定程度的透明度,而语义匹配则提升了隐式关联的发现能力 |
代码示例 3:概念性混合搜索流程(结合 BM25 和 SBERT)
这个示例将演示如何结合 rank_bm25 库进行词法匹配,并结合 sentence_transformers 进行语义匹配,然后对结果进行融合。
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
import re
# --- 1. 准备数据 ---
corpus_docs = [
"如何冲泡一杯完美的咖啡豆?",
"咖啡豆的种类与烘焙方法。了解不同产地的咖啡。",
"制作美味意式浓缩咖啡的指南。家用咖啡机。",
"学习如何制作冷萃咖啡。夏天清凉饮品。",
"汽车保养的日常小贴士。发动机维护。", # 不相关文档
"关于茶的冲泡艺术。红茶与绿茶。", # 相关度低但有重叠概念
"深度学习在自然语言处理中的应用。词嵌入技术。", # 不相关文档
"购买一台全自动咖啡机,价格在3000元以下。" # 长尾相关文档
]
query_long_tail = "寻找一款3000元以下的全自动家用咖啡机推荐"
query_short_semantic = "最好的咖啡"
query_short_lexical = "咖啡机"
print("--- 原始文档 ---")
for i, doc in enumerate(corpus_docs):
print(f"文档 {i+1}: {doc}")
print("-" * 20)
# --- 2. 词法匹配 (BM25) ---
# 简单分词函数
def tokenize(text):
return re.findall(r'w+', text.lower())
tokenized_corpus = [tokenize(doc) for doc in corpus_docs]
bm25 = BM25Okapi(tokenized_corpus)
def get_bm25_scores(query, bm25_model, docs):
tokenized_query = tokenize(query)
doc_scores = bm25_model.get_scores(tokenized_query)
results = []
for i, score in enumerate(doc_scores):
results.append({"document": docs[i], "score": score, "index": i + 1})
return sorted(results, key=lambda x: x['score'], reverse=True)
print(f"n--- BM25 匹配结果 for '{query_long_tail}' ---")
bm25_results_long_tail = get_bm25_scores(query_long_tail, bm25, corpus_docs)
for res in bm25_results_long_tail:
print(f"文档 {res['index']} (BM25 Score: {res['score']:.4f}): '{res['document']}'")
print("-" * 20)
# --- 3. 语义匹配 (SBERT) ---
model_sbert = SentenceTransformer('all-MiniLM-L6-v2')
def get_sbert_scores(query, sbert_model, docs):
query_embedding = sbert_model.encode(query, convert_to_tensor=True)
document_embeddings = sbert_model.encode(docs, convert_to_tensor=True)
similarities = cosine_similarity(query_embedding.cpu().numpy().reshape(1, -1), document_embeddings.cpu().numpy())[0]
results = []
for i, sim in enumerate(similarities):
results.append({"document": docs[i], "score": sim, "index": i + 1})
return sorted(results, key=lambda x: x['score'], reverse=True)
print(f"n--- SBERT 匹配结果 for '{query_long_tail}' ---")
sbert_results_long_tail = get_sbert_scores(query_long_tail, model_sbert, corpus_docs)
for res in sbert_results_long_tail:
print(f"文档 {res['index']} (SBERT Similarity: {res['score']:.4f}): '{res['document']}'")
print("-" * 20)
# --- 4. 混合排名 (简单的加权融合) ---
# 归一化分数以便融合
def normalize_scores(results):
scores = np.array([res['score'] for res in results])
if len(scores) == 0 or np.max(scores) == np.min(scores):
return [0.0] * len(scores) # 避免除零,或所有分数相同的情况
normalized_scores = (scores - np.min(scores)) / (np.max(scores) - np.min(scores))
for i, res in enumerate(results):
res['normalized_score'] = normalized_scores[i]
return results
# 融合函数
def fuse_results(bm25_res, sbert_res, alpha=0.5):
fused_scores = {}
for res in bm25_res:
fused_scores[res['index']] = {'document': res['document'], 'bm25_norm': res['normalized_score'], 'sbert_norm': 0.0}
for res in sbert_res:
if res['index'] in fused_scores:
fused_scores[res['index']]['sbert_norm'] = res['normalized_score']
else:
fused_scores[res['index']] = {'document': res['document'], 'bm25_norm': 0.0, 'sbert_norm': res['normalized_score']}
final_results = []
for idx, data in fused_scores.items():
fused_score = alpha * data['bm25_norm'] + (1 - alpha) * data['sbert_norm']
final_results.append({
'index': idx,
'document': data['document'],
'bm25_score': data['bm25_norm'],
'sbert_score': data['sbert_norm'],
'fused_score': fused_score
})
return sorted(final_results, key=lambda x: x['fused_score'], reverse=True)
bm25_norm_long_tail = normalize_scores(bm25_results_long_tail)
sbert_norm_long_tail = normalize_scores(sbert_results_long_tail)
fused_results_long_tail = fuse_results(bm25_norm_long_tail, sbert_norm_long_tail, alpha=0.3) # 语义权重更高一些
print(f"n--- 混合排名结果 (alpha=0.3 for BM25) for '{query_long_tail}' ---")
for res in fused_results_long_tail:
print(f"文档 {res['index']} (Fused Score: {res['fused_score']:.4f}, BM25: {res['bm25_score']:.4f}, SBERT: {res['sbert_score']:.4f}): '{res['document']}'")
print("-" * 20)
# 观察不同查询类型的效果
print(f"n--- 针对短查询 '{query_short_semantic}' 的语义匹配效果 ---")
sbert_results_short_semantic = get_sbert_scores(query_short_semantic, model_sbert, corpus_docs)
for res in sbert_results_short_semantic:
print(f"文档 {res['index']} (SBERT Similarity: {res['score']:.4f}): '{res['document']}'")
print("-" * 20)
print(f"n--- 针对短查询 '{query_short_lexical}' 的 BM25 匹配效果 ---")
bm25_results_short_lexical = get_bm25_scores(query_short_lexical, bm25, corpus_docs)
for res in bm25_results_short_lexical:
print(f"文档 {res['index']} (BM25 Score: {res['score']:.4f}): '{res['document']}'")
print("-" * 20)
代码解释:
这个示例模拟了一个简化的混合搜索流程:
- 词法匹配: 使用
BM25Okapi对查询进行词法匹配,计算每个文档的 BM25 分数。 - 语义匹配: 使用
SentenceTransformer对查询和文档生成嵌入,然后计算余弦相似度。 - 分数融合: 为了结合两种分数,我们首先对它们进行归一化(通常缩放到 0 到 1 之间),然后通过加权平均得到一个最终的融合分数。
alpha参数可以调整词法匹配和语义匹配的权重。 - 长尾查询效果: 针对长尾查询
query_long_tail,我们可以观察到,BM25 可能会因为词汇命中率高而给相关文档高分,而 SBERT 也会因为语义理解而给出高分。通过融合,我们可以得到一个更全面的排名。 - 短查询效果: 对于像
最好的咖啡这样的短语义查询,SBERT 会表现出色。而咖啡机这样的短词法查询,BM25 也能很好地捕获。混合策略允许系统根据查询类型,动态地利用这两种方法的优势。
通过这种混合方法,我们既能利用语义模型对复杂意图的理解,也能保留词法匹配在处理特定关键词和罕见组合时的精确性,从而为用户提供更全面、更准确的搜索结果,尤其是在长尾场景下。
4. 总结:未来的搜索——智能、精准与协同
语义向量匹配无疑是AI搜索领域的一项革命性进展,它使得搜索引擎能够从字面匹配跃升至意义理解的层面。通过将文本转化为高维向量,我们得以在数学空间中衡量语义相关性,极大地提升了搜索的智能程度和用户体验。
然而,这并不意味着长尾关键词的终结。相反,长尾关键词以其独特的精确性和对用户明确意图的表达,成为了语义搜索不可或缺的补充。它们帮助语义模型聚焦于特定需求,发现利基内容,并驱动更高的转化率。未来的AI搜索系统将持续演进,但其核心将是语义理解与词法精确性的智能协同,确保每一次搜索都能既广博又精准,满足用户从泛泛探索到特定需求的各种信息渴望。