Vex
Fast hybrid structural + semantic code search. Vector + index.
Why Vex? · How It Compares · Installation · Quick Start · Commands · Configuration · How Search Works · Benchmarks · Supported Languages · Integration · Testing · Architecture
$ vex check "TelemetryProcessor" # 4ms — does it exist? where? (exact name)
$ vex show "TelemetryProcessor" # extract the class body (not the whole file)
$ vex usages "Config" --strict # who references this symbol? (binder-resolved, no noise)
$ vex callers "process_event" # who calls this function? (~4ms; covers module-scope + Python/Java decorators)
$ vex implementations "BaseService" # who extends/implements this?
$ vex search "timeout retry" # fuzzy / multi-word — BM25 finds rare body terms
$ vex search "handle alert" --semantic # find by meaning, not just name
$ vex pattern 'fn $NAME($$$) -> Result' # AST pattern matching (like ast-grep)
$ vex similar "PaymentService" # semantically close symbols
$ vex duplicates --threshold 0.95 # near-duplicate pairs
$ vex bundle --mode symbol --symbol Foo # body + callers + callees + similar in 1 call
Pick the right tool: vex check for "does Foo exist?", vex search for "find me something about retries". search is a ranked blend — it surfaces neighbors (callers / imports) when no symbol literally matches, which is great for exploration and wrong for exact-name lookup. v1.15.0 prints a stderr hint when an identifier-shaped search returns 0 FST hits.
Why Vex?
- ~4-5ms search after indexing — FST-based O(query_len) lookup, not O(symbols); constant regardless of project size. Requires a pre-built index. Indexing is a one-time cost (hundreds of ms on typical projects) and builds more than a plain text index — FST + BM25 + call graph + type-hierarchy + trigram skip-index — so it trades a slower build for far cheaper, richer queries (see Benchmarks)
- 3-channel hybrid search — structural FST (names) + BM25 (rare body terms) + semantic HNSW (meaning), fused via Reciprocal Rank Fusion. Find symbols when you don't know the exact name AND when generic semantic-only search would be too noisy
- Persistent call graph —
vex callers/vex calleesreads from an FST built at index time (~4ms), not a live tree-sitter scan (seconds). Module-scope expressions are reported via synthetic<module:path>callers (Phase 14.1); Python + Java function/method decorators (Phase 14.2), Kotlin annotations + C# method/constructor attributes (Phase 14.2.2), and TypeScript method decorators + Rust outer attributes (Phase 14.2.1) emit forward edges to their targets. Class-level decorators remain invisible — seedocs/LIMITATIONS.md - Pluggable embedder —
Embeddertrait + registry; swap MiniLM-L6-v2 for future code-specific models (BGE, CodeBERT) without touching call sites - Token-efficient — compact output saves typically 6-10x fewer tokens than grep on average lookups (up to 88x on minified JS/CSS);
vex showextracts just the symbol body instead of the whole file - 19 languages indexed via tree-sitter, with three coverage tiers: type-aware
--strict usageson 8 binder languages (Rust / TypeScript / Python / C# / C++ / Go / Java / Kotlin); indexed pattern prefilter on 15 T1+T2a languages; baseline structural + semantic search on all 19 (see Supported Languages for the matrix) - Single binary, zero config — no LSP servers, no databases, no Docker. Just
vex index && vex check Foo
What Vex isn't
vex is a static-analysis indexing tool, not a language server. Set expectations honestly:
- Not an LSP replacement. No go-to-definition into third-party packages, no rename refactoring, no type-checking, no hover docs. For those, keep your LSP.
vex searchis a ranked blend, not an exact-name lookup. Structural FST + BM25 + semantic fused via RRF return relevance-ordered results — when no symbol literally namedFoolives in the index (imported from a dependency, deleted, typo), BM25 may surface callers / imports as if they were the definition. For exact-symbol questions ("does it exist?", "show me the body", "who calls it?") usevex check Foo/vex show Foo/vex usages Foo --strict— they bypass the ranker. v1.15.0 prints a one-line stderr hint when an identifier-shaped query gets zero FST hits.- No dynamic-dispatch visibility. Decorator routing (
@router.get("/path")), string-resolved factories (uvicorn.run("main:app")), reflection (getattr(obj, name)()), and macro-expanded references are all invisible to every vex command.vex grep '\bname\b'is the textual escape hatch. vex callershas uneven coverage outside function scope. Module-level expressions likeapp = create_app()are reported via synthetic<module:path>callers (Phase 14.1). Python + Java function/method decorators (Phase 14.2), Kotlin annotations + C# method/constructor attributes (Phase 14.2.2), and TypeScript method decorators + Rust outer attributes on fns/methods (Phase 14.2.1) emit forward edges —vex callers GetMappinglists every Spring handler,vex callers HttpGetevery ASP.NET action,vex callers testevery#[tokio::test]. Class-level decorators (14.6) remain on the roadmap.vex usagesquality varies by language. 8 binder-supported languages get refactor-grade--strictrefs; the other 11 use an identifier scanner with a higher false-positive rate.
See docs/LIMITATIONS.md for the full coverage matrix, concrete repros, and recommended workarounds per query type. Read it before evaluating vex on a Python/FastAPI/Django codebase — the framework patterns are the most-flagged gaps.
How It Compares
| vex | ripgrep | ast-index | ast-grep | Serena | |
|---|---|---|---|---|---|
| What it searches | Symbol definitions | All text | Symbol definitions | AST patterns | Symbols (via LSP) |
| Requires indexing? | Yes (~0.3-1s) | No | Yes (faster build) | No | No |
| Search speed | ~4-5ms (pre-built FST, constant) | scales w/ corpus (~8ms small → 100ms+ large) | ~8-12ms (SQLite) | ~30ms (scan) | LSP-dependent |
| Semantic search | HNSW + embeddings | -- | -- | -- | -- |
| Pattern matching | fn $NAME($$$) |
regex only | -- | fn $NAME($$$) |
regex only |
| Index size | ~1.5-2x smaller than ast-index | no index | SQLite + FTS5 | no index | no index |
| Token efficiency | 6-88x fewer than rg | baseline | ~3x fewer than rg | N/A | N/A |
| Symbol body extraction | vex show |
-- | -- | -- | -- |
| Languages | 19 | any | 10+ | 10+ | 40+ (LSP) |
| Refactoring | -- | -- | -- | -- | rename, move, inline |
| Runtime deps | none | none | none | none | Python + LSP |
Note: vex search speed assumes a pre-built index. Ripgrep and ast-grep require no upfront indexing and work immediately on any directory. The tradeoff is amortized: if you search the same codebase many times (typical in agent workflows), the one-time indexing cost pays for itself.
Best for: fast symbol search in AI agent workflows where token efficiency matters. Not a replacement for LSP-based tools (no refactoring, no go-to-definition in dependencies).
Installation
# Homebrew (macOS/Linux)
brew tap tenatarika/tap
brew install vex
# From source (any platform with a Rust toolchain)
git clone https://github.com/tenatarika/vex.git
cd vex
cargo build --release
cp target/release/vex ~/.local/bin/
Linux
Pre-built vex ships in every GitHub Release for x86_64-unknown-linux-gnu:
curl -L https://github.com/tenatarika/vex/releases/latest/download/vex-x86_64-unknown-linux-gnu.tar.gz | tar -xz
mv vex ~/.local/bin/ # or: sudo mv vex /usr/local/bin/
vex --version
Built on the current ubuntu-latest GitHub runner (glibc-linked). For older glibc distros, musl-based distros (Alpine, NixOS without nix-ld), or aarch64 Linux (Graviton, Pi 5, Ampere) — build from source via cargo build --release.
Windows
Pre-built vex.exe ships in every GitHub Release.
- Download
vex-x86_64-pc-windows-msvc.tar.gzfrom the latest release - Extract
vex.exesomewhere stable (e.g.C:\Users\<you>\bin\) —tar -xzf vex-x86_64-pc-windows-msvc.tar.gzfrom a recent PowerShell, or 7-Zip / WinRAR via right-click. Security note:vex.exeloads the bundledDirectML.dllfrom its own folder, so on a multi-user machine or shared drive prefer a directory other users can't write to (e.g.C:\Program Files\vex\— with the trade-off thatvex self-updatethen needs an elevated shell). See GPU_SUPPORT.md §6. - Add that folder to
PATH(System Properties → Environment Variables → editPath→ add the folder) - Open a fresh terminal and run
vex --version
To update, run vex self-update — it fetches the latest release, picks the right archive for your platform, verifies its signature, and replaces the binary in-place. On Windows it also installs/refreshes the bundled DirectML.dll sidecar (skipped when byte-identical; re-installed if an older self-update dropped it — updaters up to v1.16.0 extracted only the binary). Same command works on macOS and Linux too.
GPU acceleration is built into the prebuilt binaries — Windows ships with DirectML (any DX12 GPU, driver-only; the redist
DirectML.dllis bundled in the archive) and macOS arm64 with CoreML. NVIDIA CUDA is a source-build opt-in. Runvex gputo check, and see GPU Acceleration.
Quick Start
# Index a project (structural only — fast)
vex index --path /path/to/project
# Index with semantic embeddings (slower first time, downloads 86 MB model)
vex index --path /path/to/project --semantic
# Exact-name lookup (does this symbol exist?)
vex check "PaymentService"
# Extract a symbol's body (no whole-file read)
vex show "PaymentService"
# Fuzzy / multi-word search (returns ranked neighbors when no symbol matches)
vex search "payment processing" --semantic
# Find all usages of a symbol (--strict drops string-literal / comment / wrong-scope noise)
vex usages "IndexReader" --strict
# File structure outline
vex outline src/main.rs
# Find implementations of a trait/interface
vex implementations "Iterator"
# Callgraph: who calls / is called by a function (fast path via persistent index)
vex callers "process_event"
vex callees "process_event"
# Multi-hop call graph (v1.7)
vex paths "main" "process_event" # all caller chains from main → process_event
vex reachable "process_event" # everything that transitively reaches it
vex tests-for "process_event" # tests covering process_event (path globs + name heuristic; framework label per row)
# Symbol-level diff against a branch (v1.7)
vex diff --base main # what symbols did this branch change?
# Historical view of a symbol — every commit that touched it (v1.15.0; v1.16.0 expanded)
vex index --history # build the persistent history sidecar once
vex history "PaymentService" # ~10ms — every version reachable from HEAD
vex history "PaymentService" --diff # unified diffs between consecutive versions
vex history "Foo" --since 2026-01-01 --author alice --kind function
vex history "deleted_symbol" --exact-presence # exact commit set where each blob lived (revert-aware)
# Semantic similarity by existing symbol — explain what's actually similar (v1.7)
vex similar "PaymentService" --limit 5 --min-score 0.7 --explain
# Near-duplicate pairs with reasoning (v1.7)
vex duplicates --min-score 0.95 --min-body-lines 5 --explain
# Search with per-call scope + metadata filters (v1.7)
vex search "Repository" --include 'src/**' --exclude '**/*.gen.*' --visibility public --async-only
# Why did the search return these results? (v1.7)
vex search "Foo" --why 2>trace.json
# Bundle: 4 round-trips → 1 envelope (v1.9, Phase 13.2)
vex bundle --mode symbol --symbol PaymentService # body + callers + callees + similar
vex bundle --mode pr-impact --base origin/main # changed symbols + transitive callers + tests
vex bundle --mode project --top-n 30 # top-N by reverse call-graph indegree
# Diff-context filters on every search-shaped command (v1.9, Phase 13.7-D3)
vex search "Repository" --since-branched # only files changed since branching from main
vex usages "Config" --since HEAD~3 # refs within the last 3 commits
vex callers "Foo" --changed-only # working-tree changes only
# Extract just a symbol's body — replaces Read for a specific function/class
vex show "PaymentService" # full body of the class / fn
vex show "Foo" "Bar" "Baz" # multiple symbols in one call
# Smart show truncation for token efficiency (v1.9, Phase 13.3)
vex show "BigClass" --signature-only # just the signature line
vex show "PaymentService" --head 20 # first 20 lines of the body
vex show "Foo" --no-body # signature + docstring, no body
# Ranking-eval harness — CI regression guard (v1.9, Phase 13.12)
vex eval --bench benches/ranking_golden/queries.toml # nDCG@10 / recall@10 / MRR per query
vex eval --min-ndcg 0.85 # fail if mean nDCG drops below threshold
# Capability discovery for MCP clients (v1.9, Phase 13.0)
vex capabilities # JSON: protocol_version, signals, bundle_modes, …
# Fast existence check
vex check "Foo" "Bar" "Baz"
# Incremental update (re-parses only changed files, reuses unchanged from index)
vex update
# Watch mode (re-indexes on file changes)
vex watch
# Multi-repo: treat a set of sibling repos as one workspace (v1.22.0)
vex index --workspace # build every member of .vex-workspace.toml
vex search "RetryPolicy" --workspace # fan out, results grouped by repo
vex usages Config --strict --workspace # cross-repo strict refs (needs a v7 index)
vex watch --workspace # keep every member incrementally fresh
# Show index stats
vex status
# GPU doctor — is the compiled EP actually engaging on this machine? (v1.16.0)
vex gpu # probes the compiled-in EP with strict registration
vex gpu cuda # narrow to one EP
vex gpu --enable # persist working device to VEX_DEVICE
# Shell completions
vex completions zsh > ~/.zfunc/_vex
Commands
| Command | Description |
|---|---|
vex index [--path .] [--semantic] [--embedder ID] [--history [--history-depth N]] |
Build full index. --semantic generates embeddings + HNSW + BM25. --embedder selects embedding model (default minilm-l6-v2). --history (v1.15.0) builds the Phase 14.8 persistent history-symbol section (<index_dir>/index.git_history) so vex history <Symbol> runs in FST-lookup time. --history-depth N caps the walk at N newest commits (global, not per-file). |
vex search <query> [--semantic] [--no-bm25] [--limit N] [--kind def,fn,…] [--visibility V] [--async-only] [--code-only] [--why] |
Hybrid search: structural + BM25 + semantic (when --semantic). 3-way RRF fusion. Multi-value --kind (canonical names + meta-selectors def/comment/test/ref). Metadata post-filters narrow by signature keywords. v1.20.0 (D4): per-result signals block now carries raw bm25_score + semantic_cosine alongside the rank ordinals so agents can read absolute relevance quality; _meta.vex.dev/semantic_channel reports "not_requested" / "index_lacks_vectors" when the semantic channel didn't run; --code-only drops hits in *.md/*.markdown/*.txt/*.rst/*.adoc for code-intent queries. --why appends a JSON trace to stderr. v1.15.0 search-drift hint: when the query is identifier-shaped (compile_query, Foo, _internal) and the structural FST finds zero matches, vex prints a one-line stderr hint pointing at vex check / vex show / vex usages --strict — the typical "imported-from-dependency" case where BM25 would otherwise surface callers as if they were the definition. See docs/COOKBOOK.md FAQ. |
vex show <symbol> [--limit N] [--context N] [--kind fn] [--visibility V] [--async-only] [--signature-only | --head N | --no-body] |
Extract symbol body from source (saves tokens vs full file read). Same metadata + kind filters as search. v1.9 (Phase 13.3): smart truncation flags — --signature-only keeps only the declaration line, --head N keeps the first N body lines, --no-body returns signature + docstring only. Mutually exclusive. |
vex similar <name> [--limit N] [--min-score T] [--explain] |
Find symbols semantically close to an existing one (HNSW nearest neighbors). --explain adds identifier-Jaccard + truncated unified diff per match. --min-score is an alias for --threshold. |
vex duplicates [--min-score T] [--min-body-lines N] [--explain] |
List near-duplicate symbol pairs by embedding similarity. --explain shows what's actually different between the bodies. |
vex usages <name> [--limit N] [--strict] [--include-self] [--include-docs] |
Find all references/usages of a symbol. Non-strict path = FST lookup; v1.20.0 strips the row at the symbol's own definition line and *.md/*.markdown/*.txt/*.rst/*.adoc matches by default — use --include-self / --include-docs to restore the pre-v1.20 wide-net behaviour. --strict reads binder-resolved refs from the v5 reference_edges section (Rust / TypeScript / Python / C# / C++ / Go / Java / Kotlin). |
vex impact <name> [--depth N] [--exclude-docs] |
NEW (v1.20.0, F1). One-call delete-safety blast-radius report. Composes four reference channels — strict refs (binder-resolved), FST refs, grep \b<Name>\b, and direct call-graph callers — into a single verdict (safe / unsafe / uncertain) with a per-channel evidence sample. Use this BEFORE proposing to delete or rename a symbol; one call replaces the manual usages→grep→callers dance. Verdict rule: unsafe if strict refs OR call-graph callers report >0 (binder/graph confirms real usage); uncertain if only text channels hit (likely string-dispatch / comment / decorator); safe only when every channel returns zero. v1.21.0: --depth N (1..16) walks the call graph backward to surface indirect callers at depth ≥ 2; --exclude-docs drops prose-format mentions (*.md/*.txt/…) so a CHANGELOG-only symbol flips to safe. |
vex pattern '<pat>' --lang <lang> [--why] |
AST pattern matching with metavariables ($NAME, $_, $$$, plus the v6 named multi-line forms $$$BODY / $$ARGS). Repeated metavars enforce back-references. Space-flanked && / ` |
vex outline <file> [--kind fn] |
Show file structure, optionally filter by symbol kind. |
vex implementations <name> |
Find types that extend/implement a base class, trait, or interface (incl. generic-parameterised: class Foo : Repository<T>). Index-backed (v8 hierarchy section) — a find_hierarchy_edges_by_symbol FST + binary-search lookup, falling back to the original live tree-sitter walk only when the index lacks the section. Bench (benches/hierarchy.rs, 150 implementers): ~265 ns index-backed vs ~22.7 ms live walk — ~85,000× faster. |
vex subtypes <name> [--depth N] |
NEW. Transitive-down closure over extends/implements edges (direct children, grandchildren, …), each row labelled with its BFS hop depth. Excludes Uses (trait/mixin composition) from the walk — mixing in a trait doesn't make you a subtype of everything the trait itself composes. Index-only, no live-walk fallback (requires an index with the v8 hierarchy section). Bench: ~1.1 µs for a 20-hop transitive chain. |
vex callers <name> |
Direct callers of a function (fast path via persistent call graph; falls back to live tree-sitter scan when the index is missing). |
vex callees <name> |
Direct callees of a function (same fast path). |
vex paths <from> <to> [--max-hops N] |
NEW. Enumerate all caller chains from from to to over the persistent call graph. Bounded DFS with cycle prevention; default --max-hops 6. |
vex reachable <target> [--max-hops N] [--limit N] |
NEW. Transitive set of symbols whose callees reach target, with the BFS depth labelled per row. Blast-radius analysis. |
vex tests-for <target> [--max-hops N] [--limit N] [--test-pattern <glob>] [--include-fixtures] |
NEW (Phase 13.10). Test functions that transitively cover <target>. Post-filter on top of vex reachable: walks the call graph backwards, keeps rows under recognized test-path globs (Rust / Python / TS-JS / Go / Java / Kotlin / C# / C++), stamps each row with a framework label (pytest, jest, go-test, …) so an agent can pick the right runner. --test-pattern <glob> (repeatable) REPLACES the default set; --include-fixtures admits one forward hop of test-path helpers in addition to weakening the name-prefix filter. |
vex diff --base <rev> [--limit N] |
NEW. Symbol-level diff between an arbitrary git revision and the working tree: added / removed / moved-within-file / body-changed entries. git diff --no-renames semantics so a git mv surfaces both halves. |
vex bundle --mode <symbol|pr-impact|project> [...] |
NEW (v1.9, Phase 13.2). Unified multi-source bundle — replaces 4 round-trips (show → callers → callees → similar) with one. --mode symbol --symbol Foo returns body + callers + callees + semantic similar. --mode pr-impact --base origin/main returns changed symbols + transitive callers (depth=2 default) + tests. --mode project [--top-n 30] returns top-N by reverse call-graph indegree (experimental — see docs/MCP-SCHEMA.md#bundle-modes-v19 for the response shape and mode_hints per-mode keys). Always emits the v1 envelope { protocol_version, capabilities, _meta, results }. |
vex check <name> [name...] |
Fast existence check — which symbols exist in the index? |
vex grep <pattern> [--filter-path path/] |
Regex content search (no index needed). |
vex update [--path .] [--semantic] [--embedder ID] [--history | --no-history] |
Incremental update — re-parse only changed files, reuse unchanged symbols from existing index. --history (v1.15.0) is sticky via the manifest: if the prior build had a history section, vex update keeps it fresh via a 3-branch walker (fast-path skip on no-new-commits, incremental on linear history, full rebuild on force-push). --no-history drops the section + nulls the manifest fields. |
vex watch [--path .] [--semantic] [--embedder ID] |
Watch filesystem, auto re-index on changes. |
vex status [--path .] |
Show index stats: symbol count, size, embeddings, call graph, BM25, GPU support. |
vex gpu [device] [--enable] |
Diagnose GPU acceleration: prints the execution provider compiled into this binary and actively probes whether it engages on this machine (a silent CPU fallback shows as FAILED with setup remediation). vex gpu cuda probes one EP; --enable persists the working device to VEX_DEVICE (user env via setx on Windows; prints the export line to add on macOS/Linux) when a GPU engages. See GPU Acceleration. |
vex completions <shell> |
Generate shell completions (bash, zsh, fish). |
vex init |
Create a default .vex.toml config file in the project root. |
vex capabilities |
NEW (v1.9, Phase 13.0). Print the machine-readable capability matrix (protocol_version, signals, why, scope_filters, metadata_filters, empty_reason, bundle_modes, auto_update). MCP / agent clients probe this once at startup instead of re-reading help text. |
vex eval [--bench PATH] [--min-ndcg F] [--json] |
NEW (v1.9, Phase 13.12). Run the ranking-evaluation harness against a hand-curated golden query set; reports nDCG@10 / recall@10 / MRR per query and aggregated. CI regression guard — fails when mean nDCG drops below --min-ndcg. Default golden set: benches/ranking_golden/queries.toml. |
vex history <Symbol> [--depth N] [--limit N] [--branch REV] [--no-index] [--since YYYY-MM-DD] [--until YYYY-MM-DD] [--author SUBSTR] [--kind KIND] [--diff] [--exact-presence] |
NEW (v1.15.0); expanded in v1.16.0 (Phase 14.9). Every historical version of a symbol reachable from a chosen tip. With vex index --history previously run, queries hit a persistent FST sidecar (~10 ms — 1640× faster on tokio-scale repos than the walker). Without the section, shells out to git log (~seconds). Indexed mode also finds symbols whose name has been deleted from HEAD — the walker can't. v1.16.0 additions: date/author/kind filters (lex YYYY-MM-DD compare); --diff renders unified diffs between consecutive versions (only signature lines change shape, head of group keeps full sig); --exact-presence enumerates the exact commit set where each entry's blob lived (revert-aware, capped by --exact-presence-max-commits); prefix-FST fallback on the indexed path for identifier-shaped queries length ≥ 3; JSON envelope ported to standard ResponseEnvelope shape (BREAKING for MCP consumers reading results.items[]). See docs/HISTORY-INDEX.md for the full pipeline + cookbook. |
vex self-update [--check] [--yes] |
Update vex to the latest GitHub release. Replaces the running binary in place. Works on Linux, macOS, and Windows. |
Per-query filters (every search-shaped command)
All search-shaped commands (search, usages, pattern, show, grep, implementations, subtypes, callers, callees, paths, reachable, tests-for, similar, duplicates, diff, bundle) accept:
--include <glob>/--exclude <glob>(repeatable, gitignore syntax) — per-call path scoping that doesn't require re-indexing.--excludewins over--include. Example:vex search Foo --include 'src/**' --exclude '**/*.gen.*'.--filter-path <substring>(alias--filter) — path-substring filter. Composes AND with the globs.
vex search / vex show additionally accept:
--visibility <public|private|protected|internal>— keep only symbols whose signature carries the explicit keyword. Defaults aren't inferred (bare Rustfn foo()does NOT match--visibility private).--async-only/--no-async— keep or exclude async / Kotlin-suspendsymbols.--static-only,--sealed-only— restrict to static class members or sealed (or Java-final) types.
Reasoning flags
vex search --whyprints a JSON trace to stderr (the result list stays on stdout):normalized_query, per-channel hit counts (FST / BM25 / semantic), fallbacks engaged (fuzzy), and the active filter snapshot.vex pattern --whyprints a JSONScanTraceto stderr after the result list:mode(indexed/live_scan),root_kind_inferred,candidate_files/total_files, andfallback_reasonwhen the indexed prefilter was skipped (no-index,no-skeleton-section,empty-section,grammar-drift,partial-section,index-open-error). MCP callers see the same JSON under_meta.why.vex similar --explain/vex duplicates --explainadd ajaccardoverlap score plus a truncated unified diff between the two bodies, so you can decide whether two semantically-clustered symbols are actually duplicates before acting.
Multi-repo workspaces (--workspace) — v1.22.0
Declare a set of sibling repos in a .vex-workspace.toml and run any command with --workspace to fan out across all of them, grouped by repo:
# .vex-workspace.toml (at the directory that contains the repos)
members = ["./api", "./worker", "./shared-lib"]
vex index --workspace # build every member into its own per-repo index
vex update --workspace # incremental refresh, per-repo changed/deleted counts
vex usages Config --strict --workspace # cross-repo strict refs, grouped by repo
--workspaceis accepted byindex,update,search,grep,check,usages,impact,callers,callees,reachable, andwatch. Each member keeps its own.vex.toml(excludes / embedder / sections / cache).- Reference and call-graph resolution is per-repo by default. The one exception is
vex usages <name> --strict --workspace, which resolves a reference in repo B to a symbol defined in repo A via a gtags-style name fallback (rendered as aname-resolvedsub-tier). This needs a v7 index — re-runvex indexafter upgrading. - A member missing a capability (
--stricton an old index, a call graph forreachable) is reported unavailable for that repo instead of aborting the whole fan-out.--workspaceconflicts with--why. Seedocs/MULTIREPO.mdand LIMITATIONS §7.
Configuration
Create a .vex.toml in your project root to customize vex behavior:
vex init # generates .vex.toml with commented defaults
# .vex.toml
# Glob patterns to exclude from indexing (gitignore syntax, on top of .gitignore)
exclude = [
"vendor/**",
"node_modules/**",
"*.generated.go",
]
# Output format — "compact" (default since v1.10.1; single-line records),
# "text" (verbose multi-line), or "json" (envelope for MCP / tools).
# format = "text"
# Enable semantic embeddings by default
semantic = true
# Automatically update index before search if stale
# auto_update = false
# GPU device for semantic indexing (GPU-enabled builds only). "auto" uses the
# compiled-in GPU EP when it initializes, else CPU; or "cpu"/"cuda"/"directml"/
# "coreml". `gpu = true/false` is shorthand for auto/cpu. See GPU Acceleration.
# device = "auto"
# gpu = true
# Embedder model: minilm-l6-v2 (default), jina-code, bge-base-en-v1.5,
# bge-large-en-v1.5, mxbai-large. Changing it requires a reindex.
# Set globally across projects with the VEX_EMBEDDER env var (this file wins).
# embedder = "minilm-l6-v2"
# VCS backend for diff-scoping (--since/--since-branched/--changed-only).
# "auto" (default) detects .git/.svn/.arc; "git" | "none" | "arc" | "svn".
# git, arc (Yandex Arc), and svn (Subversion) are all functional backends;
# svn declines --since-branched (no merge-base). "none" disables diff-scoping.
# Overridden by the --vcs flag and the $VEX_VCS env var. See docs/VCS-BACKENDS.md.
# vcs = "auto"
CLI flags always override config values. Use --no-semantic to explicitly disable semantic mode when the config enables it. The VEX_DEVICE and VEX_EMBEDDER environment variables act as global defaults across all projects (lowest precedence, below .vex.toml) — see GPU Acceleration.
Keeping config out of the repo
Don't want a .vex.toml inside the repository (can't .gitignore it, shared checkout, etc.)? vex never creates one on its own — only vex init writes it — and you have two ways to keep config external:
--config <path>/$VEX_CONFIG— point vex at a config file anywhere on disk. It replaces the in-repo lookup entirely, so the repo stays clean:vex --config ~/vex/this-repo.toml search Foo export VEX_CONFIG=~/vex/this-repo.toml # or set it once per shell--configbeats$VEX_CONFIG; a missing/invalid path is a hard error (vex won't silently fall back). Relative paths inside that file resolve against the file's own directory.- A parent directory — config lookup walks up from the project to the filesystem root, so a
.vex.tomlplaced in any ancestor (e.g.~/work/.vex.toml, or~/.vex.tomlfor a machine-wide default) is picked up for every repo beneath it, with none living in the repos themselves.
The index itself is never written into the repo — it lives in the cache dir (--cache-dir / $VEX_CACHE_DIR / platform cache), so a clean repo is just a matter of config placement.
Staleness Detection
Vex detects when the index is stale and warns before search:
$ vex search "Config"
Warning: index may be stale (HEAD changed). Run `vex update`.
How it works: on every search, vex compares the git HEAD stored at index time with the current HEAD (~0.1ms, single git rev-parse). If HEAD changed → stale. For non-git repos, falls back to mtime comparison — and since v1.11 (H11), when mtime fires, vex streams a xxh3_64 content hash of the file and compares it to the manifest. If the hash matches, the touch was cosmetic (git checkout, rustfmt no-op, rsync --times) and the file stays Fresh; only a real content change re-triggers indexing.
Auto-update: skip the warning and update inline:
# Per-command
vex search "Config" --auto-update
# Always (in .vex.toml)
auto_update = true
# Disable staleness check entirely
vex search "Config" --no-stale-check
GPU Acceleration
Semantic indexing (--semantic) can run the embedding model on a GPU — a large win on a full/cold index. On an RTX 3080 over a 28k-symbol C++ module, embedding the default MiniLM model was 51× faster on CUDA and 29× on DirectML vs CPU (full benchmark + design notes in docs/GPU_SUPPORT.md).
Two layers — the binary, and the device:
- Prebuilt binaries bake in a driver-only GPU EP: Windows → DirectML (any DX12 GPU — NVIDIA/AMD/Intel; the redist
DirectML.dllis bundled in the archive), macOS arm64 → CoreML. No SDK, no extra install. The Linux prebuilt is CPU-only. - CUDA is a source-build opt-in (fastest on NVIDIA — ~1.75× DirectML):
cargo install vex --features gpu-cuda. Needs the CUDA 12 runtime + cuDNN 9 onPATH(the NVIDIA driver alone is not enough — it ships onlynvcuda.dll, not the runtime/cuDNN). Source builds for the others:--features gpu-coreml/gpu-directml.
Selecting the device (vex index / vex update):
--gpu/--no-gpu— force GPU (Auto) or CPU.--device cpu|auto|cuda|directml|coreml— pick a specific execution provider.- Default is Auto: use the compiled-in EP when it initializes, else silently fall back to CPU. On a CUDA-enabled binary Auto prefers CUDA → DirectML → CoreML; on the standard Windows prebuilt only DirectML is compiled in, so Auto uses DirectML regardless of whether the PC could do CUDA.
- A tiny incremental
vex updatestays on CPU (the GPU warm-up isn't worth a handful of symbols); cold/large--semanticbuilds use the GPU.
Is the GPU actually being used? Run vex gpu — it reports the compiled EP and actively probes it, so a silent CPU fallback shows as FAILED with targeted setup remediation. vex gpu cuda probes a single EP; vex gpu --enable persists the working device to VEX_DEVICE (user env via setx on Windows; prints the export line to add on macOS/Linux). A stale VEX_DEVICE pinned to a GPU EP that a later (e.g. CPU-only) build lacks degrades to CPU rather than erroring.
Environment variables
| Variable | Effect |
|---|---|
VEX_DEVICE |
Global default device (cpu/auto/cuda/directml/coreml) for all projects. Below --device/--gpu and .vex.toml in precedence. |
VEX_EMBEDDER |
Global default embedder id (e.g. jina-code). Below --embedder and .vex.toml. An unknown id falls back to the default embedder with a warning. |
VEX_GPU_STRICT=1 |
Turn ORT's silent CPU fallback into a hard error — proves whether the GPU engaged. (vex gpu requests the same strict mode internally, without touching the environment.) |
VEX_GPU_MEM_LIMIT=<bytes> |
Advanced: hard cap on the GPU arena VRAM. Set it generously (≥ working set) or it OOMs on long-context batches. |
VEX_GPU_ATTN_BUDGET=<n> |
Advanced: tune length-aware batch sizing (the count × max_len² budget). |
Output Formats
# Compact single-line records — default since v1.10.1 (token-efficient, agent-friendly)
vex search "Foo"
# Verbose multi-line / human-readable
vex search "Foo" --format text
# JSON envelope (for MCP / tool integration; what `vex-mcp` parses)
vex search "Foo" --format json
Pin a different default in .vex.toml via format = "text" if you want the verbose multi-line view at the terminal.
JSON envelope (v1.11.0 — BREAKING for bare-array parsers)
Every --format json subcommand wraps its payload in the Phase 13
envelope. Single shape, easy to detect via protocol_version:
{
"protocol_version": "v1",
"capabilities": { /* see `vex capabilities` */ },
"_meta": { "vex.dev/index_age_ms": 1200, "ttlMs": 30000, "cacheScope": "project" },
"results": [ /* the actual data, shape depends on the subcommand */ ]
}
Pre-v1.11 only search and bundle returned this envelope; the other
~14 subcommands (show, usages, pattern, grep, implementations,
callers, callees, paths, reachable, tests-for, check,
similar, duplicates, diff, outline, index, update,
status, eval) emitted bare arrays / objects. Migration: pre-1.11 jq '.[0].name'
or data[0]['name'] now needs jq '.results[0].name' /
data['results'][0]['name']. Detect the envelope via
response.get('protocol_version') == 'v1' to support both shapes
during a rollout window.
How Search Works
Structural Search (default)
Searches by symbol name using an inverted index with CamelCase splitting:
"PaymentService"— exact match"Payment"— prefix match, finds PaymentService, PaymentGateway"payment"— case-insensitive, also finds via CamelCase tokens
Semantic Search (--semantic)
Embeds your query with MiniLM-L6-v2 (384-dim vectors) and finds symbols with similar meaning:
"parse source code files"findsparse_file,extract_refs,parse_file_symbols"database storage"findspopulate_db,create_10k_db,add_root_persists_to_db"find implementations of an interface"findsfind_implementations,test_interface_extends
BM25 Channel (auto-on when index has BM25 data)
A classic Okapi BM25 (K1=1.2, B=0.75) over symbol body tokens — identifiers, signatures, docstrings. Closes the gap between "exact name" (structural) and "general meaning" (semantic): finds rare body terms like timeout, retry, singlestore, `idem
No comments yet
Be the first to share your take.