Local code search for AI agents: six fast, purpose-built tools that return ranked answers, not raw grep. Because maybe grep isn't all you need... 🍬
21
stars
1,122
commits
JavaScript
primary language
Sep 10, 2026
updated
Local code search for AI coding agents. Six fast, purpose-built tools that hand Claude Code, Codex & friends ranked answers, not raw grep. Zero API keys, 100% on-device.
Maybe grep isn't all you need… 🍬
Every coding agent today reaches for grep + Read by reflex. sweet-search challenges the narrative. 😎
init installs it into Claude Code by default (Codex, Gemini CLI, and Cursor via flags)|
GET STARTED 🚀 Quickstart 🖥️ Platform Support |
USE IT 🧰 The Six Tools 🧠 The Evolved Agent Prompt 🔌 Works With Your Agent |
UNDER THE HOOD ⚡ GPU-Accelerated Indexing 🔄 An Index That Never Goes Stale 🦀 The Native Engine Room |
THE RECEIPTS 📊 Benchmarks 🧭 Where sweet-search Fits 🙏 Prior Art & Acknowledgements 📄 License |
Requirements: Node.js 18+ on macOS (Apple silicon or Intel) or Linux (x64 or ARM64). On Windows, run sweet-search inside WSL2.
npm install -g sweet-search
cd your-repo
sweet-search init # one-time: downloads local models, wires up your agent
sweet-search index # builds the index — GPU-accelerated where available
sweet-search "where do we validate JWT tokens?"
That's it. init is idempotent and SHA256-verifies every model binary; re-running it is always safe.
From then on, the index stays up to date automatically as you work.
sweet-search stores its index in SQLite via better-sqlite3, which downloads its
prebuilt native binary from an install script. Recent npm versions block those
scripts by default and only print a warning, which leaves the binding missing and
makes indexing fail. sweet-search init detects this and stops with instructions
rather than reporting success.
For a global install, approve the scripts once:
npm install -g sweet-search --allow-scripts=sweet-search,better-sqlite3
For a project-local install, add the allowlist to that project's package.json
(npm rejects the flag for project-scoped installs), then reinstall:
"allowScripts": { "sweet-search": true, "better-sqlite3": true }
Verify with sweet-search init — the native:sqlite check must pass.
For Claude Code, init automatically installs and activates the sweet-search
output style, which adds the compact routing override at system-prompt priority.
Start a new Claude session or run /clear after init, and keep that output style
selected for reliable ss-* routing.
To uninstall 😢:
sweet-search uninstall --keep-models # current repo only; keeps shared models and the global CLI
sweet-search uninstall # current repo plus its shared model downloads; keeps the global CLI
npm uninstall -g sweet-search # global CLI only; does not clean initialized repos
Run sweet-search uninstall inside each initialized repo before removing the global CLI.
sweet-search init --wizard # interactive: shows your hardware, recommends a model tier
sweet-search init --profile core # lexical-only, no model downloads (CI-friendly)
sweet-search init --li-model edge # compact late-interaction model for constrained machines
sweet-search init --agents # also configure AGENTS.md for Codex/OpenCode
sweet-search init --no-claude --agents # streamlined AGENTS-only configuration
sweet-search uninstall --dry-run # preview cleanup for the current repo
~/.cache/sweet-search/models/ and is used strictly on-device.CLAUDE.md untouched, writes the verbatim evolved guide to .claude/rules/sweet-search.md, installs .claude/output-styles/sweet-search.md, and selects it in .claude/settings.json. It also registers a session-start prewarm hook and installs the /sweet-index skill./config, then emits a warning. A higher-priority .claude/settings.local.json selection is also detected and reported. Select sweet-search, then run /clear or restart Claude Code.--agents to place the same verbatim guide directly in AGENTS.md. Use --no-claude --agents when AGENTS.md is the only integration you want; --codex additionally installs Codex's project prewarm hook..gitignore is respected, node_modules/build dirs/minified artifacts are denied, files over 1 MB skipped, with a .sweet-search-ignore for extra rules.| Flag | Behavior |
|---|---|
--profile core|full | Select lexical-only core or the full model-backed profile. |
--li-model standard|edge|none | Select the late-interaction model tier or disable it. |
--search-reranking auto|on|off | Control search-time late-interaction reranking. |
--wizard | Choose model and reranking settings interactively. |
--verify-deep | Load modules and verify checksums after setup. |
--force | Re-download models even when cached. |
--build-coreml-cascade | Build the optional CoreML cascade locally on eligible Apple silicon. |
--skip-coreml-cascade | Skip fetching or building the CoreML cascade. |
--skip-dedup | Skip near-duplicate-detection readiness checks. |
--skip-cuda | Disable the CUDA backend even when available. |
--skip-prewarm-hook | Do not register the Claude/Codex session-start prewarm hook. |
--agents | Also write the verbatim guide to AGENTS.md for Codex/OpenCode. |
--codex | Add the Codex prewarm hook and project feature flag; implies --agents. |
--codex-enable-global-hooks | Advanced opt-in: also enable hooks in the user-level Codex config. |
--no-claude | Write nothing under .claude/; combine with --agents for AGENTS-only setup. |
--gemini | Also write GEMINI.md (sharing AGENTS.md when enabled). |
--cursor | Also write .cursor/rules/sweet-search.mdc. |
--symlink-instruction-files | Explicitly use the default GEMINI.md symlink behavior. |
--no-symlink-instruction-files | Use a regular GEMINI.md import instead of a symlink. |
--no-agent-instructions | Skip all agent policy and Claude output-style installation. |
--mcp | Also register the project MCP server; the CLI remains the default contact surface. |
--no-cli | With --mcp, give agents the MCP-specific guide and remove the CLI-specific Claude output style. |
--enforce-tools | Optional strict Claude mode: deny native Grep and hint native Read. |
--verbose, -v | Print additional setup diagnostics. |
--help, -h | Show the complete CLI help. |
| Flag | Behavior |
|---|---|
--dry-run | Preview all detected removals. |
--keep-models | Preserve shared model and CoreML caches. |
--purge | Also uninstall the npm package and Sweet Search native packages. |
--force | Skip the confirmation prompt. |
--help, -h | Show uninstall help. |
sweet-search uninstall removes Sweet Search's rule, output style and active
selection, AGENTS/GEMINI/Cursor instruction blocks, project hooks and skill,
MCP registration, enforcement/reminder artifacts, and .sweet-search/. It
preserves user-authored content. Generic Codex [features] hooks = true flags
are left in place because other tools may share them, and an otherwise-empty
settings file may remain as {}.
We measure sweet-search four ways — from how much it helps a real agent down to raw engine throughput:
|
🤖 ① Code-retrieval (agent-in-the-loop) |
🚧 ② Task-completion (coming soon) |
|
📄 ③ Paper-type IR (academic) |
⚡ ④ Engine speed |
One variable changes: how the agent searches a real repository.
Same tasks, same judge, paired probe-for-probe.
sealed vault · exact paired results · five representative profiles · full 11-cell matrix and held-out/OOD replication below
The headline, in four claims:
The win is harness-adaptive: where the native loop is disciplined (Claude Code) it shows up as denser, more useful context per token; where it thrashes (Codex floods 30k+ tokens of its own grep output into context) it shows up as a large cost and tool-call cut. Either way, final-answer accuracy never significantly regresses.
| 🧰 Native agent harness | 💰 Realized cost | 🔧 Tool calls | ✨ Useful content / response | 🎯 Final accuracy |
|---|---|---|---|---|
| 🤖 Codex (GPT-5.5) | −30 to −34% | −44 to −56% | +0.06 → +0.17 ↑ | tie (saturated) |
| 🐚 opencode (GPT-5.5 / GLM-5.1) | −18 to −22% | −15 to −49% | +0.23 to +0.31 ↑ | tie |
| 🔌 bare API (GPT-5.5 / GLM / DeepSeek) | −15 to −32% ᵃ | −15 to −33% | +0.08 to +0.24 ↑ | tie · +3 pp on weak models |
| 🟣 Claude Code (Sonnet / Opus) | −10% to +14% ᵇ | −5 to −33% | +0.18 to +0.29 ↑ | tie |
↑ "Useful content / response" is the per-response delta on a 5-dimension usefulness score (answer-grounding · workable-code · navigability · edit-locality · sufficiency), 0–1 scale. "tie" = final-answer correctness statistically indistinguishable (saturated in the 0.94–0.99 band on flagships).
ᵃ the two cheapest bare models cost fractions of a cent either way (GLM +27% of $0.008; DeepSeek −15% of $0.004). ᵇ Opus −5/−10%; Sonnet +8–14%, which is ≈1¢ on a flat-rate subscription for a richer answer.
Denser, not just longer. The usefulness lift survives length-matching — comparing sweet-search and native responses of equal token length, sweet-search's content is significantly higher on 8 of 11 cells. The validated single-number usefulness composite (grounding × content × density) is significant on all 11 sealed cells.
sweet-search agent prompt + tools vs. the same model using only its built-in file-reading and shell-grep tools. Not a different model — the same model, with and without sweet-search.docs/PHASE7.md.Retrieval quality is necessary but not sufficient. Cheaper, denser context only matters if it compounds across a real, multi-step engineering task — finding the code, understanding it, changing it, and not breaking anything. The next suite measures exactly that: resolve-rate on SWE-bench-style multi-file tasks, sweet-search-wired vs. native, on the same paired, multiplicity-controlled bar as above. Harness and pilot are in progress — numbers land here when they clear that bar, and not before.
Every number below is the ss-search pipeline end-to-end — the same binary you install — run
against the full benchmark corpus (no 99-distractor shortcuts), zero-shot (we never
fine-tune on these tasks). Where a benchmark's queries are docstrings, we strip the docstring out of the
indexed code so the query can't trivially match itself — the standard retrieval protocol.
We're SOTA in June 2026 on 3/4 attempted benchmarks at HARDER settings (running on full pool) than most other attempts!
| 📚 Benchmark | 🔍 What it tests | # Queries | 📂 Pool | 🎯 MRR@10 | 🏆 SOTA? |
|---|---|---|---|---|---|
| 🌐 GenCodeSearchNet | NL→code, 6 languages | 6,000 | full 6,000 | 86.6 | YES ✅ |
| 🐍 CoSQA | web queries → Python | 500 | full 6,267 | 65.5 | ✅ (zero-shot) |
| 🗺️ M2CRB | multilingual NL→code (ES/PT/DE/FR → Py/Java/JS) | 5,795 | full 5,795 | 54.0 | YES ✅ |
| 🛡️ AdvTest | adversarial, identifier-obfuscated Python | 19,210 | full 19,210 | 51.4 | NO ❌ |
SOTA = best result we can find in the published literature as of June 2026; cross-metric/protocol comparisons are spelled out per benchmark below.
86.6 · 🏆 SOTA in June 202665.5 · 🥇 Zero-shot SOTA in June 202647.5 · OpenAI text-embedding-3-large 55.4 · OASIS 55.8 — and goes toe-to-toe with fine-tuned CodeBERT / GraphCodeBERT (64.7 / 67.5). 💪54.0 · 🏆 SOTA in June 2026auMRRc ≥ full-pool MRR for any model). Our 54.0 is full-pool MRR@10 over all 5,795 functions in one pool — a strictly harder measure, cleared with no fine-tuning. 🔥51.4 · 🧪 our honest worst case — and we publish it anywaydef Func(arg_0):) deletes the lexical + graph signals our hybrid feeds on — yet we still beat the classic fine-tuned baselines (CodeBERT 27 · GraphCodeBERT 35 · UniXcoder 41), and our stack still lifts our own encoder ~3pp even here.59.5 for the bare CodeRankEmbed encoder — the reference FP32 model scores 54.7 on our leak-free corpus, our shipped INT8 build 51.4. The gap is stricter preprocessing + INT8 quantization, not the retrieval pipeline. We report exactly what we measured.eval/results/; rerun via eval/run_all.js. The canonical full-pool loaders are in eval/download_data.py.auMRRc ≥ full-pool MRR for any model — so our 54.0 full-pool MRR@10 (all 5,795 functions, zero-shot) clears their best on a strictly harder measure. No one publishes a plain full-corpus MRR@10 on M2CRB, so ours is the best available.10.2× ripgrep's median grep · 2.9 ms warm queries · 47× MaxSim kernels · −33% HNSW search p50
| ⚙️ What | 📈 Result | 📄 Source |
|---|---|---|
| ⚡ Indexed grep vs ripgrep | 10.2× faster at the median (8.5–17.7× across 5 repos, 353 realistic queries, 1 ms p50 — identical match counts on every query) | docs/GREP_INDEXING_STRATEGY.md |
| ⏱️ Warm query latency (native CLI) | 2.9 ms warm · 108 ms cold | docs/INIT_STRATEGY.md |
| 🧮 MaxSim rerank kernels | 1.26 s → 27 ms for a 231-candidate pass (47× native Rust; 16× WASM SIMD) | docs/MAXSIM_OPTIMIZATION.md |
| 🧠 HNSW tuning for code | −33% search p50, +5.9 pp recall@200 | docs/HNSW_APPROACH.md |
| 💾 Indexing memory | peak JS heap 785 MB → 213 MB | docs/DISK_FLUSHING_STRATEGY.md |
| 🍏 CoreML cascade (M3 Max) | 18% faster full indexing vs the Metal baseline | docs/INIT_STRATEGY.md |
Code search is a crowded space. Here's an honest read on where sweet-search wins and where it gives ground, against the trending leaders and our closest local peers.
| Capability | sweet-search | claude-context | Cursor index | codebase-memory | SocratiCode |
|---|---|---|---|---|---|
| 100% local — code never leaves your machine | ✅ | ✅¹ | ❌ | ✅ | ✅ |
| Works with zero API keys | ✅ | ✅¹ | ❌ | ✅ | ✅ |
| No external service to run (vector DB · Ollama · Docker) | ✅ | ❌ Milvus | ❌ cloud | ✅ | ⚠️⁵ |
| ColBERT late-interaction rerank | ✅ | ❌ | ❌ | ❌ | ❌ |
| Faster-than-ripgrep exact grep | ✅ | ❌ | ✅⁷ | ❌ | ❌ |
| Call-graph trace (callers · callees · impact) | ✅ | ❌ | ❌ | ✅ | ✅ |
| Drives any terminal agent (Claude Code · Codex · Gemini CLI) | ✅ | ✅ | ❌² | ✅ | ✅ |
| Published NL→code retrieval benchmarks | ✅ | ⚠️³ | ❌ | ⚠️³ | ⚠️³ |
| …and where sweet-search gives ground | |||||
| Native Windows | ❌⁴ | ✅ | ✅ | ✅ | ⚠️⁸ |
| Deep-AST language coverage | ⚠️ 14 (+70 via regex) | ⚠️ | ⚠️ | ✅ 158 | ⚠️ |
| In-editor GUI · writes & edits code | ❌ | ❌ | ✅ | ❌ | ❌⁶ |
| Org-wide, multi-repo scale | ❌ | ⚠️ | ⚠️ | ⚠️ | ✅ |
✅ yes · ⚠️ partial / with caveats · ❌ no. Verified June 2026; capabilities drift.
¹ claude-context's local path (Milvus Lite + Ollama embeddings) needs no API key, but it defaults to OpenAI/Voyage embeddings + Zilliz Cloud — and still runs Milvus + Ollama either way. ² Cursor's index is editor-locked — external terminal agents can't query it. ³ Reports token-reduction / efficiency, not a public NL→code retrieval-quality leaderboard. ⁴ Runs on Windows via WSL2. ⁵ SocratiCode manages a bundled Qdrant for you, but uses an auto-detected Ollama for local embeddings. ⁶ Ships an interactive HTML graph viewer, but doesn't edit code. ⁷ Cursor's local Instant Grep — a literal + regex index it benchmarks at ripgrep 16.8 s → 13 ms (the post that inspired our own n-gram prefilter). ⁸ SocratiCode runs on Windows via Docker only — no native binary, and no GPU there.
Where we lose, plainly: no native Windows yet, no editor GUI, and we index one repo at a time. If you need org-wide search across many repos and branches, that's where SocratiCode and Sourcegraph are built to win. If you live inside one editor, Cursor's index is already there. sweet-search is for the terminal agent that wants the best local retrieval on the repo in front of it. No one else combines all of it: ColBERT late-interaction reranking and faster-than-grep search, fully on-device, with nothing to sign up for.
Also in the space: Sourcegraph/Cody (org-scale, server-based), Continue.dev (local-default RAG), Serena (LSP symbol search, no embeddings), grepai (local CLI + trace), and cocoindex-code (embedded AST search).
Six small tools, one shared index. Each returns ranked, deduplicated, token-budgeted output designed to be consumed by an agent — a useful answer, not a wall of matches to scroll through.
| Tool | What you give it | What you get back |
|---|---|---|
1. ss-search | a natural-language query | ranked, self-contained code blocks |
2. ss-grep | an exact regex/literal | every file:line hit, ripgrep-identical |
3. ss-find | a regex + a query | regex matches, semantically re-ranked, as code blocks |
4. ss-semantic | a file + a question | just the relevant spans of that file |
5. ss-trace | a symbol | callers + callees + impact, in one call |
6. ss-read | a file (± line range) | exact bytes + symbol metadata |
ss-search — hybrid search powerhouseA hybrid search pipeline with late interaction reranking that returns actual code blocks.
Leading published-benchmark results — strongest we can find on GenCodeSearchNet, and above every published
zero-shot model on CoSQA. See benchmarks.
flowchart TD
Q(["🔍 natural-language query"]) --> ROUTE{{"🧭 WASM CatBoost router · lexical / hybrid"}}
ROUTE --> BM["📑 <b>BM25F</b><br/>field-weighted FTS5"]
ROUTE --> ANN
subgraph ANN ["🧬 three-stage ANN cascade"]
direction LR
BIN["binary <b>HNSW</b><br/>Hamming · ~100µs"] --> INT["INT8<br/>rescore"] --> FL["float32<br/>mmap sidecar"]
end
BM --> FUSE
ANN --> FUSE
FUSE["🔀 <b>CCFusion</b><br/>convex combo · RRF fallback"] --> ROW1
subgraph ROW1 [" "]
direction LR
IAR["⚓ <b>IAR</b><br/>exact-symbol injection"] --> INTENT["🎯 intent rerank<br/>demote docs · tests · config"]
end
ROW1 --> ROW2
subgraph ROW2 [" "]
direction LR
GRAPH["🕸️ graph expansion<br/>typed edges · 1–2 hops · <b>PathRAG</b>"] --> MAXSIM["🧮 <b>Late-Interaction Rerank</b><br/>⚡ native Rust MaxSim kernel"] --> OUT(["🏁 <b>self-contained code blocks</b><br/>whole functions · 3k/8k/12k budget"])
end
classDef io fill:#fde68a,stroke:#f59e0b,color:#000;
classDef out fill:#bbf7d0,stroke:#15803d,color:#000,stroke-width:3px;
classDef route fill:#e0e7ff,stroke:#818cf8,color:#000;
classDef lex fill:#dbeafe,stroke:#60a5fa,color:#000;
classDef fuse fill:#f3e8ff,stroke:#c084fc,color:#000;
classDef rank fill:#ffe4e6,stroke:#fb7185,color:#000;
class Q io;
class OUT out;
class ROUTE route;
class BM,BIN,INT,FL lex;
class FUSE,IAR fuse;
class INTENT,GRAPH,MAXSIM rank;
style ANN fill:#eff6ff,stroke:#93c5fd,color:#000;
style ROW1 fill:none,stroke:none;
style ROW2 fill:none,stroke:none;
↑ The diagram traces the hybrid route. A pure-lexical query — or a literal file path — short-circuits at the router straight to BM25F, skipping the vector cascade and fusion.
| Stage | What it actually does |
|---|---|
| 🧭 Route | WASM-exported CatBoost · lexical / hybrid · ~10 µs routing · low-confidence → max-recall hybrid |
| 🧬 Retrieve | • Lexical — BM25F over field-weighted FTS5 (name 10× · signature 5× · alias 4× · doc 1×) • Embed — query vectorized by the local CodeRankEmbed model (swappable for Voyage / Jina / Codestral) • Vector cascade — binary HNSW (Hamming, 64-byte, ~100 µs) → INT8 rescore → exact float32 from a memory-mapped sidecar |
| 🔀 Fuse | • CCFusion — convex-combine both rankings · per-route weights · quantile-normalized • MMR (λ=0.9) diversity pass over the fused list • auto RRF (k=60) fallback on degenerate score distributions |
| ⚓ Anchor | • IAR (Identifier Anchor Retrieval) — a real symbol in the query fires an exact-name code-graph lookup that injects that entity, even when the encoder ranked it too low |
| 🎯 Intent Rerank | • demote docs / tests / config when you want implementation • log-scaled call-site boosts surface the most-referenced function |
| 🕸️ Graph Expansion | • typed-edge walks (imports/extends/calls/uses) · adaptive 2-hop on the AST graph · edges picked by intent• PathRAG flow pruning + degree normalization → hubs can't dominate |
| 🧮 Late interaction Rerank | • Query embedded per-token by LateOn-Code (149M; a 17M edge variant auto-selected on low-RAM hosts) • MaxSim against the pre-indexed quantized token vectors • native Rust+Rayon MaxSim kernel ⚡ · WASM-SIMD fallback (1.26 s → 27 ms on a 231-candidate rerank) |
| 📦 Package | • entity-aware expansion → whole functions (imports, docstrings, decorators) • same-file overlap demotion → diverse, non-overlapping spans • symbol-family completion (agent mode) — generated/width families surface as a compact indexed manifest instead of truncating silently, inside the same budget • auto-selected 3k / 8k / 12k token budget |
🧠 The HNSW, in full (full writeup). Stage 1 is a from-scratch binary HNSW, and every "advanced" trick ships on by default:
⚡ Why it's quick. A native Rust + Rayon MaxSim kernel (47× over scalar; 16× WASM-SIMD fallback) · int4-quantized, binary-packed token vectors (plain INT4 is the shipped path — the full TurboQuant algorithm is researched but deferred; binary packing alone cut the LI index ~3.4×, 1.34 GiB → ~396 MiB) · a memory-mapped float32 sidecar that skips SQL on the rescore hot path · score-spread adaptive pooling (decisive queries shrink the rescore pool, ambiguous ones widen it) · and a warm daemon that answers in a single NAPI call — no process is ever forked.
🎛️ Priors & structure.
🛟 Rescues & honest trade-offs.
AND fall back to multi-query BM25F + RRF — one query per content keyword, fused.SWEET_SEARCH_CASCADE_ENABLED=true to try it). We'd rather ship the faster path than a fancier diagram.--full / --xl, or pick a mode with --mode lexical|semantic|hybrid|pattern.Also available as sweet-search "<query>" on the CLI and the search MCP tool.
ss-grep — grep, minus every wasted millisecond10.2× faster than ripgrep end-to-end at the median — measured across 353 realistic queries on 5 real repos (range 8.5–17.7× per repo, 1 ms p50), with identical match counts on every single query. Three things buy that:
Every match comes back in stable file:line order — ripgrep-identical counts, optional context lines — with no relevance guessing, no subprocess, in one warm call.
docs/GREP_INDEXING_STRATEGY.md.foo\|bar, \(group\)) are literals in Rust's regex dialect and used to silently match nothing — a zero-hit exact search now gets one gated auto-retry with the translated pattern instead of a false "no matches".ss-find — ColGrep, on a faster enginess-find "token refresh logic" --regex "refresh.*[Tt]oken"
Inspired by LightOn's ColGrep — regex precision, semantically ranked — but rebuilt on our own substrate:
ss-grep (in-process, no subprocess), not a filesystem scan.ss-search, it answers with ranked, self-contained code snippets — not bare file:line — so the find and the read collapse into one tool call. In our 30-question agent-workflow eval that eliminated every follow-up read and cut tokens 25.4% vs a grep + read workflow, at quality parity (gap of 0.01 on a 5-point scale).--li-model none disables pattern mode).sweet-search --mode pattern and via the search MCP tool's regex argument.ss-semantic — hybrid retrieval, scoped to one filess-semantic src/auth/session.ts "where does the cookie get its expiry?"
You know the file; this finds the lines. Every indexed chunk of the file is scored by three independent signals — BM25-style lexical term match, exact symbol-name match (weighted 1.5×), and per-token MaxSim late interaction over the LateOn-Code embeddings — fused with Reciprocal Rank Fusion (k=60), with symbol-less fragment chunks demoted 0.85× so real definitions win ties. The top spans are then re-read from disk (±2 context lines, overlapping spans merged), so the answer is filesystem ground truth even mid-edit; if the file is newer than its index entry you get an explicit staleness warning.
The useful answer: just the relevant spans with line numbers — not the whole file through your context window.
sweet-search read-semantic and the read-semantic MCP tool.ss-trace — graph algorithms, not grep guessworkss-trace processOrder --in src/orders/service.py
One call returns a symbol's callers, callees, and transitive impact paths from the AST-derived code
graph (entities + typed calls/imports/extends/uses edges, persisted in SQLite at index time).
Ranking fuses three signals:
page_rank column — a function called from five sites carries five units of mass, and it costs zero at query time;Because the graph is prebuilt, the global ranking is precomputed, and the personalized walk is local,
a full three-section trace costs milliseconds. The relation word (callers / callees / impact)
re-weights how the response token budget is split; --in disambiguates duplicate names; --depth
bounds impact traversal (1–4).
sweet-search trace and the trace MCP tool.ss-read — exact bytes, with the index's knowledge attachedss-read src/db/pool.js 120 180
A read tool that is filesystem-grounded by construction: bytes come straight from disk (never from the index, so never stale), but each indexed file arrives annotated with its cAST chunk metadata — symbol name, entity type, signature, line span — joined from the AST chunk index. The agent gets the code and the structural map of what it's looking at in one call: cite, navigate, or trace next without another search.
sweet-search read <file...> (and the read MCP tool) batches 1–20 files in a single call, each with the same symbol metadata — twenty files for the price of one tool invocation.The
ss-*wrappers ship in the npm package and are what the installed agent prompt drives. Every capability is equally available assweet-searchCLI subcommands and as MCP tools — see Works With Your Agent.
Shipping six tools is easy. Getting an agent to stop grepping in circles is the hard part.
So sweet-search init installs a ~1k-token system prompt that we didn't write — we grew it.
A GEPA-style loop mutated candidate prompts, scored each on a dual Pareto front (accuracy × cost)
against two different production agents at once — Claude Code (Sonnet) and Codex (GPT-5.5) — kept the
survivors, and repeated. A final correctness pass hardened the winner. ~1k tokens, one job: teach the
agent to search well.
🎓 The six rules it encodes:
| Rule | What it kills | |
|---|---|---|
| 🥇 | Cheapest tool first | Got an exact symbol? One ss-grep, trust the top hit, stop — no semantic search "just to confirm." |
| 🎯 | Trust the ranking | At most one narrow read to confirm; never re-run a hit that already matched. |
| 🚫 | Absence is an answer | Two empty probes (one semantic, one lexical) settle a negative — no third synonym, no find/ls spiral. |
| ⛔ | No raw-shell escape | The #1 token-waster in our trace analysis: agents bailing to dozens of raw grep/find calls after one miss. Door closed. |
| 📝 | Think before you dig | Before a third probe, the agent states what it knows and what its blind spot is. |
| 🗺️ | Map the fix surface | Before a visibly multi-site edit, make one mapping call and inspect the full function instead of fixing only the first match. |
🧾 The receipts — held-out discipline throughout: a dev set to iterate on, a held-out set touched only at milestones, a sealed vault opened exactly once.
| Validation gate | Result |
|---|---|
| 🎯 Held-out (30 probes × both agents) | joint score (worst of the two) 0.988 |
| 🌍 Out-of-distribution (8 languages never seen in the loop) | 0.952 — every language ≥ 0.79, zero weak spots |
| 🛡️ Adversarial counter-probes | 1.00 / 1.00 |
| 🔀 Held-out model families (never optimized on) | MiMo 0.988 · Qwen 0.980 — it generalizes, it doesn't memorize |
| 🧩 Paraphrase robustness (reword the prompt, same behavior) | correctness-weighted 0.95 / 0.93 |
p7-v1-mppppp-fs guide is the evolved champion plus a verdict-gated trust rule and a narrowly triggered fix-surface mapping rule. The latter came from full-200 failure forensics and was retained only after targeted fix/control smokes rejected a costlier wording.docs/PHASE7.md (internal probe suites; an externally-reproducible suite is in progress).AGENTS.md / GEMINI.md / .cursor/rules. Re-run init freely: owned content updates in place and user prose is preserved.Chunk → enrich → embed → quantize — every step on-device and in Rust. Batches are sized to your CPU's actual cache, two open code-models do the encoding, and two separate quantizations make the index both faster to build and small enough to live in RAM. Zero API keys; nothing ever leaves the machine.
|
① 🧩 Structure-aware chunk |
② 🏷️ Enrich from structure |
|
③ 🤖 Embed — two models |
④ 🗜️ Quantize + persist |
The inference engine, picked for your silicon:
| Your hardware | What runs |
|---|---|
| 🍏 Apple Silicon (M1+) | candle Metal, BF16, fused SDPA attention |
| 🍏 Apple Silicon (M3+) | … plus a CoreML Neural Engine cascade — ~18% faster full index (measured, M3 Max) |
| 🟩 NVIDIA GPU (SM 7.0+) | candle CUDA; flash-attention on Ampere+ |
| 💻 No accelerator | ONNX Runtime INT8 — tuned CPU path, 132 MB model, zero GPU weights downloaded |
JS · TS · TSX · Python · Go · Rust · Java · C · C++ · Ruby · PHP · Kotlin · Swift · C# — and a 39-config regex registry carries structure-aware chunking to 70+ more extensions.ss-read annotations, and the self-contained answers everywhere else.getId() still retrieves on the class and module around it.hw.perflevel0.l2cachesize (the 16 MB P-cluster on Apple Silicon, not the smaller E-cluster), Intel L3, or /sys/.../cache on Linux — then size every embedding batch so one transformer layer's weights plus the batch's activations stay resident in cache. No spilling to main memory mid-layer; on a long-sequence tail that's the difference between B=1 and a measured 2.1× per-chunk slowdown.AsyncTask, so tokenization and SQLite writes overlap compute instead of stalling behind it.| Model weights · INT8 ORT | Index vectors · INT4 binary | |
|---|---|---|
| Job | build the index faster on CPU | keep the on-disk index tiny |
| Win | ~2× faster indexing · 4× smaller model (132 MB) | LI index 1.34 GiB → ~396 MiB · INT4 nibble-packing halves it again |
| Fidelity | ≥ 0.96 cosine vs FP32 | no measurable retrieval loss (A/B-tested vs INT8) |
edge LateOn-Code (~9× smaller FP32 backbone) auto-selects on low-RAM hosts, and the whole CPU path runs INT8 with no GPU weights ever downloaded — full local search on a laptop with no accelerator.candle-flash-attn with varlen packing on CUDA Ampere+, and byte-for-byte upstream math on CPU so the fallback is provably identical.f32::MIN mask to -Inf and quietly produces NaN on padded rows — collapsing retrieval quality. We clamp the mask and serialize Metal command-buffer submissions (concurrent submission corrupts outputs on shared queues). Details in crates/sweet-search-native/src/inference/..mlpackage variants (bucketed by sequence length) dispatched to the Apple Neural Engine through an Objective-C shim; oversized batches fall through to Metal. Gated to M3+ because on M1/M2 the ANE doesn't beat its own compile overhead — we measured, so it's off there.Most code indexes rot the moment you start typing. sweet-search ships a reconcile daemon that keeps every tier of the index converged with your working tree — uncommitted edits included — without you ever running a command.
waiting_for_initial_index otherwise — no corrupted partial baselines.createAdmissionPolicy module (include globs → deny list → .sweet-search-ignore → 1 MB size cap → batched git check-ignore), so the two paths cannot drift.sweet-search reconcile status / reconcile inspect <path> explain exactly what the daemon thinks and why. Opt out any time with SWEET_SEARCH_RECONCILE_V2=0.Memory controls. The resident daemons show up in ps / Activity Monitor as
sweet-search-maintainer and sweet-search-daemon. A maintainer's steady state is
roughly 2–3 GB (embedding + late-interaction models stay loaded so ticks are fast),
and four independent mechanisms keep that bounded:
| Mechanism | Default | Override |
|---|---|---|
| Background ORT profile (arena-off + parked threads) in the maintainer | on | SWEET_SEARCH_ORT_BACKGROUND=0 |
| Per-process recycle ceiling — the maintainer finishes its tick, exits cleanly, and respawns fresh on the next edit when its RSS crosses the line | clamp(25 % of RAM, 4 GiB, 8 GiB) | SWEET_SEARCH_MAINTAINER_RSS_MAX_MB (0 disables) |
| Idle TTL — unattended daemons shut down and respawn on demand | tier-aware | SWEET_SEARCH_MAINTAINER_IDLE_TTL_MS / SWEET_SEARCH_DAEMON_IDLE_TTL_MS |
| Fleet RSS budget — across all repos' daemons, the longest-idle one is evicted when the sum crosses a RAM-scaled budget | tier-aware | SWEET_SEARCH_RSS_BUDGET_FRACTION |
A recycle or eviction never touches index state: every tick publishes atomically before the process exits, and the next edit (or query) respawns a fresh daemon.
Four Rust crates do the heavy lifting, each with a graceful fallback so the engine runs everywhere:
| Crate | What it does |
|---|---|
sweet-search-native | candle GPU/CPU inference, sparse-gram grep engine, SIMD posting-list intersection, SimHash/MinHash-LSH dedup, HuggingFace tokenizers — all over zero-copy NAPI |
wasm-maxsim | a hand-written WASM SIMD kernel computing ColBERT MaxSim in ~4 KB (~1.6 KB gzipped), with fused INT8 dequantization inside the SIMD pipeline plus a 4-bit nibble-packed path |
wasm-router | the 498-tree CatBoost query router, loop-unrolled, zero-allocation |
sweet-search-cli | a native CLI that talks to a warm search daemon over a per-project Unix socket — 2.9 ms measured warm-path queries |
tokenizers crate over NAPI — batched, cached, no Python anywhere in the stack.The quantization headline lives up in indexing — 1.34 GiB → ~396 MiB,
INT4-halved again. Here's the SSLX segment format that delivers it: crash-safe by construction, and
the three-stage retrieval it feeds at query time.
mmap (USearch view()), contributing 0 MB to the V8 heap at search time; the OS reclaims pages under pressure.docs/HNSW_APPROACH.md).sweet-search meets your agent wherever it is — shell tools, MCP, or injected instructions:
// .mcp.json (project root) — that's the whole integration
// or just run: sweet-search init --mcp
{
"mcpServers": {
"sweet-search": {
"command": "npx",
"args": ["-y", "sweet-search-mcp", "--project-root", "/absolute/path/to/your/repo"]
}
}
}
search, trace, read, read-semantic, index, health, repo-map, vocab-prewarm), 2 resources, 2 prompts; all search tools declared read-only and idempotent.claude/rules/sweet-search.md plus an automatically selected output style; Codex/OpenCode use AGENTS.md (--agents or --codex), while Gemini (--gemini) and Cursor (--cursor) use their native project filesrepo-map tool returns a PageRank-ranked symbol overview squeezed into any token budget, perfect for briefing a delegated agentinit --enforce-tools denies the native Grep tool in Claude Code and installs a hint hook nudging native Read toward ss-read/ss-semantic — for when you want the discipline guaranteed, not suggested./sweet-index skill: a Claude Code slash command for a full GPU-aware reindex, installed by init.sweet-search prewarm-vocab mines your repo's real identifiers, detects code communities (Leiden), and pre-warms all three search modes so even the first semantic query of a session is cache-warm.| Platform | Engine | Acceleration |
|---|---|---|
| macOS arm64 (Apple Silicon) | native | Metal (M1+) · CoreML Neural Engine (M3+) |
| macOS x64 (Intel) | native | ONNX Runtime INT8 CPU |
| Linux x64 (glibc) | native | CUDA (SM 7.0+, flash-attn on Ampere+) or INT8 CPU |
| Linux arm64 (glibc) | native | CUDA (Jetson Orin / Grace) or INT8 CPU |
| Windows | — | via WSL2 (= Linux x64) |
| Everything else | WASM/JS fallback | runs everywhere Node ≥ 18 runs |
Native binaries are selected automatically at npm install time via optionalDependencies — no flags, no postinstall scripts to debug. Every native fast path has a WASM or JS fallback that produces the same results.
sweet-search stands on a lot of shoulders, and we'd rather name them than pretend otherwise:
JavaScript
86.9%
Python
7.2%
Rust
4.8%
Local code search for AI agents: six fast, purpose-built tools that return ranked answers, not raw grep. Because maybe grep isn't all you need... 🍬
21
stars
1,122
commits
JavaScript
primary language
Sep 10, 2026
updated
Local code search for AI coding agents. Six fast, purpose-built tools that hand Claude Code, Codex & friends ranked answers, not raw grep. Zero API keys, 100% on-device.
Maybe grep isn't all you need… 🍬
Every coding agent today reaches for grep + Read by reflex. sweet-search challenges the narrative. 😎
init installs it into Claude Code by default (Codex, Gemini CLI, and Cursor via flags)|
GET STARTED 🚀 Quickstart 🖥️ Platform Support |
USE IT 🧰 The Six Tools 🧠 The Evolved Agent Prompt 🔌 Works With Your Agent |
UNDER THE HOOD ⚡ GPU-Accelerated Indexing 🔄 An Index That Never Goes Stale 🦀 The Native Engine Room |
THE RECEIPTS 📊 Benchmarks 🧭 Where sweet-search Fits 🙏 Prior Art & Acknowledgements 📄 License |
Requirements: Node.js 18+ on macOS (Apple silicon or Intel) or Linux (x64 or ARM64). On Windows, run sweet-search inside WSL2.
npm install -g sweet-search
cd your-repo
sweet-search init # one-time: downloads local models, wires up your agent
sweet-search index # builds the index — GPU-accelerated where available
sweet-search "where do we validate JWT tokens?"
That's it. init is idempotent and SHA256-verifies every model binary; re-running it is always safe.
From then on, the index stays up to date automatically as you work.
sweet-search stores its index in SQLite via better-sqlite3, which downloads its
prebuilt native binary from an install script. Recent npm versions block those
scripts by default and only print a warning, which leaves the binding missing and
makes indexing fail. sweet-search init detects this and stops with instructions
rather than reporting success.
For a global install, approve the scripts once:
npm install -g sweet-search --allow-scripts=sweet-search,better-sqlite3
For a project-local install, add the allowlist to that project's package.json
(npm rejects the flag for project-scoped installs), then reinstall:
"allowScripts": { "sweet-search": true, "better-sqlite3": true }
Verify with sweet-search init — the native:sqlite check must pass.
For Claude Code, init automatically installs and activates the sweet-search
output style, which adds the compact routing override at system-prompt priority.
Start a new Claude session or run /clear after init, and keep that output style
selected for reliable ss-* routing.
To uninstall 😢:
sweet-search uninstall --keep-models # current repo only; keeps shared models and the global CLI
sweet-search uninstall # current repo plus its shared model downloads; keeps the global CLI
npm uninstall -g sweet-search # global CLI only; does not clean initialized repos
Run sweet-search uninstall inside each initialized repo before removing the global CLI.
sweet-search init --wizard # interactive: shows your hardware, recommends a model tier
sweet-search init --profile core # lexical-only, no model downloads (CI-friendly)
sweet-search init --li-model edge # compact late-interaction model for constrained machines
sweet-search init --agents # also configure AGENTS.md for Codex/OpenCode
sweet-search init --no-claude --agents # streamlined AGENTS-only configuration
sweet-search uninstall --dry-run # preview cleanup for the current repo
~/.cache/sweet-search/models/ and is used strictly on-device.CLAUDE.md untouched, writes the verbatim evolved guide to .claude/rules/sweet-search.md, installs .claude/output-styles/sweet-search.md, and selects it in .claude/settings.json. It also registers a session-start prewarm hook and installs the /sweet-index skill./config, then emits a warning. A higher-priority .claude/settings.local.json selection is also detected and reported. Select sweet-search, then run /clear or restart Claude Code.--agents to place the same verbatim guide directly in AGENTS.md. Use --no-claude --agents when AGENTS.md is the only integration you want; --codex additionally installs Codex's project prewarm hook..gitignore is respected, node_modules/build dirs/minified artifacts are denied, files over 1 MB skipped, with a .sweet-search-ignore for extra rules.| Flag | Behavior |
|---|---|
--profile core|full | Select lexical-only core or the full model-backed profile. |
--li-model standard|edge|none | Select the late-interaction model tier or disable it. |
--search-reranking auto|on|off | Control search-time late-interaction reranking. |
--wizard | Choose model and reranking settings interactively. |
--verify-deep | Load modules and verify checksums after setup. |
--force | Re-download models even when cached. |
--build-coreml-cascade | Build the optional CoreML cascade locally on eligible Apple silicon. |
--skip-coreml-cascade | Skip fetching or building the CoreML cascade. |
--skip-dedup | Skip near-duplicate-detection readiness checks. |
--skip-cuda | Disable the CUDA backend even when available. |
--skip-prewarm-hook | Do not register the Claude/Codex session-start prewarm hook. |
--agents | Also write the verbatim guide to AGENTS.md for Codex/OpenCode. |
--codex | Add the Codex prewarm hook and project feature flag; implies --agents. |
--codex-enable-global-hooks | Advanced opt-in: also enable hooks in the user-level Codex config. |
--no-claude | Write nothing under .claude/; combine with --agents for AGENTS-only setup. |
--gemini | Also write GEMINI.md (sharing AGENTS.md when enabled). |
--cursor | Also write .cursor/rules/sweet-search.mdc. |
--symlink-instruction-files | Explicitly use the default GEMINI.md symlink behavior. |
--no-symlink-instruction-files | Use a regular GEMINI.md import instead of a symlink. |
--no-agent-instructions | Skip all agent policy and Claude output-style installation. |
--mcp | Also register the project MCP server; the CLI remains the default contact surface. |
--no-cli | With --mcp, give agents the MCP-specific guide and remove the CLI-specific Claude output style. |
--enforce-tools | Optional strict Claude mode: deny native Grep and hint native Read. |
--verbose, -v | Print additional setup diagnostics. |
--help, -h | Show the complete CLI help. |
| Flag | Behavior |
|---|---|
--dry-run | Preview all detected removals. |
--keep-models | Preserve shared model and CoreML caches. |
--purge | Also uninstall the npm package and Sweet Search native packages. |
--force | Skip the confirmation prompt. |
--help, -h | Show uninstall help. |
sweet-search uninstall removes Sweet Search's rule, output style and active
selection, AGENTS/GEMINI/Cursor instruction blocks, project hooks and skill,
MCP registration, enforcement/reminder artifacts, and .sweet-search/. It
preserves user-authored content. Generic Codex [features] hooks = true flags
are left in place because other tools may share them, and an otherwise-empty
settings file may remain as {}.
We measure sweet-search four ways — from how much it helps a real agent down to raw engine throughput:
|
🤖 ① Code-retrieval (agent-in-the-loop) |
🚧 ② Task-completion (coming soon) |
|
📄 ③ Paper-type IR (academic) |
⚡ ④ Engine speed |
One variable changes: how the agent searches a real repository.
Same tasks, same judge, paired probe-for-probe.
sealed vault · exact paired results · five representative profiles · full 11-cell matrix and held-out/OOD replication below
The headline, in four claims:
The win is harness-adaptive: where the native loop is disciplined (Claude Code) it shows up as denser, more useful context per token; where it thrashes (Codex floods 30k+ tokens of its own grep output into context) it shows up as a large cost and tool-call cut. Either way, final-answer accuracy never significantly regresses.
| 🧰 Native agent harness | 💰 Realized cost | 🔧 Tool calls | ✨ Useful content / response | 🎯 Final accuracy |
|---|---|---|---|---|
| 🤖 Codex (GPT-5.5) | −30 to −34% | −44 to −56% | +0.06 → +0.17 ↑ | tie (saturated) |
| 🐚 opencode (GPT-5.5 / GLM-5.1) | −18 to −22% | −15 to −49% | +0.23 to +0.31 ↑ | tie |
| 🔌 bare API (GPT-5.5 / GLM / DeepSeek) | −15 to −32% ᵃ | −15 to −33% | +0.08 to +0.24 ↑ | tie · +3 pp on weak models |
| 🟣 Claude Code (Sonnet / Opus) | −10% to +14% ᵇ | −5 to −33% | +0.18 to +0.29 ↑ | tie |
↑ "Useful content / response" is the per-response delta on a 5-dimension usefulness score (answer-grounding · workable-code · navigability · edit-locality · sufficiency), 0–1 scale. "tie" = final-answer correctness statistically indistinguishable (saturated in the 0.94–0.99 band on flagships).
ᵃ the two cheapest bare models cost fractions of a cent either way (GLM +27% of $0.008; DeepSeek −15% of $0.004). ᵇ Opus −5/−10%; Sonnet +8–14%, which is ≈1¢ on a flat-rate subscription for a richer answer.
Denser, not just longer. The usefulness lift survives length-matching — comparing sweet-search and native responses of equal token length, sweet-search's content is significantly higher on 8 of 11 cells. The validated single-number usefulness composite (grounding × content × density) is significant on all 11 sealed cells.
sweet-search agent prompt + tools vs. the same model using only its built-in file-reading and shell-grep tools. Not a different model — the same model, with and without sweet-search.docs/PHASE7.md.Retrieval quality is necessary but not sufficient. Cheaper, denser context only matters if it compounds across a real, multi-step engineering task — finding the code, understanding it, changing it, and not breaking anything. The next suite measures exactly that: resolve-rate on SWE-bench-style multi-file tasks, sweet-search-wired vs. native, on the same paired, multiplicity-controlled bar as above. Harness and pilot are in progress — numbers land here when they clear that bar, and not before.
Every number below is the ss-search pipeline end-to-end — the same binary you install — run
against the full benchmark corpus (no 99-distractor shortcuts), zero-shot (we never
fine-tune on these tasks). Where a benchmark's queries are docstrings, we strip the docstring out of the
indexed code so the query can't trivially match itself — the standard retrieval protocol.
We're SOTA in June 2026 on 3/4 attempted benchmarks at HARDER settings (running on full pool) than most other attempts!
| 📚 Benchmark | 🔍 What it tests | # Queries | 📂 Pool | 🎯 MRR@10 | 🏆 SOTA? |
|---|---|---|---|---|---|
| 🌐 GenCodeSearchNet | NL→code, 6 languages | 6,000 | full 6,000 | 86.6 | YES ✅ |
| 🐍 CoSQA | web queries → Python | 500 | full 6,267 | 65.5 | ✅ (zero-shot) |
| 🗺️ M2CRB | multilingual NL→code (ES/PT/DE/FR → Py/Java/JS) | 5,795 | full 5,795 | 54.0 | YES ✅ |
| 🛡️ AdvTest | adversarial, identifier-obfuscated Python | 19,210 | full 19,210 | 51.4 | NO ❌ |
SOTA = best result we can find in the published literature as of June 2026; cross-metric/protocol comparisons are spelled out per benchmark below.
86.6 · 🏆 SOTA in June 202665.5 · 🥇 Zero-shot SOTA in June 202647.5 · OpenAI text-embedding-3-large 55.4 · OASIS 55.8 — and goes toe-to-toe with fine-tuned CodeBERT / GraphCodeBERT (64.7 / 67.5). 💪54.0 · 🏆 SOTA in June 2026auMRRc ≥ full-pool MRR for any model). Our 54.0 is full-pool MRR@10 over all 5,795 functions in one pool — a strictly harder measure, cleared with no fine-tuning. 🔥51.4 · 🧪 our honest worst case — and we publish it anywaydef Func(arg_0):) deletes the lexical + graph signals our hybrid feeds on — yet we still beat the classic fine-tuned baselines (CodeBERT 27 · GraphCodeBERT 35 · UniXcoder 41), and our stack still lifts our own encoder ~3pp even here.59.5 for the bare CodeRankEmbed encoder — the reference FP32 model scores 54.7 on our leak-free corpus, our shipped INT8 build 51.4. The gap is stricter preprocessing + INT8 quantization, not the retrieval pipeline. We report exactly what we measured.eval/results/; rerun via eval/run_all.js. The canonical full-pool loaders are in eval/download_data.py.auMRRc ≥ full-pool MRR for any model — so our 54.0 full-pool MRR@10 (all 5,795 functions, zero-shot) clears their best on a strictly harder measure. No one publishes a plain full-corpus MRR@10 on M2CRB, so ours is the best available.10.2× ripgrep's median grep · 2.9 ms warm queries · 47× MaxSim kernels · −33% HNSW search p50
| ⚙️ What | 📈 Result | 📄 Source |
|---|---|---|
| ⚡ Indexed grep vs ripgrep | 10.2× faster at the median (8.5–17.7× across 5 repos, 353 realistic queries, 1 ms p50 — identical match counts on every query) | docs/GREP_INDEXING_STRATEGY.md |
| ⏱️ Warm query latency (native CLI) | 2.9 ms warm · 108 ms cold | docs/INIT_STRATEGY.md |
| 🧮 MaxSim rerank kernels | 1.26 s → 27 ms for a 231-candidate pass (47× native Rust; 16× WASM SIMD) | docs/MAXSIM_OPTIMIZATION.md |
| 🧠 HNSW tuning for code | −33% search p50, +5.9 pp recall@200 | docs/HNSW_APPROACH.md |
| 💾 Indexing memory | peak JS heap 785 MB → 213 MB | docs/DISK_FLUSHING_STRATEGY.md |
| 🍏 CoreML cascade (M3 Max) | 18% faster full indexing vs the Metal baseline | docs/INIT_STRATEGY.md |
Code search is a crowded space. Here's an honest read on where sweet-search wins and where it gives ground, against the trending leaders and our closest local peers.
| Capability | sweet-search | claude-context | Cursor index | codebase-memory | SocratiCode |
|---|---|---|---|---|---|
| 100% local — code never leaves your machine | ✅ | ✅¹ | ❌ | ✅ | ✅ |
| Works with zero API keys | ✅ | ✅¹ | ❌ | ✅ | ✅ |
| No external service to run (vector DB · Ollama · Docker) | ✅ | ❌ Milvus | ❌ cloud | ✅ | ⚠️⁵ |
| ColBERT late-interaction rerank | ✅ | ❌ | ❌ | ❌ | ❌ |
| Faster-than-ripgrep exact grep | ✅ | ❌ | ✅⁷ | ❌ | ❌ |
| Call-graph trace (callers · callees · impact) | ✅ | ❌ | ❌ | ✅ | ✅ |
| Drives any terminal agent (Claude Code · Codex · Gemini CLI) | ✅ | ✅ | ❌² | ✅ | ✅ |
| Published NL→code retrieval benchmarks | ✅ | ⚠️³ | ❌ | ⚠️³ | ⚠️³ |
| …and where sweet-search gives ground | |||||
| Native Windows | ❌⁴ | ✅ | ✅ | ✅ | ⚠️⁸ |
| Deep-AST language coverage | ⚠️ 14 (+70 via regex) | ⚠️ | ⚠️ | ✅ 158 | ⚠️ |
| In-editor GUI · writes & edits code | ❌ | ❌ | ✅ | ❌ | ❌⁶ |
| Org-wide, multi-repo scale | ❌ | ⚠️ | ⚠️ | ⚠️ | ✅ |
✅ yes · ⚠️ partial / with caveats · ❌ no. Verified June 2026; capabilities drift.
¹ claude-context's local path (Milvus Lite + Ollama embeddings) needs no API key, but it defaults to OpenAI/Voyage embeddings + Zilliz Cloud — and still runs Milvus + Ollama either way. ² Cursor's index is editor-locked — external terminal agents can't query it. ³ Reports token-reduction / efficiency, not a public NL→code retrieval-quality leaderboard. ⁴ Runs on Windows via WSL2. ⁵ SocratiCode manages a bundled Qdrant for you, but uses an auto-detected Ollama for local embeddings. ⁶ Ships an interactive HTML graph viewer, but doesn't edit code. ⁷ Cursor's local Instant Grep — a literal + regex index it benchmarks at ripgrep 16.8 s → 13 ms (the post that inspired our own n-gram prefilter). ⁸ SocratiCode runs on Windows via Docker only — no native binary, and no GPU there.
Where we lose, plainly: no native Windows yet, no editor GUI, and we index one repo at a time. If you need org-wide search across many repos and branches, that's where SocratiCode and Sourcegraph are built to win. If you live inside one editor, Cursor's index is already there. sweet-search is for the terminal agent that wants the best local retrieval on the repo in front of it. No one else combines all of it: ColBERT late-interaction reranking and faster-than-grep search, fully on-device, with nothing to sign up for.
Also in the space: Sourcegraph/Cody (org-scale, server-based), Continue.dev (local-default RAG), Serena (LSP symbol search, no embeddings), grepai (local CLI + trace), and cocoindex-code (embedded AST search).
Six small tools, one shared index. Each returns ranked, deduplicated, token-budgeted output designed to be consumed by an agent — a useful answer, not a wall of matches to scroll through.
| Tool | What you give it | What you get back |
|---|---|---|
1. ss-search | a natural-language query | ranked, self-contained code blocks |
2. ss-grep | an exact regex/literal | every file:line hit, ripgrep-identical |
3. ss-find | a regex + a query | regex matches, semantically re-ranked, as code blocks |
4. ss-semantic | a file + a question | just the relevant spans of that file |
5. ss-trace | a symbol | callers + callees + impact, in one call |
6. ss-read | a file (± line range) | exact bytes + symbol metadata |
ss-search — hybrid search powerhouseA hybrid search pipeline with late interaction reranking that returns actual code blocks.
Leading published-benchmark results — strongest we can find on GenCodeSearchNet, and above every published
zero-shot model on CoSQA. See benchmarks.
flowchart TD
Q(["🔍 natural-language query"]) --> ROUTE{{"🧭 WASM CatBoost router · lexical / hybrid"}}
ROUTE --> BM["📑 <b>BM25F</b><br/>field-weighted FTS5"]
ROUTE --> ANN
subgraph ANN ["🧬 three-stage ANN cascade"]
direction LR
BIN["binary <b>HNSW</b><br/>Hamming · ~100µs"] --> INT["INT8<br/>rescore"] --> FL["float32<br/>mmap sidecar"]
end
BM --> FUSE
ANN --> FUSE
FUSE["🔀 <b>CCFusion</b><br/>convex combo · RRF fallback"] --> ROW1
subgraph ROW1 [" "]
direction LR
IAR["⚓ <b>IAR</b><br/>exact-symbol injection"] --> INTENT["🎯 intent rerank<br/>demote docs · tests · config"]
end
ROW1 --> ROW2
subgraph ROW2 [" "]
direction LR
GRAPH["🕸️ graph expansion<br/>typed edges · 1–2 hops · <b>PathRAG</b>"] --> MAXSIM["🧮 <b>Late-Interaction Rerank</b><br/>⚡ native Rust MaxSim kernel"] --> OUT(["🏁 <b>self-contained code blocks</b><br/>whole functions · 3k/8k/12k budget"])
end
classDef io fill:#fde68a,stroke:#f59e0b,color:#000;
classDef out fill:#bbf7d0,stroke:#15803d,color:#000,stroke-width:3px;
classDef route fill:#e0e7ff,stroke:#818cf8,color:#000;
classDef lex fill:#dbeafe,stroke:#60a5fa,color:#000;
classDef fuse fill:#f3e8ff,stroke:#c084fc,color:#000;
classDef rank fill:#ffe4e6,stroke:#fb7185,color:#000;
class Q io;
class OUT out;
class ROUTE route;
class BM,BIN,INT,FL lex;
class FUSE,IAR fuse;
class INTENT,GRAPH,MAXSIM rank;
style ANN fill:#eff6ff,stroke:#93c5fd,color:#000;
style ROW1 fill:none,stroke:none;
style ROW2 fill:none,stroke:none;
↑ The diagram traces the hybrid route. A pure-lexical query — or a literal file path — short-circuits at the router straight to BM25F, skipping the vector cascade and fusion.
| Stage | What it actually does |
|---|---|
| 🧭 Route | WASM-exported CatBoost · lexical / hybrid · ~10 µs routing · low-confidence → max-recall hybrid |
| 🧬 Retrieve | • Lexical — BM25F over field-weighted FTS5 (name 10× · signature 5× · alias 4× · doc 1×) • Embed — query vectorized by the local CodeRankEmbed model (swappable for Voyage / Jina / Codestral) • Vector cascade — binary HNSW (Hamming, 64-byte, ~100 µs) → INT8 rescore → exact float32 from a memory-mapped sidecar |
| 🔀 Fuse | • CCFusion — convex-combine both rankings · per-route weights · quantile-normalized • MMR (λ=0.9) diversity pass over the fused list • auto RRF (k=60) fallback on degenerate score distributions |
| ⚓ Anchor | • IAR (Identifier Anchor Retrieval) — a real symbol in the query fires an exact-name code-graph lookup that injects that entity, even when the encoder ranked it too low |
| 🎯 Intent Rerank | • demote docs / tests / config when you want implementation • log-scaled call-site boosts surface the most-referenced function |
| 🕸️ Graph Expansion | • typed-edge walks (imports/extends/calls/uses) · adaptive 2-hop on the AST graph · edges picked by intent• PathRAG flow pruning + degree normalization → hubs can't dominate |
| 🧮 Late interaction Rerank | • Query embedded per-token by LateOn-Code (149M; a 17M edge variant auto-selected on low-RAM hosts) • MaxSim against the pre-indexed quantized token vectors • native Rust+Rayon MaxSim kernel ⚡ · WASM-SIMD fallback (1.26 s → 27 ms on a 231-candidate rerank) |
| 📦 Package | • entity-aware expansion → whole functions (imports, docstrings, decorators) • same-file overlap demotion → diverse, non-overlapping spans • symbol-family completion (agent mode) — generated/width families surface as a compact indexed manifest instead of truncating silently, inside the same budget • auto-selected 3k / 8k / 12k token budget |
🧠 The HNSW, in full (full writeup). Stage 1 is a from-scratch binary HNSW, and every "advanced" trick ships on by default:
⚡ Why it's quick. A native Rust + Rayon MaxSim kernel (47× over scalar; 16× WASM-SIMD fallback) · int4-quantized, binary-packed token vectors (plain INT4 is the shipped path — the full TurboQuant algorithm is researched but deferred; binary packing alone cut the LI index ~3.4×, 1.34 GiB → ~396 MiB) · a memory-mapped float32 sidecar that skips SQL on the rescore hot path · score-spread adaptive pooling (decisive queries shrink the rescore pool, ambiguous ones widen it) · and a warm daemon that answers in a single NAPI call — no process is ever forked.
🎛️ Priors & structure.
🛟 Rescues & honest trade-offs.
AND fall back to multi-query BM25F + RRF — one query per content keyword, fused.SWEET_SEARCH_CASCADE_ENABLED=true to try it). We'd rather ship the faster path than a fancier diagram.--full / --xl, or pick a mode with --mode lexical|semantic|hybrid|pattern.Also available as sweet-search "<query>" on the CLI and the search MCP tool.
ss-grep — grep, minus every wasted millisecond10.2× faster than ripgrep end-to-end at the median — measured across 353 realistic queries on 5 real repos (range 8.5–17.7× per repo, 1 ms p50), with identical match counts on every single query. Three things buy that:
Every match comes back in stable file:line order — ripgrep-identical counts, optional context lines — with no relevance guessing, no subprocess, in one warm call.
docs/GREP_INDEXING_STRATEGY.md.foo\|bar, \(group\)) are literals in Rust's regex dialect and used to silently match nothing — a zero-hit exact search now gets one gated auto-retry with the translated pattern instead of a false "no matches".ss-find — ColGrep, on a faster enginess-find "token refresh logic" --regex "refresh.*[Tt]oken"
Inspired by LightOn's ColGrep — regex precision, semantically ranked — but rebuilt on our own substrate:
ss-grep (in-process, no subprocess), not a filesystem scan.ss-search, it answers with ranked, self-contained code snippets — not bare file:line — so the find and the read collapse into one tool call. In our 30-question agent-workflow eval that eliminated every follow-up read and cut tokens 25.4% vs a grep + read workflow, at quality parity (gap of 0.01 on a 5-point scale).--li-model none disables pattern mode).sweet-search --mode pattern and via the search MCP tool's regex argument.ss-semantic — hybrid retrieval, scoped to one filess-semantic src/auth/session.ts "where does the cookie get its expiry?"
You know the file; this finds the lines. Every indexed chunk of the file is scored by three independent signals — BM25-style lexical term match, exact symbol-name match (weighted 1.5×), and per-token MaxSim late interaction over the LateOn-Code embeddings — fused with Reciprocal Rank Fusion (k=60), with symbol-less fragment chunks demoted 0.85× so real definitions win ties. The top spans are then re-read from disk (±2 context lines, overlapping spans merged), so the answer is filesystem ground truth even mid-edit; if the file is newer than its index entry you get an explicit staleness warning.
The useful answer: just the relevant spans with line numbers — not the whole file through your context window.
sweet-search read-semantic and the read-semantic MCP tool.ss-trace — graph algorithms, not grep guessworkss-trace processOrder --in src/orders/service.py
One call returns a symbol's callers, callees, and transitive impact paths from the AST-derived code
graph (entities + typed calls/imports/extends/uses edges, persisted in SQLite at index time).
Ranking fuses three signals:
page_rank column — a function called from five sites carries five units of mass, and it costs zero at query time;Because the graph is prebuilt, the global ranking is precomputed, and the personalized walk is local,
a full three-section trace costs milliseconds. The relation word (callers / callees / impact)
re-weights how the response token budget is split; --in disambiguates duplicate names; --depth
bounds impact traversal (1–4).
sweet-search trace and the trace MCP tool.ss-read — exact bytes, with the index's knowledge attachedss-read src/db/pool.js 120 180
A read tool that is filesystem-grounded by construction: bytes come straight from disk (never from the index, so never stale), but each indexed file arrives annotated with its cAST chunk metadata — symbol name, entity type, signature, line span — joined from the AST chunk index. The agent gets the code and the structural map of what it's looking at in one call: cite, navigate, or trace next without another search.
sweet-search read <file...> (and the read MCP tool) batches 1–20 files in a single call, each with the same symbol metadata — twenty files for the price of one tool invocation.The
ss-*wrappers ship in the npm package and are what the installed agent prompt drives. Every capability is equally available assweet-searchCLI subcommands and as MCP tools — see Works With Your Agent.
Shipping six tools is easy. Getting an agent to stop grepping in circles is the hard part.
So sweet-search init installs a ~1k-token system prompt that we didn't write — we grew it.
A GEPA-style loop mutated candidate prompts, scored each on a dual Pareto front (accuracy × cost)
against two different production agents at once — Claude Code (Sonnet) and Codex (GPT-5.5) — kept the
survivors, and repeated. A final correctness pass hardened the winner. ~1k tokens, one job: teach the
agent to search well.
🎓 The six rules it encodes:
| Rule | What it kills | |
|---|---|---|
| 🥇 | Cheapest tool first | Got an exact symbol? One ss-grep, trust the top hit, stop — no semantic search "just to confirm." |
| 🎯 | Trust the ranking | At most one narrow read to confirm; never re-run a hit that already matched. |
| 🚫 | Absence is an answer | Two empty probes (one semantic, one lexical) settle a negative — no third synonym, no find/ls spiral. |
| ⛔ | No raw-shell escape | The #1 token-waster in our trace analysis: agents bailing to dozens of raw grep/find calls after one miss. Door closed. |
| 📝 | Think before you dig | Before a third probe, the agent states what it knows and what its blind spot is. |
| 🗺️ | Map the fix surface | Before a visibly multi-site edit, make one mapping call and inspect the full function instead of fixing only the first match. |
🧾 The receipts — held-out discipline throughout: a dev set to iterate on, a held-out set touched only at milestones, a sealed vault opened exactly once.
| Validation gate | Result |
|---|---|
| 🎯 Held-out (30 probes × both agents) | joint score (worst of the two) 0.988 |
| 🌍 Out-of-distribution (8 languages never seen in the loop) | 0.952 — every language ≥ 0.79, zero weak spots |
| 🛡️ Adversarial counter-probes | 1.00 / 1.00 |
| 🔀 Held-out model families (never optimized on) | MiMo 0.988 · Qwen 0.980 — it generalizes, it doesn't memorize |
| 🧩 Paraphrase robustness (reword the prompt, same behavior) | correctness-weighted 0.95 / 0.93 |
p7-v1-mppppp-fs guide is the evolved champion plus a verdict-gated trust rule and a narrowly triggered fix-surface mapping rule. The latter came from full-200 failure forensics and was retained only after targeted fix/control smokes rejected a costlier wording.docs/PHASE7.md (internal probe suites; an externally-reproducible suite is in progress).AGENTS.md / GEMINI.md / .cursor/rules. Re-run init freely: owned content updates in place and user prose is preserved.Chunk → enrich → embed → quantize — every step on-device and in Rust. Batches are sized to your CPU's actual cache, two open code-models do the encoding, and two separate quantizations make the index both faster to build and small enough to live in RAM. Zero API keys; nothing ever leaves the machine.
|
① 🧩 Structure-aware chunk |
② 🏷️ Enrich from structure |
|
③ 🤖 Embed — two models |
④ 🗜️ Quantize + persist |
The inference engine, picked for your silicon:
| Your hardware | What runs |
|---|---|
| 🍏 Apple Silicon (M1+) | candle Metal, BF16, fused SDPA attention |
| 🍏 Apple Silicon (M3+) | … plus a CoreML Neural Engine cascade — ~18% faster full index (measured, M3 Max) |
| 🟩 NVIDIA GPU (SM 7.0+) | candle CUDA; flash-attention on Ampere+ |
| 💻 No accelerator | ONNX Runtime INT8 — tuned CPU path, 132 MB model, zero GPU weights downloaded |
JS · TS · TSX · Python · Go · Rust · Java · C · C++ · Ruby · PHP · Kotlin · Swift · C# — and a 39-config regex registry carries structure-aware chunking to 70+ more extensions.ss-read annotations, and the self-contained answers everywhere else.getId() still retrieves on the class and module around it.hw.perflevel0.l2cachesize (the 16 MB P-cluster on Apple Silicon, not the smaller E-cluster), Intel L3, or /sys/.../cache on Linux — then size every embedding batch so one transformer layer's weights plus the batch's activations stay resident in cache. No spilling to main memory mid-layer; on a long-sequence tail that's the difference between B=1 and a measured 2.1× per-chunk slowdown.AsyncTask, so tokenization and SQLite writes overlap compute instead of stalling behind it.| Model weights · INT8 ORT | Index vectors · INT4 binary | |
|---|---|---|
| Job | build the index faster on CPU | keep the on-disk index tiny |
| Win | ~2× faster indexing · 4× smaller model (132 MB) | LI index 1.34 GiB → ~396 MiB · INT4 nibble-packing halves it again |
| Fidelity | ≥ 0.96 cosine vs FP32 | no measurable retrieval loss (A/B-tested vs INT8) |
edge LateOn-Code (~9× smaller FP32 backbone) auto-selects on low-RAM hosts, and the whole CPU path runs INT8 with no GPU weights ever downloaded — full local search on a laptop with no accelerator.candle-flash-attn with varlen packing on CUDA Ampere+, and byte-for-byte upstream math on CPU so the fallback is provably identical.f32::MIN mask to -Inf and quietly produces NaN on padded rows — collapsing retrieval quality. We clamp the mask and serialize Metal command-buffer submissions (concurrent submission corrupts outputs on shared queues). Details in crates/sweet-search-native/src/inference/..mlpackage variants (bucketed by sequence length) dispatched to the Apple Neural Engine through an Objective-C shim; oversized batches fall through to Metal. Gated to M3+ because on M1/M2 the ANE doesn't beat its own compile overhead — we measured, so it's off there.Most code indexes rot the moment you start typing. sweet-search ships a reconcile daemon that keeps every tier of the index converged with your working tree — uncommitted edits included — without you ever running a command.
waiting_for_initial_index otherwise — no corrupted partial baselines.createAdmissionPolicy module (include globs → deny list → .sweet-search-ignore → 1 MB size cap → batched git check-ignore), so the two paths cannot drift.sweet-search reconcile status / reconcile inspect <path> explain exactly what the daemon thinks and why. Opt out any time with SWEET_SEARCH_RECONCILE_V2=0.Memory controls. The resident daemons show up in ps / Activity Monitor as
sweet-search-maintainer and sweet-search-daemon. A maintainer's steady state is
roughly 2–3 GB (embedding + late-interaction models stay loaded so ticks are fast),
and four independent mechanisms keep that bounded:
| Mechanism | Default | Override |
|---|---|---|
| Background ORT profile (arena-off + parked threads) in the maintainer | on | SWEET_SEARCH_ORT_BACKGROUND=0 |
| Per-process recycle ceiling — the maintainer finishes its tick, exits cleanly, and respawns fresh on the next edit when its RSS crosses the line | clamp(25 % of RAM, 4 GiB, 8 GiB) | SWEET_SEARCH_MAINTAINER_RSS_MAX_MB (0 disables) |
| Idle TTL — unattended daemons shut down and respawn on demand | tier-aware | SWEET_SEARCH_MAINTAINER_IDLE_TTL_MS / SWEET_SEARCH_DAEMON_IDLE_TTL_MS |
| Fleet RSS budget — across all repos' daemons, the longest-idle one is evicted when the sum crosses a RAM-scaled budget | tier-aware | SWEET_SEARCH_RSS_BUDGET_FRACTION |
A recycle or eviction never touches index state: every tick publishes atomically before the process exits, and the next edit (or query) respawns a fresh daemon.
Four Rust crates do the heavy lifting, each with a graceful fallback so the engine runs everywhere:
| Crate | What it does |
|---|---|
sweet-search-native | candle GPU/CPU inference, sparse-gram grep engine, SIMD posting-list intersection, SimHash/MinHash-LSH dedup, HuggingFace tokenizers — all over zero-copy NAPI |
wasm-maxsim | a hand-written WASM SIMD kernel computing ColBERT MaxSim in ~4 KB (~1.6 KB gzipped), with fused INT8 dequantization inside the SIMD pipeline plus a 4-bit nibble-packed path |
wasm-router | the 498-tree CatBoost query router, loop-unrolled, zero-allocation |
sweet-search-cli | a native CLI that talks to a warm search daemon over a per-project Unix socket — 2.9 ms measured warm-path queries |
tokenizers crate over NAPI — batched, cached, no Python anywhere in the stack.The quantization headline lives up in indexing — 1.34 GiB → ~396 MiB,
INT4-halved again. Here's the SSLX segment format that delivers it: crash-safe by construction, and
the three-stage retrieval it feeds at query time.
mmap (USearch view()), contributing 0 MB to the V8 heap at search time; the OS reclaims pages under pressure.docs/HNSW_APPROACH.md).sweet-search meets your agent wherever it is — shell tools, MCP, or injected instructions:
// .mcp.json (project root) — that's the whole integration
// or just run: sweet-search init --mcp
{
"mcpServers": {
"sweet-search": {
"command": "npx",
"args": ["-y", "sweet-search-mcp", "--project-root", "/absolute/path/to/your/repo"]
}
}
}
search, trace, read, read-semantic, index, health, repo-map, vocab-prewarm), 2 resources, 2 prompts; all search tools declared read-only and idempotent.claude/rules/sweet-search.md plus an automatically selected output style; Codex/OpenCode use AGENTS.md (--agents or --codex), while Gemini (--gemini) and Cursor (--cursor) use their native project filesrepo-map tool returns a PageRank-ranked symbol overview squeezed into any token budget, perfect for briefing a delegated agentinit --enforce-tools denies the native Grep tool in Claude Code and installs a hint hook nudging native Read toward ss-read/ss-semantic — for when you want the discipline guaranteed, not suggested./sweet-index skill: a Claude Code slash command for a full GPU-aware reindex, installed by init.sweet-search prewarm-vocab mines your repo's real identifiers, detects code communities (Leiden), and pre-warms all three search modes so even the first semantic query of a session is cache-warm.| Platform | Engine | Acceleration |
|---|---|---|
| macOS arm64 (Apple Silicon) | native | Metal (M1+) · CoreML Neural Engine (M3+) |
| macOS x64 (Intel) | native | ONNX Runtime INT8 CPU |
| Linux x64 (glibc) | native | CUDA (SM 7.0+, flash-attn on Ampere+) or INT8 CPU |
| Linux arm64 (glibc) | native | CUDA (Jetson Orin / Grace) or INT8 CPU |
| Windows | — | via WSL2 (= Linux x64) |
| Everything else | WASM/JS fallback | runs everywhere Node ≥ 18 runs |
Native binaries are selected automatically at npm install time via optionalDependencies — no flags, no postinstall scripts to debug. Every native fast path has a WASM or JS fallback that produces the same results.
sweet-search stands on a lot of shoulders, and we'd rather name them than pretend otherwise:
JavaScript
86.9%
Python
7.2%
Rust
4.8%