Vamshi Pokala commited on
Commit
a1b0654
·
1 Parent(s): 6f11cb0

docs: README with architecture and Mermaid flow diagrams

Browse files
Files changed (1) hide show
  1. README.md +429 -0
README.md ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Doc-Ingestion
2
+
3
+ Local-first **document ingestion**, **hybrid retrieval** (sparse + dense), and **LLM-grounded Q&A** over your own files. Phase 1 covers extraction, chunking, BM25 indexing, and Chroma vector storage. Phase 2 adds query understanding, **reciprocal rank fusion (RRF)**, evaluation helpers, and a small CLI.
4
+
5
+ ---
6
+
7
+ ## Table of contents
8
+
9
+ - [Features](#features)
10
+ - [Tech stack](#tech-stack)
11
+ - [Architecture](#architecture)
12
+ - [System context](#system-context)
13
+ - [Code components](#code-components)
14
+ - [Ingestion pipeline](#ingestion-pipeline)
15
+ - [Query pipeline](#query-pipeline)
16
+ - [RRF fusion](#rrf-fusion)
17
+ - [Prerequisites](#prerequisites)
18
+ - [Installation](#installation)
19
+ - [Ingest documents](#ingest-documents)
20
+ - [Query documents](#query-documents)
21
+ - [Retrieval strategy](#retrieval-strategy)
22
+ - [Local models (Ollama)](#local-models-ollama)
23
+ - [Configuration](#configuration)
24
+ - [Development](#development)
25
+ - [Project layout](#project-layout)
26
+ - [Roadmap](#roadmap)
27
+
28
+ ---
29
+
30
+ ## Features
31
+
32
+ - **Formats:** PDF, DOCX, TXT, Markdown, HTML (see `DocumentProcessor`).
33
+ - **Chunking:** Configurable size and overlap (`config.yaml`).
34
+ - **Sparse retrieval:** Custom **BM25** index persisted to JSON; optional **title/metadata weighting** in the indexed text (display text stays chunk-only).
35
+ - **Dense retrieval:** **ChromaDB** (dev) or **Qdrant** (prod path in code) with embeddings from **Ollama**.
36
+ - **Hybrid fusion:** **RRF** over BM25-ranked and vector-ranked chunk IDs, then top‑`k` passed to the chat model.
37
+ - **Query layer:** Normalization, stop-word trimming, light synonym expansion, heuristic intent.
38
+ - **Evaluation:** Pure-Python IR metrics (`precision@k`, recall, F1, MRR, MAP, NDCG, etc.) under `src/evaluation/`.
39
+
40
+ ---
41
+
42
+ ## Tech stack
43
+
44
+ | Layer | Technology |
45
+ |--------|------------|
46
+ | Language | Python **3.13** (see CI) |
47
+ | Document parsing | PyPDF2, python-docx, BeautifulSoup |
48
+ | Sparse index | In-house **BM25** (`src/core/bm25_index.py`) |
49
+ | Vector store (default) | **ChromaDB** persistent client |
50
+ | Vector store (alternate) | **Qdrant** (`VectorDatabase(mode="prod")`) |
51
+ | Embeddings | **Ollama** — `nomic-embed-text` (768-d), via `ollama` Python client |
52
+ | Chat / answers | **Ollama** — any pulled chat model (default: `deepseek-r1:8b`) |
53
+ | Config | YAML + `pydantic` / utilities in `src/utils/config.py` |
54
+ | Tests | `pytest` (unit + integration; Ollama mocked in tests where noted) |
55
+
56
+ Dependencies are listed in [`requirements/base.txt`](requirements/base.txt).
57
+
58
+ ---
59
+
60
+ ## Architecture
61
+
62
+ High-level view: **ingest** builds a lexical index and a vector store; **query** runs both retrievers, **fuses ranks with RRF**, then optionally calls a **local chat model** to summarize grounded context.
63
+
64
+ ### System context
65
+
66
+ ```mermaid
67
+ flowchart LR
68
+ subgraph userLayer [Operator]
69
+ CLI[CLI ingest and query]
70
+ end
71
+ subgraph localApp [Doc-Ingestion]
72
+ SRC[Python src]
73
+ end
74
+ subgraph storage [Local storage]
75
+ FS[Document files]
76
+ BM25File[bm25_index.json]
77
+ ChromaDir[Chroma DB dir]
78
+ end
79
+ subgraph ollamaSvc [Ollama]
80
+ EmbedAPI[Embedding API]
81
+ ChatAPI[Chat API]
82
+ end
83
+ CLI --> SRC
84
+ SRC --> FS
85
+ SRC --> BM25File
86
+ SRC --> ChromaDir
87
+ SRC --> EmbedAPI
88
+ SRC --> ChatAPI
89
+ ```
90
+
91
+ ### Code components
92
+
93
+ ```mermaid
94
+ flowchart TB
95
+ subgraph entry [Entrypoints]
96
+ ING[src/ingest.py]
97
+ QRY[src/query.py]
98
+ end
99
+ subgraph corePkg [src/core]
100
+ DP[document_processor]
101
+ BI[bm25_index]
102
+ BS[bm25_search]
103
+ HY[hybrid_retriever]
104
+ QP[query_processor]
105
+ RR[retrieval_result]
106
+ VS[vector_search]
107
+ end
108
+ subgraph utilPkg [src/utils]
109
+ VDB[database VectorDatabase]
110
+ CFG[config]
111
+ LOG[log]
112
+ end
113
+ ING --> DP
114
+ ING --> BI
115
+ ING --> VDB
116
+ QRY --> QP
117
+ QRY --> HY
118
+ HY --> BS
119
+ HY --> VS
120
+ BS --> BI
121
+ VS --> VDB
122
+ ING --> CFG
123
+ QRY --> CFG
124
+ ```
125
+
126
+ Offline IR metrics and fixtures live under `src/evaluation/` and `tests/fixtures/` (not on the hot query path).
127
+
128
+ ### Ingestion pipeline
129
+
130
+ ```mermaid
131
+ flowchart TD
132
+ START([Start ingest])
133
+ FILES[Collect supported files]
134
+ PROC[DocumentProcessor extract chunk metadata]
135
+ CHUNKS[Chunk text per config.yaml]
136
+ IDXTXT[BM25Index.compose_index_text]
137
+ BM25ADD[BM25Index.add_document]
138
+ EMB[Ollama embeddings per batch]
139
+ UPSERT[Chroma collection upsert]
140
+ SAVE[Save bm25_index.json]
141
+ END([Done])
142
+ START --> FILES
143
+ FILES --> PROC
144
+ PROC --> CHUNKS
145
+ CHUNKS --> IDXTXT
146
+ IDXTXT --> BM25ADD
147
+ CHUNKS --> EMB
148
+ EMB --> UPSERT
149
+ BM25ADD --> SAVE
150
+ UPSERT --> SAVE
151
+ SAVE --> END
152
+ ```
153
+
154
+ ### Query pipeline
155
+
156
+ End-to-end path for `python -m src.query` (retrieval runs inside `HybridRetriever.retrieve`; the LLM step is in `query.py` after fusion).
157
+
158
+ ```mermaid
159
+ flowchart TD
160
+ QIN([User query])
161
+ LOAD[Load BM25 JSON and Chroma]
162
+ QP[QueryProcessor.process_query]
163
+ HR[HybridRetriever.retrieve]
164
+ PAR[BM25 and vector searches thread pool or sequential]
165
+ FUSE[RRF over two ranked id lists]
166
+ TOP[Top k RetrievalResult rows]
167
+ CACHE[(LRU cache optional)]
168
+ LLM{LLM flag}
169
+ ANS[generate_answer Ollama chat]
170
+ OUT([Stdout chunks and answer])
171
+ QIN --> LOAD
172
+ LOAD --> QP
173
+ QP --> HR
174
+ HR --> PAR
175
+ PAR --> FUSE
176
+ FUSE --> TOP
177
+ TOP --> CACHE
178
+ CACHE --> LLM
179
+ LLM -->|with LLM| ANS
180
+ LLM -->|retrieval only| OUT
181
+ ANS --> OUT
182
+ ```
183
+
184
+ ### RRF fusion
185
+
186
+ Only **ordered chunk ids** from BM25 and from Chroma participate. Raw BM25 and distance scores are **not** mixed mathematically; ranks are merged with a standard RRF score per id.
187
+
188
+ ```mermaid
189
+ flowchart LR
190
+ subgraph sparseLeg [BM25 ranked ids]
191
+ B1["rank 1 id_a"]
192
+ B2["rank 2 id_b"]
193
+ B3["rank 3 id_c"]
194
+ end
195
+ subgraph denseLeg [Vector ranked ids]
196
+ V1["rank 1 id_b"]
197
+ V2["rank 2 id_a"]
198
+ V3["rank 3 id_d"]
199
+ end
200
+ RRF[RRF score per id]
201
+ SORT[Sort by score then id]
202
+ TOPK[Top k chunk payloads]
203
+ B1 --> RRF
204
+ B2 --> RRF
205
+ B3 --> RRF
206
+ V1 --> RRF
207
+ V2 --> RRF
208
+ V3 --> RRF
209
+ RRF --> SORT
210
+ SORT --> TOPK
211
+ ```
212
+
213
+ **Merge step (after scores):** `HybridRetriever` re-attaches `text`, `metadata`, BM25 score, and vector distance from the hit maps to build [`RetrievalResult`](src/core/retrieval_result.py) rows for the LLM context.
214
+
215
+ ---
216
+
217
+ ## Prerequisites
218
+
219
+ 1. **Python 3.13** (or align with your environment; CI uses 3.13).
220
+ 2. **[Ollama](https://ollama.com/)** installed and running locally.
221
+ 3. Pull models you will use (minimum for the default code paths):
222
+
223
+ ```bash
224
+ ollama pull nomic-embed-text
225
+ ollama pull deepseek-r1:8b
226
+ ```
227
+
228
+ `nomic-embed-text` is used for **embeddings** during ingest and vector search. The **chat** model is configurable (see below).
229
+
230
+ ---
231
+
232
+ ## Installation
233
+
234
+ ```bash
235
+ git clone git@github.com:vampokala/doc-ingestion.git
236
+ cd doc-ingestion
237
+
238
+ python3 -m venv .venv
239
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
240
+
241
+ pip install -r requirements/base.txt
242
+ pip install pytest # optional, for running tests like CI
243
+ ```
244
+
245
+ Verify Ollama:
246
+
247
+ ```bash
248
+ ollama list
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Ingest documents
254
+
255
+ Put files under a folder (e.g. `data/documents/`) or point at a single file. Supported extensions: `.pdf`, `.docx`, `.txt`, `.md`, `.html`.
256
+
257
+ ```bash
258
+ python -m src.ingest --docs data/documents
259
+ ```
260
+
261
+ This will:
262
+
263
+ - Read [`config.yaml`](config.yaml) for `chunk_size` and `overlap`.
264
+ - Write **BM25** index to `data/embeddings/bm25_index.json`.
265
+ - Write **Chroma** data under `data/embeddings/chroma/` (collection name: `documents`).
266
+
267
+ Optional post-ingest smoke query (prints BM25 and vector lists separately):
268
+
269
+ ```bash
270
+ python -m src.ingest --docs data/documents --query "your keywords" --top-k 5
271
+ ```
272
+
273
+ > **Note:** `data/embeddings/` is gitignored by default (generated artifacts). Re-run ingest after cloning or when the corpus changes.
274
+
275
+ ---
276
+
277
+ ## Query documents
278
+
279
+ Hybrid retrieval + optional LLM answer:
280
+
281
+ ```bash
282
+ python -m src.query "What is hybrid retrieval?"
283
+ python -m src.query "Explain chunking" --top-k 8
284
+ python -m src.query "keywords only" --no-llm
285
+ ```
286
+
287
+ - **`--top-k`:** Number of fused chunks sent to the model (default `5`).
288
+ - **`--no-llm`:** Show retrieval only (BM25 + Chroma fused); no chat call.
289
+ - **`--model`:** Ollama chat model name (overrides default).
290
+ - **Env `OLLAMA_QUERY_MODEL`:** Default chat model if set.
291
+
292
+ Example:
293
+
294
+ ```bash
295
+ export OLLAMA_QUERY_MODEL=qwen2.5-coder:14b
296
+ python -m src.query "How does BM25 scoring work?"
297
+ ```
298
+
299
+ ---
300
+
301
+ ## Retrieval strategy
302
+
303
+ ### 1. Query processing
304
+
305
+ [`QueryProcessor`](src/core/query_processor.py) builds a `ProcessedQuery`:
306
+
307
+ - Lowercasing and light normalization.
308
+ - Stop-word removal and tokenization.
309
+ - Small synonym table for expansion (used to build the **BM25 query string**).
310
+ - Heuristic **intent** (factual / exploratory / comparative) and a simple **complexity** flag.
311
+
312
+ The **vector** leg uses the **original user question**; the **BM25** leg uses the **joined expanded tokens** so keyword recall can improve.
313
+
314
+ ### 2. Dual retrieval
315
+
316
+ - **BM25:** `BM25Search` → `BM25Index.score()` over the persisted inverted index.
317
+ - **Vector:** `VectorSearch` → Chroma similarity search using an embedding of the query from Ollama.
318
+
319
+ Each leg requests a **candidate pool** (by default up to 50 hits, or `max(top_k, 50)`), so fusion sees more than the final `k`.
320
+
321
+ ### 3. Reciprocal Rank Fusion (RRF)
322
+
323
+ Fusion is implemented in [`src/core/hybrid_retriever.py`](src/core/hybrid_retriever.py). Only **ranks** matter, not raw BM25 vs cosine scores (so scales do not need alignment).
324
+
325
+ For each chunk id \(d\) appearing in either ranked list:
326
+
327
+ \[
328
+ \text{RRF}(d) = \sum_{\text{list } i} \frac{1}{k_{\text{rrf}} + \text{rank}_i(d)}
329
+ \]
330
+
331
+ - \(\text{rank}_i(d)\) is **1-based** in that list.
332
+ - If \(d\) is missing from a list, that list contributes **0** for \(d\).
333
+ - Default **`k_rrf = 60`** (`FusionConfig`), which dampens rank sensitivity.
334
+ - Final ordering: sort by **RRF score descending**, then by **chunk id** ascending for stable ties.
335
+
336
+ The top **`k`** fused results become [`RetrievalResult`](src/core/retrieval_result.py) rows (text, metadata, per-leg ranks, fused score, `sources`, heuristic confidence). The CLI maps them to legacy dicts for printing and for the LLM context block.
337
+
338
+ ### 4. Caching and parallelism
339
+
340
+ - Optional **in-process LRU** cache on the hybrid retriever (keyed by queries + fusion parameters + collection name).
341
+ - BM25 and vector calls can run in **parallel** via a thread pool (`FusionConfig.parallel`).
342
+
343
+ ### 5. Why hybrid + RRF?
344
+
345
+ - **BM25** excels at lexical overlap (names, acronyms, rare terms).
346
+ - **Dense retrieval** excels at paraphrase and semantic neighborhood.
347
+ - **RRF** combines two rankers without normalizing incompatible scores and tends to improve robustness over “concat BM25 then vector” or score averaging.
348
+
349
+ ---
350
+
351
+ ## Local models (Ollama)
352
+
353
+ | Role | Config location | Default model |
354
+ |------|-----------------|---------------|
355
+ | **Embeddings** (ingest + vector search) | [`src/utils/database.py`](src/utils/database.py) `OLLAMA_MODEL` | `nomic-embed-text` |
356
+ | **Chat** (answer generation) | [`src/query.py`](src/query.py) `DEFAULT_LLM_MODEL` / `--model` / `OLLAMA_QUERY_MODEL` | `deepseek-r1:8b` |
357
+
358
+ To use a different embedding model you would change `OLLAMA_MODEL` and ensure **Chroma collection dimension** matches the new model (re-ingest after any embedding change).
359
+
360
+ Chat models must be pulled in Ollama, e.g.:
361
+
362
+ ```bash
363
+ ollama pull deepseek-r1:8b
364
+ ollama pull qwen2.5-coder:14b
365
+ ```
366
+
367
+ ---
368
+
369
+ ## Configuration
370
+
371
+ | File | Purpose |
372
+ |------|---------|
373
+ | [`config.yaml`](config.yaml) | `chunk_size`, `overlap`, paths for data/output (used by ingest / config loader). |
374
+ | [`src/query.py`](src/query.py) | Paths: `BM25_INDEX_PATH`, `CHROMA_PATH`, `COLLECTION_NAME`; default LLM. |
375
+ | [`src/ingest.py`](src/ingest.py) | Same BM25 path and Chroma path defaults as query flow. |
376
+
377
+ ---
378
+
379
+ ## Development
380
+
381
+ ```bash
382
+ # Lint (matches CI)
383
+ pip install ruff
384
+ ruff check src/ tests/
385
+
386
+ # Typecheck (matches CI)
387
+ pip install -r requirements/base.txt mypy types-PyYAML types-beautifulsoup4 types-requests
388
+ mypy src/ --ignore-missing-imports
389
+
390
+ # Tests
391
+ pip install -r requirements/base.txt pytest
392
+ pytest tests/unit/ -v
393
+ pytest tests/integration/ -v
394
+ ```
395
+
396
+ CI is defined in [`.github/workflows/ci.yml`](.github/workflows/ci.yml).
397
+
398
+ ---
399
+
400
+ ## Project layout
401
+
402
+ ```
403
+ src/
404
+ core/ # BM25, hybrid retriever, query processor, document processor
405
+ evaluation/ # retrieval_metrics
406
+ ingest.py # CLI: folder/file → BM25 + Chroma
407
+ query.py # CLI: hybrid retrieve + optional Ollama answer
408
+ utils/ # config, logging, VectorDatabase (Chroma / Qdrant)
409
+ tests/
410
+ unit/
411
+ integration/
412
+ fixtures/ # e.g. qrels for metric tests
413
+ Docs/ # phase specs (design docs)
414
+ ```
415
+
416
+ Design references: [`Docs/phase1_core_infrastructure.md`](Docs/phase1_core_infrastructure.md), [`Docs/phase2_hybrid_retrieval.md`](Docs/phase2_hybrid_retrieval.md).
417
+
418
+ ---
419
+
420
+ ## Roadmap
421
+
422
+ - **Phase 3:** Reranking and generation improvements (see [`Docs/phase3_reranking_generation.md`](Docs/phase3_reranking_generation.md) and `data/documents/` copies if present).
423
+ - **Phase 4:** Citations / API surface (see [`Docs/phase4_citation_api.md`](Docs/phase4_citation_api.md)).
424
+
425
+ ---
426
+
427
+ ## License
428
+
429
+ Specify your license here (repository default not set in this README).