spectrafuse 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spectrafuse-0.2.0/LICENSE +21 -0
- spectrafuse-0.2.0/NOTICE +23 -0
- spectrafuse-0.2.0/PKG-INFO +606 -0
- spectrafuse-0.2.0/README.md +559 -0
- spectrafuse-0.2.0/pyproject.toml +57 -0
- spectrafuse-0.2.0/setup.cfg +4 -0
- spectrafuse-0.2.0/src/spectrafuse/__init__.py +27 -0
- spectrafuse-0.2.0/src/spectrafuse/_version.py +7 -0
- spectrafuse-0.2.0/src/spectrafuse/cli.py +115 -0
- spectrafuse-0.2.0/src/spectrafuse/config.py +96 -0
- spectrafuse-0.2.0/src/spectrafuse/errors.py +52 -0
- spectrafuse-0.2.0/src/spectrafuse/ingestion.py +118 -0
- spectrafuse-0.2.0/src/spectrafuse/log.py +30 -0
- spectrafuse-0.2.0/src/spectrafuse/retrieval.py +158 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/__init__.py +1 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/bm25.py +53 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/chunker.py +112 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/clusters.py +63 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/diffusion.py +105 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/edge_list.py +18 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/edges.py +38 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/edges_content.py +110 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/edges_structure.py +86 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/embedder.py +89 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/embedder_id.py +78 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/entities.py +72 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/evaluation.py +112 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/expansion.py +68 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/graph.py +90 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/graph_build.py +27 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/graph_template.py +105 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/graph_view.py +126 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/index_folder.py +78 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/llm.py +62 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/loader.py +61 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/metrics.py +61 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/prompt.py +62 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/questions.py +40 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/references.py +43 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/sections.py +98 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/seeding.py +106 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/selection.py +93 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/selectors.py +104 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/semantic_split.py +54 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/spectral.py +72 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/store.py +94 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/tokens.py +64 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/tuning.py +78 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/units.py +106 -0
- spectrafuse-0.2.0/src/spectrafuse/utils/vector_index.py +124 -0
- spectrafuse-0.2.0/src/spectrafuse/validation.py +84 -0
- spectrafuse-0.2.0/src/spectrafuse.egg-info/PKG-INFO +606 -0
- spectrafuse-0.2.0/src/spectrafuse.egg-info/SOURCES.txt +94 -0
- spectrafuse-0.2.0/src/spectrafuse.egg-info/dependency_links.txt +1 -0
- spectrafuse-0.2.0/src/spectrafuse.egg-info/entry_points.txt +2 -0
- spectrafuse-0.2.0/src/spectrafuse.egg-info/requires.txt +22 -0
- spectrafuse-0.2.0/src/spectrafuse.egg-info/top_level.txt +1 -0
- spectrafuse-0.2.0/tests/test_benchmark_helpers.py +41 -0
- spectrafuse-0.2.0/tests/test_bm25.py +47 -0
- spectrafuse-0.2.0/tests/test_chunker.py +103 -0
- spectrafuse-0.2.0/tests/test_clusters.py +49 -0
- spectrafuse-0.2.0/tests/test_config.py +29 -0
- spectrafuse-0.2.0/tests/test_diffusion.py +90 -0
- spectrafuse-0.2.0/tests/test_docs.py +70 -0
- spectrafuse-0.2.0/tests/test_edges.py +143 -0
- spectrafuse-0.2.0/tests/test_embedder.py +104 -0
- spectrafuse-0.2.0/tests/test_embedder_id.py +118 -0
- spectrafuse-0.2.0/tests/test_end_to_end.py +95 -0
- spectrafuse-0.2.0/tests/test_entities.py +41 -0
- spectrafuse-0.2.0/tests/test_equivalence.py +70 -0
- spectrafuse-0.2.0/tests/test_errors.py +75 -0
- spectrafuse-0.2.0/tests/test_evaluation.py +70 -0
- spectrafuse-0.2.0/tests/test_expansion.py +39 -0
- spectrafuse-0.2.0/tests/test_graph.py +57 -0
- spectrafuse-0.2.0/tests/test_graph_view.py +61 -0
- spectrafuse-0.2.0/tests/test_index_folder.py +79 -0
- spectrafuse-0.2.0/tests/test_ingestion.py +89 -0
- spectrafuse-0.2.0/tests/test_llm.py +71 -0
- spectrafuse-0.2.0/tests/test_loader.py +50 -0
- spectrafuse-0.2.0/tests/test_metadata.py +30 -0
- spectrafuse-0.2.0/tests/test_metrics.py +36 -0
- spectrafuse-0.2.0/tests/test_phase2.py +125 -0
- spectrafuse-0.2.0/tests/test_production.py +134 -0
- spectrafuse-0.2.0/tests/test_prompt.py +38 -0
- spectrafuse-0.2.0/tests/test_questions.py +66 -0
- spectrafuse-0.2.0/tests/test_references.py +28 -0
- spectrafuse-0.2.0/tests/test_retrieval.py +79 -0
- spectrafuse-0.2.0/tests/test_sections.py +65 -0
- spectrafuse-0.2.0/tests/test_seeding.py +88 -0
- spectrafuse-0.2.0/tests/test_selection.py +75 -0
- spectrafuse-0.2.0/tests/test_selectors.py +74 -0
- spectrafuse-0.2.0/tests/test_semantic_split.py +40 -0
- spectrafuse-0.2.0/tests/test_spectral.py +74 -0
- spectrafuse-0.2.0/tests/test_store.py +48 -0
- spectrafuse-0.2.0/tests/test_tokens.py +44 -0
- spectrafuse-0.2.0/tests/test_units.py +37 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kumar Nishant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
spectrafuse-0.2.0/NOTICE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
SpectraFuse: an independent implementation inspired by SpectRAG
|
|
2
|
+
=================================================================
|
|
3
|
+
|
|
4
|
+
SpectraFuse (Python package `spectrafuse`, published as `spectrag` until version 0.1.4) is an
|
|
5
|
+
independent implementation inspired by SpectRAG, the method described in:
|
|
6
|
+
|
|
7
|
+
Zafer Sahinoglu, Abdulazeez Mohammed, Buse Yaren Kazangirler and Cahit Berkay Kazangirler.
|
|
8
|
+
SpectRAG: Weighted Chunk Graphs for Token-Budgeted Context Selection in Multi-Hop
|
|
9
|
+
Retrieval-Augmented Generation. arXiv:2610.36991, 2026. https://arxiv.org/abs/2610.36991
|
|
10
|
+
|
|
11
|
+
SpectraFuse is not SpectRAG and not an official release. It is not affiliated with or endorsed
|
|
12
|
+
by the paper's authors, and differences in its behaviour or results are its own.
|
|
13
|
+
|
|
14
|
+
SpectRAG's official implementation and releases are maintained by the authors at:
|
|
15
|
+
|
|
16
|
+
https://github.com/SightAndCite/SpectRAG
|
|
17
|
+
MIT License, Copyright (c) 2026 The SpectRAG Authors
|
|
18
|
+
|
|
19
|
+
SpectraFuse was written from the paper. It does not include code from the official
|
|
20
|
+
implementation. The method, its equations, tables and default values are the work of the
|
|
21
|
+
paper's authors; please cite the paper if you use the method.
|
|
22
|
+
|
|
23
|
+
The code of SpectraFuse is Copyright (c) 2026 Kumar Nishant, under the MIT License (see LICENSE).
|
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spectrafuse
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: SpectraFuse: an independent implementation inspired by SpectRAG (Sahinoglu, Mohammed, Kazangirler and Kazangirler, arXiv:2610.36991). Not the official SpectRAG, which is at github.com/SightAndCite/SpectRAG
|
|
5
|
+
Author: Kumar Nishant
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aion7891/SpectraFuse
|
|
8
|
+
Project-URL: Documentation, https://aion7891.github.io/SpectraFuse/
|
|
9
|
+
Project-URL: Repository, https://github.com/aion7891/SpectraFuse
|
|
10
|
+
Project-URL: Issues, https://github.com/aion7891/SpectraFuse/issues
|
|
11
|
+
Project-URL: Paper, https://arxiv.org/abs/2610.36991
|
|
12
|
+
Project-URL: Official implementation, https://github.com/SightAndCite/SpectRAG
|
|
13
|
+
Keywords: rag,retrieval-augmented-generation,graph,spectral,retrieval,llm,multi-hop
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
25
|
+
Classifier: Topic :: Text Processing :: Indexing
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
License-File: NOTICE
|
|
30
|
+
Requires-Dist: numpy>=1.24
|
|
31
|
+
Provides-Extra: tokens
|
|
32
|
+
Requires-Dist: tiktoken>=0.5; extra == "tokens"
|
|
33
|
+
Provides-Extra: pdf
|
|
34
|
+
Requires-Dist: pypdf>=4; extra == "pdf"
|
|
35
|
+
Provides-Extra: scale
|
|
36
|
+
Requires-Dist: scipy>=1.10; extra == "scale"
|
|
37
|
+
Requires-Dist: faiss-cpu>=1.8; extra == "scale"
|
|
38
|
+
Requires-Dist: hnswlib>=0.8; extra == "scale"
|
|
39
|
+
Provides-Extra: all
|
|
40
|
+
Requires-Dist: spectrafuse[pdf,scale,tokens]; extra == "all"
|
|
41
|
+
Provides-Extra: extras
|
|
42
|
+
Requires-Dist: spectrafuse[all]; extra == "extras"
|
|
43
|
+
Provides-Extra: dev
|
|
44
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
45
|
+
Requires-Dist: build>=1; extra == "dev"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
<p align="center"><img src="docs/assets/banner.svg" alt="SpectraFuse" width="100%"></p>
|
|
49
|
+
|
|
50
|
+
<h3 align="center">Spectral graph-augmented retrieval with token-budgeted context selection for multi-hop RAG</h3>
|
|
51
|
+
|
|
52
|
+
<p align="center">
|
|
53
|
+
<a href="https://aion7891.github.io/SpectraFuse/"><img src="https://img.shields.io/badge/docs-online-3F51B5" alt="documentation"></a>
|
|
54
|
+
<a href="https://pypi.org/project/spectrafuse/"><img src="https://img.shields.io/pypi/v/spectrafuse" alt="PyPI version"></a>
|
|
55
|
+
<a href="https://github.com/aion7891/SpectraFuse/actions/workflows/tests.yml"><img src="https://github.com/aion7891/SpectraFuse/actions/workflows/tests.yml/badge.svg" alt="tests"></a>
|
|
56
|
+
<img src="https://img.shields.io/badge/python-3.10%20%E2%80%93%203.14-blue" alt="python 3.10 to 3.14">
|
|
57
|
+
<img src="https://img.shields.io/badge/license-MIT-green" alt="MIT license">
|
|
58
|
+
<img src="https://img.shields.io/badge/required%20dependency-numpy-lightgrey" alt="only numpy required">
|
|
59
|
+
<a href="https://arxiv.org/abs/2610.36991"><img src="https://img.shields.io/badge/paper-arXiv%3A2610.36991-b31b1b" alt="paper"></a>
|
|
60
|
+
<a href="https://github.com/SightAndCite/SpectRAG"><img src="https://img.shields.io/badge/inspired%20by-SpectRAG%20(SightAndCite%2FSpectRAG)-orange" alt="independent implementation inspired by SpectRAG; official code: SightAndCite/SpectRAG"></a>
|
|
61
|
+
</p>
|
|
62
|
+
|
|
63
|
+
<p align="center">
|
|
64
|
+
<a href="https://aion7891.github.io/SpectraFuse/"><b>Documentation</b></a> ·
|
|
65
|
+
<a href="#installation">Installation</a> ·
|
|
66
|
+
<a href="#quick-start">Quick start</a> ·
|
|
67
|
+
<a href="#how-it-works">How it works</a> ·
|
|
68
|
+
<a href="#graph-view">Graph view</a> ·
|
|
69
|
+
<a href="#configuration">Configuration</a> ·
|
|
70
|
+
<a href="#benchmark-multihoprag">Benchmark</a> ·
|
|
71
|
+
<a href="#citation">Citation</a>
|
|
72
|
+
</p>
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
> [!IMPORTANT]
|
|
77
|
+
> **SpectraFuse is an independent implementation inspired by SpectRAG.** It is not SpectRAG and
|
|
78
|
+
> not the official release. It is not affiliated with or endorsed by the SpectRAG authors, and
|
|
79
|
+
> where its behaviour or results differ from SpectRAG, those differences are SpectraFuse's own.
|
|
80
|
+
> **Official SpectRAG:** [SightAndCite/SpectRAG](https://github.com/SightAndCite/SpectRAG), where
|
|
81
|
+
> the authors maintain SpectRAG's releases. **Paper:** [arXiv:2610.36991](https://arxiv.org/abs/2610.36991).
|
|
82
|
+
> SpectraFuse contains no code from the official repository. It was published on PyPI as
|
|
83
|
+
> `spectrag` until 0.1.4 and is now `spectrafuse`.
|
|
84
|
+
|
|
85
|
+
SpectraFuse follows the method of the paper *SpectRAG: Weighted Chunk Graphs for Token-Budgeted
|
|
86
|
+
Context Selection in Multi-Hop Retrieval-Augmented Generation* by **Zafer Sahinoglu, Abdulazeez
|
|
87
|
+
Mohammed, Buse Yaren Kazangirler and Cahit Berkay Kazangirler** (2026). It is a readable, tested
|
|
88
|
+
Python implementation written from that paper by Kumar Nishant, with its own choices where the
|
|
89
|
+
paper leaves details open. The method is the authors' work: if you use it, please
|
|
90
|
+
[cite the paper](#citation).
|
|
91
|
+
|
|
92
|
+
> **📖 Documentation: [aion7891.github.io/SpectraFuse](https://aion7891.github.io/SpectraFuse/)** covers installation, the quick
|
|
93
|
+
> start, architecture, configuration, tuning, API reference, benchmarks and FAQ, plus
|
|
94
|
+
> [interactive explainers](https://aion7891.github.io/SpectraFuse/interactive/) of the method.
|
|
95
|
+
|
|
96
|
+
| | Plain top-k RAG | SpectraFuse |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| **Unit of retrieval** | chunks ranked one by one | a *set* of chunks chosen together |
|
|
99
|
+
| **Near-duplicates** | often fill the context | penalised (redundancy term) |
|
|
100
|
+
| **Bridge evidence** (not similar to the question, but needed) | usually missed | reached by walking a weighted chunk graph |
|
|
101
|
+
| **Structure** (sections, entities, citations, order) | ignored | six edge signals and spectral coordinates |
|
|
102
|
+
| **Context size** | a fixed *k* | a token budget, filled by relevance, coverage and diversity |
|
|
103
|
+
|
|
104
|
+
<p align="center"><img src="docs/assets/pipeline.svg" alt="SpectraFuse pipeline: offline ingestion builds a chunk graph and its spectral coordinates; online retrieval seeds, expands, re-scores and selects within a token budget" width="100%"></p>
|
|
105
|
+
|
|
106
|
+
## Highlights
|
|
107
|
+
|
|
108
|
+
<table>
|
|
109
|
+
<tr>
|
|
110
|
+
<td width="50%" valign="top">
|
|
111
|
+
|
|
112
|
+
**The full method from the paper**<br>
|
|
113
|
+
Six edge signals, the normalised Laplacian and spectral coordinates, hybrid seeds with RRF,
|
|
114
|
+
two-hop graph expansion, PageRank and spectral proximity, the token-budget selector, and the
|
|
115
|
+
paper's alternative selectors (`mmr`, `protected_mmr`, `anchor_expand_ce`).
|
|
116
|
+
|
|
117
|
+
</td>
|
|
118
|
+
<td width="50%" valign="top">
|
|
119
|
+
|
|
120
|
+
**Measured, not claimed**<br>
|
|
121
|
+
A reproducible MultiHopRAG benchmark (1,955 test questions) against standard RAG, BM25, hybrid
|
|
122
|
+
RAG, MMR, HyDE and LightRAG, with confidence intervals, ablations, latency and memory.
|
|
123
|
+
|
|
124
|
+
</td>
|
|
125
|
+
</tr>
|
|
126
|
+
<tr>
|
|
127
|
+
<td valign="top">
|
|
128
|
+
|
|
129
|
+
**Light and offline by default**<br>
|
|
130
|
+
One required dependency (`numpy`). An offline embedder out of the box; plug in
|
|
131
|
+
`nomic-embed-text`, any embedding function, and any LLM.
|
|
132
|
+
|
|
133
|
+
</td>
|
|
134
|
+
<td valign="top">
|
|
135
|
+
|
|
136
|
+
**Built to scale**<br>
|
|
137
|
+
Exact, FAISS or HNSW vector search, a sparse eigensolver above 2,000 chunks, binary index
|
|
138
|
+
files: about 4 ms per query on 10k chunks.
|
|
139
|
+
|
|
140
|
+
</td>
|
|
141
|
+
</tr>
|
|
142
|
+
<tr>
|
|
143
|
+
<td valign="top">
|
|
144
|
+
|
|
145
|
+
**Configurable and tunable**<br>
|
|
146
|
+
39 validated settings, per-query overrides, graph settings changed without re-embedding, and a
|
|
147
|
+
grid / random-search tuner.
|
|
148
|
+
|
|
149
|
+
</td>
|
|
150
|
+
<td valign="top">
|
|
151
|
+
|
|
152
|
+
**See what it retrieves**<br>
|
|
153
|
+
An interactive graph view of the chunk graph and of each question's retrieval path, in one
|
|
154
|
+
offline HTML file.
|
|
155
|
+
|
|
156
|
+
</td>
|
|
157
|
+
</tr>
|
|
158
|
+
</table>
|
|
159
|
+
|
|
160
|
+
> [!TIP]
|
|
161
|
+
> On MultiHopRAG, tuned SpectraFuse finds more of the evidence than **standard RAG (+0.088)** and
|
|
162
|
+
> **hybrid RAG (+0.014, 95% CI +0.007 … +0.021)** within the same 12,000-token budget, with
|
|
163
|
+
> its largest gain on inference questions. [See the benchmark.](#benchmark-multihoprag)
|
|
164
|
+
|
|
165
|
+
## Installation
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
pip install spectrafuse # core (numpy only)
|
|
169
|
+
pip install "spectrafuse[all]" # every extra
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The latest code from `main`: `pip install "git+https://github.com/aion7891/SpectraFuse.git"`.
|
|
173
|
+
|
|
174
|
+
From a clone (for development):
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
git clone https://github.com/aion7891/SpectraFuse.git && cd SpectraFuse
|
|
178
|
+
pip install -e ".[all,dev]" # or: uv sync --all-extras
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
| Extra | Installs | Adds |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| `tokens` | `tiktoken` | exact `cl100k_base` token counts (otherwise a word-based estimate) |
|
|
184
|
+
| `pdf` | `pypdf` | reading `.pdf` files |
|
|
185
|
+
| `scale` | `scipy`, `faiss-cpu`, `hnswlib` | sparse eigensolver and approximate nearest-neighbour search for large corpora |
|
|
186
|
+
| `all` | all of the above | |
|
|
187
|
+
| `dev` | `pytest`, `build` | tests and packaging |
|
|
188
|
+
|
|
189
|
+
Real semantic embeddings and LLM answers use a local [Ollama](https://ollama.com) server:
|
|
190
|
+
`ollama pull nomic-embed-text` (the paper's embedding model) and, for answers, e.g.
|
|
191
|
+
`ollama pull gemma3:4b`. Without Ollama, SpectraFuse uses an offline hashing embedder.
|
|
192
|
+
|
|
193
|
+
Python 3.10–3.14 are tested. There is no upper version limit; Python 3.15 is waiting for a
|
|
194
|
+
numpy release that supports it.
|
|
195
|
+
|
|
196
|
+
## Quick start
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
python quickstart.py
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`quickstart.py` needs no arguments. It ingests the sample documents in `examples/docs/`, builds
|
|
203
|
+
the index, and prints the context for two questions. The documents, questions and every setting
|
|
204
|
+
are at the top of the file; set `USE_OLLAMA = True` for nomic embeddings and LLM answers.
|
|
205
|
+
|
|
206
|
+
### Python
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from spectrafuse import SpectRAGIngestion, SpectRAGRetrieval
|
|
210
|
+
|
|
211
|
+
# Offline, once per document set: chunk, embed, build the graph, spectral coordinates
|
|
212
|
+
SpectRAGIngestion("my_index").ingest("examples/docs") # a file or a folder (.txt .md .pdf)
|
|
213
|
+
|
|
214
|
+
# Online, per question
|
|
215
|
+
search = SpectRAGRetrieval("my_index")
|
|
216
|
+
result = search.search("Which company did the co-founder of Aurora Labs join after leaving?")
|
|
217
|
+
print(result["context"]) # text for your LLM, grouped by source document
|
|
218
|
+
for chunk in result["chunks"]: # ranked sources with their scores
|
|
219
|
+
print(chunk["rank"], chunk["doc_id"], round(chunk["score"], 3), chunk["reason"])
|
|
220
|
+
|
|
221
|
+
# Optional: an answer from any LLM (any function prompt -> text)
|
|
222
|
+
from spectrafuse.utils.llm import ollama_llm
|
|
223
|
+
print(search.answer("Which company did the co-founder of Aurora Labs join?", ollama_llm("gemma3:4b"))["answer"])
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Documents can also come from memory:
|
|
227
|
+
`SpectRAGIngestion("my_index").ingest_documents([{"doc_id": "a", "text": "..."}])`.
|
|
228
|
+
|
|
229
|
+
### Command line
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
spectrafuse ingest examples/docs --index my_index --set chunk_tokens=256 --set overlap_tokens=64
|
|
233
|
+
spectrafuse search "Where is Helios Energy based?" --index my_index --budget 2000
|
|
234
|
+
spectrafuse answer "Where is Helios Energy based?" --index my_index --model gemma3:4b
|
|
235
|
+
spectrafuse graph --index my_index --out graph.html --question "Where is Helios Energy based?"
|
|
236
|
+
spectrafuse info --index my_index
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`--set key=value` changes any setting (values are read as JSON); `-v` prints progress.
|
|
240
|
+
|
|
241
|
+
## How it works
|
|
242
|
+
|
|
243
|
+
**Offline: `SpectRAGIngestion`** (once per document set)
|
|
244
|
+
|
|
245
|
+
| Step | What happens | Paper |
|
|
246
|
+
|---|---|---|
|
|
247
|
+
| 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 |
|
|
248
|
+
| 2. Embedding | `nomic-embed-text` with the `search_document:` prefix (or the offline embedder, or your function), L2-normalised | Table 1 |
|
|
249
|
+
| 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 |
|
|
250
|
+
| 4. Weighted graph | Weighted sum of the signals; weak edges dropped; each chunk keeps its `graph_k` strongest edges | Eq. 10 |
|
|
251
|
+
| 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 |
|
|
252
|
+
|
|
253
|
+
**Online: `SpectRAGRetrieval`** (per question)
|
|
254
|
+
|
|
255
|
+
| Step | What happens | Paper |
|
|
256
|
+
|---|---|---|
|
|
257
|
+
| 1. Hybrid seeds | Dense, BM25 and utility-question rankings fused with RRF → 20 seeds and a relevance score for every chunk | Table 2 |
|
|
258
|
+
| 2. Graph expansion | Breadth-first search, 2 hops from the seeds, at most 200 candidates: this is where bridge evidence enters | Section 3.6 |
|
|
259
|
+
| 3. Proximity re-scoring | Relevance blended with graph proximity (Personalized PageRank + spectral closeness) | Eq. 17–18 |
|
|
260
|
+
| 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 |
|
|
261
|
+
| 5. Answer | The context, grouped by document, in a grounded prompt for any LLM | Algorithm 1 |
|
|
262
|
+
|
|
263
|
+
<details>
|
|
264
|
+
<summary><b>Key equations</b></summary>
|
|
265
|
+
|
|
266
|
+
Weighted graph (Eq. 10), with edges below $\tau$ removed:
|
|
267
|
+
|
|
268
|
+
```math
|
|
269
|
+
w_{ij} = \sum_{r} \alpha_r \, w^{(r)}_{ij}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Normalised Laplacian (Eq. 11); the spectral coordinates are eigenvectors $u_2 \dots u_{33}$:
|
|
273
|
+
|
|
274
|
+
```math
|
|
275
|
+
L_{sym} = I - D^{-1/2} \, W \, D^{-1/2}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Personalized PageRank from the seeds (Eq. 18), and the re-score:
|
|
279
|
+
|
|
280
|
+
```math
|
|
281
|
+
s^{(t+1)} = \beta \, r + (1-\beta)\, \tilde{A}\, s^{(t)}, \qquad
|
|
282
|
+
\text{score} = (1-\alpha)\,\text{relevance} + \alpha\,\text{proximity}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Greedy selection under the token budget $B$ (Eq. 21–22):
|
|
286
|
+
|
|
287
|
+
```math
|
|
288
|
+
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),
|
|
289
|
+
\qquad c^{*} = \arg\max_{c} \frac{U(c \mid C)}{\ell(c)^{\rho}}, \quad \sum_{c \in C} \ell(c) \le B
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
</details>
|
|
293
|
+
|
|
294
|
+
Every place where the paper leaves a detail open, and what this code chose, is listed in
|
|
295
|
+
[docs/DESIGN.md](docs/DESIGN.md) (D1–D64).
|
|
296
|
+
|
|
297
|
+
## Graph view
|
|
298
|
+
|
|
299
|
+
`spectrafuse graph` (or `spectrafuse.utils.graph_view.save_graph_view`) writes one self-contained HTML
|
|
300
|
+
file, similar to Figure 5 of the paper. Chunks are drawn as nodes, starting from their spectral
|
|
301
|
+
coordinates, and edges are coloured by their strongest signal. For a question it shows the
|
|
302
|
+
retrieval path: seeds, chunks reached by expansion, and the selected context. Hover for a
|
|
303
|
+
chunk's text, click to highlight its neighbours, scroll to zoom, drag to pan, and hide signals
|
|
304
|
+
with the checkboxes. It needs no internet. Example: [`examples/graph_view.html`](examples/graph_view.html)
|
|
305
|
+
(download and open it in a browser).
|
|
306
|
+
|
|
307
|
+
<p align="center"><img src="docs/assets/graph_view.png" alt="Graph view of 600 MultiHopRAG chunks with the selected context for one question highlighted" width="100%"></p>
|
|
308
|
+
|
|
309
|
+
## Configuration
|
|
310
|
+
|
|
311
|
+
Settings are a plain dict. Pass changes to `SpectRAGIngestion(dir, settings)`,
|
|
312
|
+
`SpectRAGRetrieval(dir, settings)`, `search(q, settings=...)`, or `--set` on the CLI. Every
|
|
313
|
+
value is checked: a typo gets "did you mean …?", a bad value says which range is allowed.
|
|
314
|
+
**Source** says whether the default comes from the paper or was chosen because the paper gives no value.
|
|
315
|
+
|
|
316
|
+
<details open>
|
|
317
|
+
<summary><b>Chunking (ingestion)</b></summary>
|
|
318
|
+
|
|
319
|
+
| Setting | Default | Meaning | Source |
|
|
320
|
+
|---|---|---|---|
|
|
321
|
+
| `chunk_tokens` / `overlap_tokens` | 1200 / 100 | maximum chunk size and overlap, in tokens | paper |
|
|
322
|
+
| `min_chunk_tokens` | 32 | smaller chunks are merged into the previous one | paper |
|
|
323
|
+
| `semantic_percentile` | 90 | topic breaks at this percentile of sentence distances | paper |
|
|
324
|
+
| `semantic_embedder` | `None` | embedder for those distances; `None` = the same as `embedder`, `"hash"` = much faster | chosen |
|
|
325
|
+
|
|
326
|
+
</details>
|
|
327
|
+
|
|
328
|
+
<details>
|
|
329
|
+
<summary><b>Embedding (ingestion)</b></summary>
|
|
330
|
+
|
|
331
|
+
| Setting | Default | Meaning | Source |
|
|
332
|
+
|---|---|---|---|
|
|
333
|
+
| `embedder` | `"hash"` | `"hash"` (offline), `"ollama"`, or a function `texts -> vectors` | chosen |
|
|
334
|
+
| `embed_dims` | 768 | size of the hash vectors | paper |
|
|
335
|
+
| `ollama_model` / `ollama_url` | `nomic-embed-text` / `http://localhost:11434` | Ollama embedding model and server | paper |
|
|
336
|
+
|
|
337
|
+
</details>
|
|
338
|
+
|
|
339
|
+
<details>
|
|
340
|
+
<summary><b>Vector search</b></summary>
|
|
341
|
+
|
|
342
|
+
| Setting | Default | Meaning | Source |
|
|
343
|
+
|---|---|---|---|
|
|
344
|
+
| `ann_backend` | `"auto"` | `"exact"`, `"faiss"`, `"hnsw"`, or `"auto"` (exact up to 20,000 chunks) | chosen |
|
|
345
|
+
| `hnsw_m` / `hnsw_ef_construction` / `hnsw_ef_search` | 32 / 200 / 128 | HNSW graph size and search effort | chosen |
|
|
346
|
+
| `dense_top_n` | 1000 | with an approximate backend, dense scores for the top *n* chunks only | chosen |
|
|
347
|
+
|
|
348
|
+
</details>
|
|
349
|
+
|
|
350
|
+
<details>
|
|
351
|
+
<summary><b>Graph (ingestion; change later with <code>rebuild_graph()</code>)</b></summary>
|
|
352
|
+
|
|
353
|
+
| Setting | Default | Meaning | Source |
|
|
354
|
+
|---|---|---|---|
|
|
355
|
+
| `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 |
|
|
356
|
+
| `semantic_k` | 10 | nearest neighbours per chunk for semantic and utility edges | chosen |
|
|
357
|
+
| `edge_threshold` | 0.05 | τ: fused edges below this are dropped | chosen |
|
|
358
|
+
| `graph_k` | 10 | each chunk keeps at most its `graph_k` strongest edges | chosen |
|
|
359
|
+
| `spectral_dims` | 32 | spectral coordinates per chunk | paper |
|
|
360
|
+
| `n_clusters` | `None` | spectral clusters; `None` = about √(n/2) | chosen |
|
|
361
|
+
|
|
362
|
+
</details>
|
|
363
|
+
|
|
364
|
+
<details>
|
|
365
|
+
<summary><b>Retrieval</b></summary>
|
|
366
|
+
|
|
367
|
+
| Setting | Default | Meaning | Source |
|
|
368
|
+
|---|---|---|---|
|
|
369
|
+
| `seed_count` | 20 | seeds from hybrid search | paper |
|
|
370
|
+
| `hops` / `max_candidates` | 2 / 200 | graph expansion depth and cap | paper |
|
|
371
|
+
| `token_budget` | 12000 | tokens of context per question | paper |
|
|
372
|
+
| `rrf_k` | 60 | RRF constant | chosen |
|
|
373
|
+
| `bm25_k1` / `bm25_b` | 1.5 / 0.75 | BM25 parameters | chosen |
|
|
374
|
+
| `bm25_weight` | 0.5 | hybrid relevance = (1 − w)·dense + w·BM25 | chosen |
|
|
375
|
+
| `alpha` | 0.3 | weight of graph proximity in the re-score | chosen |
|
|
376
|
+
| `proximity_mode` | `"hybrid"` | `"hybrid"` (PageRank + spectral), `"ppr"`, `"spectral"`, `"heat"` (Eq. 17), `"none"` | paper |
|
|
377
|
+
| `ppr_beta` / `ppr_steps` | 0.15 / 30 | PageRank restart share and iterations (Eq. 18) | chosen |
|
|
378
|
+
| `diffusion_t` | 1.0 | heat-kernel scale t | chosen |
|
|
379
|
+
|
|
380
|
+
</details>
|
|
381
|
+
|
|
382
|
+
<details>
|
|
383
|
+
<summary><b>Selection</b></summary>
|
|
384
|
+
|
|
385
|
+
| Setting | Default | Meaning | Source |
|
|
386
|
+
|---|---|---|---|
|
|
387
|
+
| `selector` | `"spectrag"` | `"spectrag"`, `"mmr"`, `"protected_mmr"`, `"anchor_expand_ce"`, `"topk"` | paper |
|
|
388
|
+
| `core_size` | 3 | protected core (and anchors) | chosen |
|
|
389
|
+
| `precision_floor` | 0.4 | greedy-tail candidates must score ≥ this × the best score | chosen |
|
|
390
|
+
| `select_weights` | a 1.0, b 0.3, c 0.2, d 0.1, λ 0.5 | weights of Eq. 21 | chosen |
|
|
391
|
+
| `length_power` | 0.5 | ρ of Eq. 22 (how much short chunks are preferred) | chosen |
|
|
392
|
+
| `mmr_lambda` | 0.7 | MMR: relevance vs. novelty | chosen |
|
|
393
|
+
|
|
394
|
+
</details>
|
|
395
|
+
|
|
396
|
+
> [!NOTE]
|
|
397
|
+
> The defaults of this first release follow the paper where it gives values. On MultiHopRAG,
|
|
398
|
+
> settings tuned on validation questions were clearly better (see the benchmark); tuned
|
|
399
|
+
> defaults are planned for a later release.
|
|
400
|
+
|
|
401
|
+
### Tuning
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
from spectrafuse import SpectRAGIngestion, SpectRAGRetrieval
|
|
405
|
+
from spectrafuse.utils.evaluation import load_questions
|
|
406
|
+
from spectrafuse.utils.tuning import tune
|
|
407
|
+
|
|
408
|
+
SpectRAGIngestion("eval_index").ingest("examples/eval/docs") # the documents the questions are about
|
|
409
|
+
search = SpectRAGRetrieval("eval_index")
|
|
410
|
+
questions = load_questions("examples/eval/questions.json") # [{"question", "evidence": [...]}, ...]
|
|
411
|
+
grid = {"alpha": [0.1, 0.3, 0.5], "selector": ["spectrag", "protected_mmr"], "edge_threshold": [0.05, 0.1]}
|
|
412
|
+
results = tune(search, questions, grid, objective="recall", budget=4000, trials=10)
|
|
413
|
+
print(results[0]) # best settings and their scores
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Graph settings (`edge_threshold`, `graph_k`, `edge_weights`, `spectral_dims`, `n_clusters`) are
|
|
417
|
+
applied by `search.rebuild_graph({...})` from the saved signals, so nothing is embedded again.
|
|
418
|
+
|
|
419
|
+
### Your own models
|
|
420
|
+
|
|
421
|
+
| Plug-in | Where | Signature |
|
|
422
|
+
|---|---|---|
|
|
423
|
+
| Embedder | `settings={"embedder": fn}` (pass `embed_fn=fn` again when searching) | `fn(texts) -> vectors` |
|
|
424
|
+
| LLM for answers | `search.answer(question, llm_fn)` | `llm_fn(prompt) -> text` |
|
|
425
|
+
| Entity extractor | `SpectRAGIngestion(..., entity_fn=fn)` | `fn(text) -> list of strings` |
|
|
426
|
+
| Utility questions | `SpectRAGIngestion(..., question_fn=make_question_fn(llm_fn))` | `fn(text) -> list of questions` |
|
|
427
|
+
| Cross-encoder | `SpectRAGRetrieval(..., rerank_fn=fn)` with `selector="anchor_expand_ce"` | `fn(question, texts) -> scores` |
|
|
428
|
+
|
|
429
|
+
### Errors and logging
|
|
430
|
+
|
|
431
|
+
All errors raised on purpose subclass `spectrafuse.SpectRAGError` and the matching built-in:
|
|
432
|
+
`SettingsError` (`ValueError`), `UnknownSettingError` (`KeyError`), `IndexError_`
|
|
433
|
+
(`FileNotFoundError`), `EmbedderError` (`ConnectionError`), `DocumentError` (`ValueError`),
|
|
434
|
+
`DocumentNotFoundError` (`FileNotFoundError`), `MissingPackageError` (`ImportError`) and
|
|
435
|
+
`LLMError` (`ConnectionError`). The CLI prints them as one `spectrafuse: error: ...` line.
|
|
436
|
+
SpectraFuse is silent by default; `spectrafuse.enable_logging()` prints one timed line per pipeline
|
|
437
|
+
step, and `enable_logging("DEBUG")` one line per query.
|
|
438
|
+
|
|
439
|
+
## Benchmark: MultiHopRAG
|
|
440
|
+
|
|
441
|
+
Real runs on [MultiHopRAG](https://arxiv.org/abs/2401.15391): 609 news articles, 7,807 chunks,
|
|
442
|
+
`nomic-embed-text` embeddings, a **12,000-token budget for every method**. 300 questions were
|
|
443
|
+
used only for tuning; all numbers below are on the other **1,955 test questions**.
|
|
444
|
+
|
|
445
|
+
| Method | Recall@5 | Recall@10 | Evidence recall (budget) | All evidence found | MRR | ms / query |
|
|
446
|
+
|---|---|---|---|---|---|---|
|
|
447
|
+
| Standard RAG (dense top-k) | 0.355 | 0.483 | 0.758 | 0.512 | 0.507 | 1.3 |
|
|
448
|
+
| BM25 | 0.462 | 0.577 | 0.788 | 0.545 | 0.617 | 1.0 |
|
|
449
|
+
| Dense + MMR | 0.359 | 0.482 | 0.756 | 0.507 | 0.508 | 1.3 |
|
|
450
|
+
| Hybrid RAG (dense + BM25, RRF) | 0.445 | 0.574 | 0.832 | 0.615 | 0.609 | 2.8 |
|
|
451
|
+
| SpectraFuse, default settings | 0.385 | 0.433 | 0.803 | 0.550 | 0.592 | 4.4 |
|
|
452
|
+
| **SpectraFuse, tuned on validation** | **0.470** | **0.592** | **0.846** | **0.639** | **0.617** | 3.5 |
|
|
453
|
+
|
|
454
|
+
<p align="center"><img src="benchmarks/results/multihoprag/recall_at_k_chunk1200.svg" alt="Recall at 2, 5 and 10 per method" width="49%"> <img src="benchmarks/results/multihoprag/budget_chunk1200.svg" alt="Evidence recall and all evidence found within the budget per method" width="49%"></p>
|
|
455
|
+
|
|
456
|
+
- **Tuned SpectraFuse beats hybrid RAG**: evidence recall +0.014 (95% CI +0.007 … +0.021),
|
|
457
|
+
Recall@10 +0.017 (+0.009 … +0.025). Its largest gain is on **inference** questions
|
|
458
|
+
(0.802 vs 0.757); on comparison questions hybrid stays slightly ahead, as in the paper.
|
|
459
|
+
- Removing graph expansion drops evidence recall to 0.705: the graph walk is where most of the
|
|
460
|
+
extra evidence comes from.
|
|
461
|
+
- **Smaller tests:** HyDE (60 questions): 0.760, against dense 0.807. Answers with `gemma3:4b`
|
|
462
|
+
(30 questions): exact match 0.400 tuned SpectraFuse, 0.333 dense, 0.267 hybrid (one answer = 0.033).
|
|
463
|
+
LightRAG on a 4-article subset could not be compared fairly (the subset fits in the budget), and
|
|
464
|
+
it needed about 225 s of LLM time per 1,000-word article to index.
|
|
465
|
+
|
|
466
|
+
> [!IMPORTANT]
|
|
467
|
+
> **The default settings are weaker than hybrid RAG** (0.803 vs 0.832). The paper's greedy
|
|
468
|
+
> selector with ρ = 0.5 (a value the paper does not give) prefers very short chunks. With
|
|
469
|
+
> `length_power = 0`, Recall@10 rises from 0.433 to 0.580, and the `topk` / `mmr` selectors reach
|
|
470
|
+
> 0.845 with no tuning. Tune on your own validation questions before relying on the defaults.
|
|
471
|
+
|
|
472
|
+
Reproduce (needs Ollama; quick mode takes minutes, `SPECTRAFUSE_BENCH_FULL=1` runs the full version):
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
python benchmarks/multihoprag_benchmark.py # index, tune, evaluate, latency
|
|
476
|
+
python benchmarks/mhr_llm_steps.py hyde # and: answers
|
|
477
|
+
python benchmarks/mhr_report.py # REPORT.md and the charts
|
|
478
|
+
python benchmarks/ann_benchmark.py # exact vs FAISS vs HNSW
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
Full results, per-type tables, ablations and the method: [benchmarks/results/multihoprag/REPORT.md](benchmarks/results/multihoprag/REPORT.md)
|
|
482
|
+
and [docs/PROGRESS.md](docs/PROGRESS.md). The dataset (ODC-BY) is stored in
|
|
483
|
+
[`benchmarks/data/multihoprag/`](benchmarks/data/multihoprag/SOURCE.md) with checksums.
|
|
484
|
+
|
|
485
|
+
<details>
|
|
486
|
+
<summary><b>Scale and memory</b></summary>
|
|
487
|
+
|
|
488
|
+
| Corpus | Ingestion | Search (median) | Index | Memory |
|
|
489
|
+
|---|---|---|---|---|
|
|
490
|
+
| MultiHopRAG, 609 articles → 10,335 chunks (hash embedder) | 7.7 s | 4.1 ms | 49 MB | about 1 GB peak during ingestion |
|
|
491
|
+
| 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 |
|
|
492
|
+
| 100,000 synthetic 768-d vectors, kNN graph | exact 70.9 s, **FAISS 10.4 s** (same edges) | | | |
|
|
493
|
+
|
|
494
|
+
</details>
|
|
495
|
+
|
|
496
|
+
## Project layout
|
|
497
|
+
|
|
498
|
+
<details>
|
|
499
|
+
<summary><b>Show the folders and modules</b></summary>
|
|
500
|
+
|
|
501
|
+
```
|
|
502
|
+
src/spectrafuse/
|
|
503
|
+
ingestion.py, retrieval.py the two main classes
|
|
504
|
+
config.py, validation.py settings and their checks
|
|
505
|
+
errors.py, log.py, cli.py exceptions, logging, the spectrafuse command
|
|
506
|
+
utils/ one small module per step: loader, tokens, sections, units,
|
|
507
|
+
chunker, embedder, entities, references, questions, edges*,
|
|
508
|
+
graph, graph_build, spectral, clusters, vector_index, bm25,
|
|
509
|
+
seeding, expansion, diffusion, selection, selectors, prompt,
|
|
510
|
+
llm, metrics, evaluation, tuning, graph_view, store
|
|
511
|
+
quickstart.py runs with no arguments
|
|
512
|
+
examples/ sample documents, a 14-document evaluation set, demo scripts
|
|
513
|
+
benchmarks/ MultiHopRAG and ANN benchmarks, their data and results
|
|
514
|
+
tests/ unit, integration and equivalence tests
|
|
515
|
+
docs/ the paper, DESIGN.md, PROGRESS.md, PRODUCTION_READINESS.md,
|
|
516
|
+
DEVELOPMENT_LOG.md
|
|
517
|
+
tools/ validate_release.py (release checks on a real index)
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
</details>
|
|
521
|
+
|
|
522
|
+
Every module is short (under about 150 lines), and functions do one thing.
|
|
523
|
+
|
|
524
|
+
## Testing
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
pytest -q # 300+ tests, under 10 seconds, offline
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
The tests cover every formula against hand-computed or closed-form values (BM25, RRF,
|
|
531
|
+
PageRank, the heat kernel against `scipy.linalg.expm`, the sparse eigensolver against the dense
|
|
532
|
+
one, the paper's metrics). The fast vectorised code is checked against simple loop versions on
|
|
533
|
+
random cases, and the full pipeline runs end to end on real documents. Live Ollama tests run
|
|
534
|
+
when Ollama is available (`SPECTRAFUSE_LIVE_LLM=1` for the LLM test). CI runs on every pull request
|
|
535
|
+
and on `main`: Linux × Python 3.10 / 3.12 / 3.14 × minimal / all extras, one macOS job (Python
|
|
536
|
+
3.12, all extras), and a build that installs the wheel and runs the CLI. A release tag runs two
|
|
537
|
+
quick Linux test jobs, because the merged commit has already passed the full set.
|
|
538
|
+
|
|
539
|
+
Before a release, `python tools/validate_release.py` re-checks the mathematics on the real
|
|
540
|
+
MultiHopRAG index (eigenpairs, PageRank, BM25, every selector and proximity mode) and that the
|
|
541
|
+
benchmark reproduces. The results are in the
|
|
542
|
+
[production-readiness report](docs/PRODUCTION_READINESS.md).
|
|
543
|
+
|
|
544
|
+
## Development status
|
|
545
|
+
|
|
546
|
+
The hardening work in [docs/PROGRESS.md](docs/PROGRESS.md) (Phase 7) runs in small, reviewed
|
|
547
|
+
steps. Green = done, yellow = in review, grey = next.
|
|
548
|
+
|
|
549
|
+
```mermaid
|
|
550
|
+
flowchart LR
|
|
551
|
+
errors["0.1 errors.py: every error is a SpectRAGError"] --> embedder["0.2 embedder marker and fingerprint"]
|
|
552
|
+
embedder --> replace["0.3 safe index replace"] --> escape["0.4 graph view escaping"]
|
|
553
|
+
classDef done fill:#d4edda,stroke:#2e7d32,color:#1b5e20
|
|
554
|
+
classDef doing fill:#fff3cd,stroke:#f9a825,color:#6d4c00
|
|
555
|
+
classDef todo fill:#eceff1,stroke:#90a4ae,color:#455a64
|
|
556
|
+
class errors,embedder done
|
|
557
|
+
class replace doing
|
|
558
|
+
class escape todo
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
## Limitations
|
|
562
|
+
|
|
563
|
+
- Two ingests into the same index folder at the same time are not safe (both write
|
|
564
|
+
`<folder>.partial`). Searching while an index is replaced is safe.
|
|
565
|
+
- The default settings are not tuned (see the benchmark). Tuning on your own validation
|
|
566
|
+
questions is recommended.
|
|
567
|
+
- Entity extraction is rule-based unless you pass an `entity_fn`; utility questions need an LLM.
|
|
568
|
+
- Semantic edges and the dense eigensolver are `O(n²)` without the `scale` extra.
|
|
569
|
+
- The benchmark is a quick-mode run on one dataset; LightRAG was only compared on a tiny subset.
|
|
570
|
+
- On macOS with numpy 2.2 (Python 3.10), matrix products print spurious `RuntimeWarning`s; the
|
|
571
|
+
results are correct.
|
|
572
|
+
|
|
573
|
+
More: [docs/DESIGN.md](docs/DESIGN.md) (every decision), [docs/PROGRESS.md](docs/PROGRESS.md)
|
|
574
|
+
(the upgrade, with measurements), [CHANGELOG.md](CHANGELOG.md).
|
|
575
|
+
|
|
576
|
+
## Citation
|
|
577
|
+
|
|
578
|
+
SpectRAG is the method of Sahinoglu, Mohammed, Kazangirler and Kazangirler. If you use it, please
|
|
579
|
+
cite their paper. GitHub's "Cite this repository" button gives the same citation, from
|
|
580
|
+
[CITATION.cff](CITATION.cff).
|
|
581
|
+
|
|
582
|
+
```bibtex
|
|
583
|
+
@article{sahinoglu2026spectrag,
|
|
584
|
+
title = {SpectRAG: Weighted Chunk Graphs for Token-Budgeted Context Selection in
|
|
585
|
+
Multi-Hop Retrieval-Augmented Generation},
|
|
586
|
+
author = {Sahinoglu, Zafer and Mohammed, Abdulazeez and Kazangirler, Buse Yaren and
|
|
587
|
+
Kazangirler, Cahit Berkay},
|
|
588
|
+
journal = {arXiv preprint arXiv:2610.36991},
|
|
589
|
+
year = {2026}
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
## Credits and license
|
|
594
|
+
|
|
595
|
+
- **Method and paper:** Zafer Sahinoglu, Abdulazeez Mohammed, Buse Yaren Kazangirler and Cahit
|
|
596
|
+
Berkay Kazangirler, [arXiv:2610.36991](https://arxiv.org/abs/2610.36991). The equations,
|
|
597
|
+
tables and settings this code refers to (Eq. 10, Table 9, ...) are theirs.
|
|
598
|
+
- **SpectRAG, the official implementation and releases:**
|
|
599
|
+
[SightAndCite/SpectRAG](https://github.com/SightAndCite/SpectRAG), by the paper's authors, MIT
|
|
600
|
+
license.
|
|
601
|
+
- **SpectraFuse, this package:** an independent implementation inspired by SpectRAG, by Kumar
|
|
602
|
+
Nishant, written from the paper. It is not SpectRAG and contains no code from the official
|
|
603
|
+
implementation; see [NOTICE](NOTICE).
|
|
604
|
+
|
|
605
|
+
The code in this repository is MIT, see [LICENSE](LICENSE). The MultiHopRAG data in `benchmarks/data/` is ODC-BY
|
|
606
|
+
(see its [SOURCE.md](benchmarks/data/multihoprag/SOURCE.md)).
|