Skip to content

feat: ingestion pipeline plus Qdrant + BM25 hybrid retrieval#3

Merged
adel-saoud merged 1 commit into
mainfrom
feat/ingestion-retrieval
Jun 17, 2026
Merged

feat: ingestion pipeline plus Qdrant + BM25 hybrid retrieval#3
adel-saoud merged 1 commit into
mainfrom
feat/ingestion-retrieval

Conversation

@adel-saoud

Copy link
Copy Markdown
Owner

Summary

  • Ingestion: PDF (text-only via pypdf, lazy import) / markdown / txt loader, hierarchical chunker (parent 1024 / child 256 with 10% overlap via tiktoken cl100k_base), pipeline orchestrator, on-disk RedactionMapStore with atomic writes and path-traversal guard
  • Retrieval: Embedder + EmbedderBackend Protocol (real sentence-transformers backend lazy-loaded), Qdrant adapter (single collection with parent payload attached to each child), BM25Index + HybridRetriever with min-max score fusion
  • Hermetic tests against QdrantClient(location=":memory:")

Test plan

  • uv run ruff check . clean
  • uv run pyright 0 errors / 0 warnings
  • uv run pytest tests/test_ingestion_*.py tests/test_retrieval_*.py all green
  • CI green on the PR

ADRs referenced

Ingestion side:
- `loader.py` reads text PDFs (lazy `pypdf` import), markdown, and plain
  text. Scanned PDFs are out of scope.
- `chunker.py` builds the hierarchical split: parent chunks at 1024
  tokens, child chunks at 256 with 25-token (10%) overlap. Tokenisation
  via tiktoken `cl100k_base`. Chunk ids are dashed UUIDs so they double
  as Qdrant point ids.
- `pipeline.py` chains detect, redact, persist redaction map, chunk,
  then hands clean parents + children to the injected `VectorSink`.
  Only redacted text leaves the function.
- `redaction_store.py` persists `RedactionMap` per document as JSON with
  atomic tmp + rename writes and a path-traversal guard.

Retrieval side:
- `embedder.py` exposes `EmbedderBackend` Protocol so tests inject a
  hash-based fake. The real sentence-transformers backend lives in
  `_real_backends.py` and is lazy-loaded.
- `store.py` adapts Qdrant. Single collection: child vectors with
  parent text + order stored as payload, so retrieval gives parent
  context with no second round-trip. Implements `VectorSink` so the
  ingestion pipeline can use it transparently.
- `hybrid.py` adds `BM25Index` (rank_bm25 over child texts) and
  `HybridRetriever` that fuses dense + BM25 scores via min-max
  normalisation and a configurable weight.

All tests are hermetic: `QdrantClient(location=":memory:")` for the
store, deterministic fake embedders, no network.

ADRs:
  004 Hierarchical chunking
  007 Qdrant over Chroma
@adel-saoud adel-saoud self-assigned this Jun 17, 2026
@adel-saoud
adel-saoud merged commit 63d9e34 into main Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant