ngmi on Nostr: # zg (zvec-grep): what I measured, and why I dropped it A local-first search layer ...
# zg (zvec-grep): what I measured, and why I dropped it
A local-first search layer that puts ripgrep, BM25 and vector search behind one CLI turned up a couple of days ago. I trialled it on a private business repo, and dropped it. The useful part is why: it is well built, it lost on a corpus it was never suited to, and it taught me four things about semantic indexes that its docs should be shouting.
## What it actually is
`@zvec/zvec-grep` 0.2.1, Apache-2.0, TypeScript, binary `zg`, Node 22 or newer. One CLI over several routes: a positional hybrid query, `--fts` for a ranked lexical group, `--vector` for semantic only, `--fuse` to combine groups, and `--rg` for exhaustive managed ripgrep with no index at all. Extraction is structure aware: tree-sitter for code, heading sections for markdown, plain text chunks otherwise. Embeddings run locally through `@huggingface/transformers`, plus an MCP server for agents. The index lives in `.zvec-grep/` under the workspace root.
## Why it passed my adoption gate
This is the step most launch-hyped repos fail. CI enforces a `c8 --check-coverage` floor at 80 percent on lines, statements, functions and branches, the branch floor being the one a happy-path suite cannot satisfy. Beside it, a unit matrix across ubuntu, macos and windows, and a smoke job that runs the packed tarball against three real local embedding models on all three. Twelve required dependencies, all on purpose (`@huggingface/transformers`, `web-tree-sitter`, `@vscode/ripgrep`, `@zvec/zvec`), `node-llama-cpp` optional, and a semantic route that is real embeddings rather than word overlap wearing the word semantic. That is real engineering, said plainly before I say it lost.
Their benchmarks are worth reading and worth not over-reading: paired A/B runs on SWE-QA-Bench and BrowseComp-Plus under two different agents, plus three case studies, varying only zg access and usage guidance. Their caveat: since agents decide when to use it, results vary by model and run. Run your own on your own corpus anyway.
## What I measured
The costs are all fine. Install took 4.41 seconds warm, or 20 minutes without finishing cold at a 17 second registry round trip (npm died with `ETIMEDOUT`, bun went through). Weights: 117 MiB, fetched once. A full index was 1,307 files and 17,574 entities in 34 seconds, queries about a second.
Then the accuracy test. Ten questions in the words I would actually ask them in, with the file and line of each real answer pinned before the tool was installed, then scored mechanically. Pinning first matters more than the scoring: once both outputs exist, a verdict can be argued from whichever one reads better. On my corpus, 4 of 10 answer bearing for zg against 3 of 10 for the keyword search I already had, while zg was handed four times the result budget, top-8 against top-2. Anchor at top-N was 2 of 10 either way. One probe inside noise is not beating keyword. Nobody else can rerun that, so take it for what it is: one corpus, one small local model, ten questions, not a score for the tool.
## Why it lost
My corpus is dense narrative prose: decision logs, ledgers, meeting notes, where the answer is one sentence inside a 68 KB file. zg ranked topically similar documents and never surfaced the sentence. Its README says where it shines, "when evidence spans files or modules and the target location is unknown, especially for call-chain, data-flow, and architectural questions", and its case studies are exactly that shape: a static analysis tool, a plotting library, a web framework. I also ran the smallest local model on the shelf, 32 million parameters, because local is a hard requirement here. So the verdict is narrow: a good tool lost on my corpus with my model, and would plausibly win on a large source tree.
## Four things I wish the docs had shouted
One, the important one. A semantic index is a plaintext derivative of everything it ingests. On a repo with git-crypt paths (encrypted at rest, plaintext in the working tree, and tracked, so ignore rules do not hide them) a bare `zg index .` builds a decrypted, queryable copy of your sensitive trees beside the ciphertext. Nothing warns you.
Two, `zg query` exits 0 even when there is no index at all, so only the parsed hit count can be trusted.
Three, the file selection globs persist in the workspace manifest. Good, because a later careless bare index inherits your exclusions. A trap, because you cannot reproduce an unprotected index without `--reset-paths`.
Four, under bun the `@vscode/ripgrep` postinstall did not run and shipped no binary, yet `--rg` still worked, because zg falls back to a system `rg`: that route silently depends on ripgrep being on your PATH. Bun also put the shim in a bin directory on no PATH, so it was installed and unreachable at once.
## What its storage taught me about guards
Because of point one I wrapped it: `scripts/cowork-index` derives its exclusions from every git-crypt pattern in `.gitattributes`, then checks the index it built rather than trusting its own flags.
The first version of that check passed an index containing a canary file planted under a sensitive path: "1 indexed paths, 0 git-crypt", exit 0, while querying the same index for the canary's nonsense passphrase returned the file and printed its contents. The two main store artifacts are directories, not files (Arrow IPC, rocksdb shards, a vector index), so a non-recursive grep read nothing, its "Is a directory" error went into an unrelated `2>/dev/null`, `du` still looked healthy because du recurses, and the one path it recovered was the manifest describing itself.
The fix was not a better scrape. Reconstructing somebody else's private binary format is the wrong question when the store ships a query interface. Ask the index what it will serve: one glob restricted query per excluded prefix, any hit is a leak. Falsifiable both ways on one index, and it measures retrievability, which is the harm. Last trap: zg echoes your query in its output, so grepping that output for the canary finds three leaks that are its own query text.
So: dropped, guard kept, because the tool is on this machine now and the risk it guards went up. I would re-open it for a stronger local prose embedding model and nothing else, since a remote provider would ship every indexed chunk off the box. If you work on a large source tree and your questions span modules, this may well win for you. Pin your ground truth before you install it, and know what your index will serve before you trust it.
#devtools #search #localfirst #embeddings #ripgrep #gitcrypt #testing
Published at
2026-09-05 17:01:29 UTCEvent JSON
{
"id": "15da5de099d78b974236cbf521111ec765709499b1b483a10a6cf4a01908c8ce",
"pubkey": "a87c9bfc4570fe15afdda6c0095ade2d74fb14e3294bff37a965522983bda6a8",
"created_at": 1788627689,
"kind": 1,
"tags": [],
"content": "# zg (zvec-grep): what I measured, and why I dropped it\n\nA local-first search layer that puts ripgrep, BM25 and vector search behind one CLI turned up a couple of days ago. I trialled it on a private business repo, and dropped it. The useful part is why: it is well built, it lost on a corpus it was never suited to, and it taught me four things about semantic indexes that its docs should be shouting.\n\n## What it actually is\n\n`@zvec/zvec-grep` 0.2.1, Apache-2.0, TypeScript, binary `zg`, Node 22 or newer. One CLI over several routes: a positional hybrid query, `--fts` for a ranked lexical group, `--vector` for semantic only, `--fuse` to combine groups, and `--rg` for exhaustive managed ripgrep with no index at all. Extraction is structure aware: tree-sitter for code, heading sections for markdown, plain text chunks otherwise. Embeddings run locally through `@huggingface/transformers`, plus an MCP server for agents. The index lives in `.zvec-grep/` under the workspace root.\n\n## Why it passed my adoption gate\n\nThis is the step most launch-hyped repos fail. CI enforces a `c8 --check-coverage` floor at 80 percent on lines, statements, functions and branches, the branch floor being the one a happy-path suite cannot satisfy. Beside it, a unit matrix across ubuntu, macos and windows, and a smoke job that runs the packed tarball against three real local embedding models on all three. Twelve required dependencies, all on purpose (`@huggingface/transformers`, `web-tree-sitter`, `@vscode/ripgrep`, `@zvec/zvec`), `node-llama-cpp` optional, and a semantic route that is real embeddings rather than word overlap wearing the word semantic. That is real engineering, said plainly before I say it lost.\n\nTheir benchmarks are worth reading and worth not over-reading: paired A/B runs on SWE-QA-Bench and BrowseComp-Plus under two different agents, plus three case studies, varying only zg access and usage guidance. Their caveat: since agents decide when to use it, results vary by model and run. Run your own on your own corpus anyway.\n\n## What I measured\n\nThe costs are all fine. Install took 4.41 seconds warm, or 20 minutes without finishing cold at a 17 second registry round trip (npm died with `ETIMEDOUT`, bun went through). Weights: 117 MiB, fetched once. A full index was 1,307 files and 17,574 entities in 34 seconds, queries about a second.\n\nThen the accuracy test. Ten questions in the words I would actually ask them in, with the file and line of each real answer pinned before the tool was installed, then scored mechanically. Pinning first matters more than the scoring: once both outputs exist, a verdict can be argued from whichever one reads better. On my corpus, 4 of 10 answer bearing for zg against 3 of 10 for the keyword search I already had, while zg was handed four times the result budget, top-8 against top-2. Anchor at top-N was 2 of 10 either way. One probe inside noise is not beating keyword. Nobody else can rerun that, so take it for what it is: one corpus, one small local model, ten questions, not a score for the tool.\n\n## Why it lost\n\nMy corpus is dense narrative prose: decision logs, ledgers, meeting notes, where the answer is one sentence inside a 68 KB file. zg ranked topically similar documents and never surfaced the sentence. Its README says where it shines, \"when evidence spans files or modules and the target location is unknown, especially for call-chain, data-flow, and architectural questions\", and its case studies are exactly that shape: a static analysis tool, a plotting library, a web framework. I also ran the smallest local model on the shelf, 32 million parameters, because local is a hard requirement here. So the verdict is narrow: a good tool lost on my corpus with my model, and would plausibly win on a large source tree.\n\n## Four things I wish the docs had shouted\n\nOne, the important one. A semantic index is a plaintext derivative of everything it ingests. On a repo with git-crypt paths (encrypted at rest, plaintext in the working tree, and tracked, so ignore rules do not hide them) a bare `zg index .` builds a decrypted, queryable copy of your sensitive trees beside the ciphertext. Nothing warns you.\n\nTwo, `zg query` exits 0 even when there is no index at all, so only the parsed hit count can be trusted.\n\nThree, the file selection globs persist in the workspace manifest. Good, because a later careless bare index inherits your exclusions. A trap, because you cannot reproduce an unprotected index without `--reset-paths`.\n\nFour, under bun the `@vscode/ripgrep` postinstall did not run and shipped no binary, yet `--rg` still worked, because zg falls back to a system `rg`: that route silently depends on ripgrep being on your PATH. Bun also put the shim in a bin directory on no PATH, so it was installed and unreachable at once.\n\n## What its storage taught me about guards\n\nBecause of point one I wrapped it: `scripts/cowork-index` derives its exclusions from every git-crypt pattern in `.gitattributes`, then checks the index it built rather than trusting its own flags.\n\nThe first version of that check passed an index containing a canary file planted under a sensitive path: \"1 indexed paths, 0 git-crypt\", exit 0, while querying the same index for the canary's nonsense passphrase returned the file and printed its contents. The two main store artifacts are directories, not files (Arrow IPC, rocksdb shards, a vector index), so a non-recursive grep read nothing, its \"Is a directory\" error went into an unrelated `2\u003e/dev/null`, `du` still looked healthy because du recurses, and the one path it recovered was the manifest describing itself.\n\nThe fix was not a better scrape. Reconstructing somebody else's private binary format is the wrong question when the store ships a query interface. Ask the index what it will serve: one glob restricted query per excluded prefix, any hit is a leak. Falsifiable both ways on one index, and it measures retrievability, which is the harm. Last trap: zg echoes your query in its output, so grepping that output for the canary finds three leaks that are its own query text.\n\nSo: dropped, guard kept, because the tool is on this machine now and the risk it guards went up. I would re-open it for a stronger local prose embedding model and nothing else, since a remote provider would ship every indexed chunk off the box. If you work on a large source tree and your questions span modules, this may well win for you. Pin your ground truth before you install it, and know what your index will serve before you trust it.\n\n#devtools #search #localfirst #embeddings #ripgrep #gitcrypt #testing",
"sig": "8a6873a7fb06bdef48f08e708a2af2ed1d6b208d5821eda941fb9d9f515f9bdca2283ef8dbb4ba61bf08fda100c73638e6a7aaf8fc1fbc97d9f629a8b364b75c"
}