ragx-cli — similarity-graph RAG for your files
ragx-cli indexes a corpus of files into a chunk-level embedding similarity graph and answers
queries by combining vector search, graph traversal, and cross-encoder reranking — all
from a single local CLI. Indexing needs no LLM (only an embedding model); an LLM is used
optionally at query time, for query expansion.
It works on any directory of text: pair it with a
Karpathy-style LLM Wiki,
an OpenWiki instance, an Obsidian-style notes vault, or any other knowledgebase repository or
arbitrary docs/code tree — ragx-cli init drops a ragx.toml next to the files and
everything else stays untouched. LLM-maintained wikis and ragx-cli are complementary: the wiki
distills knowledge into curated pages, while ragx-cli gives agents fast graph-backed retrieval
over those pages (and the raw sources beside them) without re-reading everything per question.
The goal: local semantic search that finds documents plain vector search misses, stays
cheap to (re)index, and is built to be driven by coding agents as much as by humans — stable
JSON schemas, deterministic exit codes, byte-exact source locations, and an --explain mode
that can justify every result via the exact graph path that produced it.
uv tool install ragx-cli --with ragx-cli[rerank] # install and use `ragx-cli`
ragx-cli init # create ragx.toml next to your corpus (interactive on a TTY; --yes for defaults)
ragx-cli index # chunk -> embed -> HNSW + kNN similarity graph (incremental after the first run)
ragx-cli query "why did we switch build tools?" --json --files-only
ragx-cli index --full # full rebuild: re-chunk + re-embed everything
ragx-cli models # recommend + download an embedding/reranker combo via LM Studio
ragx-cli doctor # check the configured providers start and answer, and the index matches
ragx-cli --version # version, repo, and the effective models (corpus config + ~/.ragxrc overrides)
Runs against any OpenAI-compatible embedding endpoint (LM Studio, Ollama, OpenAI). Reranking
uses a local sentence-transformers cross-encoder (ragx-cli[rerank] extra). Config lives in
ragx.toml at the corpus root (commit it); all index data lives in a .ragx/ directory beside
your files (gitignore it) — like .git/, delete .ragx/ and the corpus is untouched.
- ragx-cli — similarity-graph RAG for your files
How it works
Indexing (LLM-free)
Files are chunked structure-aware (markdown headings / code boundaries / recursive fallback), embedded, and stored in an HNSW index. The similarity graph then falls out almost for free: one kNN pass over the vectors that are already in memory — each chunk gets edges to its top-k nearest neighbors above a similarity floor.
Experimental: graph.edge_source = "subchunk" derives edge weights from sentence-aligned
sub-chunks instead of whole-chunk cosine — each chunk is split into ~subchunk_size_tokens
windows, embedded separately (stored in SQLite, never in the query-time HNSW), and the edge
weight between two chunks becomes the max similarity over their sub-chunk pairs, so a chunk
mixing several concepts gets one sharp edge per concept instead of a diluted average. Edges
between near-duplicate chunks (whole-chunk cosine ≥ near_dup_sim) are dropped — those are the
measured precision-killers. Costs roughly 5–10× more embedding calls at index time; retrieval
units, traversal, and query flow are unchanged. Switching edge_source (or the sub-chunk size)
requires index --full; incremental runs fail loud on the mismatch. Literature grounding:
research/fine-grained-sub-chunk-edges-with-coarse-chunk-nodes-multi-granularity-graph-rag-literature-validation.md.
In this mode k counts links per sub-chunk with no per-chunk cap, so the graph is denser and
shallower traversal suffices. Measured on the tuned eval corpus (2026-07): at hops=2 it ties
the chunk-edge baseline exactly (same recall, same MRR) at ~4.5× the indexing cost — that
corpus's short, single-topic chunks leave no concept-dilution headroom to exploit. Worth trying
only on corpora with long, genuinely multi-concept chunks (use hops=2); the default stays "chunk".
After edge construction, a Leiden partition (graspologic-native, seeded, deterministic) is
computed over the whole edge list every index run and stored read-only — see
[communities] and inspect communities/inspect community.
flowchart TD
A[files] --> B["discover + hash<br/>(gitignore, binary/junk filters,<br/>xxhash for incremental)"]
B --> C["chunk<br/>(headings / code boundaries,<br/>byte-exact slices + line ranges)"]
C --> D["embed<br/>(any OpenAI-compatible endpoint)"]
D --> E[("HNSW index<br/>vectors.hnsw")]
D --> F[("SQLite<br/>files · chunks · edges · manifest")]
E -- "kNN per chunk<br/>(k=8, cos ≥ 0.55)" --> G["similarity graph<br/>undirected weighted edges"]
G --> F
ragx-cli index is incremental by default (0.4.0; --changed is a deprecated alias): it
re-embeds only new/modified files, drops deleted ones, and repairs only the edge lists those
chunks touch. Content hashes make touch-ed but unchanged files free, and include/exclude glob
changes converge to the same file set a rebuild would produce. --full rebuilds from scratch;
changing embeddings.model, chunking.*, or graph.edge_source requires it — incremental runs
fail loud on the manifest mismatch. ragx-cli status reports drift (files new/changed/deleted
since the last index) so you can see when a re-index is due without running one.
Querying
Every stage is individually skippable (--no-expand, --no-graph, --no-rerank) so callers
can trade quality for latency.
flowchart TD
Q[query] --> X["1 · expansion (optional, one LLM call)<br/>2–4 reformulations + HyDE passage"]
X --> S["2 · fan-out vector search<br/>top-20 per variant"]
Q -. "--no-expand" .-> S
S --> R["3 · Reciprocal Rank Fusion<br/>merged seed set"]
R --> H["4 · heat propagation over the graph<br/>2 hops · decay 0.5 · max-aggregation<br/>query-similarity floor · frontier cap"]
R -. "--no-graph" .-> K
H --> K["5 · cross-encoder rerank<br/>(query, chunk) pairs, capped shortlist"]
K --> F["6 · combined score<br/>α·rerank + β·heat + γ·vector"]
F --> O["ranked chunks (or files via --files-only)<br/>+ --explain traversal trace"]
Heat propagation is what sets ragx-cli apart from plain RAG: seed chunks (from vector search)
push "heat" along similarity edges — heat × edge_weight × decay per hop. A neighbor's heat is
the max of incoming contributions (not the sum, so hub chunks can't inflate themselves), and
a neighbor is only admitted if it's similar enough to the original query — traversal stays
anchored to the question instead of drifting through the corpus.
flowchart LR
subgraph seeds["seeds (vector hits)"]
S1["chunk A · heat 1.0"]
end
S1 -- "edge 0.86" --> N1["chunk B<br/>heat = 1.0 × 0.86 × 0.5 = 0.43"]
N1 -- "edge 0.70" --> N2["chunk C<br/>heat = 0.43 × 0.70 × 0.5 = 0.15"]
S1 -- "edge 0.60" --> X1["chunk D — below query floor<br/>✗ not admitted, doesn't relay"]
Because every admitted chunk records which seed and edge produced it, --explain can print the
full justification: seed → edge(weight) → chunk, per result.
Does it actually help? (benchmarks)
Measured with the built-in harness (ragx-cli eval queries.jsonl) on a real, decade-spanning
personal wiki — organic notes, not a synthetic benchmark. Corpus provenance:
| corpus | 636 markdown files indexed (644 on disk; 8 auto-excluded as node_modules/hidden) · 2.9 MB · avg 4.6 KB/file |
| structure | topical top-level dirs (clients/, projects/, workstreams/, personal/, …), nested up to 10 levels deep |
| content | mixed English + Dutch: meeting/daily notes, research docs, transcripts, reference material |
| chunks | 1,323 (avg 2.1 per file, 452 files are single-chunk; median 2,390 chars ≈ 600 tokens, max 4,888) |
| graph | 5,246 edges · avg degree 7.9 (k=8 cap) · weights 0.59–1.00 above the 0.55 floor · only 3 isolated chunks |
| index | 5.1 MB SQLite + 4.1 MB HNSW (768-dim nomic-embed-text-v1.5 via LM Studio) · full build ≈ 2 min on an M-series laptop |
| labels | 18 queries (EN + NL) with known-relevant files, single- and multi-target (.ragx/queries.jsonl) |
Reranker: BAAI/bge-reranker-v2-m3 (local cross-encoder). Results:
| config | recall@5 | recall@10 | MRR |
|---|---|---|---|
baseline — vector search only |
0.833 | 0.833 | 0.593 |
graph — + heat propagation |
0.778 | 0.833 | 0.522 |
rerank — graph + cross-encoder |
0.759 | 0.889 | 0.568 |
full — + LLM expansion |
0.759 | 0.889 | 0.613 |
The recall win is exactly the designed mechanism, and it's traceable. For one Dutch query ("zonnepanelen offerte en terugverdientijd"), the relevant document is never retrieved by vector search — and a reranker alone can't help, because you can't rerank what retrieval never surfaced:
| pipeline | rank of the relevant file |
|---|---|
| vector search only | not found |
| rerank without graph | not found |
| graph only | 19 |
| graph + rerank | 4 |
The graph surfaced it through a single hop-1 edge (weight 0.86) from a seed chunk, and the
cross-encoder promoted it — graph expands recall, rerank recovers precision. The --explain
output for that result shows the exact seed → edge → chunk path.
Honest caveat: graph traversal alone hurts precision on this corpus (MRR 0.593 → 0.522) —
near-duplicate neighbors (e.g. adjacent meeting notes) displace weaker direct hits. A parameter
sweep over decay/floor/weights plateaued below baseline MRR, so this is a property of
similarity-only edges, not a tuning miss. Conclusion baked into the defaults: graph and
rerank ship together. Use --no-graph --no-rerank as the explicit fast mode.
The parameter study below explains the mechanism behind this caveat — and shows that once the scoring weights stop putting heat into the final score, the graph win gets much bigger.
Parameter study: what each knob actually does (2026-07)
A follow-up study swept every traversal, scoring, and graph-build parameter to map sensitivity
and find better settings. Setup: a scoped subset of the same wiki (clients/ + workstreams/:
383 files → 720 chunks → 2,848 edges) with a fresh 26-query EN+NL label set.
Process. Sweeping through real eval runs costs ~9 minutes each, so the study used an
offline harness (.lab/harness.py): one retrieval pass per query plus a text-keyed cross-encoder
score cache lets any traversal × scoring × edge-filter combination be re-evaluated in seconds —
the combination step is deterministic post-processing, and rerank scores depend only on
(query, chunk text). The harness was validated digit-exact against the real pipeline before
use, ~125 configurations were measured in staged rounds (scoring simplex → refinement →
traversal one-factor-at-a-time → interaction grid → edge-filter simulations), the winning config
was re-verified with a real eval run (again digit-exact), and finally replicated on an
independently rebuilt index.
Result — same-index comparisons, rerank config (no expansion) unless noted:
| config | MRR | recall@5 | recall@10 |
|---|---|---|---|
defaults (hops=2, α/β/γ = .6/.25/.15) |
0.665 | 0.885 | 0.923 |
tuned (hops=3, α/β/γ = .9/0/.1) |
0.755 | 0.885 | 0.962 |
The +13.7% MRR delta replicated as +14.5% on an independently rebuilt index (absolute numbers differ per build — see the measurement caveat below).
Apply the tuned settings to a corpus with:
ragx-cli config set traversal.hops 3
ragx-cli config set scoring.alpha_rerank 0.9
ragx-cli config set scoring.beta_heat 0.0
ragx-cli config set scoring.gamma_vector 0.1
Discoveries, in decreasing order of impact:
- α (rerank weight) is the dominant knob. MRR rises monotonically from α=.6 to a plateau at α≈.85–.9 (+9% relative), with recall flat across the whole range. Heat belongs at β=0: once the cross-encoder is trusted, heat in the final score only adds near-duplicate noise — which is exactly why the earlier graph-only sweeps plateaued.
- But never α=1.0. With β=γ=0 the pre-rerank shortlist selector degenerates (it renormalizes β:γ, so every pre-score becomes 0) and MRR collapses to 0.32. Vector + heat pick which 100 candidates the cross-encoder sees at all — they are load-bearing for shortlist selection even at near-zero final weight. Keep γ > 0.
hops=3is the second win, but only after fixing the scoring. Under default weights, traversal depth is completely inert (heat dilution cancels the candidate gains); under rerank-heavy weights it adds both MRR and recall@10.hops=4degrades: ~570 candidates overwhelm the fixed rerank shortlist (RERANK_CAP=100) and good candidates get evicted before the cross-encoder ever sees them — the cap, not the graph, becomes the binding constraint.- Graph = recall channel, cross-encoder = precision channel. At identical tuned scoring, removing the graph keeps MRR (0.71) but drops recall@10 by 7.7 points. The graph's job is to put reachable targets in front of the reranker; the reranker's job is to rank them.
- Everything else is inert or already optimal:
decay(.3–.7),query_floor(.2–.5),max_frontier(50–300), andmin_edge_sim(up to .7) don't move metrics;k=8is bracketed optimal from both sides (k=4/6 lose recall, a real k=12 rebuild was no better). - LLM expansion buys recall, not ranking. The full pipeline at default params reached the
same recall@10 that
hops=3provides for free, at ~40 s/query for a local reasoning model. On tuned params expansion still stacked (+.03 MRR) — worth it only when latency doesn't matter. - Measurement caveat: index rebuilds are not reproducible across embedding-server sessions. Rebuilding is deterministic within a session, but embeddings drift between LM Studio sessions (~.04 MRR at identical params). Compare configs on the same build only; the tuned-vs-default delta replicated across two independent builds (+13.7% / +14.5% MRR).
BGE-M3 embedding study (2026-07-12)
A follow-up study asked whether switching the embedding model beats tuning parameters on top of
the existing one. Setup: same scoped corpus and 26-query EN+NL label set as the parameter study
above, tuned retrieval params held identical across both legs (hops=3, α/β/γ = .9/0/.1),
reranker unchanged (BAAI/bge-reranker-v2-m3). Leg 1 = the production nomic-embed-text-v1.5
(Q4_K_M) index; leg 2 = a full re-index with text-embedding-bge-m3 (Q8 GGUF), doc/query
prefixes emptied (BGE-M3 takes none).
| config | recall@5 | recall@10 | MRR |
|---|---|---|---|
baseline — nomic (Q4_K_M) |
0.846 | 0.846 | 0.561 |
baseline — bge-m3 (Q8) |
0.923 | 0.962 | 0.640 |
rerank — nomic (Q4_K_M) |
0.846 | 0.923 | 0.717 |
rerank — bge-m3 (Q8) |
0.885 | 0.962 | 0.755 |
Recommendation: text-embedding-bge-m3 (Q8 GGUF in LM Studio) with EMPTY doc_prefix/
query_prefix is now the recommended embedding model — it beats nomic on every metric at
identical retrieval params, with the biggest gains on Dutch/multilingual queries (3 of nomic's
4 baseline misses become hits). Two caveats before treating this as fully settled: the comparison
ran nomic at Q4_K_M against bge-m3 at Q8, so part of the delta may be quantization quality rather
than model architecture (an isolating Q8-vs-Q8 run hasn't been run); and BGE-M3's cosine
distribution sits lower than nomic's (median edge weight .70 vs .81), which makes the fixed graph
thresholds (min_edge_sim=0.55, query_floor=0.35) effectively stricter for it — a retune pass
on the BGE-M3 distribution is a known follow-up, not yet done.
Two sibling questions were studied and rejected as adoption paths right now. Sparse/lexical:
start a lexical retrieval leg with SQLite FTS5/BM25 as an extra RRF seed ranking, not BGE-M3's
lexical_weights — the latter needs a resident PyTorch model at query time for a gain that isn't
proven necessary here. ColBERT multi-vector late interaction: no-adopt for both reranking and
edge-building — it underperforms the existing cross-encoder, can't be served over ragx's
OpenAI-compatible HTTP provider architecture, and costs roughly 100x the storage of the existing
subchunk edge mechanism for a mechanism the subchunk ablation already showed has no headroom on
this corpus.
Full write-ups: research/bge-m3-dense-q8-vs-nomic-q4-benchmark-2026-07-12-worktree-eval-results.md
(this benchmark), research/bge-m3-dense-embeddings-as-ragx-provider-multilingual-quality-and-threshold-calibration.md
(dense literature review), research/bge-m3-sparse-lexical-weights-hybrid-retrieval-leg-for-ragx-vs-sqlite-fts5-bm25.md
(sparse), research/bge-m3-colbert-multi-vector-late-interaction-for-ragx-rerank-alternative-and-token-level-graph-edges.md
(ColBERT).
Picking models: ragx-cli models
ragx-cli models recommends an embedding + reranker combo, downloads the embedding model
through LM Studio (lms get — one download path, plays well with restricted networks),
pre-fetches the reranker from Hugging Face, and writes both to ragx.toml. On a TTY it walks
three steps — context first, consequences before anything is written:
- machine header — detected RAM, platform, whether
llama-serveris on PATH and thelmsCLI was found (this is what drives the recommended defaults); - numbered menus — the embedding tier table (model, size, languages, context, license)
and one menu per serving engine with a one-line tradeoff each; the recommended default is
marked
»(Enter accepts), invalid input re-prompts; - plan — what will be downloaded (or
already downloaded ✓) with approximate sizes, which config sections change, and whether a re-index is required — all shown before the final confirm.
Scriptable via flags:
ragx-cli models # interactive
ragx-cli models --quality balanced --yes --json # agents
ragx-cli models --quality fast --dry-run # recommend only, change nothing
The curated catalog (every entry verified working end-to-end):
| tier | embedding model | size | langs | ctx | notes |
|---|---|---|---|---|---|
fast |
EmbeddingGemma 300M | 308M | 100+ | 2K | lightest; small corpora, low RAM |
balanced |
BGE-M3 | 568M | ~100 | 8K | ragx's benchmarked production model |
best |
Qwen3-Embedding-0.6B | 0.6B | 100+ | 32K | highest quality; slower indexing |
jina-nano |
Jina embeddings v5 nano | 239M | multi | 8K | best sub-500M; CC-BY-NC (non-commercial); llama-server engine only — LM Studio downloads it but can't serve EuroBERT (picking it locks the embedding engine to llama-server automatically; an explicit --embed-engine lm-studio still fails loud) |
Serving engines. Embeddings run through --embed-engine: llama-server (default when
llama.cpp is installed — ragx auto-spawns llama-server --embedding on the downloaded GGUF and
terminates it at exit; LM Studio is only the downloader) or lm-studio (LM Studio serves
/v1/embeddings; zero extra processes, LM Studio must be running). The reranker —
BAAI/bge-reranker-v2-m3 (multilingual) — runs through --rerank-engine:
llama-server(default when llama.cpp is installed): the reranker's Q8_0 GGUF is also downloaded via LM Studio, and ragx auto-spawns/terminates allama-server --rerankprocess at query time (rerank.provider="llama-server",rerank.gguf,rerank.base_url,rerank.server_bin). No huggingface.co access needed anywhere. GGUF scores are validated within ~0.5 logit of the safetensors originals.sentence-transformers: the classic CrossEncoder path; model pre-fetched from Hugging Face (needs theragx-cli[rerank]extra).
With both engines on llama-server the whole stack is ragx-managed: LM Studio downloads the
GGUFs, ragx spawns and reaps the servers, and neither LM Studio nor huggingface.co is needed
at query time. Under 8 GB RAM the recommended default moves to fast: interactively, picking
a heavier tier asks for confirmation and then honors your choice; non-interactive runs
(--yes/piped) auto-downgrade to fast with a note, since there is nobody to ask. On macOS
lms get --yes picks MLX variants where LM Studio's catalog offers them. Switching the
embedding model invalidates the index — the command tells you to run ragx-cli index --full.
init offers this flow as its final step; rerun ragx-cli models anytime to switch.
Why not jina-embeddings-v5-text-nano? Excellent model, but its EuroBERT architecture needs
Jina's llama.cpp fork — stock LM Studio can't load it (and the license is CC-BY-NC). See
research/lm-studio-model-download-api-and-curated-embedding-reranker-catalog-for-ragx-models-command.md.
Checking your setup: ragx-cli doctor
ragx-cli doctor runs the configured pipeline's providers for real — it embeds a probe string,
scores a probe pair with the reranker, and checks the index on disk against the config — then
reports each stage:
✓ config /corpus/ragx.toml — embeddings=openai/text-embedding-bge-m3 · expansion=openai/qwen3.5-9b · rerank=sentence-transformers/BAAI/bge-reranker-v2-m3
✗ embeddings request to http://localhost:1234/v1/embeddings failed after 3 attempts: [Errno 61] Connection refused
→ ragx does not start this server — launch it at http://localhost:1234/v1 (LM Studio: `lms server start`),
or hand the process to ragx with `ragx-cli models --embed-engine llama-server`
✗ expansion no server answered at http://localhost:1234/v1/models
→ turn it off with `ragx-cli config set expansion.enabled false`
✓ rerank sentence-transformers/BAAI/bge-reranker-v2-m3 scored a probe pair (-8.14, 4.72) 3.1s
✓ index 634 files, 1247 chunks, built with 'text-embedding-bge-m3', no drift
Exit codes: 0 healthy, 1 at least one check failed, 2 doctor itself could not run (no
corpus, unreadable config). Warnings (no index yet, corpus drift, a model the server does not
list but may JIT-load) do not fail the run. --json emits a ragx.doctor.v1 document with a
status/detail/hint/elapsed_ms entry per check.
Which servers does ragx start? All of the local ones:
engine (ragx-cli models) |
config | lifecycle |
|---|---|---|
llama-server |
provider="llama-server" (embeddings and/or rerank) |
spawned on demand from the configured GGUF and killed at exit — ragx owns the process |
lm-studio |
embeddings.provider="openai" + a local base_url |
lms server start + lms load <model> when nothing answers — LM Studio keeps running afterwards (it is an app, not ragx's child) |
sentence-transformers |
rerank.provider="sentence-transformers" |
in-process, no server |
index, query, and doctor all do this before their first request, so a cold machine needs
no manual setup. What was started is logged to stderr and shown in doctor's report:
✓ embeddings started the LM Studio server on port 1234; loaded 'text-embedding-nomic-embed-text-v1.5' into LM Studio — openai/text-embedding-nomic-embed-text-v1.5 answered, dim=768 14.8s
Rules it follows: only a local base_url is ever touched (a cloud endpoint, including one
set via OPENAI_BASE_URL, is never ragx's to start); an already-running server is left alone,
and if LM Studio isn't installed but something else answers on that port, ragx just uses it; a
model that isn't downloaded fails loud naming the models the machine does have. Turn it off per
section with ragx-cli config set embeddings.autostart false (or machine-wide via
--global) — then ragx only ever talks to servers you started.
ragx-cli models still only downloads models and writes ragx.toml; it never launches
anything. If a run dies with "connection refused", doctor names the stage and the fix.
Agent-first conventions
--jsonemits exactly one JSON document on stdout (versioned schemas:ragx.query.v1,ragx.files.v1,ragx.status.v1,ragx.doctor.v1,ragx.eval.v1,ragx.inspect.*.v1); logs go to stderr.- Exit codes:
0results,1success-but-empty,2error. - Every chunk carries
file,line_start/line_end,byte_start/byte_end— agents jump to the exact source location and read the full text themselves (JSON chunk text is truncated). --files-onlyaggregates chunk scores per file (sum of top-3) — the mode coding agents use most.ragx-cli statusincludes adriftobject (new/changed/deletedfile counts vs the index) — agents check it to decide whether to runragx-cli indexbefore querying.ragx-cli doctor --jsonverifies the providers answer before a batch of queries (ok: falseplus a per-checkhintwhen something is down); exit1= a check failed.ragx-cli query -reads the query from stdin;ragx-cli inspect chunk|file|neighbors|communities|communitydebugs the graph.
Using ragx-cli from a coding agent (CLAUDE.md / AGENTS.md)
Give your agent standing instructions by pasting this into the repo's CLAUDE.md or AGENTS.md
(adjust the fenced block to your corpus):
## Semantic search with ragx-cli
This repo has a ragx-cli index (`.ragx/`). Prefer it over grep for "where is X discussed/decided?"
questions; fall back to grep for exact identifiers.
- Find relevant files: `ragx-cli query "<natural-language question>" --json --files-only`
- Get chunks with exact locations: `ragx-cli query "..." --json --top 8` — each result carries
`file` + `line_start/line_end`; the JSON `text` is truncated, so read the file yourself
for full context.
- Fast mode (no LLM call, no cross-encoder): add `--no-expand --no-rerank`.
- After adding or editing files: `ragx-cli index` (incremental and cheap — hash-based).
`ragx-cli status --json` shows `drift` counts if you want to check staleness first.
- stdout is exactly one JSON document; logs are on stderr.
Exit codes: 0 = results, 1 = no results (not an error), 2 = error.
- Why did this result appear? `ragx-cli query "..." --explain`.
Explore the graph: `ragx-cli inspect neighbors <chunk_id>`.
Pointing ragx-cli at your LLM — local or online
ragx-cli talks to any OpenAI-compatible API for embeddings and (optionally) query expansion.
Pick one recipe; run it inside the corpus after ragx-cli init:
LM Studio (default — nothing to change if it runs on localhost:1234):
curl -s http://localhost:1234/v1/models # see what's loaded
ragx-cli config set embeddings.model text-embedding-nomic-embed-text-v1.5
ragx-cli config set expansion.model <any-chat-model-id> # or: ragx-cli config set expansion.enabled false
Ollama (base_url switches to localhost:11434/v1 automatically):
ollama pull nomic-embed-text
ragx-cli config set embeddings.provider ollama
ragx-cli config set embeddings.model nomic-embed-text
ragx-cli config set expansion.provider ollama
ragx-cli config set expansion.model llama3.1 # any local chat model
Online / any OpenAI-compatible endpoint (OpenAI, OpenRouter, Together, …).
ragx-cli honors the conventional env vars used by generic OpenAI-compatible tooling — with
OPENAI_BASE_URL and OPENAI_API_KEY exported, only the model names need configuring:
export OPENAI_BASE_URL=https://api.openai.com/v1
export OPENAI_API_KEY=sk-...
ragx-cli config set embeddings.model text-embedding-3-small
ragx-cli config set embeddings.doc_prefix "" # prefixes are for nomic-style models
ragx-cli config set embeddings.query_prefix ""
ragx-cli config set expansion.model gpt-5.2-mini
Precedence rules (per section, embeddings and expansion independently):
base_url: an explicitragx-cli config set <section>.base_url …always wins;OPENAI_BASE_URLapplies only while the config still holds the built-in default.- API key:
ragx-cli config set <section>.api_key_env MY_VARnames an env var to read (and fails loudly if that variable is unset); without it,OPENAI_API_KEYis used when present. Secrets themselves never go inragx.toml.
Mixed setups are normal — e.g. local Ollama embeddings + online expansion via
ragx-cli config set expansion.base_url https://openrouter.ai/api/v1 +
ragx-cli config set expansion.api_key_env OPENROUTER_API_KEY. The reranker is always local
(sentence-transformers); disable it with ragx-cli config set rerank.enabled false if the model
download is unwanted (can't reach huggingface.co? — see the next section). Note: changing the embedding model invalidates the index — ragx-cli
detects the mismatch and asks you to run a full ragx-cli index.
Reranker on restricted networks (no huggingface.co)
With the default sentence-transformers engine, the reranker model
(BAAI/bge-reranker-v2-m3, ~2.3 GB) is downloaded from huggingface.co on first use. If your
network blocks huggingface.co, ragx-cli degrades gracefully — queries run without reranking
and a warning explains why — and the easiest fix is to skip huggingface.co entirely:
Option 0 — the llama-server engine (recommended). ragx-cli models --rerank-engine llama-server downloads a validated Q8_0 GGUF of the reranker through LM Studio (same
download path as the embedding model) and reranks via llama.cpp's llama-server --rerank,
which ragx starts and stops automatically. Requires a recent llama.cpp
(brew install llama.cpp); no torch, no sentence-transformers, no huggingface.co. The GGUFs
score within ~0.5 logit of the safetensors originals with identical ordering.
Prefer the sentence-transformers engine? Three more ways:
Option A — copy the model and point rerank.model at the directory (recommended).
On any machine with access:
pip install -U huggingface_hub # or: uv tool install huggingface_hub
hf download BAAI/bge-reranker-v2-m3 --local-dir bge-reranker-v2-m3
Transfer the bge-reranker-v2-m3/ directory to the restricted machine (it must contain
config.json, model.safetensors, and the tokenizer files — hf download fetches all of
them), then:
ragx-cli config set --global rerank.model /absolute/path/to/bge-reranker-v2-m3
sentence-transformers loads a local directory without any network access. --global writes
to ~/.ragxrc so the machine-local path doesn't end up in a shared corpus config.toml.
Option B — use a HuggingFace mirror. huggingface_hub honors the HF_ENDPOINT
environment variable, so if a mirror is reachable (e.g. an internal artifact proxy, or a
public mirror such as https://hf-mirror.com):
HF_ENDPOINT=https://hf-mirror.com ragx-cli query "..."
Option C — pre-seed the HuggingFace cache. Copy
~/.cache/huggingface/hub/models--BAAI--bge-reranker-v2-m3/ from a machine where the model
has already been used to the same path on the restricted machine. Set HF_HUB_OFFLINE=1 to
stop huggingface_hub from attempting update checks against huggingface.co.
If none of these are workable, ragx-cli config set rerank.enabled false turns reranking
off explicitly (scoring weights renormalize automatically).
Machine-level settings: ~/.ragxrc
Provider settings that belong to the machine rather than the corpus — which embedding
model, which LLM, which base URL — can live in ~/.ragxrc (TOML, same shape as
ragx.toml, restricted to the [embeddings], [expansion], and [rerank] sections):
ragx-cli config set --global embeddings.model text-embedding-nomic-embed-text-v1.5
ragx-cli config set --global expansion.model llama3.1
Precedence: built-in defaults < corpus ragx.toml < ~/.ragxrc. The rc
overrides corpus values, and every command warns on stderr when it does, so a
corpus config never loses silently. Set corpus-specific values without --global
as usual. Other sections (chunking, graph, …) are corpus-level and rejected from
the rc. The index-invalidation note above applies equally when the rc changes the
effective embedding model.
Configuration
ragx.toml at the corpus root, managed via ragx-cli config get|set (add --global to
write provider settings to ~/.ragxrc instead — see above). The config file is meant to be
committed with your corpus; .ragx/ (index data) is safe to gitignore — a fresh clone just
runs ragx-cli index. The config file itself is never indexed as corpus content.
Upgrading from ≤0.2.x: the config moved from
.ragx/config.tomltoragx.tomlat the corpus root, and ragx-cli migrates it for you on first use: on a TTY it asks before moving the file (declining leaves everything untouched and prints themvto run yourself); piped/agent runs migrate automatically with a notice on stderr. If both files exist, commands fail loud — keep one and delete the other. The index itself is untouched.
ragx-cli init is interactive when run on a TTY: it probes the default LM Studio
(localhost:1234) and Ollama (localhost:11434) ports, lists the models each server
reports, and walks through embeddings (provider/base URL/model/API-key env var), query
expansion (enable + model — likely thinking/reasoning models are listed last, since a
non-thinking model is the better expansion choice; detection is a best-effort name
heuristic, the APIs expose no capability flag), and corpus include/exclude
(gitignore-style globs, comma-separated). Every prompt is pre-filled with the defaults
below — Enter accepts them all. With piped stdin (agents), --yes, or
--no-interactive, it writes the defaults unchanged, exactly as before. Key defaults:
| section | defaults |
|---|---|
[chunking] |
size_tokens=800, overlap=0.15 |
[graph] |
k=8, min_edge_sim=0.55, edge_source="chunk", subchunk_size_tokens=128, near_dup_sim=0.9 |
[traversal] |
hops=2, decay=0.5, query_floor=0.35, max_frontier=150 |
[communities] |
resolution=1.0, seed=42 — recomputed every index run; changing these never invalidates the index |
[fusion] |
rrf_k=60, per_query_top=20 |
[scoring] |
alpha_rerank=0.6, beta_heat=0.25, gamma_vector=0.15 |
[embeddings] |
provider="openai", base_url="http://localhost:1234/v1", prefixes for nomic-style models, api_key_env="", autostart=true (start LM Studio + load the model when the URL is local) |
[expansion] |
optional LLM for multi-query/HyDE; reasoning models supported (4096-token budget); api_key_env="", autostart=true |
[rerank] |
BAAI/bge-reranker-v2-m3 via sentence-transformers (uv tool install 'ragx-cli[rerank]') |
Features & roadmap
Checked features are built and validated per the implementation plan; unchecked ones are next up. Release history: CHANGELOG.md.
- CLI & storage: typer CLI, SQLite schema, provider abstraction (Embedder/Generator/Reranker)
- Baseline vector RAG: discovery, chunking, embeddings, HNSW search, incremental indexing
- Similarity graph: kNN edge construction, heat-propagation traversal,
inspect,--explain - Quality & measurement: multi-query/HyDE expansion, RRF fusion, cross-encoder rerank,
evalharness - Communities: Leiden detection over the edge list (index-time, read-only via status/inspect)
- Interactive
init: LM Studio/Ollama server + model detection, guided embeddings/expansion/corpus setup (--yesfor defaults) - Committable config:
ragx.tomlat the corpus root (0.3.0; was.ragx/config.toml), index data stays gitignored in.ragx/ - Offline-friendly rerank: graceful degradation + pre-seeding docs when huggingface.co is unreachable
- Sub-chunk edges (opt-in
graph.edge_source="subchunk"): edge weight = max cosine over sentence-aligned sub-chunk pairs, for corpora with long multi-concept chunks - Incremental by default (0.4.0):
indexhash-diffs,--fullrebuilds,statusreports corpus drift, chunking params are manifest-guarded -
ragx-cli models: curated embedding/reranker recommendation (quality tier + RAM detection), downloads via LM Studio'slms get, serving via llama-server or LM Studio - Community labels: name the Leiden communities so they're browsable without reading member chunks
- query --global for corpus-level questions (answer from community summaries, not individual chunks)
- MCP server: a second thin shell over
ragx.core(the core/CLI split it needs is already enforced) - Temporal weighting: opt-in
--since/--until/--temporal recent|oldest, date cascade filename/frontmatter → git → mtime - Release: publish to PyPI as
ragx-cli(plainragxis name-blocked, too similar to an existing project) souvx ragx-cliworks out of the box
Development: uv sync --group dev && uv run pytest. 183 tests; module contracts live in
CONTRACTS.md / CONTRACTS-PHASE23.md.
Metadata
Release files for ragx-cli 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ragx_cli-0.7.0.tar.gz | 208.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ragx_cli-0.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 289.9 kB
Release files / ragx_cli-0.7.0.tar.gz
| Download URL | ragx_cli-0.7.0.tar.gz |
|---|---|
| Size | 208.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
531338863094f1c9d2ce6dcbef11e9362aabfe0fb6607e289a883dae0ca7a403
|
|
BLAKE2b-256 checksum How to use checksums |
87a9eb59a1a46838cc22c16e5ff8e75532ad8f0cd0be36079b20f59ba7703715
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / ragx_cli-0.7.0-py3-none-any.whl
| Download URL | ragx_cli-0.7.0-py3-none-any.whl |
|---|---|
| Size | 81.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d5b6323cb4d09bf64c8e28cd2606e242eee2d82a07b9d11f0484ddcf7c5d9c01
|
|
BLAKE2b-256 checksum How to use checksums |
9f98ffc503edf546f6bdaddad32a340d15b1c85130ba8e2ec35af4c3e055aed6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|