Search your codebase by meaning: a hands-on tutorial for zvec-grep (zg)
zvec-grep (zg) unifies ripgrep, BM25, and vector search behind one local-first CLI — no API key, no server, no GPU. This tutorial installs it, indexes two real workspaces, and runs every query route against them, then wires it into a coding agent over MCP.

Every developer has lived through this: you join a codebase, something needs to change in "the part that validates sessions," and grep -r "session" returns four hundred lines of noise while the actual function is called is_token_current. ripgrep is the fastest tool in the world at finding words you can already spell. It cannot help you when you don't know the words — which is most of the time for humans, and all of the time for an AI agent dropped into an unfamiliar repository.
That gap is exactly why zvec-grep is climbing. The project (zvec-ai/zvec-grep, Apache 2.0) has collected roughly 3,800 GitHub stars since its July 2026 debut and is currently sitting on GitHub's weekly trending page. Its pitch, in the project's own words: "Know the words — or don't. Just zg."
Under one zg command it unifies four search routes: ripgrep for exact text, BM25 for ranked keyword search, vector search for meaning, and a hybrid that fuses them all. Everything runs locally by default — index, embeddings, and queries — with no account, no API key, and no GPU. This tutorial installs version 0.2.2, indexes two real workspaces, runs every query route against them with the actual output, and then hands the whole thing to a coding agent over MCP. Every command below was run on September 27, 2026, and every output shown is real.
What you'll need #
- Node.js 22 or newer — check with
node --version. The package ships as an npm binary. - A terminal and roughly 160 MB of free disk for one-time model downloads (observed: 125 MB for the English retrieval model, 32 MB for the code model).
- No API key, no GPU, no account. The bundled "Potion" embedding models are static lookup tables — there is no tokenization step at query time, so even a modest CPU answers in under a second.
- A workspace to search. We'll build two disposable ones, so you can follow along without touching your own code.
Step 1 — Install zg #
The package lives on npm as @zvec/zvec-grep:
npm install -g @zvec/zvec-grep
zg --version
# 0.2.2
Two things worth knowing up front. First, this tutorial pins version 0.2.2 and every flag below was verified against that build's --help. The project is pre-1.0 and its online docs track the development branch — at the time of writing, the docs show a newer flag form (zg --index) while 0.2.2 ships subcommands (zg index, zg query, zg status, zg install). When in doubt, zg <command> --help on your machine is the source of truth. Second, zg query --help reveals the full surface: hybrid/FTS/vector/fuse routes, a managed ripgrep passthrough, glob and file-type filters, and symbol-aware search. That's the whole tour in one help screen — worth reading once.
Step 2 — Your first index: the bookshelf test #
Before pointing zg at code, verify that its semantic search actually works — with a test no keyword engine can pass. Download two public-domain books into a scratch directory:
mkdir -p /tmp/zg-bookshelf && cd /tmp/zg-bookshelf
curl -sL -o alice-in-wonderland.txt \
https://www.gutenberg.org/files/11/11-0.txt
curl -sL -o sherlock-holmes.txt \
https://www.gutenberg.org/files/1661/1661-0.txt
Now build the index. For English prose, the docs recommend the retrieval-tuned model local/potion-retrieval-32m:
zg index --embedding local/potion-retrieval-32m
On first use zg downloads the model (~125 MB) from Hugging Face and caches it under ~/.zvec-grep/models — after that, everything is offline. The observed result: 2 files scanned, 433 entities, 0 failures, in about a minute. The index itself lands in <workspace>/.zvec-grep/ (manifest.json, files.zvec, index.zvec), so it's per-project and travels with the repo.
Here's the test. Ask about a passage using words that never appear in it:
zg query --human \
"An unseen creature left a few marks. What did the detective infer?" \
--limit 3
Real output (abridged):
query groups (1):
Q1 [primary]: An unseen creature left a few marks. What did the detective infer?
hits: 3
#1 matchedBy=fts+vector sherlock-holmes.txt:5463-5505
...
#2 matchedBy=fts+vector sherlock-holmes.txt:5455-5462
#3 matchedBy=fts+vector sherlock-holmes.txt:5506-5513
All three hits land in The Hound of the Baskervilles, and the top hit is the exact passage: the detective examining the footprints — five pads, long nails, a print "about the size of a dessert spoon," a long body, very short legs, able to climb a curtain, carnivorous. No query term appears in that passage; the match is pure meaning. Note the matchedBy=fts+vector label: the default positional query is hybrid, running BM25 and vector search in parallel and fusing the rankings.
Step 3 — Index a real codebase #
Books prove the semantics; now the actual use case. Build a tiny demo repo:
mkdir -p /tmp/zg-code/src /tmp/zg-code/tests && cd /tmp/zg-code
# src/auth.py — PBKDF2 password hashing, credential checks, sessions
# src/theme.py — saves/restores the UI theme in a JSON prefs file
# src/retry.py — exponential-backoff decorator
# tests/test_auth.py, README.md
For code, switch to the code-tuned model:
zg index --embedding local/potion-code-16m-v2
This one downloads just 32 MB and produces 256-dimensional cosine vectors. Result: 5 files, 10 entities. That entity count is the interesting part — zg doesn't just chunk text. A structural extractor parses supported languages (Python, JS/TS, C/C++, Go, Java, Rust) into symbols with signatures, so a function definition becomes a searchable unit that carries its name, parameters, and docstring. Markdown sections get their own treatment, and formats that can't be searched meaningfully — PDFs, Office files, archives, executables, audio/video, databases — are skipped outright.
Check the index health any time with:
zg status
✓ Workspace index is ready
/tmp/zg-code
Coverage ████████████████████ 100% 5 / 5 files
Entities 10
Truncated 0 fragments
Queue 0 pending · 0 failed
Embedding local/potion-code-16m-v2
256 dimensions · cosine
Storage .zvec-grep/index.zvec
Step 4 — Learn the four query routes #
zg's power is that it doesn't make you choose one search philosophy. Ask the way that's natural, then narrow down:
# Hybrid (default): BM25 + vectors, fused and ranked
zg query "where authentication is validated" --limit 3
# → #1 matchedBy=fts+vector src/auth.py:11-18
# def validate_credentials(username: str, password: str, user_store: dict) -> bool:
# Vector only: pure meaning, no keyword overlap required
zg query --vector "theme preference persistence on startup" --limit 3
# → #1 matchedBy=vector src/theme.py:15-25 (save_theme)
# #2 matchedBy=vector src/theme.py:7-13 (load_theme)
# FTS only: classic ranked keywords, scoped by glob
zg query --fts "load_theme" -g "src/**" --limit 3
# Fuse: run several query groups in parallel, merge the rankings
zg query "authentication flow" --fts "SESSION_TTL" --fuse --limit 4
The vector-only query is the one worth staring at: "theme preference persistence on startup" shares zero words with load_theme/save_theme, yet both surface at the top. That is the query a new hire — or an agent — would actually type.
Two more routes complete the picture. --rg passes through to a managed ripgrep (exact, exhaustive, with symbol breadcrumbs in the output):
zg query --rg -F "SESSION_TTL" src
# src/auth.py
# 6: SESSION_TTL_SECONDS = 3600
# 20-26 [function create_session] 25: "expires_at": time.time() + SESSION_TTL_SECONDS,
And --prefer-symbol biases results toward definitions rather than usages — pair it with --symbol-type function when you want the declaration, not the call sites:
zg query "session" --prefer-symbol --symbol-type function --limit 3
# → is_session_valid, test_expired_session_invalid, create_session
A quick decision table:
| Route | Use it when… |
|---|---|
zg query "…" (hybrid) | You don't know what to call it — the daily driver |
--vector | You know the concept, not the vocabulary ("persistence on startup") |
--fts | You know a distinctive term and want ranked keyword hits |
--fuse | Several angles on one question; merge their rankings |
--rg | Exact identifier, regex, or constant — ripgrep, exhaustively |
--prefer-symbol | You want the definition, not every usage |
Practical knobs: --limit caps results (default 7), --preview none|short|full controls how much context each hit shows, and --human forces the readable layout when output is piped.

Step 5 — Keep the index honest #
An index is only useful if you can trust its freshness. zg exposes that explicitly instead of hiding it:
# Default: background refresh (file events + interval sweeps)
zg query "retry backoff"
# Reindex changed files first, then answer
zg query "retry backoff" --refresh wait
# Answer from the index as-is, even if files changed
zg query "retry backoff" --refresh off
Hits carry a freshness marker, so --refresh off will tell you a result is possibly_stale rather than silently serving it. When the index needs surgery: zg index --rebuild rebuilds it in place, and zg index --drop --yes removes it entirely. This explicitness is a design choice worth appreciating — most "smart search" tools either reindex opaquely or go stale quietly; zg does neither.
Step 6 — Hand it to your agent #
Semantic code search is arguably more valuable to agents than to humans — an agent exploring a new repo asks "where is X handled?" constantly, and burning context on blind grep sweeps is expensive. zg ships a first-class MCP path:
# Wire zg into your coding agent (codex, claude, qwen, qoder,
# opencode, cursor, copilot, vscode, all, or auto)
zg install --target claude
# Or run the daemon yourself: stdio for agents, HTTP for tools
zg server on
# Server: ready
# URL: http://127.0.0.1:7999/mcp
zg server status
zg server off
Two details matter. The MCP endpoint binds to loopback only (127.0.0.1) — your index never leaves the machine, which is the whole point of the local-first design. And the default agent toolset exposes exactly one tool, zvec_grep_search, keeping the agent's tool surface minimal; --mcp-toolset full adds rg, index, index_drop, index_status, and server_status for operators who want them.
The project's own guidance is refreshingly honest about the division of labor: use semantic search for meaning, but keep native grep/ripgrep for exact identifiers, paths, and regex. The --rg route exists precisely so the agent doesn't have to shell out to a second tool.
zg vs the alternatives #
| Tool | Strength | Where zg wins |
|---|---|---|
| ripgrep | Instant, exact, zero setup | When you don't know the words; zg keeps rg inside it anyway |
| GitHub code search | Symbol-aware, huge index | Local, private, works on unpushed code, no cloud round-trip |
| Sourcegraph / Cody | Full platform, cross-repo | One npm install, no account, no per-seat anything |
| DIY vector RAG | Fully customizable | No chunking/embedding plumbing; BM25+vectors+rg pre-fused |
The honest summary: zg doesn't replace ripgrep — it absorbs it and adds the two things rg can't do (ranked keywords, meaning). Its real competitors are the hosted code-intelligence platforms, and its pitch against them is radical simplicity: one CLI, one local index, no bill.

Limitations worth knowing #
No tutorial is complete without the rough edges — all observed or documented, none guessed:
- Pre-1.0 API churn is real. The online docs already describe a newer flag form than the 0.2.2 you install from npm today. Pin your version, and trust
zg --helpover blog posts (including this one, in six months). - Semantic search costs latency. One published independent evaluation measured roughly 0.9 s per semantic query versus 0.01 s for ripgrep, and found semantic hits skew toward explanatory prose (READMEs, comments) over production code. Use
--rgwhen milliseconds matter. - Some formats are skipped by design — PDFs, Office documents, archives, executables, media files, and databases never enter the index. If your answers live in a Confluence export, zg won't see them.
- Remote embeddings are gated. Cloud models (OpenAI, Qwen, custom endpoints) require explicit opt-in flags and your own API key — local is the default and the path this tutorial tested.
- Benchmarks are vendor-reported. The project cites SWE-QA-Bench (with Claude Code + Opus 5) and BrowseComp-Plus (with Codex + GPT-5.6-class models) plus case studies on pylint, matplotlib, and Django. Treat those as the project's claims, not independent verification — the one independent evaluation linked below is more mixed, which is normal for a three-month-old tool.
The takeaway #
The reason zvec-grep is trending isn't a benchmark number — it's that the workflow finally matches how developers actually think. You describe the behavior; it finds the code. The hybrid default means you rarely have to care which engine answered, the managed ripgrep means you never lose exact search, and the MCP server means your agent gets the same superpower with one install command. For the price of npm install -g @zvec/zvec-grep and ~160 MB of one-time model downloads, "where is the session logic?" becomes an answered question instead of a twenty-minute spelunk.
Index one of your own repos tonight. Run zg status, then ask it the question you'd normally grep for — phrased the way you'd explain it to a new teammate. If the top hit is right, you'll understand the 3,800 stars.