Raw capture β raw/qmd.md
url: https://github.com/tobi/qmd date_fetched: 2026-07-05 backfilled: true
An on-device search engine for everything you need to remember. Index your markdown notes, meeting transcripts, documentation, and knowledge bases. Search with keywords or natural language. Ideal for your agentic flows.
QMD combines BM25 full-text search, vector semantic search, and LLM re-rankingβall running locally via node-llama-cpp with GGUF models.
You can read more about QMD's progress in the CHANGELOG.
# Install globally (Node or Bun)
npm install -g @tobilu/qmd
# or
bun install -g @tobilu/qmd
# Or run directly
npx @tobilu/qmd ...
bunx @tobilu/qmd ...
# Create collections for your notes, docs, and meeting transcripts
qmd collection add ~/notes --name notes
qmd collection add ~/Documents/meetings --name meetings
qmd collection add ~/work/docs --name docs
# Add context to help with search results, each piece of context will be returned when matching sub documents are returned. This works as a tree. This is the key feature of QMD as it allows LLMs to make much better contextual choices when selecting documents. Don't sleep on it!
qmd context add qmd://notes "Personal notes and ideas"
qmd context add qmd://meetings "Meeting transcripts and notes"
qmd context add qmd://docs "Work documentation"
# Generate embeddings for semantic search
qmd embed
# Search across everything
qmd search "project timeline" # Fast keyword search
qmd vsearch "how to deploy" # Semantic search
qmd query "quarterly planning process" # Hybrid + reranking (best quality)
# Get a specific document
qmd get "meetings/2024-01-15.md"
# Get a document by docid (shown in search results)
qmd get "#abc123"
# Get multiple documents by glob pattern
qmd multi-get "journals/2025-05*.md"
# Search within a specific collection
qmd search "API" -c notes
# Export all matches for an agent
qmd search "API" --all --files --min-score 0.3
QMD's --json and --files output formats are designed for agentic workflows:
# Get structured results for an LLM
qmd search "authentication" --json -n 10
# List all relevant files above a threshold
qmd query "error handling" --all --files --min-score 0.4
# Retrieve full document content
qmd get "docs/api-reference.md" --full
Although the tool works perfectly fine when you just tell your agent to use it on the command line, it also exposes an MCP (Model Context Protocol) server for tighter integration.
Tools exposed:
queryβ Search with typed sub-queries (- lex/- vec/- hyde), combined via RRF + rerankinggetβ Retrieve a document by path or docid (with fuzzy matching suggestions)multi_getβ Batch retrieve by glob pattern, comma-separated list, or docidsstatusβ Index health and collection infoClaude Desktop configuration (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"qmd": {
"command": "qmd",
"args": ["mcp"]
}
}
}
Claude Code β Install the plugin (recommended):
claude plugin marketplace add tobi/qmd
claude plugin install qmd@qmd
Or configure MCP manually in ~/.claude/settings.json:
{
"mcpServers": {
"qmd": {
"command": "qmd",
"args": ["mcp"]
}
}
}
By default, QMD's MCP server uses stdio (launched as a subprocess by each client). For a shared, long-lived server that avoids repeated model loading, use the HTTP transport:
# Foreground (Ctrl-C to stop)
qmd mcp --http # localhost:8181
qmd mcp --http --port 8080 # custom port
qmd mcp --http --host 0.0.0.0 # bind all interfaces (e.g. container probes)
# Background daemon
qmd mcp --http --daemon # start, writes PID to ~/.cache/qmd/mcp.pid
qmd mcp stop # stop via PID file
qmd status # shows "MCP: running (PID ...)" when active
The server binds to localhost by default. Pass --host (or set the QMD_HOST
environment variable) to override β --host 0.0.0.0 is useful when the server
runs in a container and a liveness probe connects from a non-loopback address.
The HTTP server exposes two endpoints:
POST /mcpβ MCP Streamable HTTP (JSON responses, stateless)GET /healthβ liveness check with uptimeLLM models stay loaded in VRAM across requests. Embedding/reranking contexts are disposed after 5 min idle and transparently recreated on the next request (~1s penalty, models remain loaded).
Point any MCP client at http://localhost:8181/mcp to connect.
| Tool | Parameter | Type | Notes |
|---|---|---|---|
query |
searches |
array | Typed sub-queries ( lex/vec/hyde), 1β10.Required.First gets 2x weight. |
query |
collections |
string[] | Filter by collection names (OR). Array onlyβ singularcollectionis silently ignored. |
query |
intent |
string | Disambiguation context (does not search on its own) |
query |
limit |
number | Max results (default 10) |
query |
minScore |
number | Minimum relevance 0β1 (default 0) |
query |
candidateLimit |
number | Max candidates to rerank (default 40) |
query |
rerank |
boolean | Run LLM reranking (default true); set false for RRF-only |
get |
file |
string | Path, docid ( #abc123), orpath:from:count(e.g.#abc123:120:40) |
get |
fromLine |
number | Start line (1-indexed); overrides the :fromsuffix |
get |
maxLines |
number | Limit returned lines |
get |
lineNumbers |
boolean | Prefix lines with numbers (default true) |
multi_get |
pattern |
string | Glob pattern or comma-separated list |
multi_get |
maxBytes |
number | Skip files larger than N (default 10240) |
multi_get |
maxLines |
number | Limit lines per file |
multi_get |
lineNumbers |
boolean | Prefix lines with numbers (default true) |
Unknown parameters are silently ignored (not rejected) β double-check names if
results seem unscoped. The HTTP /query and /search endpoints return
qmd://collection/path URIs in the file field, matching the CLI and MCP output.
Use QMD as a library in your own Node.js or Bun applications.
npm install @tobilu/qmd````
import { createStore } from '@tobilu/qmd'
const store = await createStore({
dbPath: './my-index.sqlite',
config: {
collections: {
docs: { path: '/path/to/docs', pattern: '**/*.md' },
},
},
})
const results = await store.search({ query: "authentication flow" })
console.log(results.map(r =>${r.title} (${Math.round(r.score * 100)}%)`))
await store.close()
`createStore()` accepts three modes:
import { createStore } from '@tobilu/qmd' // 1. Inline config β no files needed besides the DB const store = await createStore({ dbPath: './index.sqlite', config: { collections: { docs: { path: '/path/to/docs', pattern: '*/.md' }, notes: { path: '/path/to/notes' }, }, }, }) // 2. YAML config file β collections defined in a file const store2 = await createStore({ dbPath: './index.sqlite', configPath: './qmd.yml', }) // 3. DB-only β reopen a previously configured store const store3 = await createStore({ dbPath: './index.sqlite' })
The unified `search()` method handles both simple queries and pre-expanded structured queries:
// Simple query β auto-expanded via LLM, then BM25 + vector + reranking const results = await store.search({ query: "authentication flow" }) // With options const results2 = await store.search({ query: "rate limiting", intent: "API throttling and abuse prevention", collection: "docs", limit: 5, minScore: 0.3, explain: true, }) // Pre-expanded queries β skip auto-expansion, control each sub-query const results3 = await store.search({ queries: [ { type: 'lex', query: '"connection pool" timeout -redis' }, { type: 'vec', query: 'why do database connections time out under load' }, ], collections: ["docs", "notes"], }) // Skip reranking for faster results const fast = await store.search({ query: "auth", rerank: false })
For direct backend access:
// BM25 keyword search (fast, no LLM) const lexResults = await store.searchLex("auth middleware", { limit: 10 }) // Vector similarity search (embedding model, no reranking) const vecResults = await store.searchVector("how users log in", { limit: 10 }) // Manual query expansion for full control const expanded = await store.expandQuery("auth flow", { intent: "user login" }) const results4 = await store.search({ queries: expanded })
// Get a document by path or docid const doc = await store.get("docs/readme.md") const byId = await store.get("#abc123") if (!("error" in doc)) { console.log(doc.title, doc.displayPath, doc.context) } // Get document body with line range const body = await store.getDocumentBody("docs/readme.md", { fromLine: 50, maxLines: 100, }) // Batch retrieve by glob or comma-separated list const { docs, errors } = await store.multiGet("docs/*/.md", { maxBytes: 20480, })
// Add a collection await store.addCollection("myapp", { path: "/src/myapp", pattern: "/*.ts", ignore: ["node_modules/", "*.test.ts"], }) // List collections with document stats const collections = await store.listCollections() // => [{ name, pwd, glob_pattern, doc_count, active_count, last_modified, includeByDefault }] // Get names of collections included in queries by default const defaults = await store.getDefaultCollectionNames() // Remove / rename await store.removeCollection("myapp") await store.renameCollection("old-name", "new-name")
Context adds descriptive metadata that improves search relevance and is returned alongside results:
// Add context for a path within a collection await store.addContext("docs", "/api", "REST API reference documentation") // Set global context (applies to all collections) await store.setGlobalContext("Internal engineering documentation") // List all contexts const contexts = await store.listContexts() // => [{ collection, path, context }] // Remove context await store.removeContext("docs", "/api") await store.setGlobalContext(undefined) // clear global
// Re-index collections by scanning the filesystem
const result = await store.update({
collections: ["docs"], // optional β defaults to all
onProgress: ({ collection, file, current, total }) => {
console.log([${collection}] ${current}/${total} ${file})
},
})
// => { collections, indexed, updated, unchanged, removed, needsEmbedding }
// Generate vector embeddings
const embedResult = await store.embed({
force: false, // true to re-embed everything
chunkStrategy: "auto", // "regex" (default) or "auto" (AST for code files)
onProgress: ({ current, total, collection }) => {
console.log(Embedding ${current}/${total})
},
})
Key types exported for SDK consumers:
import type { QMDStore, // The store interface SearchOptions, // Options for search() LexSearchOptions, // Options for searchLex() VectorSearchOptions, // Options for searchVector() HybridQueryResult, // Search result with score, snippet, context SearchResult, // Result from searchLex/searchVector ExpandedQuery, // Typed sub-query { type: 'lex'|'vec'|'hyde', query } DocumentResult, // Document metadata + body DocumentNotFound, // Error with similarFiles suggestions MultiGetResult, // Batch retrieval result UpdateProgress, // Progress callback info for update() UpdateResult, // Aggregated update result EmbedProgress, // Progress callback info for embed() EmbedResult, // Embedding result StoreOptions, // createStore() options CollectionConfig, // Inline config shape IndexStatus, // From getStatus() IndexHealthInfo, // From getIndexHealth() } from '@tobilu/qmd'
Utility exports:
import { extractSnippet, // Extract a relevant snippet from text addLineNumbers, // Add line numbers to text DEFAULT_MULTI_GET_MAX_BYTES, // Default max file size for multiGet (64KB) Maintenance, // Database maintenance operations } from '@tobilu/qmd'
// Close the store β disposes LLM models and DB connection await store.close()
The SDK requires explicit `dbPath` β no defaults are assumed. This makes it safe to embed in any application without side effects.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β QMD Hybrid Search Pipeline β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ βββββββββββββββββββ β User Query β ββββββββββ¬βββββββββ β ββββββββββββββββ΄βββββββββββββββ βΌ βΌ ββββββββββββββββββ ββββββββββββββββββ β Query Expansionβ β Original Queryβ β (fine-tuned) β β (Γ2 weight) β βββββββββ¬βββββββββ βββββββββ¬βββββββββ β β β 2 alternative queries β ββββββββββββββββ¬βββββββββββββββ β βββββββββββββββββββββββββΌββββββββββββββββββββββββ βΌ βΌ βΌ βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β Original Query β β Expanded Query 1β β Expanded Query 2β ββββββββββ¬βββββββββ ββββββββββ¬βββββββββ ββββββββββ¬βββββββββ β β β βββββββββ΄ββββββββ βββββββββ΄ββββββββ βββββββββ΄ββββββββ βΌ βΌ βΌ βΌ βΌ βΌ βββββββββ βββββββββ βββββββββ βββββββββ βββββββββ βββββββββ β BM25 β βVector β β BM25 β βVector β β BM25 β βVector β β(FTS5) β βSearch β β(FTS5) β βSearch β β(FTS5) β βSearch β βββββ¬ββββ βββββ¬ββββ βββββ¬ββββ βββββ¬ββββ βββββ¬ββββ βββββ¬ββββ β β β β β β βββββββββ¬ββββββββ ββββββββ¬βββββββ ββββββββ¬βββββββ β β β ββββββββββββββββββββββββββΌββββββββββββββββββββββββ β βΌ βββββββββββββββββββββββββ β RRF Fusion + Bonus β β Original query: Γ2 β β Top-rank bonus: +0.05β β Top 30 Kept β βββββββββββββ¬ββββββββββββ β βΌ βββββββββββββββββββββββββ β LLM Re-ranking β β (qwen3-reranker) β β Yes/No + logprobs β βββββββββββββ¬ββββββββββββ β βΌ βββββββββββββββββββββββββ β Position-Aware Blend β β Top 1-3: 75% RRF β β Top 4-10: 60% RRF β β Top 11+: 40% RRF β βββββββββββββββββββββββββ
| Backend | Raw Score | Conversion | Range |
|---|---|---|---|
| FTS (BM25) | SQLite FTS5 BM25 | `Math.abs(score)` | 0 to ~25+ |
| Vector | Cosine distance | `1 / (1 + distance)` | 0.0 to 1.0 |
| Reranker | LLM 0-10 rating | `score / 10` | 0.0 to 1.0 |
The `query` command uses **Reciprocal Rank Fusion (RRF)** with position-aware blending:
- **Query Expansion**: Original query (Γ2 for weighting) + 1 LLM variation
- **Parallel Retrieval**: Each query searches both FTS and vector indexes
- **RRF Fusion**: Combine all result lists using- `score = Ξ£(1/(k+rank+1))`where k=60
- **Top-Rank Bonus**: Documents ranking #1 in any list get +0.05, #2-3 get +0.02
- **Top-K Selection**: Take top 30 candidates for reranking
- **Re-ranking**: LLM scores each document (yes/no with logprobs confidence)
- **Position-Aware Blending**:- RRF rank 1-3: 75% retrieval, 25% reranker (preserves exact matches)
- RRF rank 4-10: 60% retrieval, 40% reranker
- RRF rank 11+: 40% retrieval, 60% reranker (trust reranker more)
**Why this approach**: Pure RRF can dilute exact matches when expanded queries don't match. The top-rank bonus preserves documents that score #1 for the original query. Position-aware blending prevents the reranker from destroying high-confidence retrieval results.
| Score | Meaning |
|---|---|
| 0.8 - 1.0 | Highly relevant |
| 0.5 - 0.8 | Moderately relevant |
| 0.2 - 0.5 | Somewhat relevant |
| 0.0 - 0.2 | Low relevance |
- **Node.js**>= 22
- **Bun**>= 1.0.0
- **macOS**: Homebrew SQLite (for extension support)- brew install sqlite
QMD uses three local GGUF models (auto-downloaded on first use):
| Model | Purpose | Size |
|---|---|---|
| `embeddinggemma-300M-Q8_0` | Vector embeddings (default) | ~300MB |
| `qwen3-reranker-0.6b-q8_0` | Re-ranking | ~640MB |
| `qmd-query-expansion-1.7B-q4_k_m` | Query expansion (fine-tuned) | ~1.1GB |
Models are downloaded from HuggingFace and cached in `~/.cache/qmd/models/`.
Override the default embedding model via the `QMD_EMBED_MODEL` environment variable.
This is useful for multilingual corpora (e.g. Chinese, Japanese, Korean) where
`embeddinggemma-300M` has limited coverage.
export QMD_EMBED_MODEL="hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf"
qmd embed -f
Supported model families:
- **embeddinggemma**(default) β English-optimized, small footprint
- **Qwen3-Embedding**β Multilingual (119 languages including CJK), MTEB top-ranked
Note:When switching embedding models, you must re-index with`qmd embed -f`since vectors are not cross-compatible between models. The prompt format is automatically adjusted for each model family.
npm install -g @tobilu/qmd
bun install -g @tobilu/qmd
git clone https://github.com/tobi/qmd cd qmd npm install npm link
qmd collection add . --name myproject
qmd collection add ~/Documents/notes --name notes --mask "*/.md"
qmd collection list
qmd collection remove myproject
qmd collection rename myproject my-project
qmd ls notes qmd ls notes/subfolder
qmd collection show notes
qmd collection include notes qmd collection exclude notes
qmd update (e.g. git pull); empty arg clears itqmd collection update-cmd notes 'git pull --rebase' qmd collection update-cmd notes
qmd embed
qmd embed -f
qmd embed --chunk-strategy auto
qmd query "auth flow" --chunk-strategy auto
qmd embed --max-docs-per-batch 50 # cap docs per embedding batch qmd embed --max-batch-mb 64 # cap batch size in MB
**AST-aware chunking** (`--chunk-strategy auto`) uses tree-sitter to chunk code
files at function, class, and import boundaries instead of arbitrary text
positions. This produces higher-quality chunks and better search results for
codebases. Markdown and other file types always use regex-based chunking
regardless of strategy.
The default is `regex` (existing behavior). Use `--chunk-strategy auto` to
opt in. Run `qmd status` to verify which grammars are available.
Note:Tree-sitter grammars are optional dependencies. If they are not installed,`--chunk-strategy auto`falls back to regex-only chunking automatically. Tested on both Node.js and Bun.
Context adds descriptive metadata to collections and paths, helping search understand your content.
qmd context add qmd://notes "Personal notes and ideas" qmd context add qmd://docs/api "API documentation"
cd ~/notes && qmd context add "Personal notes and ideas" cd ~/notes/work && qmd context add "Work-related notes"
qmd context add / "Knowledge base for my projects"
qmd context list
qmd context rm qmd://notes/old
The `collection` and `context` commands above all read and write a single YAML
config file β you can also edit it directly. Everything QMD knows about your
collections (paths, masks, exclusions, per-collection update hooks, contexts, and
optional model overrides) lives here. A fully-commented starter template ships as
`example-index.yml` in this repo.
**Location:** `~/.config/qmd/index.yml` by default. The directory honors
`XDG_CONFIG_HOME` (β `$XDG_CONFIG_HOME/qmd/index.yml`) and `QMD_CONFIG_DIR`. A
named index uses `{name}.yml` β `qmd --index work β¦` reads/writes `work.yml`.
A **project-local** index created with `qmd init` lives at `.qmd/index.yml`
(`.qmd/index.yaml` is also accepted) alongside a project-local `index.sqlite`,
so config and index stay inside the project instead of `~/.config` / `~/.cache`.
global_context: "Knowledge base for my projects"
editor_uri: "vscode://file{path}:{line}:{col}"
qmd init writes this block pre-filled with themodels: embed: "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf" rerank: "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf" generate: "hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf"
collections:
notes:
path: /Users/me/notes # absolute path to index (required)
pattern: "/*.md" # glob mask (default: /.md)
ignore: # glob patterns to exclude from indexing
- "Archive/"
- "/drafts/*"
update: "git pull --rebase" # bash command run before each qmd update
includeByDefault: true # include in unscoped queries (default: true)
context: # path prefix β description; longest match wins
"/": "Personal notes and ideas"
"/work": "Work-related notes"
| Key | Scope | Purpose |
|---|---|---|
| `global_context` | top-level | Context prepended for every collection. Set via `qmd context add /`. |
| `editor_uri`(alias`editor_uri_template`) | top-level | Hyperlink template for clickable result paths; `QMD_EDITOR_URI`overrides. |
| `models.embed`/`.rerank`/`.generate` | top-level | HuggingFace GGUF URIs ( `hf:<user>/<repo>/<file>`) overriding the built-in defaults per role. |
| `collections.<name>.path` | per-collection | Absolute directory to index. |
| `collections.<name>.pattern` | per-collection | Glob mask. Set via `qmd collection add --mask`. Default`**/*.md`. |
| `collections.<name>.ignore` | per-collection | Glob patterns excluded from indexing β useful to stop nested collections double-indexing. YAML-only β no CLI command sets this.Additive with QMD's built-in exclusions (`node_modules`,`.git`,`.cache`,`vendor`,`dist`,`build`), which you cannot un-ignore. |
| `collections.<name>.update` | per-collection | Bash command run before `qmd update`re-indexes this collection. Set via`qmd collection update-cmd`. |
| `collections.<name>.includeByDefault` | per-collection | Whether unscoped queries search it. Toggle with `qmd collection include`/`exclude`. Default`true`. |
| `collections.<name>.context` | per-collection | Path-prefix β description map; the most specific (longest) matching prefix wins. Set via `qmd context add`. |
Note:Editing`index.yml`changes which directories and models QMDuses, but does not re-index on its own. Run`qmd update`after changing`path`,`pattern`, or`ignore`, and`qmd embed`after changing`models.embed`.
A collection's `update` field is QMD's built-in refresh hook: when you run
`qmd update`, each collection's `update` command runs **first**, then the
collection is re-indexed. This keeps a collection in sync with an upstream source
(a git remote, a sync script) without wrapping `qmd` yourself.
collections: wiki: path: ~/reference/wiki update: "git pull --ff-only"
$ qmd update [1/3] wiki (/*.md) Running update command: git pull --ff-only Already up to date. Collection: ~/reference/wiki (/*.md) Indexed: 0 new, 2 updated, 340 unchanged, 0 removed
The command runs via `bash -c` in the collection's own directory (its `path`), not
your current working directory. If it exits non-zero, `qmd update` prints the
failure and **aborts the entire run** β collections after the failing one are not
re-indexed. Set or clear it from the CLI instead of editing YAML by hand:
qmd collection update-cmd wiki 'git pull --ff-only' # set qmd collection update-cmd wiki # clear
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β Search Modes β ββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β search β BM25 full-text search only β β vsearch β Vector semantic search only β β query β Hybrid: FTS + Vector + Query Expansion + Re-ranking β ββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
qmd search "authentication flow"
qmd vsearch "how to login"
qmd query "user authentication"
Two aliases exist for the semantic/hybrid modes: `vector-search` (β `vsearch`)
and `deep-search` (β `query`).
-n # Number of results (default: 5, or 20 for --files/--json) -c, --collection # Restrict search to a specific collection --all # Return all matches (use with --min-score to filter) --min-score # Minimum score threshold (default: 0) --full # Show full document content --line-numbers # Add line numbers to output --explain # Include retrieval score traces (query, JSON/CLI output) --index # Use named index --intent "" # Disambiguation context (e.g. "web page load times") --no-rerank # Skip LLM reranking (RRF scores only; faster on CPU) -C, --candidate-limit # Max candidates to rerank (default: 40) --full-path # Emit on-disk filesystem paths instead of qmd:// URIs
--format # cli (default) | json | csv | md | xml | files # (--json, --csv, --md, --xml, --files are legacy aliases)
qmd get [:from[:count]] # Get document; optional start line and count -l # Maximum lines to return --from # Start line (overrides the :from suffix) --no-line-numbers # Disable line numbering (on by default)
-l # Maximum lines per file --max-bytes # Skip files larger than N bytes (default: 64KB)
The `-c`/`--collection` flag filters results by collection **name** (as shown by
`qmd collection list`). Collections are a global registry β you can search any
collection from any directory:
qmd search "auth" -c notes # single collection qmd search "auth" -c notes -c docs # multiple collections (OR)
With no `-c` flag, all default-included collections are searched. Collections
marked excluded (`qmd collection exclude <name>`) are skipped unless named
explicitly with `-c`.
Note:With multiple`-c`flags, results come from a global top-K pool and are then filtered. If one collection dominates the rankings, matches from smaller collections may not appear at the default limit β raise`-n`or use`--all`.
Default output is colorized CLI format (respects `NO_COLOR` env).
When stdout is a TTY, result paths are emitted as clickable terminal hyperlinks (OSC 8). Clicking a path opens the file in your editor using an editor URI template.
When stdout is not a TTY (for example piped to another command or redirected to a file), QMD emits plain text paths with no escape sequences.
TTY example:
docs/guide.md:42 #a1b2c3 Title: Software Craftsmanship Context: Work documentation Score: 93% This section covers the craftsmanship of building quality software with attention to detail. See also: engineering principles notes/meeting.md:15 #d4e5f6 Title: Q4 Planning Context: Personal notes and ideas Score: 67% Discussion about code quality and craftsmanship in the development process.
Configure the editor link target with `QMD_EDITOR_URI` (or `editor_uri` in config):
export QMD_EDITOR_URI="vscode://file/{path}:{line}:{col}"
export QMD_EDITOR_URI="cursor://file/{path}:{line}:{col}"
export QMD_EDITOR_URI="zed://file/{path}:{line}:{col}"
export QMD_EDITOR_URI="subl://open?url=file://{path}&line={line}"
Template placeholders:
-
`{path}`absolute filesystem path (URI-encoded)
-
`{line}`1-based line number
-
`{col}`or`{column}`1-based column number
-
**Path**: Collection-relative path (e.g.,`docs/guide.md`)
-
**Docid**: Short hash identifier (e.g.,`#a1b2c3`) - use with`qmd get #a1b2c3`
-
**Title**: Extracted from document (first heading or filename)
-
**Context**: Path context if configured via`qmd context add`
-
**Score**: Color-coded (green >70%, yellow >40%, dim otherwise)
-
**Snippet**: Context around match with query terms highlighted
qmd query -n 10 --min-score 0.3 "API design patterns"
qmd search --md --full "error handling"
qmd query --json "quarterly reports"
qmd query --json --explain "quarterly reports"
qmd --index work search "quarterly reports"
The `--explain` flag attaches a score breakdown to each result: the FTS/vector
backend scores plus the RRF fusion math (rank, weight, top-rank bonus) and every
sub-query's contribution. Abbreviated:
{ "docid": "#6c90f0", "score": 0.89, "file": "qmd://qmd/README.md", "explain": { "ftsScores": [0.892, 0.907], "vectorScores": [0.540, 0.484], "rrf": { "rank": 1, "weight": 0.75, "baseScore": 0.123, "topRankBonus": 0.05, "totalScore": 0.173, "contributions": [ { "source": "fts", "queryType": "original", "query": "reranking", "rank": 1, "weight": 2, "backendScore": 0.892, "rrfContribution": 0.0328 } ] } } }
qmd status
git pull), it runs first β set one with qmd collection update-cmd.qmd update
qmd doctor
qmd init
qmd get notes/meeting.md
qmd get "#abc123"
qmd get notes/meeting.md:50 -l 100
qmd get notes/meeting.md:120:40 qmd get "#abc123:120:40"
qmd get notes/meeting.md --no-line-numbers
qmd multi-get "journals/2025-05*.md"
qmd multi-get "doc1.md, doc2.md, #abc123"
qmd multi-get "docs/*.md" --max-bytes 20480
qmd multi-get "docs/*.md" --json
qmd cleanup
Measure search quality across all four backends with `qmd bench` and a fixture file
of queries with known-relevant documents.
**From a git checkout**, an example fixture and its test corpus ship in the repo:
qmd collection add test/eval-docs --name eval-docs qmd embed -c eval-docs
qmd bench src/bench/fixtures/example.json
qmd bench src/bench/fixtures/example.json --json
The example fixture (
`src/bench/fixtures/example.json`) and its test corpus (`test/eval-docs/`) exist only in a git checkout β they arenotpart of the published npm package. If you installed via`npm`/`npx`, write your own fixture (see below) against a collection you have already indexed:qmd bench my-fixture.json -c my-collection
Each query runs against four backends, reporting precision@k, recall, MRR, and F1:
| Backend | What it tests | LLM required |
|---|---|---|
| `bm25` | Keyword search only (FTS5) | No |
| `vector` | Semantic similarity only | Embedding model |
| `hybrid` | BM25 + vector fusion (no reranking) | Embedding model |
| `full` | Full pipeline with LLM reranking | All three models |
**Score interpretation:** `1.00` = perfect (all expected docs in top results),
`0.00` = complete miss. The example fixture typically shows bm25 ~0.50, vector
~0.70, and hybrid/full ~1.00 β a concrete demonstration of why hybrid search beats
either backend alone.
**Custom fixtures** are JSON:
{ "description": "My benchmark", "version": 1, "collection": "my-collection", "queries": [ { "id": "find-auth", "query": "authentication flow", "type": "semantic", "expected_files": ["docs/auth-design.md"], "expected_in_top_k": 3 } ] }
`expected_files` are collection-relative paths as shown by `qmd ls`. The `type`
field (`exact`, `semantic`, `topical`, `cross-domain`, `alias`) labels queries for
grouping β it does not change search behavior.
Heads-up:if the fixture's collection isn't indexed, bench currently runs to completion and reports all zeros with no warning. Verify setup with`qmd ls <collection>`first.
Index stored in: `~/.cache/qmd/index.sqlite`
collections -- Indexed directories with name and glob patterns path_contexts -- Context descriptions by virtual path (qmd://...) documents -- Markdown content with metadata and docid (6-char hash) documents_fts -- FTS5 full-text index content_vectors -- Embedding chunks (hash, seq, pos, 900 tokens each) vectors_vec -- sqlite-vec vector index (hash_seq key) llm_cache -- Cached LLM responses (query expansion, rerank scores)
| Variable | Default | Description |
|---|---|---|
| `XDG_CACHE_HOME` | `~/.cache` | Cache directory location |
| `XDG_CONFIG_HOME` | `~/.config` | Config directory location (where `index.yml`lives) |
| `QMD_CONFIG_DIR` | unset | Override the config directory outright (takes precedence over `XDG_CONFIG_HOME`) |
| `QMD_LLAMA_GPU` | `auto` | Force llama.cpp GPU backend ( `metal`,`vulkan`,`cuda`) or disable GPU with`false` |
| `QMD_FORCE_CPU` | unset | Set to `1`/`true`to force CPU mode before any CUDA/Vulkan/Metal probing. Equivalent CLI flag:`--no-gpu`. |
| `QMD_EMBED_PARALLELISM` | automatic | Override embedding/reranking context parallelism (1-8). Windows CUDA defaults to `1`because parallel CUDA contexts can crash with`ggml-cuda.cu:98`; use Vulkan or raise this only if your driver is stable. |
Collection βββΊ Glob Pattern βββΊ Markdown Files βββΊ Parse Title βββΊ Hash Content β β β β β βΌ β β Generate docid β β (6-char hash) β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββΊββββΊ Store in SQLite β βΌ FTS5 Index
Documents are chunked into ~900-token pieces with 15% overlap using smart boundary detection:
Document βββΊ Smart Chunk (~900 tokens) βββΊ Format each chunk βββΊ node-llama-cpp βββΊ Store Vectors β "title | text" embedBatch() β βββΊ Chunks stored with: - hash: document hash - seq: chunk sequence (0, 1, 2...) - pos: character position in original
Instead of cutting at hard token boundaries, QMD uses a scoring algorithm to find natural markdown break points. This keeps semantic units (sections, paragraphs, code blocks) together.
**Break Point Scores:**
| Pattern | Score | Description |
|---|---|---|
| `# Heading` | 100 | H1 - major section |
| `## Heading` | 90 | H2 - subsection |
| `### Heading` | 80 | H3 |
| `#### Heading` | 70 | H4 |
| `##### Heading` | 60 | H5 |
| `###### Heading` | 50 | H6 |
| ````` | 80 | Code block boundary |
| `---`/`***` | 60 | Horizontal rule |
| Blank line | 20 | Paragraph boundary |
| `- item`/`1. item` | 5 | List item |
| Line break | 1 | Minimal break |
**Algorithm:**
- Scan document for all break points with scores
- When approaching the 900-token target, search a 200-token window before the cutoff
- Score each break point: `finalScore = baseScore Γ (1 - (distance/window)Β² Γ 0.7)`
- Cut at the highest-scoring break point
The squared distance decay means a heading 200 tokens back (score ~30) still beats a simple line break at the target (score 1), but a closer heading wins over a distant one.
**Code Fence Protection:** Break points inside code blocks are ignoredβcode stays together. If a code block exceeds the chunk size, it's kept whole when possible.
**AST-Aware Chunking (Code Files):**
For supported code files, QMD also parses the source with tree-sitter and adds AST-derived break points that are merged with the regex scores above:
| AST Node | Score | Languages |
|---|---|---|
| Class / interface / struct / impl / trait | 100 | All |
| Function / method | 90 | All |
| Type alias / enum | 80 | All |
| Import / use declaration | 60 | All |
Supported for `.ts`, `.tsx`, `.js`, `.jsx`, `.py`, `.go`, and `.rs` files. Enable with `--chunk-strategy auto`. Markdown and other file types always use regex chunking.
Query βββΊ LLM Expansion βββΊ [Original, Variant 1, Variant 2] β βββββββββββ΄ββββββββββ βΌ βΌ For each query: FTS (BM25) β β βΌ βΌ Vector Search Ranked List β βΌ Ranked List β βββββββββββ¬ββββββββββ βΌ RRF Fusion (k=60) Original query Γ2 weight Top-rank bonus: +0.05/#1, +0.02/#2-3 β βΌ Top 30 candidates β βΌ LLM Re-ranking (yes/no + logprob confidence) β βΌ Position-Aware Blend Rank 1-3: 75% RRF / 25% reranker Rank 4-10: 60% RRF / 40% reranker Rank 11+: 40% RRF / 60% reranker β βΌ Final Results
The default models are defined in `src/llm.ts` as HuggingFace URIs:
const DEFAULT_EMBED_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf"; const DEFAULT_RERANK_MODEL = "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf"; const DEFAULT_GENERATE_MODEL = "hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf";
Override them per-role without touching source via the `models:` block in
`index.yml` (see Configuring `index.yml`) or the
`QMD_EMBED_MODEL` env var. Re-run `qmd embed` after changing the embedding model.
// For queries
"task: search result | query: {query}"
// For documents
"title: {title} | text: {content}"
``
Uses node-llama-cpp'screateRankingContext()andrankAndSort()` API for cross-encoder reranking. Returns documents sorted by relevance score (0.0 - 1.0).
Used for generating query variations via LlamaChatSession.
MIT