Skip to main content
Yanked

This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.

SpectRAG

Spectral graph-augmented retrieval with token-budgeted context selection for multi-hop RAG

documentation PyPI version tests python 3.10 to 3.14 MIT license only numpy required paper

Documentation · Installation · Quick start · How it works · Graph view · Configuration · Benchmark · Citation


A readable, tested Python implementation of the paper SpectRAG: Weighted Chunk Graphs for Token-Budgeted Context Selection in Multi-Hop Retrieval-Augmented Generation (Sahinoglu et al., arXiv:2610.36991, PDF).

📖 Documentation: aion7891.github.io/Spect-RAG covers installation, the quick start, architecture, configuration, tuning, API reference, benchmarks and FAQ, plus interactive explainers of the method.

Plain top-k RAG SpectRAG
Unit of retrieval chunks ranked one by one a set of chunks chosen together
Near-duplicates often fill the context penalised (redundancy term)
Bridge evidence (not similar to the question, but needed) usually missed reached by walking a weighted chunk graph
Structure (sections, entities, citations, order) ignored six edge signals and spectral coordinates
Context size a fixed k a token budget, filled by relevance, coverage and diversity

SpectRAG pipeline: offline ingestion builds a chunk graph and its spectral coordinates; online retrieval seeds, expands, re-scores and selects within a token budget

Highlights

The full method from the paper
Six edge signals, the normalised Laplacian and spectral coordinates, hybrid seeds with RRF, two-hop graph expansion, PageRank and spectral proximity, the token-budget selector, and the paper's alternative selectors (mmr, protected_mmr, anchor_expand_ce).

Measured, not claimed
A reproducible MultiHopRAG benchmark (1,955 test questions) against standard RAG, BM25, hybrid RAG, MMR, HyDE and LightRAG, with confidence intervals, ablations, latency and memory.

Light and offline by default
One required dependency (numpy). An offline embedder out of the box; plug in nomic-embed-text, any embedding function, and any LLM.

Built to scale
Exact, FAISS or HNSW vector search, a sparse eigensolver above 2,000 chunks, binary index files: about 4 ms per query on 10k chunks.

Configurable and tunable
39 validated settings, per-query overrides, graph settings changed without re-embedding, and a grid / random-search tuner.

See what it retrieves
An interactive graph view of the chunk graph and of each question's retrieval path, in one offline HTML file.

Installation

pip install spectrag              # core (numpy only)
pip install "spectrag[all]"       # every extra

The latest code from main: pip install "git+https://github.com/aion7891/Spect-RAG.git".

From a clone (for development):

git clone https://github.com/aion7891/Spect-RAG.git && cd Spect-RAG
pip install -e ".[all,dev]"        # or: uv sync --all-extras
Extra Installs Adds
tokens tiktoken exact cl100k_base token counts (otherwise a word-based estimate)
pdf pypdf reading .pdf files
scale scipy, faiss-cpu, hnswlib sparse eigensolver and approximate nearest-neighbour search for large corpora
all all of the above
dev pytest, build tests and packaging

Real semantic embeddings and LLM answers use a local Ollama server: ollama pull nomic-embed-text (the paper's embedding model) and, for answers, e.g. ollama pull gemma3:4b. Without Ollama, SpectRAG uses an offline hashing embedder.

Python 3.10–3.14 are tested. There is no upper version limit; Python 3.15 is waiting for a numpy release that supports it.

Quick start

python quickstart.py

quickstart.py needs no arguments. It ingests the sample documents in examples/docs/, builds the index, and prints the context for two questions. The documents, questions and every setting are at the top of the file; set USE_OLLAMA = True for nomic embeddings and LLM answers.

Python

from spectrag import SpectRAGIngestion, SpectRAGRetrieval

# Offline, once per document set: chunk, embed, build the graph, spectral coordinates
SpectRAGIngestion("my_index").ingest("examples/docs")      # a file or a folder (.txt .md .pdf)

# Online, per question
search = SpectRAGRetrieval("my_index")
result = search.search("Which company did the co-founder of Aurora Labs join after leaving?")
print(result["context"])                # text for your LLM, grouped by source document
for chunk in result["chunks"]:          # ranked sources with their scores
    print(chunk["rank"], chunk["doc_id"], round(chunk["score"], 3), chunk["reason"])

# Optional: an answer from any LLM (any function prompt -> text)
from spectrag.utils.llm import ollama_llm
print(search.answer("Which company did the co-founder of Aurora Labs join?", ollama_llm("gemma3:4b"))["answer"])

Documents can also come from memory: SpectRAGIngestion("my_index").ingest_documents([{"doc_id": "a", "text": "..."}]).

Command line

spectrag ingest examples/docs --index my_index --set chunk_tokens=256 --set overlap_tokens=64
spectrag search "Where is Helios Energy based?" --index my_index --budget 2000
spectrag answer "Where is Helios Energy based?" --index my_index --model gemma3:4b
spectrag graph --index my_index --out graph.html --question "Where is Helios Energy based?"
spectrag info --index my_index

--set key=value changes any setting (values are read as JSON); -v prints progress.

How it works

Offline: SpectRAGIngestion (once per document set)

Step What happens Paper
1. Chunking Split by headings, keeping the section path, or where the topic changes (90th-percentile sentence distance). At most 1,200 tokens, 100 overlap; sentences and table rows are never cut Section 3.3
2. Embedding nomic-embed-text with the search_document: prefix (or the offline embedder, or your function), L2-normalised Table 1
3. Edge signals Six links between chunks, each in [0, 1]: semantic (k nearest neighbours), adjacency, section, shared entities, utility questions, citations Section 3.2.1
4. Weighted graph Weighted sum of the signals; weak edges dropped; each chunk keeps its graph_k strongest edges Eq. 10
5. Spectral coordinates The smallest non-trivial eigenvectors of the normalised Laplacian give each chunk 32 numbers that say where it sits in the graph's communities, not what it says Eq. 11–13

Online: SpectRAGRetrieval (per question)

Step What happens Paper
1. Hybrid seeds Dense, BM25 and utility-question rankings fused with RRF → 20 seeds and a relevance score for every chunk Table 2
2. Graph expansion Breadth-first search, 2 hops from the seeds, at most 200 candidates: this is where bridge evidence enters Section 3.6
3. Proximity re-scoring Relevance blended with graph proximity (Personalized PageRank + spectral closeness) Eq. 17–18
4. Token-budget selection A protected core, a greedy tail that trades relevance against redundancy, coverage and length, then backfill; never over the budget Eq. 19–22
5. Answer The context, grouped by document, in a grounded prompt for any LLM Algorithm 1
Key equations

Weighted graph (Eq. 10), with edges below $\tau$ removed:

w_{ij} = \sum_{r} \alpha_r \, w^{(r)}_{ij}

Normalised Laplacian (Eq. 11); the spectral coordinates are eigenvectors $u_2 \dots u_{33}$:

L_{sym} = I - D^{-1/2} \, W \, D^{-1/2}

Personalized PageRank from the seeds (Eq. 18), and the re-score:

s^{(t+1)} = \beta \, r + (1-\beta)\, \tilde{A}\, s^{(t)}, \qquad
\text{score} = (1-\alpha)\,\text{relevance} + \alpha\,\text{proximity}

Greedy selection under the token budget $B$ (Eq. 21–22):

U(c \mid C) = a\, s_c + b\,\text{Prox}(c) + c\,\text{ClusterGain}(c, C) + d\,\text{DocGain}(c, C) - \lambda\,\text{Red}(c, C),
\qquad c^{*} = \arg\max_{c} \frac{U(c \mid C)}{\ell(c)^{\rho}}, \quad \sum_{c \in C} \ell(c) \le B

Every place where the paper leaves a detail open, and what this code chose, is listed in docs/DESIGN.md (D1–D64).

Graph view

spectrag graph (or spectrag.utils.graph_view.save_graph_view) writes one self-contained HTML file, similar to Figure 5 of the paper. Chunks are drawn as nodes, starting from their spectral coordinates, and edges are coloured by their strongest signal. For a question it shows the retrieval path: seeds, chunks reached by expansion, and the selected context. Hover for a chunk's text, click to highlight its neighbours, scroll to zoom, drag to pan, and hide signals with the checkboxes. It needs no internet. Example: examples/graph_view.html (download and open it in a browser).

Graph view of 600 MultiHopRAG chunks with the selected context for one question highlighted

Configuration

Settings are a plain dict. Pass changes to SpectRAGIngestion(dir, settings), SpectRAGRetrieval(dir, settings), search(q, settings=...), or --set on the CLI. Every value is checked: a typo gets "did you mean …?", a bad value says which range is allowed. Source says whether the default comes from the paper or was chosen because the paper gives no value.

Chunking (ingestion)
Setting Default Meaning Source
chunk_tokens / overlap_tokens 1200 / 100 maximum chunk size and overlap, in tokens paper
min_chunk_tokens 32 smaller chunks are merged into the previous one paper
semantic_percentile 90 topic breaks at this percentile of sentence distances paper
semantic_embedder None embedder for those distances; None = the same as embedder, "hash" = much faster chosen
Embedding (ingestion)
Setting Default Meaning Source
embedder "hash" "hash" (offline), "ollama", or a function texts -> vectors chosen
embed_dims 768 size of the hash vectors paper
ollama_model / ollama_url nomic-embed-text / http://localhost:11434 Ollama embedding model and server paper
Vector search
Setting Default Meaning Source
ann_backend "auto" "exact", "faiss", "hnsw", or "auto" (exact up to 20,000 chunks) chosen
hnsw_m / hnsw_ef_construction / hnsw_ef_search 32 / 200 / 128 HNSW graph size and search effort chosen
dense_top_n 1000 with an approximate backend, dense scores for the top n chunks only chosen
Graph (ingestion; change later with rebuild_graph())
Setting Default Meaning Source
edge_weights semantic .300, section .250, entity .200, adjacency .084, utility .083, citation .083 α of Eq. 10 (paper configuration 1); empty signals share their weight paper
semantic_k 10 nearest neighbours per chunk for semantic and utility edges chosen
edge_threshold 0.05 τ: fused edges below this are dropped chosen
graph_k 10 each chunk keeps at most its graph_k strongest edges chosen
spectral_dims 32 spectral coordinates per chunk paper
n_clusters None spectral clusters; None = about √(n/2) chosen
Retrieval
Setting Default Meaning Source
seed_count 20 seeds from hybrid search paper
hops / max_candidates 2 / 200 graph expansion depth and cap paper
token_budget 12000 tokens of context per question paper
rrf_k 60 RRF constant chosen
bm25_k1 / bm25_b 1.5 / 0.75 BM25 parameters chosen
bm25_weight 0.5 hybrid relevance = (1 − w)·dense + w·BM25 chosen
alpha 0.3 weight of graph proximity in the re-score chosen
proximity_mode "hybrid" "hybrid" (PageRank + spectral), "ppr", "spectral", "heat" (Eq. 17), "none" paper
ppr_beta / ppr_steps 0.15 / 30 PageRank restart share and iterations (Eq. 18) chosen
diffusion_t 1.0 heat-kernel scale t chosen
Selection
Setting Default Meaning Source
selector "spectrag" "spectrag", "mmr", "protected_mmr", "anchor_expand_ce", "topk" paper
core_size 3 protected core (and anchors) chosen
precision_floor 0.4 greedy-tail candidates must score ≥ this × the best score chosen
select_weights a 1.0, b 0.3, c 0.2, d 0.1, λ 0.5 weights of Eq. 21 chosen
length_power 0.5 ρ of Eq. 22 (how much short chunks are preferred) chosen
mmr_lambda 0.7 MMR: relevance vs. novelty chosen

Tuning

from spectrag import SpectRAGIngestion, SpectRAGRetrieval
from spectrag.utils.evaluation import load_questions
from spectrag.utils.tuning import tune

SpectRAGIngestion("eval_index").ingest("examples/eval/docs")     # the documents the questions are about
search = SpectRAGRetrieval("eval_index")
questions = load_questions("examples/eval/questions.json")   # [{"question", "evidence": [...]}, ...]
grid = {"alpha": [0.1, 0.3, 0.5], "selector": ["spectrag", "protected_mmr"], "edge_threshold": [0.05, 0.1]}
results = tune(search, questions, grid, objective="recall", budget=4000, trials=10)
print(results[0])                                            # best settings and their scores

Graph settings (edge_threshold, graph_k, edge_weights, spectral_dims, n_clusters) are applied by search.rebuild_graph({...}) from the saved signals, so nothing is embedded again.

Your own models

Plug-in Where Signature
Embedder settings={"embedder": fn} (pass embed_fn=fn again when searching) fn(texts) -> vectors
LLM for answers search.answer(question, llm_fn) llm_fn(prompt) -> text
Entity extractor SpectRAGIngestion(..., entity_fn=fn) fn(text) -> list of strings
Utility questions SpectRAGIngestion(..., question_fn=make_question_fn(llm_fn)) fn(text) -> list of questions
Cross-encoder SpectRAGRetrieval(..., rerank_fn=fn) with selector="anchor_expand_ce" fn(question, texts) -> scores

Errors and logging

All errors raised on purpose subclass spectrag.SpectRAGError and the matching built-in: SettingsError (ValueError), UnknownSettingError (KeyError), IndexError_ (FileNotFoundError), EmbedderError (ConnectionError), DocumentError (ValueError), DocumentNotFoundError (FileNotFoundError), MissingPackageError (ImportError) and LLMError (ConnectionError). The CLI prints them as one spectrag: error: ... line. SpectRAG is silent by default; spectrag.enable_logging() prints one timed line per pipeline step, and enable_logging("DEBUG") one line per query.

Benchmark: MultiHopRAG

Real runs on MultiHopRAG: 609 news articles, 7,807 chunks, nomic-embed-text embeddings, a 12,000-token budget for every method. 300 questions were used only for tuning; all numbers below are on the other 1,955 test questions.

Method Recall@5 Recall@10 Evidence recall (budget) All evidence found MRR ms / query
Standard RAG (dense top-k) 0.355 0.483 0.758 0.512 0.507 1.3
BM25 0.462 0.577 0.788 0.545 0.617 1.0
Dense + MMR 0.359 0.482 0.756 0.507 0.508 1.3
Hybrid RAG (dense + BM25, RRF) 0.445 0.574 0.832 0.615 0.609 2.8
SpectRAG, default settings 0.385 0.433 0.803 0.550 0.592 4.4
SpectRAG, tuned on validation 0.470 0.592 0.846 0.639 0.617 3.5

Recall at 2, 5 and 10 per method Evidence recall and all evidence found within the budget per method

  • Tuned SpectRAG beats hybrid RAG: evidence recall +0.014 (95% CI +0.007 … +0.021), Recall@10 +0.017 (+0.009 … +0.025). Its largest gain is on inference questions (0.802 vs 0.757); on comparison questions hybrid stays slightly ahead, as in the paper.
  • Removing graph expansion drops evidence recall to 0.705: the graph walk is where most of the extra evidence comes from.
  • Smaller tests: HyDE (60 questions): 0.760, against dense 0.807. Answers with gemma3:4b (30 questions): exact match 0.400 tuned SpectRAG, 0.333 dense, 0.267 hybrid (one answer = 0.033). LightRAG on a 4-article subset could not be compared fairly (the subset fits in the budget), and it needed about 225 s of LLM time per 1,000-word article to index.

Reproduce (needs Ollama; quick mode takes minutes, SPECTRAG_BENCH_FULL=1 runs the full version):

python benchmarks/multihoprag_benchmark.py      # index, tune, evaluate, latency
python benchmarks/mhr_llm_steps.py hyde          # and: answers
python benchmarks/mhr_report.py                 # REPORT.md and the charts
python benchmarks/ann_benchmark.py              # exact vs FAISS vs HNSW

Full results, per-type tables, ablations and the method: benchmarks/results/multihoprag/REPORT.md and docs/PROGRESS.md. The dataset (ODC-BY) is stored in benchmarks/data/multihoprag/ with checksums.

Scale and memory
Corpus Ingestion Search (median) Index Memory
MultiHopRAG, 609 articles → 10,335 chunks (hash embedder) 7.7 s 4.1 ms 49 MB about 1 GB peak during ingestion
MultiHopRAG, 7,807 chunks (nomic-embed-text) dominated by embedding through Ollama 3.5–4.4 ms + 19 ms query embedding 39.5 MB 256 MB after loading
100,000 synthetic 768-d vectors, kNN graph exact 70.9 s, FAISS 10.4 s (same edges)

Project layout

Show the folders and modules
src/spectrag/
  ingestion.py, retrieval.py         the two main classes
  config.py, validation.py           settings and their checks
  errors.py, log.py, cli.py          exceptions, logging, the spectrag command
  utils/                             one small module per step: loader, tokens, sections, units,
                                     chunker, embedder, entities, references, questions, edges*,
                                     graph, graph_build, spectral, clusters, vector_index, bm25,
                                     seeding, expansion, diffusion, selection, selectors, prompt,
                                     llm, metrics, evaluation, tuning, graph_view, store
quickstart.py                        runs with no arguments
examples/                            sample documents, a 14-document evaluation set, demo scripts
benchmarks/                          MultiHopRAG and ANN benchmarks, their data and results
tests/                               unit, integration and equivalence tests
docs/                                the paper, DESIGN.md, PROGRESS.md, PRODUCTION_READINESS.md,
                                     DEVELOPMENT_LOG.md
tools/                               validate_release.py (release checks on a real index)

Every module is short (under about 150 lines), and functions do one thing.

Testing

pytest -q          # 300+ tests, under 10 seconds, offline

The tests cover every formula against hand-computed or closed-form values (BM25, RRF, PageRank, the heat kernel against scipy.linalg.expm, the sparse eigensolver against the dense one, the paper's metrics). The fast vectorised code is checked against simple loop versions on random cases, and the full pipeline runs end to end on real documents. Live Ollama tests run when Ollama is available (SPECTRAG_LIVE_LLM=1 for the LLM test). CI runs on every pull request and on main: Linux × Python 3.10 / 3.12 / 3.14 × minimal / all extras, one macOS job (Python 3.12, all extras), and a build that installs the wheel and runs the CLI. A release tag runs two quick Linux test jobs, because the merged commit has already passed the full set.

Before a release, python tools/validate_release.py re-checks the mathematics on the real MultiHopRAG index (eigenpairs, PageRank, BM25, every selector and proximity mode) and that the benchmark reproduces. The results are in the production-readiness report.

Development status

The hardening work in docs/PROGRESS.md (Phase 7) runs in small, reviewed steps. Green = done, yellow = in review, grey = next.

flowchart LR
    errors["0.1 errors.py: every error is a SpectRAGError"] --> embedder["0.2 embedder marker and fingerprint"]
    embedder --> replace["0.3 safe index replace"] --> escape["0.4 graph view escaping"]
    classDef done fill:#d4edda,stroke:#2e7d32,color:#1b5e20
    classDef doing fill:#fff3cd,stroke:#f9a825,color:#6d4c00
    classDef todo fill:#eceff1,stroke:#90a4ae,color:#455a64
    class errors doing
    class embedder,replace,escape todo

Limitations

  • An index built with a functools.partial or a callable object as its embedder cannot be opened again, and an index opened with a different embedder of the same size is not detected. Both are fixed in step 0.2.
  • The default settings are not tuned (see the benchmark). Tuning on your own validation questions is recommended.
  • Entity extraction is rule-based unless you pass an entity_fn; utility questions need an LLM.
  • Semantic edges and the dense eigensolver are O(n²) without the scale extra.
  • The benchmark is a quick-mode run on one dataset; LightRAG was only compared on a tiny subset.
  • On macOS with numpy 2.2 (Python 3.10), matrix products print spurious RuntimeWarnings; the results are correct.

More: docs/DESIGN.md (every decision), docs/PROGRESS.md (the upgrade, with measurements), CHANGELOG.md.

Citation

If you use SpectRAG, please cite the paper:

@article{sahinoglu2026spectrag,
  title   = {SpectRAG: Weighted Chunk Graphs for Token-Budgeted Context Selection in
             Multi-Hop Retrieval-Augmented Generation},
  author  = {Sahinoglu, Zafer and Mohammed, Abdulazeez and Kazangirler, Buse Yaren and
             Kazangirler, Cahit Berkay},
  journal = {arXiv preprint arXiv:2610.36991},
  year    = {2026}
}

License

MIT, see LICENSE. The MultiHopRAG data in benchmarks/data/ is ODC-BY (see its SOURCE.md).

Metadata

Release files for spectrag 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for spectrag 0.1.1
File Size Uploaded Provenance Actions
spectrag-0.1.1.tar.gz 103.4 kB With provenance Details for spectrag-0.1.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for spectrag 0.1.1
File Interpreter ABI Platform Actions
spectrag-0.1.1-py3-none-any.whl Python 3 none any Details for spectrag-0.1.1-py3-none-any.whl

Total release size: 179.8 kB

Release files / spectrag-0.1.1.tar.gz

Download URL spectrag-0.1.1.tar.gz
Size 103.4 kB
Tags Source
SHA-256 checksum
How to use checksums
c07bc9d2debe4dc513d801b9c0b4a0cbc70004076c9e3b202c527a0a06780303
BLAKE2b-256 checksum
How to use checksums
57f45c66526f5f73cfd5d2c06ad115eb0e031f2db6744e1f21fd503373924b80
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

Release files / spectrag-0.1.1-py3-none-any.whl

Download URL spectrag-0.1.1-py3-none-any.whl
Size 76.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4c828bdade9f6bf9876d6f589f4ec74e8060932e59c8f5b0d019b3b50c33354e
BLAKE2b-256 checksum
How to use checksums
c4a97832900729ed93c82130f23a538978f1d3f53f7398ff74ee5520844b68a8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

Security for spectrag 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for spectrag-0.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page