关键词不足:为什么你的 Next.js 应用需要向量搜索

发布: (2025年12月26日 GMT+8 11:23)
11 min read
原文: Dev.to

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 @100.680.740.78
Recall @100.550.710.77
Avg. Query Latency45 ms120 ms85 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' }
  });
}
Back to Blog

相关文章

阅读更多 »