zg (zvec-grep) combines vector-based semantic search, BM25 keyword search, and ripgrep-style exact matching in a local command-line tool. It also exposes these capabilities over MCP for coding agents such as Claude Code, Codex, and Cursor. In version 0.3.0, we replaced the original Node.js implementation entirely with Rust. In our benchmarks, this reduced the installed size by 79.5%, CLI startup time by 82.5%, indexing time by 64.5%, and peak memory during indexing by 53.2%.
Repository: zvec-ai/zvec-grep
What is zg?
zg is an open-source local search tool from Alibaba, built for both developers and AI agents. It provides a single interface for semantic search, BM25 keyword search, and ripgrep-style exact matching.

Many Rust developers use rg every day. If you know the function name, configuration key, or error message you are looking for, it is fast, precise, and useful when you need to find every occurrence.
Increasingly, though, search requests come from AI agents and start with a natural-language question: “How is the theme preference restored at startup?” or “Where are user permissions checked?” The implementation might be named hydratePreferences, or the documentation might discuss “account authorization and approval,” without sharing any words with the query. Text matching alone can miss the relevant material, or require such a broad search that it produces a large set of unranked matching lines. The agent then has to keep trying different keywords, searching again, and reading files. That adds tool calls, latency, and token usage.
zg is designed to help with this. It keeps the exact, fast, exhaustive matching that makes rg useful, and adds semantic discovery, relevance ranking, and structured context:
- Several search modes, one interface. Vector search, BM25, hybrid search using reciprocal rank fusion (RRF), and
rg-style exact/regex matching. You can start with semantic search, narrow the scope with keywords, and use a regex to check specific matches. - Structure-aware parsing. zg uses tree-sitter to parse languages including C/C++, Go, Java, JavaScript/TypeScript, Python, and Rust, splitting content into chunks based on symbols, signatures, and hierarchy. Markdown is split by headings and sections. Text data such as JSON, TOML, YAML, and CSV can also be searched.
- Compact results with source locations. By default, results are ranked and include short previews, file paths, and line numbers. The agent can read the full content when needed, keeping unrelated material out of its context.
- Local by default. File scanning, embedding generation, indexing, and search all run locally by default. Several local embedding models are supported out of the box. Remote embeddings require explicit user authorization.
- One index for the CLI and agents. zg uses the Zvec embedded vector database for both vector and BM25 indexes, so there is no separate database service to deploy. The CLI and MCP interface share the same index, with incremental updates and automatic refresh.
We also ran paired A/B evaluations on SWE-QA-Bench, which covers questions about code repositories, and BrowseComp-Plus, which covers deep-research questions. Each pair used the same agent, model, and prompt; the only difference was whether zg was available:
- SWE-QA-Bench: tool calls fell by more than half, input token usage fell by nearly half, and the evaluation score increased by 1.50 points.
- BrowseComp-Plus: accuracy increased from 98.67% to 99.00%, while input token usage fell by 37.56%, tool calls by 43.52%, and agent execution time by 38.58%.
From Node.js to Rust
Why we rewrote it
The earlier releases of zg (0.2.x) were written in TypeScript and ran on Node.js. For a CLI search tool that is invoked frequently, that implementation had a few recurring costs:
- Each standalone CLI invocation had to start the Node.js runtime and load JavaScript modules. Those costs added up when an agent made repeated calls.
- Parts of the indexing pipeline converted between regular JavaScript arrays and typed arrays, and passed data between worker threads. This introduced extra copies and temporary allocations.
- The installation included the JavaScript dependency tree and native addons, which made it relatively large.
Large workspaces and agent workflows are sensitive to indexing time, query latency, and resource usage, so we rewrote the core execution pipeline in Rust.
Benchmark results
The benchmarks ran on an Apple M3 Pro with 18 GB of RAM, macOS 27.2, and Node.js v24.16.0. For indexing, we used four public BEIR datasets—SciFact, NFCorpus, ArguAna, and FiQA—containing 75,128 documents in total. Both versions used the same local embedding model, ran inference on the CPU, and had embedding concurrency set to 4.
| Metric | Node.js 0.2.2 | Rust 0.3.0 | Reduction |
|---|---|---|---|
| Installed size, including dependencies | 167.5 MiB | 34.3 MiB | 79.5% |
| CLI startup time | 153.1 ms | 26.8 ms | 82.5% |
| Total indexing time | 219.13 s | 77.84 s | 64.5% |
| Average peak memory during indexing (RSS) | 4,210.26 MiB | 1,968.61 MiB | 53.2% |
| Average search latency over MCP | 20.93 ms | 14.88 ms | 28.9% |
In practical terms, building the initial indexes for all four datasets went from about 3 minutes 39 seconds to about 1 minute 18 seconds. Average peak RSS during indexing dropped from about 4.11 GiB to 1.92 GiB, with lower peak memory on every dataset. That leaves more memory available when an editor, an agent, and other development tools are running on the same machine.
Where the gains came from
- Native startup. The compiled executable runs directly, without initializing Node.js or loading JavaScript modules. CLI cold-start time dropped from 153 ms to 27 ms.
- A more direct computation path. Text encoding and vector operations run in native code with explicit numeric types and contiguous arrays, making the computation loops easier for the compiler to optimize.
- Fewer intermediate conversions. Results stay in native numeric arrays instead of moving back and forth between regular JavaScript arrays and typed arrays. This reduces copying and temporary allocations.
- Shared resources and centralized scheduling. On supported local inference paths, threads share loaded tokenizers and model data, avoiding repeated initialization. A native thread pool handles task scheduling.
Getting started
1. Install
zg is currently distributed through npm, with other installation methods planned. The installed executable is native and does not require Node.js at runtime. macOS, Linux, and Windows are supported.
npm install -g @zvec/zvec-grep --allow-scripts=@zvec/zvec-grep zg --version Installation uses an npm
postinstallscript to put the native executable in place and download CUDA components when needed. The--allow-scriptsoption allows this script to run.
2. Set up an agent integration (optional)
# Detect installed agents and configure MCP automatically zg --install # Or select a specific integration zg --install --target claude --yes zg --install --target codex --yes Supported targets currently include claude, codex, opencode, cursor, qwen, qoder, copilot, vscode, grok, and pi. Reconnect your agent after setup; it can then decide when to use zg based on the question.
3. Build an index
cd your-repository # Use the default lightweight local model for code, local/potion-code-16m-v2 zg --index # Check index coverage, the embedding model, and storage status zg --status The index is stored in .zvec-grep/ at the workspace root. Subsequent changes are processed incrementally; files whose contents have not changed do not need new embeddings. To switch models, rebuild with zg --index --rebuild --embedding <model>.
In an interactive terminal, you can skip this step and start with a search: zg will guide you through selecting a model and building an index first. In non-interactive environments such as scripts or CI, indexing must be started explicitly with
zg --index.
4. Search
# Natural-language search: hybrid vector + BM25 search with RRF by default zg "where is the connection pool timeout configured" zg "where are user permissions checked" # BM25 keyword search only, or vector search only zg --fts "AuthService" zg --vector "retry with exponential backoff" # Run multiple queries; --fuse merges and ranks their results together zg --hybrid "connection pool" --hybrid "socket timeout" --fuse # rg-style exact/regex matching, with no index required zg --rg -n "AuthService" src zg --rg -F "fn main(" crates # Filter by file type and glob, and limit the number of results zg "error handling for invalid config" -t rust -g 'crates/**' --limit 5 # Choose a preview level: none / short (default) / full / detail zg --preview detail "how indexing jobs are scheduled" --limit 3 5. Try it on some fiction
mkdir zg-mystery && cd zg-mystery curl --retry 3 --retry-all-errors --progress-bar -fL \ -o alice-in-wonderland.txt https://raw.githubusercontent.com/GITenberg/Alice-s-Adventures-in-Wonderland_11/master/11.txt \ -o sherlock-holmes.txt https://raw.githubusercontent.com/GITenberg/The-Memoirs-of-Sherlock-Holmes_834/master/834.txt # Use a local text-retrieval model for this collection of English fiction zg --index --embedding local/potion-retrieval-32m zg --preview detail "An unseen creature left a few marks. What did the detective infer?" --limit 3 The query does not contain Holmes, weasel, or foot-marks, all of which appear in the source text. Even so, zg ranks the passage in “The Crooked Man,” from The Memoirs of Sherlock Holmes, where Holmes identifies an animal from its footprints, as the top result, with line numbers pointing to the passage.
What's next
zg 0.3.0 is the first official release of the Rust implementation. Next, we plan to work on graph-based retrieval and reranking, support for more formats such as PDF and Office documents, more efficient use of context, and further improvements to local model and indexing performance.
We'd welcome feedback from the Rust community, whether through issues, pull requests, or reports of how zg works with your codebases, knowledge bases, and agent workflows.
More details: Rust release blog post.
1 post - 1 participant
