在本地知识库中,单独依赖关键词检索容易漏掉同义表达,单独依赖向量检索又可能忽略错误码、产品型号和专有名词。更稳妥的方案,是让 Ollama 负责本地向量化与答案生成,让 Elasticsearch 同时执行 BM25 全文检索和 kNN 向量检索,再通过 RRF 融合两路排序结果。这样既保留精确匹配能力,又能理解用户问题的语义,是一套兼顾隐私、成本与检索质量的实践方案。🚀
一、整体架构与数据流
系统可以拆成“文档入库”和“在线问答”两条链路。入库阶段负责解析 PDF、Word 或 Markdown,清洗正文并切分为若干文本块;随后调用 Ollama 的嵌入接口生成向量,将文本、向量及来源信息写入 Elasticsearch。查询阶段则把用户问题同时送入 BM25 和向量检索,融合候选片段后,再交给 Ollama 生成基于知识库内容的回答。
文档 → 清洗与分块 → Ollama Embedding → Elasticsearch 索引
问题 → BM25 召回 + kNN 召回 → RRF 融合 → 上下文组装 → Ollama 生成答案
这套架构不要求把两种索引分散到不同数据库。Elasticsearch 可以在同一个索引中保存可检索文本与 dense_vector 字段,降低数据同步和运维复杂度。
二、准备 Ollama 与 Elasticsearch
首先安装并启动 Ollama,然后选择一个适合中文或多语言语料的嵌入模型,同时准备一个用于回答问题的生成模型。调用嵌入模型时,可向本地的 /api/embed 接口提交单条文本或文本数组,具体参数可参考 Ollama Embedding API 文档。
ollama pull embeddinggemma
ollama pull qwen3:8b
模型只是示例,实际选择应结合机器内存、中文能力和业务许可证要求。需要特别注意:文档入库和用户查询必须使用同一个嵌入模型及相同向量维度,否则向量距离没有可比性。模型更换后,通常需要重新生成并写入全部文档向量。
三、设计 Elasticsearch 索引
建议以“文本块”而不是整篇文档作为检索单元。每条记录至少保存 chunk_id、doc_id、title、content、embedding、source 和 updated_at。content 使用 text 类型承载 BM25 检索,embedding 使用 dense_vector 类型并开启索引。
content:text,用于 match 或 multi_match 查询
embedding:dense_vector,维度与 Ollama 输出一致
title:text,可设置高于正文的查询权重
source:keyword,用于来源过滤与答案引用
切块不宜机械地按固定字符截断。更实用的方式是优先按标题、段落和列表边界切分,再设置适量重叠,避免定义与解释被拆到两个片段。表格、接口参数和操作步骤应尽量作为完整语义单元保存。📚
四、完成向量化与批量入库
入库程序读取每个文本块,调用 Ollama 获取 embedding,再通过 Elasticsearch Bulk API 批量写入。批次大小要根据文本长度、模型速度和节点内存逐步调整,不要一次加载全部文档。为了支持增量更新,可保存文件哈希或内容哈希,仅对新增、修改的片段重新计算向量。
- 解析文件并保留文件名、章节、页码等元数据。
- 清理重复页眉、页脚、乱码和无意义空白。
- 按语义边界切块,并为每个块生成稳定 ID。
- 批量调用 Ollama 生成向量。
- 使用 Bulk API 写入 Elasticsearch,并记录失败项。
生产环境还应为嵌入请求设置超时、重试和并发上限。Ollama 本地接口默认地址通常为 localhost:11434;若需要跨主机访问,应通过反向代理、网络隔离和访问控制进行保护,而不是直接暴露服务端口。🔐
五、并行执行 BM25 与向量召回
用户提问后,先用 match 或 multi_match 查询 title 与 content,得到 BM25 候选列表;同时调用同一嵌入模型生成问题向量,再对 embedding 字段执行 kNN 查询。BM25 擅长命中“ERR_CONNECTION_RESET”“订单号 A1024”之类的精确词项,向量召回则更容易找到措辞不同但含义接近的内容。
kNN 查询中的 k 表示希望保留的近邻数量,num_candidates 表示近似检索阶段考察的候选规模。候选越大,通常越有机会提升召回,但也会增加延迟和资源消耗。可以先让两路分别召回数十条,再融合并截取最终上下文;Elasticsearch 的组合方式可参考 全文与 kNN 混合检索示例。
六、使用 RRF 融合两路结果
BM25 分数和向量相似度处于不同尺度,直接相加容易让某一路结果失真。RRF(倒数排名融合)不直接比较原始分数,而是根据文档在各结果列表中的名次累计得分,因此实现简单,也减少了手工归一化的麻烦。
RRF 得分 = 各召回列表中 1 ÷(排名常数 + 当前名次)之和
如果同一个文本块在 BM25 和 kNN 中都排名靠前,它会得到更高的融合分数;只被其中一路召回的片段仍有机会进入最终结果。使用 Elasticsearch 原生 RRF 时,可将标准查询和 kNN 查询配置为两个检索器,并设置 rank_window_size。不同版本的请求结构可能存在差异,部署前应对照对应版本的 来源链接 官方说明。
七、组装上下文并调用 Ollama
融合后不要简单拼接所有候选。应先按 doc_id、章节和文本相似度去重,再根据模型上下文窗口选择若干高质量片段。提示词中要明确要求模型只依据给定资料回答,资料不足时说明无法确认,并在回答中标注来源名称或片段编号。调用方式可参考 Ollama Generate API。🤖
- 限制上下文:避免大量相似片段挤占窗口。
- 保留出处:返回文件名、章节、页码或内部文档地址。
- 防止注入:将检索内容视为资料,而不是系统指令。
- 设置阈值:低相关度时不强行生成确定性答案。
八、评估与调优建议
不要只凭几次问答判断效果。可以整理一组包含标准答案和相关文档的测试问题,分别运行 BM25、向量检索和混合检索,比较 Recall@K、MRR、nDCG、无答案识别率及端到端延迟。测试集应覆盖缩写、错误码、口语化问题、同义改写和跨章节问题。
调优顺序建议从文档质量开始,再调整切块方式、字段权重、k、num_candidates 和 RRF 窗口。若融合后的前几条仍包含相近但不够准确的内容,可增加重排模型,但应同时评估额外延迟。对于部门、权限、时间范围等条件,两路召回必须使用一致过滤规则,避免向量结果绕过业务权限。
总结
Ollama 与 Elasticsearch 的组合,能够构建数据本地化、来源可追踪的混合检索知识库:BM25 守住关键词与专有标识的精确命中,向量检索补充同义表达和语义关联,RRF 则在不直接混合异构分数的情况下统一排序。真正决定效果的并非某个单一参数,而是文档清洗、语义切块、模型一致性、候选融合、权限过滤和离线评估这一整条工程链路。✅