Skip to main content

qi-cli

Guide for using qi, a local knowledge search CLI for macOS and Linux. Use this skill when the user wants to index documents into a searchable knowledge base, search or retrieve from one, configure qi's collections or embedding providers (Ollama, OpenAI-compatible), choose between its search modes (lexical BM25 or hybrid BM25 + vector), or diagnose why qi search results look wrong — or on any mention of the qi CLI.

Source facts

Repository
itsmostafa/qi
Last source activity
October 26, 2026 at 16:27
Detected SKILL.md language
English
Stars
43
Forks
7

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
qi-cli
description
Guide for using qi, a local knowledge search CLI for macOS and Linux. Use this skill when the user wants to index documents into a searchable knowledge base, search or retrieve from one, configure qi's collections or embedding providers (Ollama, OpenAI-compatible), choose between its search modes (lexical BM25 or hybrid BM25 + vector), or diagnose why qi search results look wrong — or on any mention of the qi CLI.
qi is a local-first knowledge search CLI. It indexes documents into SQLite and supports BM25 full-text search and vector/hybrid search (with a local or remote embedding provider). ## Quick Start ```bash qi init # Create config + database (config lists a sample ~/notes collection) qi index ~/notes # Index a directory; it is saved to config as a collection qi doctor # Verify setup qi search "your query" # BM25 keyword search (no provider needed) qi query "your semantic question" # Hybrid search; keyword-only, with a warning, if no provider is configured ``` `index`, `search`, `query`, `get`, `list`, `stats`, `delete` and `hook` need a config file, so run `qi init` first. `qi update`, `--version` and `--help` work without one, and `qi doctor` reports a missing file as a failed check. --- ## Commands ### `qi init` Writes `~/.config/qi/config.yaml` (if absent) and initializes the SQLite database. ```bash qi init ``` ### `qi index [path|collection]...` Indexes documents. A file whose size and mtime match the last run is skipped without being read; otherwise SHA-256 content hashing decides whether it changed. Files over 10 MiB and binary files (a NUL byte in the first 8000 bytes) are skipped and counted in `skipped`, as are hidden directories, common tool directories (`node_modules`, `vendor`, `dist`, `build`, …) and symlinks. Documents whose files are gone are deleted from the database in the same run. A second `qi index` against the same database waits for the first to finish rather than failing. Use `--force` to reprocess unchanged files, e.g. after a qi upgrade changes parsing. Without an `extensions` list, a collection indexes only `.md`, `.markdown`, `.txt` and `.text`. List other formats explicitly: `.mdx` (Markdown; MDX `import`/`export` and JSX tag lines are ignored), `.rst` (reStructuredText) and `.adoc`/`.asciidoc` (AsciiDoc) are split at section titles into heading paths; any other listed extension is plain text. Embeddings are generated at index time. If you add an embedding provider after an initial index run, or change its `model`, `dimension`, `base_url` or `max_input_chars`, re-run `qi index` to embed what is missing or stale. An argument starting with `/`, `./`, `../` or `~` (or `.`/`..`) is a path; anything else is a collection name. A new path is saved to config as a collection named after its own directory (`~/Projects/tools/qi` → `qi`); when two collections would share a name, each takes leading path segments until unique (`work-notes`, `personal-notes`). There is no `--name` flag. `qi list` shows current names. ```bash qi index # indexes current working directory qi index ~/notes # an absolute or ~ path qi index ./docs # a relative path needs ./ (plain "docs" is a collection name) qi index notes wiki # collections by name; several arguments are indexed in turn qi index --changed-since ORIG_HEAD . # no-op unless an indexed file changed since that git revision ``` ### `qi hook install [path|collection]` Installs `post-merge`, `post-rewrite` and `post-commit` git hooks in the repository holding the collection. After `git pull` (merge or rebase) or the commit that concludes a conflicted merge, every configured collection inside that repository is reindexed in the background with `qi index --changed-since <sha>`; pulls that touch no indexed file return immediately, and ordinary commits cost one git call. A squash pull leaves no base commit to diff against, so it runs a plain `qi index`, which still skips unchanged files by stat. Installing also registers the collection in config if it is new. Output goes to `qi-index.log` in the git directory. Respects `core.hooksPath`; reinstalling (for any collection in the same repo) rewrites the same hooks, and a hook qi did not write is left alone with the line to add printed instead. ```bash qi hook install # the current directory's repository qi hook install ~/src/monorepo/docs # a collection below the repository root ``` ### `qi search <query>` BM25 full-text search. Fast, offline, no provider needed. ```bash qi search "authentication" qi search "deploy" -c wiki -n 5 ``` ### `qi query <query>` Semantic/hybrid search. Falls back to BM25 and prints a warning on stderr when no embedding provider is configured, or when embedding the query or the vector search fails. ```bash qi query "how does auth work" qi query "deploy pipeline" --mode lexical # BM25 only qi query "deployment steps" --mode hybrid # BM25 + vector (default) qi query "critical path" --mode deep # alias for hybrid qi query "question" --explain # show BM25/vector/RRF score breakdown ``` **Modes:** - `lexical` — BM25 only - `hybrid` — BM25 + vector fused with RRF; skips vector if BM25 has a clear winner. Vector scoring covers only BM25's candidate documents (topped up with any-term matches), so a document sharing no word with the query surfaces only when BM25 finds nothing at all - `deep` — alias for `hybrid` With no `--mode`, the mode comes from `search.default_mode` in the config (`hybrid` by default). ### `qi get <id>` Retrieve the indexed source snapshot by the full `hash` returned in search JSON (or an unambiguous hex prefix). Use the hit's `start_line:end_line` for its source passage. An ambiguous prefix is an error; identical content at multiple paths is returned once, with other locations in `also_at`. ```bash qi get abc123 qi get abc123 --lines 40:80 # 1-indexed, inclusive; "40:" and ":80" also work qi get abc123 --max-bytes 4096 # truncate and say so (0 = unlimited) qi get abc123 --format json # indexed source body and citation metadata ``` Prefer `--lines`/`--max-bytes` over reading whole documents: they are what keeps a retrieved document from swallowing an agent's context. ### `qi list` List all collections defined in config (name and path). ```bash qi list ``` ### `qi delete <collection>` Delete a collection: removes all indexed data from the database and removes the collection entry from config. Irreversible. ```bash qi delete notes ``` ### `qi stats` Show document counts, chunk counts, embedding health, and database size per collection. If `Embeddings` reports missing or stale vectors, vector search lacks data for those chunks — re-run `qi index`. ### `qi doctor` Health-check config, database, collection paths, and embedding coverage (missing, stale, orphaned or corrupt vectors, with the command that repairs each). It does **not** contact the provider: a `SKIP` line means no embedding provider is configured, not that one is unreachable. When none is configured, `qi query` falls back to BM25 with a warning. ### `qi update` Update the binary in place from the latest GitHub release. --- ## Global Flags | Flag | Description | |---|---| | `--verbose` | Log debug detail to stderr (no `-v` shorthand; `-v` is `--version` on `qi` itself) | | `-f, --format text\|json\|markdown` | Output format (default: text) | | `--config <path>` | Override config path | ## `search` and `query` Flags These are per-command, not global. | Flag | Description | |---|---| | `-c, --collection <name>` | Limit to a specific collection | | `-n, --limit <N>` | Number of results (default: 10) | | `--passages <N>` | Additional supporting passages per document, 0–5 (default: 0) | | `--since YYYY-MM-DD` | Only documents dated on or after this day | | `--until YYYY-MM-DD` | Only documents dated on or before this day | | `--sort date` | Newest first instead of by relevance | | `--path <glob>` | Only documents whose collection-relative path matches the glob | Every document has a date. It comes from the frontmatter `timestamp:`, `date:` or `created:` when one of them parses, otherwise from the file's modification time — so no document is excluded from `--since`/`--until` for lack of a date. `--path` takes one glob in the same dialect as a collection's `ignore` list: it matches the path, its basename or any parent directory, so `meetings` and `meetings/*` both select `meetings/q3/retro.md` and `*.log.md` matches at any depth. Paths are relative to their collection, so without `-c` the glob is applied inside every collection. Scope is applied while candidates are still being ranked, so an in-scope hit is not lost behind out-of-scope ones. --- ## Configuration (`~/.config/qi/config.yaml`) ```yaml database_path: ~/.local/share/qi/qi.db collections: - name: notes # regenerated from the path on load path: ~/notes extensions: [.md, .txt] ignore: [drafts, "*.draft.md"] # path.Match globs on the relative path, basename or any parent dir - name: wiki path: ~/wiki extensions: [.md, .mdx, .rst, .adoc] providers: embedding: # optional — enables vector/hybrid search base_url: http://localhost:11434 # Ollama, LM Studio, llama.cpp, OpenAI-compatible model: nomic-embed-text dimension: 768 max_input_chars: 0 # optional — truncate long texts before embedding (0 = no limit) search: default_mode: hybrid # lexical | hybrid (deep is an alias for hybrid) bm25_top_k: 50 vector_top_k: 50 rrf_k: 60 chunk_size: 512 prefer_extensions: [.md, .txt] # optional — boost results with these extensions extension_boost: 2.0 # optional — score multiplier for preferred extensions (default 2.0) ``` `base_url` and `api_key` support `${VAR}` environment-variable expansion, and a trailing `/v1` in `base_url` is normalized automatically (no doubled `/v1/v1`). Relative paths resolve against the config file's directory. `.git`, `node_modules` and other tool directories are always skipped, so they need no `ignore` entry. Changing `model`, `dimension`, `base_url`, `max_input_chars` or the provider `name` makes existing vectors stale until the next `qi index` re-embeds them; `api_key` and `batch_size` (default 32) do not. ### Common provider setups **Ollama (fully local):** ```yaml providers: embedding: base_url: http://localhost:11434 model: nomic-embed-text dimension: 768 ``` **OpenAI:** ```yaml providers: embedding: name: openai # fills in base_url and api_key (from OPENAI_API_KEY) when left empty model: text-embedding-3-small dimension: 1536 ``` --- ## How search works - **BM25** — SQLite FTS5. Always available, very fast, good for keyword queries. - **Vector** — Cosine similarity, computed in Go, over embedding BLOBs stored in SQLite. Requires an embedding provider. Captures semantic intent. Used only inside hybrid mode, where it scores the chunks of BM25's candidate documents, and scans every embedded chunk only when BM25 finds nothing. - **Hybrid (RRF)** — Runs both, fuses rankings with Reciprocal Rank Fusion (`score = Σ 1/(k + rank)`), counting each document once at its best rank. Skips vector if BM25 has a dominant winner (top score > 3× second place). - Results are one per document: a document is represented by its best chunk. - **Deep** — Accepted as an alias of Hybrid. --- ## Document references Search JSON includes `hash` (full SHA-256 retrieval handle/source version), `source_uri` (collection/path identity), and `start_line`/`end_line` (1-indexed, inclusive raw-source range). Text/Markdown output includes a ready-to-run `qi get <hash> --lines A:B` command. Do not pass integer `doc_id` or `chunk_id` to `get`. Reuse the same `--config` override when retrieving a hit. Cite the source URI, hash, and line range together. `--passages N` optionally attaches bounded supporting passages that inherit the parent hash and URI; it does not add ranking votes or result slots for the document. `get` returns the indexed snapshot even if the file has since changed on disk. The hash is not a claim of live-file freshness. Reindexing can remove old versions; qi is not a revision archive. For identical-content paths, retain the search hit's URI rather than substituting get's first location. Search skips chunks with missing or invalid source ranges. Repair older indexes with `qi index <collection>`, or `qi index /path/to/directory` if the collection is no longer in config. --- ## Typical workflows **Index and search (no provider needed):** ```bash qi init qi index ~/notes # saves and indexes ~/notes as "notes" qi search "my keyword" -c notes ``` **Manage collections:** ```bash qi list # see all configured collections qi index ~/projects # add a new collection qi delete old-notes # remove collection data + config entry ``` **Semantic search with Ollama:** ```bash # pull a model: ollama pull nomic-embed-text # add embedding provider to config qi index notes qi query "how does X work" --explain ``` **Debug / inspect:** ```bash qi doctor # check config, database, and embedding coverage qi stats # see document/chunk/embedding counts qi query "question" --explain # see score breakdown qi get abc123 # read the full source document ``` --- ## Troubleshooting ### `qi query` returns keyword-only results despite embedding provider being configured Work through these steps in order: **1. Check if the provider is actually wired up** ```bash qi doctor ``` Look for an `embeddings:` line with `OK`. `SKIP embedding provider (not configured)` means qi found no provider block — common causes: wrong YAML key name (`embedding` not `embeddings`) or bad indentation (it must be under `providers:`). A missing or zero `dimension` is different: the config fails to load and every command reports the error. **2. Check if embeddings were actually generated** ```bash qi stats ``` If `Embeddings` reports missing or stale vectors, they were never written or no longer match the provider. This happens when the embedding provider was added to config *after* the initial `qi index` run, or when its model, dimension or URL changed — re-index to fix it: ```bash qi index <collection-name> ``` Only chunks missing embeddings are re-processed; unchanged files aren't re-parsed. **3. Verify the provider is reachable at query time** `qi query` embeds the query string at runtime. If the provider is down, qi falls back to BM25 and prints `embedding query failed, falling back to BM25` on stderr, but exits successfully. Test connectivity: ```bash # Ollama: curl http://localhost:11434/v1/embeddings -H "Content-Type: application/json" \ -d '{"model":"nomic-embed-text","input":["test"]}' ``` Also confirm the model is pulled: `ollama list`. **4. Use `--explain` to see what's actually happening** ```bash qi query "your question" --explain ``` This shows each result's BM25 rank, vector rank, and fused RRF score. If every result is missing a vector rank, the vector path isn't contributing — confirming one of the issues above. **Note:** hybrid mode also skips vector search if the top BM25 score is more than 3× the second result (intentional optimization for dominant keyword matches). If your queries are exact keyword matches, try a more paraphrased, natural-language query to avoid this shortcut. When BM25 finds at least one candidate, vector scoring covers only those candidates, so a document with no word in common with the query cannot be pulled in by meaning alone. When BM25 finds nothing, qi scans every embedded chunk, and such a document can surface.
View on GitHub