Hotdry.
systems

QMD 混合搜索架构:本地 Markdown 检索的工程实践

深入解析 QMD 如何在本地环境实现 BM25 全文检索与语义向量的混合搜索,涵盖查询扩展、RRF 融合与 LLM 重排序的工程参数调优。

在本地文档检索场景中,如何平衡检索速度与结果相关性始终是核心挑战。传统的 BM25 全文检索虽然响应迅速,但在处理同义词和语义关联时表现欠佳;而纯向量检索虽然能够捕捉语义相似性,却需要较大的计算资源且对小规模数据集存在过度索引的风险。QMD 作为一款专为本地 Markdown 文件设计的混合搜索引擎,通过巧妙的架构设计将两种检索范式融合,并在上层加入查询扩展与 LLM 重排序机制,在保证毫秒级响应时间的同时显著提升了检索质量。

混合检索管线的核心设计

QMD 的混合搜索管线采用三阶段架构:查询扩展阶段生成语义相关的查询变体,并行检索阶段同时执行 BM25 全文检索与向量相似度搜索,融合排序阶段通过 RRF(Reciprocal Rank Fusion)算法整合多路结果,最后由轻量级 LLM 进行重排序。整个管线在设计时遵循一个核心原则:让计算复杂度与结果置信度成正比。具体而言,高置信度的检索结果直接采用排名,低置信度的候选集才进入重排序阶段,从而在保证质量的前提下控制推理成本。

在查询扩展环节,QMD 使用 Qwen3-1.7B 模型为原始查询生成两个变体查询。原始查询在后续融合中享有两倍权重,这种设计确保了精确匹配不会被语义扩展所淹没。模型选择 1.7B 参数规模是经过权衡的决策:模型足够小,可以在本地快速推理(通常在 100 毫秒以内完成),同时又具备足够的语言理解能力来生成有意义的查询变体。对于个人知识库场景,这个规模的模型在消费级 CPU 上即可流畅运行,无需 GPU 加速。

并行检索阶段对每个查询变体同时执行 FTS5 全文检索与向量检索。FTS5 是 SQLite 内置的全文索引模块,采用 BM25 算法进行词项匹配,其优势在于无需额外依赖、索引构建速度快、查询延迟稳定在亚毫秒级别。向量检索则使用 sqlite-vec 扩展存储 EmbeddingGemma-300M 生成的语义向量,通过余弦距离计算相似度。两种检索方式的结果各自形成按相关性排序的文档列表,为后续融合提供基础。

RRF 融合与位置感知权重策略

多路检索结果的融合是混合搜索的核心难点。QMD 采用 RRF 算法作为基础融合策略,其核心思想是将文档在不同检索列表中的排名位置转化为融合分数。公式为 score = Σ(1/(k + rank)),其中 k=60 是平滑参数,用于避免排名靠前文档的分数过于悬殊。RRF 的优势在于对检索系统数量不敏感,且对单一检索系统的排序错误具有较好的鲁棒性。

然而,QMD 在标准 RRF 基础上增加了两项关键改进。第一项是原始查询权重加倍,由于查询扩展生成的变体可能引入噪声,原始查询的两倍权重确保了精确匹配在融合结果中保持优势。第二项是排名首位 bonus:文档在任意检索列表中排名第一可获得额外 +0.05 分,排名第二至三位获得 +0.02 分。这项设计解决了纯 RRF 的一个典型问题 —— 当扩展查询的匹配结果排名高于原始查询时,精确匹配可能被稀释。首位 bonus 确保了那些在某个检索维度表现优异的文档不会被埋没。

融合后的候选集取前 30 名进入重排序阶段。这个阈值是经验值:过少可能导致优质结果被遗漏,过多则增加不必要的 LLM 推理开销。30 这个数字在大多数本地知识库场景下能够覆盖绝大多数相关文档,同时保持重排序阶段的响应时间在可接受范围内。

轻量级 LLM 重排序的实现细节

重排序阶段使用 Qwen3-Reranker-0.6B 模型,这是一个专门为排序任务设计的交叉编码器模型。与双编码器式的向量检索不同,交叉编码器将查询与文档同时输入模型,直接输出相关性分数。这种架构虽然计算成本更高,但能够捕捉查询与文档之间更细粒度的交互关系。0.6B 的参规模型在推理效率与排序质量之间取得了良好平衡,在本地环境中完成 30 个候选文档的重排序通常只需数百毫秒。

QMD 对重排序结果采用位置感知的混合策略,而非简单加权平均。具体而言,排名 1 到 3 的文档其最终分数中 RRF 权重占 75%,重排序权重占 25%;排名 4 到 10 的文档两者权重分别为 60% 与 40%;排名 11 之后的文档则反过来,40% 来自 RRF,60% 来自重排序。这种设计背后的直觉是:排名靠前的文档通常已经具有较高相关性,此时过度依赖重排序可能反而引入不必要的波动;而排名靠后的文档相关性不确定性较高,需要重排序发挥更大作用。

模型返回的分数范围为 0 到 10,最终被归一化到 0 到 1 之间用于展示。0.8 到 1.0 被视为高度相关,0.5 到 0.8 为中度相关,0.2 到 0.5 为轻度相关,0.2 以下为低相关。这些阈值帮助用户在检索结果中快速筛选高质量匹配。

向量索引与嵌入生成策略

向量检索的质量高度依赖于嵌入模型与分块策略的选择。QMD 使用 EmbeddingGemma-300M 模型生成文档向量,该模型专为检索任务优化,在保持较小参数量(300MB)的同时具备良好的语义捕捉能力。文档在嵌入前被切分为 800 token 的块,块与块之间保留 15% 的重叠以避免关键信息被切分点割裂。

分块大小的选择需要权衡语义完整性与索引精度。800 token 的块大小能够覆盖大多数 Markdown 文档的段落级别语义单元,同时保持向量表示的聚焦性。15% 的重叠确保了当查询命中块边界附近的关键词时,相邻块仍能提供补充上下文。嵌入时的提示格式为 "title | text",这种设计让模型在计算相似度时能够同时考虑文档标题与正文内容,提升了跨标题 - 内容匹配的准确性。

sqlite-vec 扩展将向量存储为 SQLite 的 BLOB 类型,与文档元数据共享同一个数据库文件。这种设计简化了数据管理,无需维护独立的向量数据库实例,但也在一定程度上限制了向量维度的扩展性。对于个人知识库场景,这种权衡是合理的 —— 单库存储降低了运维复杂度,向量规模也远未达到需要分布式方案的量级。

面向 Agent 工作流的工程考量

QMD 的设计目标之一是服务于 AI Agent 场景,因此输出格式的灵活性与机器可读性被置于重要位置。工具支持 JSON、CSV、Markdown、XML 四种输出格式,并提供 --files 模式专门返回文件路径与相关性分数,便于 Agent 批量获取文档内容。--min-score 参数允许用户设置相关性阈值,避免低质量结果进入下游处理流程。

MCP(Model Context Protocol)服务器的暴露进一步简化了 Agent 与 QMD 的集成。Agent 无需通过命令行调用,直接通过 MCP 协议调用 qmd_search、qmd_vsearch、qmd_query、qmd_get 等工具函数。这种设计让 Agent 能够在对话上下文中动态检索相关文档,无需预定义检索逻辑。对于需要跨知识库进行深度推理的场景,这种即时检索能力尤为关键。

在工程实践中,索引维护的自动化程度也值得关注。qmd update 命令支持 --pull 参数,在重新索引前自动拉取远程仓库更新,这对于将文档存放在 Git 仓库中的团队非常实用。cleanup 命令则用于清理缓存与孤立数据,防止长期使用后的存储膨胀。

本地部署的性能参数参考

对于计划部署 QMD 的团队,以下参数可作为基准参考。查询扩展阶段使用 Qwen3-1.7B-Q8_0 模型,在 M 系列芯片上推理延迟约为 80 到 150 毫秒,在 Intel 酷睿处理器上约为 200 到 400 毫秒。嵌入生成阶段使用 EmbeddingGemma-300M-Q8_0 模型,单文档嵌入延迟约为 50 到 100 毫秒,批量嵌入时吞吐量可达每秒数十个文档。重排序阶段使用 Qwen3-Reranker-0.6B-Q8_0 模型,30 个候选文档的完整重排序延迟约为 300 到 600 毫秒。

在存储方面,三个模型文件总计约 3.1GB,缓存于~/.cache/qmd/models/ 目录。SQLite 索引文件存储于~/.cache/qmd/index.sqlite,其大小取决于文档数量与平均文档长度,典型个人知识库(数千个 Markdown 文件)的索引文件约为数十 MB 级别。

QMD 的混合搜索架构为本地文档检索提供了可参考的工程范式。其核心价值在于展示了如何通过多检索范式的有机组合,在资源受限的本地环境中实现接近云端质量的检索体验。对于构建个人知识库或团队文档搜索系统的开发者,QMD 的设计理念与参数配置值得深入研究与借鉴。

资料来源:QMD GitHub 仓库(https://github.com/tobi/qmd)

查看归档