从 RAG to Riches:用 Meilisearch 混合搜索构建更可靠的检索增强生成
如果你正在构建基于大语言模型的应用,大概率绕不开 RAG。但很多团队在完成第一个 demo 后会发现一个尴尬的事实:检索环节经常拖后腿。用户问“怎么快速做一顿高蛋白早餐”,向量检索可能返回一堆讲“蛋白质粉溶解度”的文章——语义上好像沾边,实际上答非所问。反过来,纯关键词检索又搞不定用户用自然语言描述需求时产生的词汇鸿沟。
这就是混合搜索的用武之地。而 Meilisearch 的混合搜索有一个关键区别:它不做简单的排名拼接。
为什么传统混合搜索不够好
很多混合搜索的实现方式是“分别检索,然后用 Reciprocal Rank Fusion 合并排名”。这种做法有一个根本缺陷:它假设两个检索结果列表中的文档具有可比性。但现实是,当一种检索方法效果很差时,它返回的“第 1 名”可能根本不该出现在最终结果里。
Meilisearch 的做法不同。它对两种检索的分数进行归一化和统一评分,让系统根据查询性质动态调整权重,而不是依赖排名融合。当关键词检索对一个查询效果很好时,它的高分文档会自然排在前面;当语义检索更懂用户意图时,它的高分文档会胜出。
快速上手:TypeScript 示例
1. 安装与连接
bash
npm install meilisearch
# 或
yarn add meilisearch
typescript
import { Meilisearch } from 'meilisearch'
const client = new Meilisearch({
host: process.env.MEILISEARCH_URL, // 例如 http://localhost:7700
apiKey: process.env.MEILISEARCH_KEY
})
const index = client.index('movies')
2. 配置嵌入器(Embedder)
Meilisearch 会自动为文档生成向量嵌入,你只需要配置好 provider。以下以 OpenAI 为例:
typescript
// 配置 embedder,指定使用 OpenAI 的 embedding 模型
await index.updateEmbedders({
movies: {
source: 'openAi',
apiKey: process.env.OPENAI_API_KEY,
model: 'text-embedding-3-small',
// 使用 Liquid 模板聚焦最有检索价值的字段
documentTemplate: "{{doc.title}}:{{doc.overview}}。类型:{{doc.genre}}"
}
})
documentTemplate 决定了哪些字段进入嵌入模型。不要直接把整个文档 JSON 丢进去——用模板聚焦标题、摘要等核心字段,能显著提升嵌入质量,还能节省 token 成本。
3. 添加文档
typescript
const movies = [
{ id: 1, title: '盗梦空间', overview: '一个关于梦境窃取和植入的科幻惊悚片', genre: '科幻' },
{ id: 2, title: '星际穿越', overview: '宇航员穿越虫洞寻找人类新家园', genre: '科幻' },
{ id: 3, title: '当幸福来敲门', overview: '一位父亲在困境中追求幸福的励志故事', genre: '剧情' },
]
const task = await index.addDocuments(movies)
await client.waitForTask(task.taskUid)
console.log('文档已添加并完成嵌入生成')
Meilisearch 会自动批处理文档、调用嵌入 API、缓存结果,文档更新时只重新嵌入变化的部分。
4. 执行混合搜索
typescript
// 混合搜索:semanticRatio=0.5 表示关键词和语义各占一半
const results = await index.search('适合全家一起看的科幻电影', {
hybrid: {
embedder: 'movies',
semanticRatio: 0.5 // 0.0 = 纯关键词, 1.0 = 纯语义
},
limit: 5
})
for (const hit of results.hits) {
console.log(`${hit.title} (Score: ${hit._rankingScore?.toFixed(4) ?? 'N/A'})`)
}
semanticRatio 控制混合权重。0 是纯关键词,1 是纯语义,0.5 是均衡。
用 LangChain.js 集成
如果你已经在用 LangChain.js 构建 RAG pipeline,Meilisearch 有现成的 vector store 集成。以下是一个完整的检索流程:
typescript
import { Meilisearch } from 'meilisearch'
import { OpenAIEmbeddings } from '@langchain/openai'
import { MeilisearchVectorStore } from '@langchain/community/vectorstores/meilisearch'
const embeddings = new OpenAIEmbeddings()
const client = new Meilisearch({
host: process.env.MEILISEARCH_URL,
apiKey: process.env.MEILISEARCH_KEY
})
// 创建 vector store 并索引文档
const vectorStore = await MeilisearchVectorStore.fromTexts(
['盗梦空间是一部关于梦境窃取的科幻片', '星际穿越讲述宇航员穿越虫洞的故事'],
[{ genre: '科幻' }, { genre: '科幻' }],
embeddings,
{
client,
indexName: 'movies_rag'
}
)
// 相似度搜索
const docs = await vectorStore.similaritySearch('适合全家看的科幻电影', 3)
docs.forEach(doc => console.log(doc.pageContent))
不同 semanticRatio 的对比演示
以下 TypeScript 示例展示同一查询在不同 semanticRatio 下的检索结果差异:
typescript
const query = 'wireless ergonomic keyboard'
const ratios = [
{ ratio: 0.0, desc: '纯关键词搜索' },
{ ratio: 0.5, desc: '均衡混合' },
{ ratio: 1.0, desc: '纯语义搜索' },
]
for (const { ratio, desc } of ratios) {
console.log(`\n--- ${desc} (semanticRatio: ${ratio}) ---`)
const results = await index.search(query, {
hybrid: {
embedder: 'products',
semanticRatio: ratio
},
limit: 5
})
results.hits.forEach((hit, i) => {
console.log(`${i + 1}. ${hit.title} (${hit._rankingScore?.toFixed(4)})`)
})
}
纯关键词搜索会优先返回标题或描述中包含 “wireless”、“ergonomic”、“keyboard” 精确词的文档;纯语义搜索会返回概念相关的文档(比如提到“打字舒适”“人体工学设计”但没出现这些精确词的产品);混合搜索则两者兼顾。
实践建议
语义比例怎么调:官方给的起点参考——电商产品搜索 0.5–0.7,文档/知识库 0.5–0.8,代码搜索 0.0–0.3,FAQ/工单 0.7–1.0。中文知识库建议从 0.5 起步,根据查询日志微调。
文档模板决定嵌入质量:用 documentTemplate 聚焦标题、摘要等核心字段。标题 + 简短描述的嵌入效果通常比全文嵌入好得多,还能省 token。
先多召回再精排:让 Meilisearch 返回 Top 20–30,用重排序模型精排后取 Top 5 送入 LLM。混合检索负责“别漏”,重排序负责“别错”。
什么时候不该用混合搜索
如果你的数据完全没有文本内容(纯图像目录),混合搜索退化成纯语义检索,不如直接用 semanticRatio: 1.0。如果你做的是相似物品推荐,用 /similar 端点就够了。
混合搜索的价值在于它用一个相对简单的抽象(semanticRatio)封装了两种检索策略的协同,让开发者不需要在“精确但死板”和“灵活但漂移”之间二选一。对于任何需要同时处理实体查询和自然语言提问的 RAG 应用,这大概率是当前最务实的检索层方案。