Core implementation of GRAG: a document-aware retrieval pipeline built for heterogeneous RAG corpora.
ManuIndex is designed for the "document zoo" problem: policies, reports, minutes, contracts, research notes, schedules, and other formats often behave poorly when everything is dumped into one flat vector index.
Instead of retrieving chunks from one mixed search space, ManuIndex routes the query to the most relevant documents first, then runs hybrid retrieval inside those selected documents only.
- Document routing first: each document gets a compact LLM summary used as a routing vector.
- Local retrieval second: dense FAISS and sparse BM25 retrieval run inside selected documents.
- Better context locality: neighbor chunk expansion preserves nearby evidence.
- Cleaner final ranking: ONNX reranking filters noisy candidates before generation.
- Practical deployment: embeddings and reranking can run locally with ONNX Runtime.
flowchart TD
A[Document] --> B[Summary generation]
A --> C[Deterministic chunking]
B --> D[Summary embedding]
C --> E[Per-document FAISS index]
C --> F[Per-document BM25 index]
Q[Query] --> R[Query embedding]
R --> D
D --> G[Select top documents]
G --> E
G --> F
E --> H[Hybrid retrieval]
F --> H
H --> I[Neighbor expansion]
I --> J[ONNX reranking]
J --> K[Final contexts]
The benchmark evaluates 7 retrieval pipelines on a heterogeneous corpus of 25 documents and 125 questions with a fixed top_k=3.
| Method Group | Avg F1 | Avg Context Recall | Avg End-to-End Time |
|---|---|---|---|
| GRAG | 0.6631 | 0.7186 | 0.583s |
| Best non-GRAG flat / hierarchical baselines | 0.6237 | 0.6358 | slower |
| Graph RAG | 0.9565 | 0.9473 | 3.681s |
Interpretation:
- GRAG is the best efficiency-oriented system in this repo's benchmark.
- Graph RAG is the quality leader overall, but with materially higher latency and token cost.
- GRAG improves retrieval quality over simpler flat baselines without turning into the most expensive pipeline.
ManuIndex requires Python 3.11+ and uses uv.
uv syncCore dependencies include FAISS, LangChain community utilities, ONNX Runtime via Optimum, Transformers, Rank-BM25, and PDF-to-Markdown tooling.
Download the default embedding model:
python helpers/model_download.pyIf you also want the reranker weights, call download_onnx_models("reranker", "onnx_models") from the helper module.
Set these variables before running the examples:
OPENAI_API_KEY=...
OPENAI_MODEL_NAME=...
OPENAI_BASE_URL=... # optional for OpenAI-compatible endpointsIf you use the PDF image-analysis helper, also set:
GROQ_API_KEY=...import os
from openai import OpenAI
from manu_index import ManuIndex, ONNXEmbedder, ONNXReranker
client = OpenAI(
api_key=os.environ["OPENAI_API_KEY"],
base_url=os.environ.get("OPENAI_BASE_URL"),
)
embeddings = ONNXEmbedder(
model="onnx_models/bge_m3/onnx/model_q4.onnx",
tokenizer="onnx_models/bge_m3",
max_length=1024,
device="cpu",
)
reranker = ONNXReranker(
model="onnx_models/bge_reranker_v2_m3/onnx/model_q4.onnx",
tokenizer="onnx_models/bge_reranker_v2_m3",
max_length=1024,
device="cpu",
reranker_type="auto",
)
index = ManuIndex(
client=client,
model_name=os.environ["OPENAI_MODEL_NAME"],
embeddings=embeddings,
persist_directory="manu_index_db",
)
index.add_document("sample.md")
results = index.search(
query="What role is being hired for?",
reranker=reranker,
top_k=3,
top_c=5,
alpha=0.5,
lambda_mult=0.8,
)
for text in results:
print(text)Main methods:
index.add_document(documents, chunk_size=500)
index.search(query, top_k=3, top_c=5, lambda_mult=0.8, alpha=0.5, reranker=reranker)
index.info()
index.delete(doc_id)
index.clear()Search behavior:
- Embed the query.
- Route it to the top document summaries.
- Retrieve candidates with dense + sparse search.
- Expand neighbor chunks.
- Rerank the final candidate pool.
LangChain-compatible embedding wrapper with:
- CPU and CUDA execution
- batched inference
- mean pooling
- optional normalization
embed_documents()andembed_query()
ONNX reranker supporting:
- BGE classifier rerankers
- BGE decoder rerankers
- Qwen decoder rerankers
- automatic reranker type inference
- CPU and CUDA execution
PDFs can be converted to Markdown before indexing, including optional image analysis for charts, figures, or visually rich pages.
import pymupdf
import pymupdf4llm
from pymupdf4llm.helpers.image_analyzer import GroqImageAnalyzer
analyzer = GroqImageAnalyzer(
api_key="...",
model_name="meta-llama/llama-4-scout-17b-16e-instruct",
)
with pymupdf.open("report.pdf") as document:
markdown = pymupdf4llm.to_markdown(document, analyze_image=analyzer)
index.add_document(markdown)manu_index: core retrieval, embedding, reranking, and summary-routing logicbenchmark: evaluation suite, saved reports, and comparison plotshelpers: model download and PDF parsing utilitiestests: usage examples for indexing, search, reranking, and summarization
- Document summaries are generated with an LLM and stored as routing metadata.
- Each indexed document gets its own FAISS and BM25 stores rather than joining all chunks into one global index.
MATHS.mdcontains the underlying retrieval formulations and scoring notes.
MIT. See LICENSE.
