日期: 2026-04-05
環境: Docker (OrbStack) · Bun 1.3.10 · QMD 2.0.1 · Linux arm64
語料: Obsidian vault MAGI(517 個 Markdown 檔,513 成功索引)
實驗者: Claude Code
QMD(Query Markup Documents,@tobilu/qmd)是一套本機混合式搜尋引擎,支援三種查詢模式:
| 指令 | 機制 |
|---|---|
qmd search |
BM25 全文檢索(FTS5) |
qmd vsearch |
純向量語意搜尋 |
qmd query |
BM25 + 向量 + RRF fusion + LLM reranker(最高品質) |
QMD 預設使用 embeddinggemma-300M-Q8_0(768-dim)作為 embedding model。對於中文語料,該模型語言覆蓋率不足,官方支援透過環境變數 QMD_EMBED_MODEL 切換為多語言模型。
本實驗使用 Qwen3-Embedding-0.6B-Q8_0(1024-dim)作為替代模型,以改善中文檢索品質。使用者反映:索引與 qmd search 可正常運作,但 qmd query 會失敗,懷疑 query 路徑未正確沿用自訂 embedding 設定,導致向量維度不符(768 vs 1024 mismatch)。
實驗目標:
- 確認此 mismatch 是否能重現,並取得具體的 error message 與實際維度數字
- 定位 bug 在 QMD 原始碼的確切位置
- 提出可能的修復方式
基底映像:oven/bun:1-debian
額外套件:sqlite3、python3、make、g++(供 better-sqlite3 原生編譯)
QMD 安裝:bun install -g @tobilu/qmd
| 模型 | 用途 | 大小 | 向量維度 |
|---|---|---|---|
embeddinggemma-300M-Q8_0 |
預設 embedding | 328 MB | 768 |
Qwen3-Embedding-0.6B-Q8_0 |
自訂 embedding(中文) | 639 MB | 1024 |
qwen3-reranker-0.6b-q8_0 |
Re-ranking | 639 MB | — |
qmd-query-expansion-1.7B-q4_k_m |
Query 展開 | 1.28 GB | — |
- 來源:
~/Library/Mobile Documents/iCloud~md~obsidian/Documents/MAGI - 總計:517 個
.md檔,513 成功索引,507 個唯一 hash 需 embedding - Embedding 結果:896 chunks 成功,554 chunks 失敗(部分文件過長或格式問題)
export QMD_EMBED_MODEL="hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf"
qmd collection add /magi --name magi
qmd update # 索引文件
qmd embed # 使用 Qwen3 生成向量(1024-dim)嵌入完成後,透過 SQLite 查詢確認儲存的向量維度與 model tag:
sqlite3 ~/.cache/qmd/index.sqlite ".schema vectors_vec"
sqlite3 ~/.cache/qmd/index.sqlite \
"SELECT DISTINCT model, COUNT(*) FROM content_vectors GROUP BY model;"並測試三種模式(全部設定 QMD_EMBED_MODEL):
qmd search "知識管理"/"學習筆記"/"工作流程"qmd vsearch "知識管理"/"學習筆記"/"工作流程"qmd query "知識管理"/"學習筆記"/"工作流程"
unset QMD_EMBED_MODEL
qmd query "知識管理"
qmd query "學習筆記"
qmd query "工作流程"此情境模擬使用者在不同 shell session 下使用 QMD 的真實情況。
-- vectors_vec 虛擬表 schema
CREATE VIRTUAL TABLE vectors_vec USING vec0(
hash_seq TEXT PRIMARY KEY,
embedding float[1024] distance_metric=cosine
);
-- content_vectors 的 model tag
embeddinggemma | 896 chunks關鍵發現:
- 向量維度確實為 1024-dim(Qwen3 所產生)
- 但 model tag 全部被記錄為
"embeddinggemma",而非 Qwen3 的 URI- 這是一個附帶 bug:model tag 標記錯誤
三種模式在 QMD_EMBED_MODEL 設定的情況下均正常運作:
| 模式 | 知識管理 |
學習筆記 |
工作流程 |
|---|---|---|---|
search |
✅ 成功(BM25) | ✅ 成功 | ✅ 成功 |
vsearch |
✅ 成功(向量) | ✅ 成功 | ✅ 成功 |
query |
✅ 成功(混合) | ✅ 成功 | ✅ 成功 |
此結果表面上看似 bug 未重現,但實為意外成功——原因詳見第五節分析。
Phase 2 中,QMD 自動下載並載入預設 embedding 模型 embeddinggemma-300M-Q8_0(328 MB,768-dim),隨即在向量查詢時觸發維度不符錯誤。
三組 query 全部出現相同 error:
▷ qmd query "知識管理"
...
Embedding 4 queries... (8.6s)
2046 | // Step 1: Get vector matches from sqlite-vec (no JOINs allowed)
2047 | const vecResults = db.prepare(`
2048 | SELECT hash_seq, distance
2049 | FROM vectors_vec
2050 | WHERE embedding MATCH ? AND k = ?
2051 | `).all(new Float32Array(embedding), limit * 3);
^
SQLiteError: Dimension mismatch for query vector for the "embedding" column.
Expected 1024 dimensions but received 768.
errno: 1,
byteOffset: -1,
at searchVec (dist/store.js:2051:6)
at hybridQuery (dist/store.js:2830:44)
Bun v1.3.10 (Linux arm64)
| Query | 結果 |
|---|---|
知識管理 |
❌ SQLiteError: Expected 1024 dims, got 768 |
學習筆記 |
❌ SQLiteError: Expected 1024 dims, got 768 |
工作流程 |
❌ SQLiteError: Expected 1024 dims, got 768 |
Bug 成功重現。
QMD 原始碼中存在兩個各自獨立定義 default embedding model 的地方:
src/llm.ts(正確讀取 env var,控制實際載入的 GGUF 模型):
const DEFAULT_EMBED_MODEL = process.env.QMD_EMBED_MODEL
?? "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";LlamaCpp singleton 以此值初始化,因此當 QMD_EMBED_MODEL 設定時,載入的是 Qwen3(1024-dim)。
src/store.ts 約第 42 行(靜態字串,完全不讀 env var,控制 model tag 與 searchVec 呼叫):
export const DEFAULT_EMBED_MODEL = "embeddinggemma"; // 固定值,不受 env var 影響src/store.ts 中有三處呼叫 searchVec 時,傳入的 model 參數均使用這個靜態常數:
| 原始碼位置(約) | 編譯後位置 | 函式 |
|---|---|---|
src/store.ts ~3994 行 |
dist/store.js:2830 |
hybridQuery(第一趟 vec 搜尋) |
src/store.ts ~4377 行 |
dist/store.js:2830 |
hybridQuery(第二趟 vec 搜尋) |
src/store.ts ~4227 行 |
dist/store.js:2051 |
searchVec(直接呼叫) |
三處均傳入 "embeddinggemma" 字串,而非從 process.env.QMD_EMBED_MODEL 解析出的實際 URI。
qmd embed(設定 QMD_EMBED_MODEL=Qwen3)
│
├─ llm.ts: LlamaCpp singleton 載入 Qwen3(1024-dim)✅
└─ store.ts: vectorIndex("embeddinggemma", ...)
│
└─ 實際執行 embed:LlamaCpp 忽略 "embeddinggemma" 字串,
用已載入的 Qwen3 生成 1024-dim 向量
→ 向量寫入 vectors_vec(float[1024])
→ content_vectors.model = "embeddinggemma"(tag 錯誤)
↓ 使用者開新 terminal,未設定 QMD_EMBED_MODEL
qmd query(未設定 QMD_EMBED_MODEL)
│
├─ llm.ts: LlamaCpp singleton 載入預設 gemma(768-dim)
└─ store.ts: searchVec(query, "embeddinggemma", ...)
│
└─ getEmbedding() → 用 gemma 計算 768-dim query 向量
→ sqlite-vec: WHERE embedding MATCH Float32Array(768)
→ vectors_vec 期待 1024-dim
→ SQLiteError: Expected 1024 dimensions but received 768 ❌
當 QMD_EMBED_MODEL 兩邊都有設定時:
embed:LlamaCpp 用 Qwen3(1024-dim),model tag 存成"embeddinggemma"query:LlamaCpp 也用 Qwen3(1024-dim),searchVec 也傳"embeddinggemma"作為 tag filter
兩邊使用相同的錯誤 tag "embeddinggemma",向量維度也一致(1024 = 1024),因此意外地能正常運作。這是一種以錯補錯的偶然成功,並非正確行為。
在 src/store.ts 中,將三個 searchVec call site 的 model 參數從靜態常數改為讀取 env var 的動態值。
// src/store.ts 頂部
import { DEFAULT_EMBED_MODEL_URI } from './llm.js';
// 三個 searchVec call site 改為:
store.searchVec(query, DEFAULT_EMBED_MODEL_URI, limit, collection)前提是 llm.ts 需將該常數 export。
// src/store.ts 頂部新增
const RESOLVED_EMBED_MODEL = process.env.QMD_EMBED_MODEL
?? "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
// 三個 searchVec call site 改為:
store.searchVec(query, RESOLVED_EMBED_MODEL, limit, collection)此方案不需改動 llm.ts,改動範圍更小。
兩種方案都同步修正了 content_vectors.model 的 tag 錯誤問題(embed 時的 tag 也應改為使用 RESOLVED_EMBED_MODEL)。
| 項目 | 結果 |
|---|---|
| Bug 能否重現 | 是,三組 query 全部確認 |
| 錯誤類型 | SQLiteError(非靜默失敗) |
| 觸發條件 | embed 時有設 QMD_EMBED_MODEL,query 時未設 |
| 儲存向量維度 | 1024-dim(Qwen3 正確生成) |
| Query 向量維度 | 768-dim(gemma 預設,維度不符) |
| Bug 所在檔案 | src/store.ts 約第 42 行及三個 searchVec call site |
| 編譯後 crash 位置 | dist/store.js:2051(searchVec)、2830(hybridQuery) |
| 修復複雜度 | 低(3 個 call site,單行改動) |
GOAL.md 中描述的「query 模式沿用另一組預設維度」確認成立:問題不在資料索引或 Qwen3 模型本身,而在於 store.ts 的 DEFAULT_EMBED_MODEL 常數未讀取 QMD_EMBED_MODEL env var,導致 query 時 sqlite-vec 收到維度不符的查詢向量而拋出 SQLiteError。