Skip to main content

A provider-agnostic toolkit for benchmarking retrieval strategies and running RAGAS end-to-end evaluation against your own PDF corpus.

Project description


title: raglens emoji: ๐Ÿ” colorFrom: blue colorTo: green sdk: streamlit sdk_version: "1.58.0" app_file: dashboard/app.py pinned: false

Pipeline Workflow Animation

raglens

A ground-up, provider-agnostic toolkit for building, benchmarking, and rigorously evaluating Retrieval-Augmented Generation systems โ€” from PDF ingestion through RAGAS evaluation. Point it at your own PDF corpus via the CLI, or explore the reference run in experiments/pipeline_validation.ipynb.

Current Stage: Retrieval Benchmarking Complete ย |ย  In Progress: RAGAS End-to-End Evaluation (2/5 retrievers scored)


What This Project Is

This is not a chatbot project. The goal is to rigorously measure every component of a RAG pipeline โ€” from how documents are chunked to whether the final generated answer is actually correct.

The approach is deliberately ground-up: build the corpus, build the retrievers, generate the ground truth dataset by hand, benchmark exhaustively, then apply RAGAS. This gives full traceability from source chunk โ†’ Q&A pair โ†’ retrieval result โ†’ end-to-end answer quality โ€” something off-the-shelf frameworks obscure.

The current V2 pipeline is built entirely in raglens/ โ€” a pip-installable, provider-agnostic package (Ollama / OpenAI / Groq) with its own CLI and Streamlit dashboard, not just notebook cells. A previous iteration (V1, using Wikipedia + LangChain, code kept in src/ for historical reference) revealed fundamental problems that made a rebuild necessary. That transition is explained in Why We Rebuilt.


Quickstart โ€” Run This On Your Own Corpus

pip install -e .
cp .env.example .env   # fill in whichever provider(s) you'll use

# 1. Parse + structure-aware chunk your PDFs
raglens ingest --docs ./my_pdfs

# 2. Embed + index (local & free via Ollama, or --embedding-provider openai)
raglens index --docs ./my_pdfs --embedding-provider ollama

# 3. Generate Q&A ground truth (resumable โ€” safe to Ctrl-C and re-run)
raglens questions --docs ./my_pdfs --llm-provider openai

# 4. Benchmark retrieval quality (Hit@K, MRR, NDCG across 5 strategies)
raglens benchmark --docs ./my_pdfs --embedding-provider ollama

# 5. RAGAS end-to-end evaluation โ€” judge-LLM-bound, checkpointed every batch
raglens evaluate --docs ./my_pdfs --embedding-provider ollama --judge-provider openai

# 6. Visualize everything
streamlit run dashboard/app.py

Every long-running stage (questions, evaluate) is resumable: it's keyed by (chunk_id, retriever) and skips whatever's already cached, so it's safe to run for a few hours a night and pick back up exactly where it left off. Supported providers today: Ollama (local, free) and OpenAI for embeddings; Ollama, OpenAI, and Groq for generation/judging.

Want a public, zero-install link instead of running it locally? See docs/deploy-hf-space.md โ€” the dashboard falls back to bundled reference results automatically, no API keys or local corpus needed for a hosted demo.


Project Roadmap

Stage 1: PDF Corpus & Document Ingestion             โœ…  Complete
Stage 2: Section-Aware Hierarchical Chunking         โœ…  Complete
Stage 3: Embedding Generation & Vector Store         โœ…  Complete
Stage 4: Five Retrieval Strategies                   โœ…  Complete
Stage 5: LLM-Generated Benchmark Dataset (627 Q&As)  โœ…  Complete
Stage 6: Retrieval Benchmarking                      โœ…  Complete
Stage 7: RAGAS End-to-End Evaluation                 ๐Ÿ”„  In progress (2/5 retrievers scored)  โ† current

Why We Rebuilt (V1 โ†’ V2)

Three root causes forced the rebuild from scratch.

1. Naive chunking broke semantic units. RecursiveCharacterTextSplitter with a fixed size and overlap rolled a window across the raw document text, cutting across section boundaries, splitting tables mid-row, and bisecting LaTeX formulas. Chunks had no awareness of document structure โ€” a paragraph could end in one chunk and its concluding sentence begin in the next.

2. Wikipedia was the wrong corpus. Wikipedia articles are noisy: citation markers, redirect stubs, heavily cross-linked prose, and mathematical notation that does not survive plain-text extraction. The corpus introduced irrelevant matches that polluted retrieval results and made evaluation misleading.

3. The evaluation methodology measured the wrong thing. Matching a retrieved chunk ID against one "expected" chunk from hundreds of candidates โ€” when a question about transformers could reasonably be answered by many different chunks โ€” produced near-zero recall that did not reflect actual retrieval quality. The metric was too strict in the wrong way.

V2 addressed all three: curated PDF corpus + Docling-based structure-aware parsing + section-aware chunking + ground truth generated from the exact source chunk so each question has one verifiable, traceable correct answer.


Pipeline Stages

Eight stages transform raw PDFs into benchmark-ready retrieval results.

# Stage Tool(s) Input Output
1 PDF Ingestion Docling Raw PDFs (13 docs) Markdown with LaTeX formulae intact
2 Section Parsing + Hierarchy MarkdownSectionParser โ†’ LevelInference โ†’ HierarchyBuilder Markdown string Document with nested Section tree
3 Normalization SectionFlattener Nested Section tree List[FlattenedSection] with pre-computed breadcrumb paths
4 Section-Aware Chunking SectionChunker + StructurePreserver FlattenedSections List[Chunk] โ€” three types: parent_section, table_fragment, section_fragment
5 Embedding EmbeddingGenerator (Ollama nomic-embed-text or OpenAI) Chunks List[ChunkEmbedding]
6 Vector Store ChromaStore (ChromaDB) ChunkEmbeddings Persisted similarity index
7 Synthetic Q&A Generation Configurable LLM (Ollama / OpenAI / Groq) Chunks 627 Q&A pairs in JSONL cache
8 Retrieval + Evaluation 5 Retrievers + 3 Evaluators QuestionSamples Hit@K, MRR, NDCG, Section Hit, Latency

The data transformation story: A raw PDF becomes a Document (root container with metadata). The document's heading structure is parsed into a nested Section tree. That tree is flattened into List[FlattenedSection] โ€” each section pre-annotated with its full breadcrumb path (e.g. "lbdl > Chapter 3 > Optimization"). The chunker then transforms each section into one or more Chunk objects โ€” the atomic unit for embedding and retrieval. Finally, each chunk is embedded into a vector and stored in ChromaDB.


Strategies at Each Stage

Chunking Strategies

The chunker applies a three-tier decision based on section size:

Section content length?
โ”œโ”€โ”€ โ‰ค 2,500 chars
โ”‚   โ””โ”€โ”€ Single parent_section chunk  (fragment_index = -1, meaning "atomic, not split")
โ”‚
โ””โ”€โ”€ > 2,500 chars
    โ”œโ”€โ”€ Always create: parent_section chunk  (full section text โ€” used by hierarchical retrieval)
    โ”‚
    โ””โ”€โ”€ Also create fragment children:
        โ”‚
        โ”œโ”€โ”€ Tables  (StructurePreserver)
        โ”‚   โ”œโ”€โ”€ Table โ‰ค 3,000 chars โ†’ single atomic table_fragment chunk
        โ”‚   โ””โ”€โ”€ Table > 3,000 chars โ†’ row-by-row table_fragment chunks
        โ”‚         โ””โ”€โ”€ Header Duplication: every fragment = header_row + separator_row + data_rows
        โ”‚             (a row of "| 0.75 | 0.83 |" is meaningless without its column names)
        โ”‚
        โ””โ”€โ”€ Remaining text โ†’ RecursiveCharacterTextSplitter (size=1,200, overlap=200)
              โ””โ”€โ”€ section_fragment chunks, fragment_index = 0, 1, 2 ...
                    โ””โ”€โ”€ Sequential fragment_index enables neighbor retrieval later

Table-aware extraction โ€” Markdown tables are detected by the presence of | pipe characters. Small tables are preserved as a single atomic chunk (schema intact). Large tables are split at row boundaries, never mid-row, into ~1,500-character fragments.

Header duplication โ€” When a large table is fragmented, every fragment gets the original header row and separator row prepended: header + "\n" + separator + "\n" + data_rows. This makes each fragment self-interpreting when retrieved in isolation.

Formula protection โ€” LaTeX blocks ($$...$$) are replaced with __FORMULA_N__ placeholders before text splitting and restored afterward. This prevents a formula from being cut mid-expression by the character-based splitter.

Why not a sliding window? โ€” Fixed-size windows roll across the full document, crossing section boundaries. Section-aware chunking treats each section as the atomic semantic unit โ€” small sections are never fragmented, and fragments never contain content from two different sections.


Retrieval Strategies

Five strategies are implemented, each building on the previous:

Strategy Approach Score Key Design
BM25 Lexical (Okapi BM25) Raw, 0โ€“โˆž In-memory; no vector store dependency
Dense Semantic (nomic-embed-text) Cosine similarity, 0โ€“1 Retrieves kร—3 before filtering bad sections
Hybrid Dense + BM25 fusion Fused, 0โ€“1 Min-max normalization per retriever, then 50/50 weighted sum
Hierarchical Hybrid โ†’ parent section context Fused, 0โ€“1 Pre-built (doc_id, section_id) โ†’ Chunk dict for O(1) parent lookup
Hier+Neighbor Hierarchical + fragment window โ€” Expands to ยฑ1 fragment siblings using fragment_index (section-local, never crosses boundaries)

Score normalization in Hybrid โ€” BM25 scores are unbounded [0, โˆž); Dense scores are cosine similarity [0, 1]. Without normalization, BM25 magnitudes would dominate the fusion. Min-Max normalization applied independently per retriever: (score โˆ’ min) / (max โˆ’ min), then fused = 0.5 ร— dense_norm + 0.5 ร— bm25_norm.

Two flavours of neighbor retrieval โ€” NeighborRetriever uses global chunk_order (a monotonic counter) to find adjacent chunks; this can cross section boundaries. NeighborHierarchicalRetriever uses fragment_index (section-local counter) to expand only within the same section โ€” the version used in benchmarking.

Candidate over-retrieval โ€” All retrievers retrieve more candidates than k before filtering. Reference sections, bibliographies, and indices are excluded via the centralized BAD_SECTIONS config. Over-retrieval guarantees k clean results remain after filtering.


Question Generation Strategy

  • One Q&A per chunk โ€” avoids questions where multiple chunks give equivalent answers, which would make ground truth ambiguous.
  • Prompt engineering โ€” the generation prompt explicitly bans phrases like "According to the text" or "In this section," forcing standalone natural-language questions a real user might ask.
  • Skip logic โ€” chunks with insufficient content (very short fragments, context-free table rows) return {"status": "skip"} and are excluded from the evaluation dataset.
  • Resumable JSONL cache โ€” generation results are appended one record at a time to a JSONL file. If the process is interrupted, it resumes from the last completed chunk โ€” no LLM calls are repeated, no data is lost.

raglens Folder Stories

Every folder in raglens/ has a specific responsibility. Here is the story of each one.

models/ โ€” The Schema Layer

Pure dataclasses with no behavior. Defines the full data hierarchy:

Document
โ””โ”€โ”€ Section (nested tree, preserves heading hierarchy)
    โ””โ”€โ”€ FlattenedSection (tree โ†’ flat list, path pre-computed)
        โ””โ”€โ”€ Chunk (atomic unit for embedding + retrieval)
            โ””โ”€โ”€ ChunkEmbedding (Chunk + its dense vector)

Three result types exist because three retrieval paradigms return different shapes of data:

  • RetrievalResult โ€” flat retrievers (Dense, BM25, Hybrid): chunk + score + retriever_name
  • HierarchicalRetrievalResult โ€” Hierarchical retriever: parent_chunk + child_chunk + score
  • NeighborRetrievalResult โ€” NeighborRetriever: center_chunk + neighbor_chunks + score

Everything between pipeline stages is typed. No raw dicts are passed between components.


parsers/ โ€” The Document Understanding Layer

Four classes form a sequential pipeline:

  1. DoclingParser โ€” converts a PDF file to a Markdown string using Docling with formula enrichment enabled. This preserves heading levels as #/## and LaTeX as $$...$$.
  2. MarkdownSectionParser โ€” regex-matches heading lines and builds a nested Section tree, assigning parent_section_id from a stack as it descends through heading levels.
  3. LevelInference โ€” infers section depth from title numbering patterns (e.g. "3.2.1 Introduction" โ†’ level 3) for documents that use numbered headings instead of Markdown heading levels.
  4. HierarchyBuilder โ€” validates and rebuilds parent-child relationships from section levels; corrects any inconsistencies introduced by the parser.

Together they transform raw PDF bytes into a typed, hierarchical Document object.


normalization/ โ€” The Bridge Layer

SectionFlattener converts the nested Section tree into a flat List[FlattenedSection]. For each section it pre-computes the full breadcrumb path (e.g. "lbdl > Chapter 3 > Stochastic Gradient Descent") and records parent_section_id.

This layer exists so chunking receives a simple flat list. The chunker never needs to traverse the tree โ€” all the ancestry information is already embedded in each FlattenedSection.


chunking/ โ€” The Fragmentation Layer

SectionChunker orchestrates the three-tier strategy (small section โ†’ atomic, large section โ†’ parent + fragments). StructurePreserver handles everything structure-sensitive: table detection, large-table fragmentation with header duplication, and formula placeholder substitution.

The key output fields on every Chunk:

  • chunk_type โ€” parent_section, table_fragment, or section_fragment
  • fragment_index โ€” -1 for atomic parent sections; 0, 1, 2โ€ฆ for ordered fragments
  • parent_section_id โ€” links every fragment back to its originating section

These three fields are what make hierarchical and neighbor retrieval possible downstream.


embedding/ โ€” The Vectorization Layer

Stateless: takes a Chunk, returns a vector. EmbeddingGenerator wraps whatever LangChain Embeddings instance get_embedding_provider("ollama" | "openai") returns โ€” the same instance is used for both indexing (embed_documents) and query-time retrieval (embed_query), so there's exactly one embedding object per run. Ollama is free/local/deterministic with no API keys; OpenAI is available for higher-quality embeddings.


vectorstore/ โ€” The Index Layer

ChromaStore wraps ChromaDB. Each entry stores (chunk_id, embedding, metadata_dict) where the metadata dict contains all Chunk fields. This inline metadata storage means retrieved results can reconstruct full Chunk objects without a secondary database lookup.


config/ โ€” The Settings Layer

RetrievalConfig is an injectable dataclass (constructor-passed to BM25Retriever/DenseRetriever, defaulting to a shared DEFAULT_CONFIG instance) with three fields:

  • bad_sections โ€” section titles to exclude: references, bibliography, contents, index, list of figures, list of tables
  • exclude_reference_sections โ€” boolean flag
  • is_bad_section(title) โ€” normalizes title to lowercase alphanumeric, checks membership

All retrievers accept a config parameter. Without this filtering, BM25 retrieves bibliography entries because they contain topic-related keywords; Dense retrieves index pages because they share vocabulary with queries. Passing a custom RetrievalConfig lets a different corpus define its own excluded sections.


retrieval/ โ€” The Strategy Layer

Five strategies arranged by composition depth:

BM25Retriever          โ† in-memory lexical matching, no vector store
DenseRetriever         โ† semantic matching via embedding + ChromaDB
HybridRetriever        โ† composes Dense + BM25, normalizes + fuses scores
HierarchicalRetriever  โ† wraps Hybrid, promotes results to parent section context
NeighborHierarchicalRetriever  โ† wraps Hierarchical, expands to sibling fragments

BM25Retriever and DenseRetriever extend BaseRetriever (abstract class enforcing retrieve(query, k)). The remaining three compose by duck typing โ€” they accept any object with a retrieve() method.


evaluation/ โ€” The Metrics Layer

retrieval_metrics.py contains three pure functions: hit_at_k, reciprocal_rank, ndcg_at_k. No state, no side effects.

Three evaluator classes exist because each retriever returns a different result type:

Evaluator Used with Result type Metrics
RetrievalEvaluator Dense, BM25, Hybrid List[RetrievalResult] Hit@K, MRR, NDCG@K, Latency
HierarchicalRetrievalEvaluator Hierarchical List[HierarchicalRetrievalResult] Child Hit@K, Section Hit@K, Sect+Child, MRR, Latency
NeighborHierarchicalRetrievalEvaluator NeighborHierarchical List[Chunk] Child Hit@K, Latency

question_generation/ โ€” The Dataset Layer

Turns List[Chunk] into 627 evaluation Q&A pairs with full ground truth traceability.

  • QuestionGenerator โ€” wraps an injectable LLMProvider (Ollama/OpenAI/Groq, defaults to OpenAI); generates one Q&A per chunk with engineered prompts
  • QuestionDatasetBuilder โ€” orchestrates generation with resumability via get_completed_chunk_ids()
  • question_cache.py โ€” JSONL persistence; append-only so the file survives mid-run interruptions
  • QuestionDatasetLoader โ€” filters the cache to status=success records and returns List[QuestionSample]

cache/ โ€” The Persistence Layer

Two different caching formats serving two different failure modes:

  • Pickle (parser_cache.py) โ€” binary, fast, stores Document objects post-Docling. Docling parsing can take 30+ seconds per PDF; the cache avoids re-running it every session.
  • JSONL (question_cache.py) โ€” text, append-only, human-readable. LLM generation can fail mid-run; JSONL means each completed record is immediately safe. A full-file rewrite (as JSON would require) would lose data on a crash.

preprocessing/ โ€” The Cleaning Layer

formula_cleaner.py pre-processes LaTeX content before it reaches Docling, removing or normalizing malformed formula syntax that would produce parsing artifacts in the rendered Markdown output.


validation/ โ€” The Audit Layer

ChunkAuditor validates chunk integrity after chunking completes. It checks for empty chunks, missing IDs, malformed fragment_index values, and parent-child consistency. Catches issues before they silently corrupt the vector store.


Benchmark Results

Results from experiments/experiments/data/benchmark_results.json, measured across 627 evaluation samples.

Numbers

Retriever Type Hit@1 Hit@3 Hit@5 Hit@10 MRR@5 NDCG@5 Section Hit@5 Latency@5
BM25 Flat 75.6% 87.0% 90.2% 92.9% 81.3% 83.5% โ€” 2.1 ms
Dense Flat 51.9% 67.2% 73.2% 79.3% 60.4% 63.6% โ€” 13.9 ms
Hybrid Flat 73.0% 87.8% 91.5% 93.9% 80.6% 83.3% โ€” 19.3 ms
Hierarchical Hierarchical โ€” 82.2% 83.4% 85.1% 78.7% โ€” 95.0% 24.4 ms
Hier+Neighbor Hier+Context โ€” 87.8% 89.4% 91.3% โ€” โ€” โ€” 23.0 ms

Benchmark Results Table

Benchmark Chart โ€” Hit@K, MRR, Hierarchical Breakdown, Latency


Are These Numbers Good? โ€” An Objective Assessment

What the numbers show:

  • Hybrid at 91.5% Hit@5 means 9 out of 10 queries find the right chunk in the top 5 results.
  • Hierarchical at 95.0% Section Hit@5 means the correct document section is retrieved almost every time.
  • Hier+Neighbor at 89.4% vs Hierarchical at 83.4% (+6%) confirms that expanding the context window to neighboring fragments meaningfully recovers missed chunks.

Why these numbers are optimistic โ€” three honest caveats:

1. BM25 has a structural advantage in this benchmark. The ground truth questions were generated by an LLM from the source chunks. The LLM naturally uses terminology from the chunk text, so BM25 (keyword matching) is predisposed to score well. The 75.6% vs 51.9% Hit@1 gap (BM25 vs Dense) likely overstates BM25's real-world advantage โ€” on natural user queries that don't mirror chunk vocabulary, the gap would be smaller.

2. Dense retrieval underperforms its potential here. nomic-embed-text runs locally via Ollama on CPU. Frontier embedding models (text-embedding-3-large, BGE-M3, MXBAI) would likely close the gap with BM25 significantly. The 51.9% Hit@1 reflects this local model limitation, not a ceiling on semantic retrieval.

3. Single ground truth per question underestimates retrieval quality. Each question maps to exactly one "correct" chunk ID. Many questions can be correctly answered by multiple semantically similar chunks in the corpus. When a retriever returns a different-but-equally-valid chunk, the evaluation marks it as a miss. Hit@K underestimates actual retrieval usefulness โ€” a point first identified in EXP01 and still true in V2.

Bottom line: The retrieval layer is working correctly within this benchmark setup. These numbers confirm the pipeline is functioning and that the five strategies are meaningfully differentiated. RAGAS will give a more honest end-to-end assessment โ€” it measures whether the final generated answer is actually correct, not whether a specific chunk ID was found.


RAGAS End-to-End Evaluation โ€” In Progress

RAGAS (Retrieval Augmented Generation Assessment) measures end-to-end RAG quality โ€” not just whether the right chunk was retrieved, but whether the system produced a correct, faithful, relevant answer.

The Four Metrics Actually Used

Metric Question it answers
Faithfulness Does the generated answer stay within what the retrieved context actually says? Detects hallucination.
Factual Correctness Is the generated answer factually aligned with the reference answer? (Used in place of Answer Relevancy โ€” a better fit here since every question already has a verified reference answer.)
Context Precision Of the retrieved chunks, what fraction are genuinely useful for answering the question? Scored with a custom FastContextPrecision metric that parallelizes ragas's default sequential per-context loop.
Context Recall Does the retrieved context contain all the information needed to construct the correct answer?

Judge LLM: gpt-4o-mini by default (configurable โ€” raglens evaluate --judge-provider ollama\|openai\|groq).

How It Runs

  1. Retrieve + generate answers (raglens.ragas.EvaluationDatasetBuilder + AnswerDatasetBuilder) โ€” for a stratified 150-question sample (proportional per source document, seed 42) ร— each retriever, retrieve top-5 chunks and generate an answer. Cached to JSONL, resumable.
  2. Score with RAGAS (raglens.ragas.RagasScorer) โ€” batches of 10 samples sent to the judge LLM, with results appended to a JSONL cache after every batch and keyed by (retriever, chunk_id). An interrupted run โ€” expected, since judge-LLM scoring runs at roughly 1-8 minutes per sample โ€” resumes exactly where it left off; already-scored pairs are never re-sent.
  3. Aggregate (raglens report) โ€” mean scores per retriever โ†’ comparison table + chart.

Status

Retriever Answers generated RAGAS scored
BM25 โœ… 150/150 โœ… 150/150
Dense โœ… 150/150 โœ… 150/150
Hybrid โœ… 150/150 (622/622 full set) โณ 0/150
Hierarchical โœ… 150/150 โณ 0/150
Hier+Neighbor โœ… 150/150 โณ 0/150

Resume scoring with raglens evaluate โ€” it picks up on hybrid next automatically.

The Key Insight RAGAS Unlocks

Hit@K tells us a chunk was retrieved. RAGAS tells us whether the LLM used that chunk to produce a correct, faithful answer. A retriever that scores high on Hit@K may still produce hallucinations if its retrieved context is noisy or misaligned with the question. That is the gap this stage closes.


Project Structure

rag-evaluation-framework/
โ”‚
โ”œโ”€โ”€ data/
โ”‚   โ””โ”€โ”€ documents/               # Your PDF corpus (BYO โ€” 13 curated GenAI/RAG PDFs used for the reference run)
โ”‚
โ”œโ”€โ”€ experiments/
โ”‚   โ”œโ”€โ”€ pipeline_validation.ipynb   # Reference notebook โ€” all stages run cell-by-cell
โ”‚   โ””โ”€โ”€ experiments/data/
โ”‚       โ”œโ”€โ”€ benchmark_results.json         # Saved benchmark numbers
โ”‚       โ”œโ”€โ”€ Retrieval_Benchmark_Reults.png # Pandas styled results table
โ”‚       โ””โ”€โ”€ benchmark_chart.png            # 4-panel comparison chart
โ”‚
โ”œโ”€โ”€ raglens/                      # Core library (pip-installable)
โ”‚   โ”œโ”€โ”€ models/                  # Dataclasses: Document, Section, Chunk, results
โ”‚   โ”œโ”€โ”€ parsers/                 # Docling โ†’ Markdown โ†’ Section tree
โ”‚   โ”œโ”€โ”€ normalization/           # Section tree โ†’ flat FlattenedSection list
โ”‚   โ”œโ”€โ”€ chunking/                # SectionChunker + StructurePreserver
โ”‚   โ”œโ”€โ”€ embedding/               # Ollama / OpenAI embedding providers + EmbeddingGenerator
โ”‚   โ”œโ”€โ”€ llm/                     # Ollama / OpenAI / Groq text-generation providers
โ”‚   โ”œโ”€โ”€ vectorstore/             # ChromaDB wrapper
โ”‚   โ”œโ”€โ”€ config/                  # Injectable RetrievalConfig (bad sections, top-k)
โ”‚   โ”œโ”€โ”€ retrieval/               # BM25, Dense, Hybrid, Hierarchical, Neighbor
โ”‚   โ”œโ”€โ”€ evaluation/              # Hit@K, MRR, NDCG, benchmark runner + charts
โ”‚   โ”œโ”€โ”€ question_generation/     # Q&A generation + JSONL cache
โ”‚   โ”œโ”€โ”€ ragas/                   # RagasScorer, judge factory, sampling, dataset builders
โ”‚   โ”œโ”€โ”€ cache/                   # Resumable caches (parsed docs, chunks, answers)
โ”‚   โ”œโ”€โ”€ preprocessing/           # Formula cleaner
โ”‚   โ”œโ”€โ”€ validation/              # Chunk auditor
โ”‚   โ”œโ”€โ”€ pipeline.py              # RagLensPipeline โ€” orchestrates every stage
โ”‚   โ””โ”€โ”€ cli.py                   # `raglens` command (ingest/index/questions/benchmark/evaluate/report)
โ”‚
โ”œโ”€โ”€ dashboard/
โ”‚   โ””โ”€โ”€ app.py                   # Streamlit dashboard โ€” visualizes CLI-produced results
โ”‚
โ”œโ”€โ”€ tests/                       # pytest โ€” pure-logic unit tests (metrics, config, sampling, scorer)
โ”‚
โ”œโ”€โ”€ src/                          # V1 (legacy, kept for historical reference โ€” see "Why We Rebuilt")
โ”‚
โ””โ”€โ”€ artifacts/
    โ””โ”€โ”€ processtillnow.png       # V1 architecture diagram

Tech Stack

Component Tool Why
PDF Parsing Docling Formula-aware; preserves heading hierarchy from complex PDFs
LLM (Q&A gen / answers / judge) Ollama, OpenAI, or Groq (your choice per-command) Free local default via Ollama; cloud providers for quality/speed when needed
Embedding Ollama (nomic-embed-text) or OpenAI Local, deterministic, no cloud dependency โ€” or OpenAI when you want it
Vector Store ChromaDB Lightweight, local persistence, no infrastructure required
Lexical Retrieval rank-bm25 (Okapi BM25) Fast in-memory BM25, no external service
Text Splitting LangChain RecursiveCharacterTextSplitter Used only for text fragments within large sections
Evaluation Framework RAGAS Standard RAG evaluation library
CLI Typer raglens command: ingest/index/questions/benchmark/evaluate/report
Dashboard Streamlit Local visualization of CLI-produced results
Language Python 3.12 โ€”

See Quickstart above to run this on your own PDFs. To reproduce the exact reference run instead, open experiments/pipeline_validation.ipynb and run top to bottom.


Version 1 Notes (Historical Context)

The following summarises the first iteration of this project, which used a LangChain-based pipeline on Wikipedia data. It is preserved here as context for the transition to V2.

  • Data source: ~20 Wikipedia articles on ML/AI topics (Transformers, LLMs, Embeddings, Vector Databases, etc.) fetched via the Wikipedia API
  • Tech stack: LangChain + ChromaDB + nomic-embed-text (same embedding model) + Qwen3:8B
  • Chunking: RecursiveCharacterTextSplitter at chunk_size=1000, overlap=200
  • Retrieval evaluated: Vector similarity search and BM25 (LangChain abstractions)
  • Evaluation metrics: Exact Chunk Recall, Semantic Similarity (cosine ~0.63 average), LLM-as-a-Judge (~0.74 average)
  • Key finding: Exact chunk recall severely underestimated retrieval quality โ€” relevant answers came from chunks other than the "expected" source chunk
  • Key finding: LLM-as-a-Judge scores correlated better with answer quality than embedding cosine similarity
  • Identified limitations: No hybrid retrieval, no hallucination detection, Wikipedia noise, naive chunking, no section awareness
  • Outcome: All identified limitations became the design requirements for V2
  • Reference links: Hallucination Detection Guide ยท LLM Eval Projects Guide

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

raglens_toolkit-0.1.0.tar.gz (64.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

raglens_toolkit-0.1.0-py3-none-any.whl (68.6 kB view details)

Uploaded Python 3

File details

Details for the file raglens_toolkit-0.1.0.tar.gz.

File metadata

  • Download URL: raglens_toolkit-0.1.0.tar.gz
  • Upload date:
  • Size: 64.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for raglens_toolkit-0.1.0.tar.gz
Algorithm Hash digest
SHA256 66a176b1ca19627087b489cafe78753d98fe7dd89d499e777fafe9950926a7e8
MD5 92e2d17dfd5a5828b8cf4db65e6ee4ae
BLAKE2b-256 54a96361c908312e8cbbe3dfaee39ebbf2cd50a869fe7d97fb3aef93b7a9f424

See more details on using hashes here.

File details

Details for the file raglens_toolkit-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for raglens_toolkit-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5b9148f545ef64e073acddd8733e214f65080701988f177b71e38be0ca21ae3c
MD5 863b1c407af64cb72db6dd2939e043fc
BLAKE2b-256 014e9c64cb517628dbfac34d56d3bfbfeacf7aa10c96ac3e487946cf548cc6bc

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page