Skip to content
itsmostafaPublic

About

query search engine cli for humans and ai agents

Topics

Resources

Stars

43 stars

Watchers

2 watching

Forks

Repository files navigation

qi - query engine cli for ai agents and humans

License: MIT Claude Releases

qi logo

qi indexes your notes and docs into one local SQLite file and answers searches with the exact file and line range of each match, so you and your AI agents can find and cite the right passage without reading whole files. It runs on macOS and Linux, offline.

$ qi search "rollback"
1. Deploy runbook (bm25: 0.0000) [2026-09-01]
   qi://notes/runbook.md#L9-L9 [Deploy runbook > Rollback]
   qi get 92256d662922d88001924aa57cd56370da30237c5d8d9a870fcd0c9efc347486 --lines 9:9
   To roll back a deploy, run make rollback and confirm the previous tag.

Why qi

An agent that answers questions about your notes usually greps the tree and then reads every file that matched. Each file it opens costs tokens and a round trip, and most of what it reads is irrelevant.

qi does the retrieval up front. qi index splits each document into chunks at headings, code fences and paragraph breaks, and stores them in a SQLite database. A search returns one ranked hit per document, each with a snippet, its heading path and a line range. The agent then fetches only those lines with qi get. The repository ships an agent skill and a Claude Code plugin that teach an agent this workflow.

Install

curl -fsSL https://raw.githubusercontent.com/itsmostafa/qi/main/install.sh | sh

The script downloads the latest release for macOS or Linux (amd64 or arm64), checks it against the release's SHA-256 checksums, and installs it to ~/.local/bin. Set QI_INSTALL_DIR to install somewhere else. If that directory is not on your PATH, the script says so.

Run qi update later to replace the binary with the latest release. If its directory is not writable, run sudo qi update.

With a Go toolchain you can build from source instead:

go install github.com/itsmostafa/qi@latest

Claude Code plugin

/plugin marketplace add itsmostafa/qi
/plugin install qi

Quickstart

qi init                 # write ~/.config/qi/config.yaml and create the database
qi index ~/notes        # index a directory; it becomes the collection "notes"
qi search "rollback"    # keyword search across every collection

qi init writes a sample collection pointing at ~/notes. Edit or delete it if you keep your notes elsewhere; qi doctor reports a failed check for any collection whose path does not exist.

Then try:

qi index                                   # index the current directory
qi index notes                             # re-index a collection by name
qi search "rollback" -c notes              # search one collection
qi search "rollback" --path 'meetings/*'   # only paths matching a glob
qi search "rollback" --since 2026-09-01    # only documents dated on or after
qi search "rollback" -f json               # JSON output for scripts and agents
qi query "how do I undo a release"         # hybrid search, needs an embedding provider
qi hook install ~/src/monorepo             # re-index after every git pull

How search works

qi has two ways to rank chunks and a third that combines them.

  • Keyword search (BM25). BM25 is the standard keyword-ranking formula: a chunk scores higher when it contains your terms often and when those terms are rare across the corpus. qi uses SQLite's built-in FTS5 full-text index for it. It needs no setup. If every term must match and nothing does, qi retries with any term matching.
  • Vector search. An embedding model turns each chunk and your query into a list of numbers that encodes meaning, and qi ranks chunks by how close their vectors are to the query's. Two passages can match this way with no words in common, such as "undo a release" and "roll back a deploy". It needs an embedding provider (see Configuration).
  • Hybrid search. qi query runs both and merges the two rankings with Reciprocal Rank Fusion (RRF), which scores each document by its rank in each list, so a document near the top of either list ranks well. To stay fast, the vector side scores only the documents keyword search found (topped up with documents matching any query term when that list is short), and scans every chunk only when no document matches any term. On a 424,000-chunk test corpus that took a hybrid query from 1.41 seconds to 0.01 to 0.06 seconds. The cost: when keyword search finds any match, a document with no query term in it cannot surface through vector search; it can only when keyword search finds nothing.

qi search is always keyword search. qi query takes --mode lexical or --mode hybrid (deep is an alias for hybrid); the default comes from search.default_mode in the config, which is hybrid. Without an embedding provider, qi query prints a warning and falls back to keyword search. Add --explain to see each hit's BM25 rank, vector rank and RRF score:

qi query "chunking algorithm" --explain

What qi gives you

  • Cited results. Every hit carries a SHA-256 content hash, a qi://collection/path URI and a 1-indexed line range, so an answer can point at its source.
  • One result per document. A long file with many matching chunks takes one slot, at its best chunk, instead of crowding out other documents. --passages N (0 to 5) adds up to N more matching passages under that hit.
  • Date filters. --since, --until and --sort date use the document's frontmatter date (date, timestamp or created), or the file's modification time when there is none.
  • Fast re-indexing. Files whose size and modification time have not changed are not read again. On a 424,000-chunk corpus a re-index with no changes took 0.9 seconds.
  • Formats. Markdown (.md, .markdown), MDX (.mdx), reStructuredText (.rst), AsciiDoc (.adoc, .asciidoc) and plain text (.txt, .text). Headings become each chunk's heading path. YAML frontmatter and AsciiDoc header attributes supply the title, date and tags.
  • Indexing on git pull. qi hook install adds git hooks that start a background qi index after a pull that changed indexed files, for every collection in that repository.
  • Local and offline. The index is one SQLite file. Keyword search needs nothing else. Vector search works with any embedding server that speaks the OpenAI /v1/embeddings API, including local ones such as Ollama, llama.cpp and LM Studio.

Commands

Command Description
qi init Write a default config (if none exists) and create the database
qi index [path|collection]... Index directories or configured collections (current directory by default). --force rebuilds unchanged files, which you need after an upgrade that changes parsing
qi search <query> Keyword (BM25) search
qi query <query> Hybrid or keyword search (--mode, --explain)
qi get <hash> Print an indexed document by content hash or unambiguous prefix (--lines A:B, --max-bytes N)
qi list List collections
qi delete <collection> Remove a collection from the config and delete its indexed data
qi stats Documents, chunks and embedding coverage per collection, plus database size
qi doctor Check the config, database, collection paths and embedding coverage
qi hook install [path|collection] Install git hooks that re-index a repository's collections after every pull
qi update Replace qi with the latest GitHub release

search and query share these flags: -c/--collection, -n/--limit (default 10), --path <glob>, --since YYYY-MM-DD, --until YYYY-MM-DD, --sort relevance|date and --passages N. Every command takes -f/--format text|json|markdown and --config <file>.

Collections

A collection is a directory qi indexes. Its name is the directory's own name: ~/notes becomes notes, and /Users/alice/Projects/tools/qi becomes qi. When two collections would share a name, each takes on leading path segments until the names differ, so ~/work/notes and ~/personal/notes become work-notes and personal-notes. Indexing a new path saves it to the config as a collection.

qi always skips common tool and build directories (.git, node_modules, vendor, target, dist, build, .venv and others), files over 10 MiB, and binary files. A collection's ignore list adds glob patterns to skip.

See docs/named-collections.md for more.

Configuration

The config lives at ~/.config/qi/config.yaml (or $XDG_CONFIG_HOME/qi/config.yaml) and the database at the config's database_path, which qi init sets to ~/.local/share/qi/qi.db ($XDG_DATA_HOME/qi/qi.db is used only when database_path is omitted). Paths may start with ~, and relative paths resolve against the config file's directory.

database_path: ~/.local/share/qi/qi.db

collections:
  - path: ~/notes
    # Without this list qi indexes .md, .markdown, .txt and .text only.
    # List .mdx, .rst, .adoc or .asciidoc here to index those formats.
    extensions: [.md, .mdx, .rst, .adoc, .txt]
    ignore: [drafts, "*.tmp"]

providers:
  # A local Ollama server
  embedding:
    name: ollama
    base_url: http://localhost:11434
    model: nomic-embed-text
    dimension: 768

  # Or OpenAI: base_url and api_key default to https://api.openai.com
  # and the OPENAI_API_KEY environment variable.
  # embedding:
  #   name: openai
  #   model: text-embedding-3-small
  #   dimension: 1536

With an embedding provider configured, qi index embeds new and changed chunks in the same run, 32 per request unless you set batch_size. base_url and api_key can reference environment variables as ${VAR}.

Retrieving and citing search hits

Every search and query hit includes a full SHA-256 hash, a source_uri identifying its collection and path, and 1-indexed, inclusive start_line and end_line values in the original indexed source. The hash identifies the version of the source; cite the URI, hash and line range together. The integer doc_id and chunk_id fields are internal IDs, not arguments to get. If you pass --config, use the same config for search and retrieval.

qi search "deployment" --format json
# Copy hash, start_line and end_line from a hit:
qi get <hash> --lines <start_line>:<end_line>
qi get <hash> --max-bytes 4096

# Include up to two more matching passages per document:
qi query "deployment" --passages 2 --format json

Extra passages do not take result slots or add ranking weight. They inherit the parent hit's hash and source URI. Line ranges cover the source text behind each chunk, not only the highlighted words, so Markdown syntax and frontmatter are present when you retrieve them.

get reads the indexed snapshot, not the live file. Editing a file leaves the snapshot unchanged until you re-index. qi does not keep old versions: once a file is replaced or deleted and the index is compacted, its old hash no longer resolves. A hash alone does not prove the current file is unchanged.

Unambiguous hash prefixes such as abc123 also work; a prefix that matches more than one content hash is an error. Identical content at several paths is returned once, with the other locations in also_at, so keep the search hit's source_uri as the citation.

Search skips chunks with missing or invalid line ranges. After upgrading from an older index, run qi index <collection> to repair them. If the collection is no longer in the config, index its directory with qi index /path/to/directory.

Contributing

Bug reports and pull requests are welcome on GitHub. Run task check (go vet plus go test ./...) before opening a pull request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

query search engine cli for humans and ai agents

Topics

Resources

Stars

43 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages