Skip to main content

NexusRAG

Local RAG for research papers — ask questions across your PDFs, get answers with citations you can check. Every component is measured on public benchmarks. CI fails the build if quality drops.

CI License: MIT Python Tests Coverage Typed

Basics · Methodology · Results · Architecture · Quick start · Tech stack · Paper

NexusRAG web UI: upload a paper, ask a question, get a cited answer with sources and confidence

🔒 Fully local 🧪 Measured 🔁 Reproducible 🚦 Gated 🧾 Cited
Papers never leave your machine Every component ablated on 2 BEIR datasets make reproduce regenerates every number (seed 0) CI fails if any metric drops Every claim links to a checkable source

The basics, in four pictures

1 · RAG — don't ask the model what it remembers; retrieve first, answer from the retrieved text only:

flowchart LR
    Q(["❓ Question"]) --> R["🔎 Retrieve the few<br/>relevant passages"]
    K[("📚 Your papers")] --> R
    R --> G["🤖 Local LLM answers from<br/>those passages only"]
    G --> A(["✅ Answer with citations<br/>you can check"])

2 · Hybrid retrieval — two complementary signals, merged by rank position (no score calibration):

flowchart LR
    Q(["Question"]) --> D["🧠 Dense — BGE-small<br/>matches meaning"]
    Q --> S["🔤 Sparse — BM25<br/>matches exact words"]
    D --> F["⚖️ Reciprocal rank fusion<br/>k = 60"]
    S --> F
    F --> T(["Top-k passages"])

3 · Corrective retrieval — pay for a second pass only when the first one looks weak:

flowchart LR
    P1["First pass"] --> C{"top dense cosine<br/>≥ τ = 0.55?"}
    C -- yes --> OUT(["use results"])
    C -- no --> X["expand query with top<br/>terms from pass 1"] --> P2["retrieve again"] --> FU["fuse both passes"] --> OUT

4 · Verification — a citation that exists is not a citation that is true:

flowchart LR
    DA["Draft answer with<br/>citation markers"] --> V1{"marker points to<br/>a real source?"}
    V1 -- no --> ST["strip it + warn"] --> V2
    V1 -- yes --> V2{"optional NLI: does the<br/>source entail the sentence?"}
    V2 --> OUT(["verified answer<br/>+ faithfulness score"])
Term Meaning here
RAG Retrieve relevant passages first, then generate the answer from them
Hybrid retrieval Dense embeddings (meaning) + BM25 (exact words)
RRF Reciprocal rank fusion — merges the two rankings by rank position
Corrective PRF Low confidence → expand the query with top terms, retrieve again, fuse
NLI grounding An entailment model checks each answer sentence against its cited source
nDCG@10 Quality of the top-10 ranking — 1.0 is perfect, higher is better

Methodology

  • Strictly additive ablation — each component is measured against the previous stack:
flowchart LR
    A["BM25"] --> B["Dense"] --> C["Hybrid<br/>(RRF)"] --> D["+ Adaptive<br/>weights"] --> E["+ Corrective<br/>PRF"]
  • Two BEIR datasets — SciFact (300 claims / 5,183 abstracts) · NFCorpus (323 queries / 3,633 docs)
  • Deterministic — exact search, CPU-only, seed 0; models and datasets pinned to git revisions
  • Statistics, not vibes — bootstrap 95% CIs · paired randomization tests · Holm correction
  • Faithfulness as evidence detection — NLI vs lexical vs cross-encoder scorers, ROC-AUC / PR-AUC
  • Regression-gated — CI reruns a vendored sample; the build fails below committed floors
  • One-command reproduction — make reproduce regenerates every number in this file

Results

System SciFact nDCG@10 NFCorpus nDCG@10
Dense — MiniLM (the common default) 0.648 0.319
BM25 0.666 0.312
Dense — BGE-small 0.708 0.342
Hybrid (RRF) 0.704 0.352
+ Corrective PRF 0.703 0.346

Ablation bar charts with 95% bootstrap CIs: BM25, Dense, Hybrid RRF, Adaptive weights, and Corrective PRF on SciFact and NFCorpus

Three findings:

  • Embedder is the biggest lever — MiniLM → BGE-small: +0.060 nDCG@10 (SciFact, p < 0.001)
  • Hybrid fusion beats BM25 on both datasets — +0.037 (SciFact, p = 0.002) · +0.040 (NFCorpus, p < 0.001); both 95% CIs exclude zero
  • The reranker hurts here — 0.702 vs 0.734 nDCG@10 (120-query subset) at 67× the latency; reported, kept off by default
xychart-beta
    title "Retrieval cost in ms/query (120-query subset, CPU)"
    x-axis ["Adaptive hybrid", "+ Corrective PRF", "Cross-encoder rerank"]
    y-axis "ms / query" 0 --> 1400
    bar [20, 30, 1359]

Faithfulness as evidence detection (SciFact-claims dev: 188 claims, 2,031 candidate sentences, 18% positive base rate) — a plain relevance cross-encoder beats the dedicated NLI model:

Scorer ROC-AUC [95% CI] PR-AUC F1
Lexical overlap 0.686 [0.65, 0.72] 0.371 0.112
NLI (DeBERTa) 0.688 [0.65, 0.73] 0.331 0.368
Cross-encoder 0.755 [0.72, 0.79] 0.476 0.469
xychart-beta
    title "Evidence detection, ROC-AUC (higher is better)"
    x-axis ["Lexical overlap", "NLI (DeBERTa)", "Cross-encoder"]
    y-axis "ROC-AUC" 0.6 --> 0.8
    bar [0.686, 0.688, 0.755]

Full tables with CIs and p-values: paper/main.pdf · raw per-query scores: benchmarks/results/

Architecture

flowchart LR
    subgraph Ingest
        D["PDF / DOCX / MD / TXT"] --> P[Parse] --> C["Section-aware chunks"] --> E["BGE-small embeddings"]
    end
    subgraph Index
        V[("LanceDB<br/>exact cosine")]
        B[("BM25<br/>in-memory")]
    end
    subgraph Answer
        Q(["Question"]) --> H["RRF fusion, k=60"]
        H --> G{"top dense<br/>cosine ≥ 0.55?"}
        G -- yes --> L["llama3.2:3b writes from<br/>retrieved sources only"]
        G -- no --> F["PRF: expand query,<br/>re-retrieve, fuse"] --> L
        L --> CV["Citation check:<br/>strip invalid refs"] --> N["Optional NLI<br/>grounding"] --> A(["Cited answer<br/>+ confidence"])
    end
    E --> V
    C --> B
    V --> H
    B --> H
Stage What happens Code
Ingest Parse PDF/DOCX/MD/TXT → section-aware chunks (1,200 chars, 300 overlap) → embed ingestion/
Index Vectors in LanceDB (exact cosine) + in-memory BM25, kept in lock-step storage/
Retrieve Reciprocal rank fusion (k = 60); adaptive dense/sparse weights by query shape retrieval/hybrid.py
Correct If top dense cosine < τ = 0.55: one PRF pass expands the query, re-retrieves, fuses retrieval/corrective.py
Generate Local LLM answers from retrieved passages only, with inline citations generation/
Verify Out-of-range citations stripped; optional per-sentence NLI entailment check generation/verifier.py

Quality gate in CI

Every push reruns a deterministic vendored sample (50 queries, 651 abstracts, 60 claims — CPU, seed 0) via scinexusrag.eval.gate; the build fails below any floor in benchmarks/thresholds.json:

Metric Sample value Floor
nDCG@10 — Hybrid (RRF) 0.9096 0.8996
nDCG@10 — + Corrective PRF 0.8991 0.8891
Recall@10 (both systems) 0.980 0.970
Faithfulness ROC-AUC — NLI 0.752 0.737
Faithfulness ROC-AUC — cross-encoder 0.774 0.759

Same CI: 318 tests on Python 3.11 & 3.12 · 60% branch-coverage floor · ruff · strict mypy · gitleaks · pip-audit.

Reproduce the benchmark

Command What it does
make reproduce Regenerates every number above from scratch — deterministic, seed 0
make eval SciFact + NFCorpus retrieval ablation (downloads BGE-small once)
make faithfulness Evidence-detection eval (NLI + cross-encoder)
make eval-sample Vendored offline subset — no downloads, minutes on a laptop
make eval-gate The exact regression gate CI runs
make paper Rebuilds tables, figures, and the PDF (needs tectonic)

Tech stack

Layer Tools
Language Python 3.11+ · mypy --strict · ruff
Retrieval sentence-transformers (BGE-small, revision-pinned) · rank-bm25 · RRF · LanceDB (exact cosine)
Generation Ollama (llama3.2:3b) · httpx with retry/backoff
Verification Citation validator · DeBERTa NLI cross-encoder (opt-in grounding)
Serving FastAPI · Uvicorn · slowapi rate limits · static JS web UI
Evaluation BEIR SciFact + NFCorpus (revision-pinned) · NumPy/SciPy · bootstrap CIs · paired randomization + Holm
Quality & supply chain pytest · GitHub Actions · gitleaks · pip-audit · non-root Docker

Models & footprint

Everything is off-the-shelf and revision-pinned — nothing trained or redistributed here (PROVENANCE.md).

Model Role Size License
BAAI/bge-small-en-v1.5 Embeddings (default) ~130 MB MIT
cross-encoder/ms-marco-MiniLM-L-6-v2 Reranker — evaluated, off by default ~90 MB Apache-2.0
cross-encoder/nli-deberta-v3-small NLI grounding — opt-in ~280 MB Apache-2.0
llama3.2:3b (Ollama) Answer generation ~2 GB Llama 3.2 Community

Runs on a laptop: ~8 GB RAM for the full stack · full ablation 15–25 min per dataset on CPU.

Quick start

pip install -e ".[eval]" && make run   # web UI + API → http://localhost:8000 (needs local Ollama)
docker compose up                      # or: containers, with a pinned Ollama service

Demo — ingest a paper, ask a question:

curl -F "file=@paper.pdf" http://localhost:8000/api/ingest
curl -X POST http://localhost:8000/api/query -H "Content-Type: application/json" \
     -d '{"question": "What is the main contribution of this paper?"}'
{
  "answer": "The paper introduces ... [1]. Experiments show ... [2]",
  "confidence": 0.82,
  "sources": [
    {"index": 1, "filename": "paper.pdf", "section_title": "Abstract", "page": 1, "score": 0.78}
  ],
  "processing_time_ms": 2140.5,
  "warnings": []
}
from scinexusrag import NexusRAG

rag = NexusRAG()
rag.ingest("paper.pdf")
result = rag.query("What did the paper find?")   # .answer, .sources, .confidence

More: notebooks/01_quickstart.ipynb · examples/

Configuration — env vars only, no config-file ambiguity (.env.example)
Variable Default Purpose
LLM_MODEL llama3.2:3b Ollama model (drives both compose pull and app)
EMBEDDING_MODEL BAAI/bge-small-en-v1.5 Dense embedder (HF revision pinned)
INGESTION_CHUNK_SIZE / _OVERLAP 1200 / 300 Chunking in characters
RETRIEVAL_TOP_K 8 Passages handed to the generator
SELF_CORRECTION_CONFIDENCE_TAU 0.55 PRF trigger: re-retrieve below this dense cosine
SELF_CORRECTION_GROUNDING_ENABLED false Per-sentence NLI faithfulness check
API — FastAPI, rate-limited, upload-validated
Method Route Purpose
GET / Web UI
GET /health · /api/health Liveness · Ollama/model/corpus status
POST /api/ingest Upload a PDF/DOCX/MD/TXT
POST /api/query {"question": "..."} → cited answer
GET /api/documents · /api/status · /api/metrics Corpus list · stats · request metrics
DELETE /api/documents/{id} · /api/documents Delete one · clear all
Repository layout
src/scinexusrag/
├── ingestion/     parser, section-aware chunker, embedder
├── retrieval/     dense, BM25, RRF hybrid, corrective PRF, reranker, SPLADE
├── generation/    Ollama client, synthesizer, citation verifier, NLI grounding
├── storage/       LanceDB vector store, document store
├── api/           FastAPI routes, security, metrics
├── eval/          datasets, metrics, systems, CI gate, reproduce
└── pipeline.py    wires it all together
benchmarks/        vendored samples, committed results, CI floors
paper/             the study (LaTeX + PDF + figures)
frontend/          static web UI

Limitations

  • Two abstract-level BEIR datasets; SciFact caps at 300 queries
  • Exact dense search (no ANN) — query cost grows linearly with corpus size
  • BM25 index is in-memory; rebuilt on cold start
  • Corrective PRF ≈ neutral on these corpora — kept: cheap, rarely fires, never regresses
  • Full component-level limits: docs/ARCHITECTURE.md

Roadmap

  • Datasets — FiQA, SciDocs · full-paper chunking ablations
  • Models — SPECTER2 / SciNCL encoders · SPLADE, ColBERTv2, monoT5 baselines
  • Scale & scoring — persistent BM25 + ANN index · RAGAs / LLM-as-judge answer scoring

Project docs

Doc Purpose
CONTRIBUTING.md Local setup, checks, reproducing the benchmark
docs/ARCHITECTURE.md Component design, trade-offs, known limits
SECURITY.md Private vulnerability reporting
CHANGELOG.md Release history
CODE_OF_CONDUCT.md Community standards

Citation

If you use NexusRAG, please cite it (CITATION.cff):

@software{bose_nexusrag_2026,
  author  = {Bose, Urme},
  title   = {NexusRAG: Local Hybrid Retrieval and Faithfulness
             Evaluation for Scientific Papers},
  version = {1.0.2},
  year    = {2026},
  url     = {https://github.com/urme-b/NexusRAG},
  license = {MIT}
}

License

MIT. Downloaded models keep their own licenses; the default generator llama3.2:3b is under the Llama 3.2 Community License (not OSI-approved, carries an acceptable-use policy).

Metadata

Release files for scinexusrag 1.0.2

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

Source distribution (sdist)

Source distribution for scinexusrag 1.0.2
File Size Uploaded Provenance Actions
scinexusrag-1.0.2.tar.gz 76.0 kB With provenance Details for scinexusrag-1.0.2.tar.gz

Built distribution (wheel)

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

Total release size: 162.9 kB

Release files / scinexusrag-1.0.2.tar.gz

Download URL scinexusrag-1.0.2.tar.gz
Size 76.0 kB
Tags Source
SHA-256 checksum
How to use checksums
81ac929b99625beaad260bc9b6ec1094bd2252fe88691cccc87ac3c0038fc5d7
BLAKE2b-256 checksum
How to use checksums
228a8b0d61d0a1d2c611fdd7ef42ae8a17d4214d878c89db374814e606a8a4dc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

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 Jul 12, 2026.

Transparency log

Release files / scinexusrag-1.0.2-py3-none-any.whl

Download URL scinexusrag-1.0.2-py3-none-any.whl
Size 86.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
76055fb6df425f71c0aedcdf8f5c61f05663f7d54551fcf181ebb9b6208ab4bb
BLAKE2b-256 checksum
How to use checksums
e80f123d84a3be34ab1a0ca3cc1135bb679f249bbd2bf21fc17da84bd67b83cb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

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 Jul 12, 2026.

Transparency log

Security for scinexusrag 1.0.2

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 scinexusrag-1.0.2.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 Jul 12, 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.

Release history Release notifications | RSS feed

This release

1.0.2 This release

2 release files Full provenance

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