QMD 深度解析:构建本地化混合搜索引擎的完整指南
QMD 深度解析:构建本地化混合搜索引擎的完整指南
🔍 Search Engine
🤖 Local AI
⚡ BM25 + Vector
QMD (Query Markup Documents) 是一个完全在本地设备运行的混合搜索引擎,它结合了:
- 100% 本地运行 - 无需云端 API,隐私安全
- 混合搜索架构 - BM25 关键词 × 向量语义
- LLM 智能增强 - 查询扩展 + Re-ranking
- AST 感知分块 - Tree-sitter 代码理解
- 完整 MCP 支持 - 无缝集成 AI Agent
| 特性 | QMD | Elasticsearch | |
|---|---|---|---|
| 部署方式 | 🏠 单机本地 | ☁️ 分布式集群 | 🌐 SaaS |
| 隐私保护 | ✅ 100% 本地 | ⚠️ 取决于部署 | ❌ 云端处理 |
| 语义理解 | ✅ LLM 加持 | ⚠️ 需插件 | ✅ 最强 |
| 设置复杂度 | ✅ 简单 | ❌ 复杂 | ✅ 开箱即用 |
| 成本 | 💰 免费 | 💵 硬件成本 | 💳 按量付费 |
| 延迟 | ⚡ 毫秒级 | ⚡ 毫秒级 | 🌍 网络依赖 |
🎯 引言:为什么需要本地化混合搜索?
在数据爆炸的时代,如何高效地检索和组织我们自己的知识资产成为了一个重要课题。Query Markup Documents (QMD) 提供了一个强大的解决方案——一个完全运行在本地设备上的混合搜索引擎。
- 隐私担忧:云端搜索需要将数据发送到第三方服务器
- 成本压力:企业级搜索解决方案价格昂贵
- 语义鸿沟:传统关键词搜索无法理解用户真实意图
- 信息孤岛:分散的文档难以统一管理检索
🏗️ QMD 架构概览
QMD 的核心设计理念:“多种搜索技术融合 + 智能重排序 = 最佳搜索结果”。
🔄 QMD 混合搜索架构流程
📝 用户查询 Query
🤖 LLM 查询扩展 (Query Expansion)
📊 BM25 全文搜索
🧠 向量语义搜索
💡 HYDE 假设文档
🔀 RRF 融合 (Reciprocal Rank Fusion)
📋 Top 30 候选 → LLM Re-ranking
✅ 位置感知融合 → 最终结果
这个架构体现了现代信息检索领域的三大前沿技术:
- 经典 IR 的精华:BM25 算法经过几十年验证,对精确匹配依然不可替代
- 深度学习的影响:向量表示捕捉语义相似性
- 大模型的推理能力:Re-ranking 做出最终的语义判断
🤖 三剑客模型
QMD 使用三个轻量化的 GGUF 模型,全部运行在本地:
| 模型 | 用途 | 大小 | 功能 |
|---|---|---|---|
embeddinggemma-300M | 向量嵌入 | ~300MB | 将文本转换为向量 |
qwen3-reranker-0.6B | 重排序 | ~640MB | LLM 评分文档相关性 |
qmd-query-expansion-1.7B | 查询扩展 | ~1.1GB | 生成查询变体 |
⚙️ 核心技术详解
2.1 BM25 算法:经典永不过时
BM25(Best Matching 25)是信息检索领域最经典的算法之一。QMD 通过 SQLite 的 FTS5 扩展实现了高效的 BM25 搜索。
BM25 核心公式:
其中关键参数:
- ( k_1 ): 控制词频饱和度(通常 1.2-2.0)
- ( b ): 控制文档长度归一化(通常 0.75)
- IDF:逆文档频率,衡量词项的重要性
优势:对精确关键词匹配极其敏感,计算速度快,解释性强
局限:无法处理同义词,对拼写错误敏感,难以捕捉深层语义关系
2.2 向量语义搜索:理解意图
向量搜索通过将文本转换为高维空间中的向量来捕捉语义信息。
嵌入格式化源码
以下是 QMD 中 嵌入格式化 的核心实现,来自 src/llm.ts:
// src/llm.ts - 嵌入格式化核心逻辑
/** 检测是否为 Qwen3-Embedding 模型 */export function isQwen3EmbeddingModel(modelUri: string): boolean { return /qwen.*embed/i.test(modelUri) || /embed.*qwen/i.test(modelUri);}
/** * 格式化查询文本用于嵌入 * - embeddinggemma 使用 nomic 风格的 task prefix * - Qwen3-Embedding 使用 Instruct 格式 */export function formatQueryForEmbedding(query: string, modelUri?: string): string { const uri = modelUri ?? resolveEmbedModel(); if (isQwen3EmbeddingModel(uri)) { return `Instruct: Retrieve relevant documents for the given query\nQuery: ${query}`; } return `task: search result | query: ${query}`;}
/** * 格式化文档文本用于嵌入 * - embeddinggemma 使用 title + text 格式 * - Qwen3-Embedding 直接使用原始文本 */export function formatDocForEmbedding(text: string, title?: string, modelUri?: string): string { const uri = modelUri ?? resolveEmbedModel(); if (isQwen3EmbeddingModel(uri)) { return title ? `${title}\n${text}` : text; } return `title: ${title || "none"} | text: ${text}`;}通过不同的 prompt 前缀,让同一个模型既能处理查询又能处理文档,保持查询-文档向量空间的对齐性。这使得余弦相似度计算更加准确。
余弦相似度计算:
// QMD 中的距离-分数转换distance = 1 / (1 + cosine_similarity)score = 1 / (1 + distance) // 范围:0.0 到 1.02.3 HYDE:假设文档嵌入
让模型先生成一篇可能回答查询的假设文档,然后用这篇文档的向量进行搜索。
关键洞察:文档-文档相似度 > 查询-文档相似度
🔄 HYDE 工作流程
三大优势:
- 缩小语义差距 - 文档-文档相似度远高于查询-文档相似度
- 提供上下文 - 生成的假设内容可作为重排序的参考
- 提高召回率 - 找到更多相关内容,即使措辞不同
2.4 RRF 融合:兼顾多样性和准确性
RRF 核心公式:
其中 ( k ) 通常是 60(经验值)。
RRF Top-Rank 奖励源码
以下是 QMD 中 RRF Top-Rank 奖励机制 的实际实现,来自 src/store.ts:
// src/store.ts - RRF 融合中的 Top-Rank 奖励机制
// Top-rank bonus: 在某个子查询中排名靠前的文档获得额外加分for (const entry of scores.values()) { if (entry.topRank === 0) { entry.rrfScore += 0.05; // Rank 1: +0.05 bonus } else if (entry.topRank <= 2) { entry.rrfScore += 0.02; // Rank 2-3: +0.02 bonus }}原始查询加权 ×2:原始查询通常最能准确表达用户意图,展开变体可能偏离。
前几名奖励:确保在某个子查询中排名第一的文档不会被忽视,即使它在其他查询中表现一般。
2.5 LLM Re-ranking:位置感知融合
经过 RRF 选出 Top 30 候选后,QMD 调用 qwen3-reranker 进行精细评分。
位置感知融合源码
以下是 位置感知融合策略 的核心实现:
// src/store.ts - Position-Aware Blending// 根据 RRF 排名自适应调整 RRF 与 Reranker 的融合比例
const blended = reranked.map(r => { const rrfRank = rrfRankMap.get(r.file) || candidateLimit; let rrfWeight: number;
// 位置感知权重:排名越高,越信任传统搜索结果 if (rrfRank <= 3) rrfWeight = 0.75; // Top 3: 75% RRF / 25% reranker else if (rrfRank <= 10) rrfWeight = 0.60; // 4-10: 60% RRF / 40% reranker else rrfWeight = 0.40; // 11+: 40% RRF / 60% reranker
const rrfScore = 1 / rrfRank; const blendedScore = rrfWeight * rrfScore + (1 - rrfWeight) * r.score; // ...});| 排名区间 | RRF 权重 | Reranker 权重 | 设计理由 |
|---|---|---|---|
| Rank 1-3 | 75% | 25% | 保留精确匹配的权威结果 |
| Rank 4-10 | 60% | 40% | 平衡传统搜索与语义理解 |
| Rank 11+ | 40% | 60% | 更相信 LLM 的深度理解 |
数学表达:
💾 数据存储与索引
3.1 SQLite 数据库设计
QMD 使用 SQLite 作为单一的数据存储后端:
-- 集合管理:索引目录与 glob 模式CREATE TABLE collections( name TEXT PRIMARY KEY, pwd TEXT NOT NULL, glob_pattern TEXT, ignore_patterns TEXT, update_command TEXT, include_by_default BOOLEAN);
-- 文档元数据:每个文档的唯一标识CREATE TABLE documents( docid TEXT PRIMARY KEY, -- 6 字符哈希 ID collection TEXT, path TEXT NOT NULL, title TEXT, content_hash TEXT, -- 用于检测变更 created_at TIMESTAMP, updated_at TIMESTAMP);
-- BM25 全文索引:FTS5 虚拟表CREATE VIRTUAL TABLE documents_fts USING fts5( content, -- 可搜索的文本内容 docid UNINDEXED);
-- 向量存储:嵌入向量块CREATE TABLE content_vectors( hash_seq TEXT PRIMARY KEY, -- docid:chunk_index hash TEXT, -- document docid seq INTEGER, -- chunk 序列号 pos INTEGER, -- 字符偏移量 content TEXT -- chunk 内容);3.2 智能分块算法
这是 QMD 最有创意的设计之一。文档被分割成约 900 个 token 的块,但绝不是简单地切割。
分块配置源码
// src/store.ts - 智能分块核心配置
// Chunking: 900 tokens per chunk with 15% overlapexport const CHUNK_SIZE_TOKENS = 900;export const CHUNK_OVERLAP_TOKENS = Math.floor(CHUNK_SIZE_TOKENS * 0.15); // 135 tokens// Fallback char-based approximation (~4 chars per token)export const CHUNK_SIZE_CHARS = CHUNK_SIZE_TOKENS * 4; // 3600 charsexport const CHUNK_OVERLAP_CHARS = CHUNK_OVERLAP_TOKENS * 4; // 540 chars// Search window for finding optimal break pointsexport const CHUNK_WINDOW_TOKENS = 200;export const CHUNK_WINDOW_CHARS = CHUNK_WINDOW_TOKENS * 4; // 800 chars断点评分系统源码
以下是 QMD 的断点检测模式定义,来自 src/store.ts:
// src/store.ts - Break Point Pattern Definitions// 为不同类型的 Markdown 元素分配断点分数,分数越高表示越适合在此切割
export const BREAK_PATTERNS: [RegExp, number, string][] = [ [/\n#{1}(?!#)/g, 100, 'h1'], // # 最高级别章节 [/\n#{2}(?!#)/g, 90, 'h2'], // ## 主要子章节 [/\n#{3}(?!#)/g, 80, 'h3'], // ### 等同代码块边界 [/\n```/g, 80, 'codeblock'], // 代码块边界(同 h3 优先级) [/\n(?:---|\*\*\*|___)\s*\n/g, 60, 'hr'], // 分隔线 [/\n\n+/g, 20, 'blank'], // 段落边界 [/\n[-*]\s/g, 5, 'list'], // 列表项 [/\n\d+\.\s/g, 5, 'numlist'], // 有序列表 [/\n/g, 1, 'newline'], // 最小断点];距离衰减函数源码
当接近目标切点时,QMD 在窗口内寻找最佳断点,使用平方距离衰减:
// src/store.ts - findBestCutoff 核心算法// 在窗口范围内寻找质量最高的切割点
export function findBestCutoff( breakPoints: BreakPoint[], targetCharPos: number, windowChars: number = CHUNK_WINDOW_CHARS, decayFactor: number = 0.7, codeFences: CodeFenceRegion[] = []): number { const windowStart = targetCharPos - windowChars; let bestScore = -1; let bestPos = targetCharPos;
for (const bp of breakPoints) { if (bp.pos < windowStart) continue; if (bp.pos > targetCharPos) break; // sorted, can stop
// 跳过代码围栏内部的断点 if (isInsideCodeFence(bp.pos, codeFences)) continue;
const distance = targetCharPos - bp.pos; // 平方距离衰减:近处温和,远处陡峭 const normalizedDist = distance / windowChars; const multiplier = 1.0 - (normalizedDist * normalizedDist) * decayFactor; const finalScore = bp.score * multiplier;
if (finalScore > bestScore) { bestScore = finalScore; bestPos = bp.pos; } }
return bestPos;}- 距离 25%: multiplier = 0.956(几乎无损)
- 距离 50%: multiplier = 0.825(轻微衰减)
- 距离 75%: multiplier = 0.606(明显衰减)
- 距离 100%: multiplier = 0.300(大幅衰减)
效果:远处的 H2 标题(score=90, final≈58.8)仍然能胜过紧邻的空行(score=20, final≈19.9)!
核心分块算法源码
// src/store.ts - chunkDocumentWithBreakPoints 完整实现
export function chunkDocumentWithBreakPoints( content: string, breakPoints: BreakPoint[], codeFences: CodeFenceRegion[], maxChars: number = CHUNK_SIZE_CHARS, overlapChars: number = CHUNK_OVERLAP_CHARS, windowChars: number = CHUNK_WINDOW_CHARS): { text: string; pos: number }[] { // 短文档直接返回 if (content.length <= maxChars) { return [{ text: content, pos: 0 }]; }
const chunks: { text: string; pos: number }[] = []; let charPos = 0;
while (charPos < content.length) { const targetEndPos = Math.min(charPos + maxChars, content.length); let endPos = targetEndPos;
// 在窗口内寻找最佳切割点 if (endPos < content.length) { const bestCutoff = findBestCutoff( breakPoints, targetEndPos, windowChars, 0.7, codeFences ); if (bestCutoff > charPos && bestCutoff <= targetEndPos) { endPos = bestCutoff; } }
if (endPos <= charPos) { endPos = Math.min(charPos + maxChars, content.length); }
chunks.push({ text: content.slice(charPos, endPos), pos: charPos });
if (endPos >= content.length) break;
// 15% 重叠确保跨块语义连续性 charPos = endPos - overlapChars; const lastChunkPos = chunks.at(-1)!.pos; if (charPos <= lastChunkPos) { charPos = endPos; } }
return chunks;}AST 感知的代码分块
🌳 AST 感知分块流程
AST 节点优先级:
| 节点类型 | 分数 | 说明 |
|---|---|---|
| Class / Interface / Trait / Impl | 100 | 类级别边界 |
| Function / Method | 90 | 函数级别完整性 |
| Type Alias / Enum | 80 | 类型定义边界 |
| Import / Use | 60 | 导入语句分隔 |
这使得代码块保持函数级别的完整性,极大提升代码检索效果。
🔧 高级特性
4.1 查询语法:结构化搜索的力量
QMD 支持一种强大的结构化查询语法:
# 单一查询(自动展开)qmd query "error handling best practices"
# 结构化查询文档qmd query "intent: 寻找关于异步错误处理的实践lex: try-catch async await promise rejectionvec: how should we handle errors in async JavaScript functionshyde: This guide covers error handling patterns including try-catch blocks..."| 类型 | 方法 | 适用场景 |
|---|---|---|
lex | BM25 | 精确术语、专有名词、代码符号 |
vec | 向量 | 自然语言问题、概念性问题 |
hyde | 向量 | 生成假设答案,提升语义匹配 |
intent | 辅助 | 澄清歧义,引导重排序 |
不要完全依赖自动查询展开。你应该亲自编写 intent: 字段,因为你拥有模型不具备的领域知识和上下文。
4.2 MCP 协议集成
QMD 完整实现了 Model Context Protocol,可以与 Claude Desktop、Claude Code 等工具无缝集成:
{ "mcpServers": { "qmd": { "command": "qmd", "args": ["mcp"] } }}暴露的工具:query(执行搜索)、get(获取文档)、multi_get(批量获取)、status(健康检查)
支持长驻服务器模式,避免每次请求都重新加载模型:
# 启动 HTTP 服务器qmd mcp --http --port 8080📊 实战应用
安装 → 创建集合 → 嵌入 → 搜索
# 安装npm install -g @tobilu/qmd
# 创建集合qmd collection add ~/notes --name personal-notes
# 添加上下文qmd context add qmd://personal-notes "个人学习笔记和技术文档"
# 生成向量嵌入qmd embed
# 开始搜索qmd query "机器学习算法"内存控制(针对大型语料库):
qmd embed --max-docs-per-batch 50qmd embed --max-batch-mb 64GPU 加速:
export QMD_LLAMA_GPU=cuda # NVIDIA CUDAexport QMD_LLAMA_GPU=vulkan # 跨平台 Vulkanexport QMD_LLAMA_GPU=metal # macOS Apple Silicon🔬 技术对比分析
vs 纯向量数据库
| 特性 | QMD | Pure Vector DB |
|---|---|---|
| 混合搜索 | ✅ BM25 + 向量 | ❌ 仅向量 |
| 精确匹配 | ✅ 优秀 | ⚠️ 较差 |
| 可扩展性 | ⚠️ 单机为主 | ✅ 分布式 |
| 成本 | 💰 开源免费 | 💳 云服务收费 |
评分解读
QMD 的最终分数范围是 0.0 到 1.0:
| 分数范围 | 相关性 | 标记 |
|---|---|---|
| 0.8 - 1.0 | 高度相关 | 🟢 |
| 0.5 - 0.8 | 中度相关 | 🟡 |
| 0.2 - 0.5 | 一般相关 | ⚫ |
| 0.0 - 0.2 | 低相关 | ⚪ |
使用 --explain 可查看每个结果的详细评分分解。
🚀 未来展望
QMD 代表了个人化 AI 应用的一个趋势:本地化、隐私友好、高性能。可能的演进方向:
- 增量索引 - 全量 → 实时增量更新
- 分布式索引 - 单设备 → 多设备协同
- 多语言模型 - 引入更多轻量化多语言嵌入模型
- GUI 界面 - 纯命令行 → 可视化前端
- 插件系统 - 自定义分块策略、重排序器
- Robertson, S., & Zaragoza, H. (2009). The Probabilistic Relevance Framework: BM25 and Beyond.
- Lin, H. et al. (2023). HyDE: Hypothetical Document Embeddings.
- Karpukhin, V. et al. (2020). Dense Passage Retrieval for Open-Domain Question Answering.
- Nogueira, R. et al. (2022). Document Chunking for Enhanced Retrieval-Augmented Generation.
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!



