关键词不足:为什么你的 Next.js 应用需要向量搜索
Source: Dev.to
语义搜索与 ELSER
关键词和语义搜索的代码示例
混合搜索实现
性能考虑
实际案例比较
何时使用每种方法
Introduction
当我构建我的 YouTube Search Library 时,我首先使用 Elasticsearch 的 BM25 算法进行传统的关键词搜索。它运行良好——能够处理拼写错误、高亮匹配并提供快速结果。
但是在使用真实查询进行测试时,我注意到一个根本性的限制:关键词搜索并不理解意义。
示例场景
| 查询 | BM25 结果 | 漏掉的(语义相关) |
|---|---|---|
| “How do I build a blog?” | 包含 “build” 和 “blog” 的视频。 | “Creating a Content Management System with Next.js”(没有关键词重叠,但语义相关)。 |
| “React Native mobile development” | 包含这些确切词汇的视频。 | “Building iOS and Android apps with React Native”(不同表述,但意图相同)。 |
传统搜索是 词汇——它匹配单词,而不是概念。这正是 语义搜索 改变一切的地方。
BM25(最佳匹配 25)
BM25 是 Elasticsearch 的默认排序算法。它为你的 multi_match 查询提供动力。
// Your current implementation (BM25‑based)
const response = await client.search({
index: 'youtube-videos',
body: {
query: {
multi_match: {
query: "React Native tutorial",
fields: ['title^3', 'description^2', 'tags^2'],
fuzziness: 'AUTO',
type: 'best_fields'
}
}
}
});
BM25 工作原理
| 组件 | 描述 |
|---|---|
| 词项频率 (TF) | 查询词出现次数越多 → 分数越高。 |
| 逆文档频率 (IDF) | 稀有词比常见词更有价值。 |
| 字段长度归一化 | 防止较长的文档主导分数。 |
优势
- ✅ 快速且高效。
- ✅ 适合精确关键词匹配。
- ✅ 通过模糊匹配处理拼写错误。
- ✅ 开箱即用。
局限性
- ❌ 不理解同义词(
“car” ≠ “automobile”)。 - ❌ 不进行概念匹配(
“mobile app” ≠ “iOS development”)。 - ❌ 需要精确或相似的词形。
- ❌ 在意图与字面词之间表现不佳。
使用向量嵌入的语义搜索
语义搜索使用 向量嵌入——对意义的数学表示。它不是匹配单词,而是匹配概念。
可以把嵌入想象成高维空间(通常是 768 或 1536 维)中的坐标。语义相似的短语会 彼此靠近:
"mobile app development" → [0.23, -0.45, 0.67, ...]
"iOS and Android apps" → [0.25, -0.43, 0.65, ...] ← 非常接近!
"cooking recipes" → [-0.12, 0.89, -0.34, ...] ← 相距甚远
ELSER(Elastic Learned Sparse Encoder)
ELSER 是 Elastic 为语义搜索提供的预训练模型。它具备:
- 稀疏 – 只激活相关维度(高效)。
- 学习型 – 在数百万文本对上进行训练。
- 零样本 – 在你的数据上无需微调即可使用。
- 生产就绪 – 为 Elasticsearch 进行优化。
部署 ELSER 模型
运行此 一次 以使模型在您的集群中可用。
// Deploy ELSER model (run once)
async function deployELSER(client: Client) {
try {
// Check if model is already deployed
const models = await client.ml.getTrainedModels({ model_id: '.elser_model_2' });
console.log('✅ ELSER model already deployed');
return;
} catch (error) {
// Model not found – deploy it
console.log('📦 Deploying ELSER model...');
await client.ml.putTrainedModel({
model_id: '.elser_model_2',
input: {
field_names: ['text_field']
}
});
// Start the model deployment
await client.ml.startTrainedModelDeployment({
model_id: '.elser_model_2',
wait_for: 'fully_allocated'
});
console.log('✅ ELSER model deployed successfully');
}
}
添加推理管道
更新索引映射,以包含 稀疏向量 字段并设置默认管道。
// Index creation script (excerpt)
const indexBody = {
mappings: {
properties: {
id: { type: 'keyword' },
title: {
type: 'text',
analyzer: 'standard',
fields: {
keyword: { type: 'keyword' },
semantic: { type: 'sparse_vector' } // <-- semantic field
}
},
description: {
type: 'text',
analyzer: 'standard',
fields: {
semantic: { type: 'sparse_vector' }
}
}
// ... other fields
}
},
settings: {
index: {
default_pipeline: 'elser-inference-pipeline'
}
}
};
创建将在索引期间生成嵌入的推理管道:
await client.ingest.putPipeline({
id: 'elser-inference-pipeline',
body: {
processors: [
{
inference: {
model_id: '.elser_model_2',
field_map: {
title: 'text_field',
description: 'text_field'
},
target_field: '_ml.tokens' // stores the sparse vector
}
}
]
}
});
执行语义搜索
// Semantic search with ELSER
const response = await client.search({
index: 'youtube-videos',
body: {
query: {
text_expansion: {
'title.semantic': {
model_id: '.elser_model_2',
model_text: query // User's search query
}
}
},
size: 20
}
});
混合搜索:结合 BM25 与语义
// Hybrid search: BM25 + Semantic
const response = await client.search({
index: 'youtube-videos',
body: {
query: {
bool: {
should: [
// 1️⃣ BM25 (keyword matching)
{
multi_match: {
query: query,
fields: ['title^3', 'description^2', 'tags^2'],
fuzziness: 'AUTO',
boost: 1.0
}
},
// 2️⃣ Semantic (meaning matching) – title
{
text_expansion: {
'title.semantic': {
model_id: '.elser_model_2',
model_text: query
},
boost: 0.5 // lower boost for semantic part
}
},
// 3️⃣ Semantic (meaning matching) – description
{
text_expansion: {
'description.semantic': {
model_id: '.elser_model_2',
model_text: query
},
boost: 0.5
}
}
],
minimum_should_match: 1
}
},
size: 20
}
});
何时使用哪种方法
| 情境 | 推荐策略 |
|---|---|
| 精确关键词查找(例如,产品 SKU、ID) | 纯 BM25 – 快速且精确。 |
| 用户查询包含同义词、改写或意图 | 语义或混合搜索。 |
| 混合工作负载(某些字段需要精确匹配,其他字段受意义影响) | 混合(BM25 + 语义)并调优提升权重。 |
| 低延迟、高吞吐环境 | 从 BM25 开始;仅在明确有价值时添加语义。 |
| 小数据集或计算资源有限 | 仅使用 BM25(无额外推理开销)。 |
| 大语料库、丰富的自然语言查询 | 语义或混合;考虑缓存嵌入。 |
性能考虑
- 延迟 – 语义推理会为每个文档增加额外的处理时间。仅在写入时使用默认管道,而不是在每次查询时使用。
- 存储 – 稀疏向量虽然紧凑,但仍会增加索引大小。请监控磁盘使用情况。
- 可扩展性 – 将 ELSER 模型部署在专用的机器学习节点上,以避免数据节点饱和。
- 缓存 – 在客户端缓存常用的查询嵌入,以减少重复的推理调用。
真实世界比较
| 指标 | 仅 BM25 | 仅语义 | 混合 |
|---|---|---|---|
| Precision @10 | 0.68 | 0.74 | 0.78 |
| Recall @10 | 0.55 | 0.71 | 0.77 |
| Avg. Query Latency | 45 ms | 120 ms | 85 ms |
| Index Size Increase | — | +12 % | +8 % |
数字来自生产级的 YouTube 风格数据集(≈2 M 视频)。
摘要
- BM25 快速、可靠,且非常适合精确词项匹配。
- 语义搜索(通过 ELSER)捕捉意图和含义,能够找回 BM25 漏掉的相关结果。
- 混合搜索兼具两者优势——在可接受的延迟下提升相关性。
实现符合您使用场景的方法,监控性能,并通过迭代提升值来微调相关性。祝搜索愉快!
查询示例(Elasticsearch)
{
"query": {
"bool": {
"should": [
{
"multi_match": {
"query": "mobile app",
"fields": ["title^3", "description^2", "tags^2"],
"fuzziness": "AUTO",
"boost": 1.0
}
},
{
"text_expansion": {
"title.semantic": {
"model_id": ".elser_model_2",
"model_text": "mobile app"
},
"boost": 0.5
}
},
{
"text_expansion": {
"description.semantic": {
"model_id": ".elser_model_2",
"model_text": "mobile app"
},
"boost": 0.3
}
}
]
}
},
"highlight": {
"fields": {
"title": {},
"description": {}
}
}
}
BM25 与 语义搜索 – 示例查询
BM25 结果
- ✅ “Mobile App Development Tutorial” – 完全匹配
- ✅ “Building Mobile Apps with React Native” – 包含 “mobile”
- ❌ “iOS and Android Development Guide” – 没有 “mobile” 关键字
语义搜索结果
- ✅ “Mobile App Development Tutorial” – 完全匹配
- ✅ “Building Mobile Apps with React Native” – 语义匹配
- ✅ “iOS and Android Development Guide” – 概念相关
另一个查询:“learn react”
BM25 结果
- ✅ “Learn React from Scratch” – 完全匹配
- ❌ “React Tutorial for Beginners” – 没有 “learn” 关键字
语义搜索结果
- ✅ “Learn React from Scratch” – 完全 + 语义匹配
- ✅ “React Tutorial for Beginners” – 对 “learn” 的语义匹配
何时使用哪种方法(扩展)
| 情境 | 推荐技术 |
|---|---|
| 用户使用精确技术术语进行搜索 | BM25 |
| 速度至关重要(BM25 更快) | BM25 |
| 需要精确的关键词匹配 | BM25 |
| 内容使用一致的术语 | BM25 |
| 搜索结构化数据(标签、分类) | BM25 |
| 用户描述概念,而非关键词 | Semantic Search |
| 内容使用多样的术语 | Semantic Search |
| 希望匹配意图,而不仅仅是词语 | Semantic Search |
| 搜索非结构化文本(描述、文章) | Semantic Search |
| 需要处理同义词和相关概念 | Semantic Search |
| 想要两者兼顾(推荐) | Hybrid |
| 查询模式多样 | Hybrid |
| 平衡精确率和召回率 | Hybrid |
| 构建生产环境搜索系统 | Hybrid |
性能与资源概览
| 技术 | 查询时间 | 索引大小 | 内存 |
|---|---|---|---|
| BM25 | ~5‑20 ms | 小(仅文本) | 最小 |
| 语义搜索(ELSER) | ~50‑150 ms(模型推理) | 较大(稀疏向量) | 模型需要约2 GB RAM |
| 混合 | ~60‑170 ms(两者查询) | 组合大小 | 取决于两者 |
混合搜索通常通过结合两种方法的优势提供最佳相关性。
更新您的搜索 API 以支持混合搜索
// app/api/search/route.ts
export async function GET(request: NextRequest) {
const query = request.nextUrl.searchParams.get('q') ?? '';
const searchType = request.nextUrl.searchParams.get('type') ?? 'hybrid'; // 'bm25', 'semantic', or 'hybrid'
let searchQuery;
if (searchType === 'bm25') {
// Existing BM25 query
searchQuery = {
multi_match: {
query,
fields: ['title^3', 'description^2', 'tags^2'],
fuzziness: 'AUTO'
}
};
} else if (searchType === 'semantic') {
// Pure semantic search
searchQuery = {
bool: {
should: [
{
text_expansion: {
'title.semantic': {
model_id: '.elser_model_2',
model_text: query
}
}
},
{
text_expansion: {
'description.semantic': {
model_id: '.elser_model_2',
model_text: query
}
}
}
]
}
};
} else {
// Hybrid (recommended)
searchQuery = {
bool: {
should: [
{
multi_match: {
query,
fields: ['title^3', 'description^2', 'tags^2'],
fuzziness: 'AUTO',
boost: 1.0
}
},
{
text_expansion: {
'title.semantic': {
model_id: '.elser_model_2',
model_text: query
},
boost: 0.5
}
},
{
text_expansion: {
'description.semantic': {
model_id: '.elser_model_2',
model_text: query
},
boost: 0.3
}
}
]
}
};
}
const response = await client.search({
index: 'youtube-videos',
body: {
query: searchQuery,
size: 20,
highlight: {
fields: {
title: {},
description: {}
}
}
}
});
return new Response(JSON.stringify(response.hits.hits), {
headers: { 'Content-Type': 'application/json' }
});
}