Skip to content
RAG Variants

RAG Variants

The Haystack-backed RAG pipeline variants, their config, and the document model.

All variants live in django_ai_sdk.rags and subclass RAGBase.

Comparison

Variant Retrieval Document store
BM25QueryExpanderRAG BM25 keyword search with query expansion In-memory
ChromaDBQueryExpanderRAG Dense embeddings with query expansion ChromaDB
QdrantBM25HybridRAG Hybrid SPLADE + BGE with RRF fusion Qdrant

Setup

Lightweight, fast, no GPU. Best for small to medium document sets. BM25QueryExpanderRAGConfig is a plain RAGConfig: no extra options.

from django_ai_sdk.rags import BM25QueryExpanderRAG, BM25QueryExpanderRAGConfig

rag = BM25QueryExpanderRAG(
    documents=documents,
    config=BM25QueryExpanderRAGConfig(top_k=5, n_expansions=4),
)

Choosing a Variant

Override get_rag_pipeline() on your agent to return the variant you want. A common pattern is to factor each variant into its own method and dispatch from get_rag_pipeline():

async def get_rag_pipeline_qdrant(self, memory_id=None): ...  # QdrantBM25HybridRAG

async def get_rag_pipeline(self, memory_id=None):
    return await self.get_rag_pipeline_qdrant(memory_id)

The base get_rag_pipeline() returns None (RAG disabled). Override it to return a RAG instance built from one of the variants above.

Use case Variant
Small/medium docs, no GPU BM25QueryExpanderRAG
Large docs, dense embeddings ChromaDBQueryExpanderRAG
Production hybrid search QdrantBM25HybridRAG

RAGConfig

Base config for all variants:

Field Default Description
top_k 5 Maximum documents to retrieve per query
min_score None Drop documents below this relevance score (None disables)
n_expansions 4 Number of query variations to generate (1 = no expansion)
expander_model "gpt-4o-mini" LLM used for query expansion
expander_prompt built-in Prompt template for query expansion; {{context}} gets what the searched documents are about
chunk_size 100 Chunk size for document splitting
chunk_overlap 50 Chunk overlap for document splitting

Query Expansion

Expansion generates several phrasings of the user’s query to improve recall. The first query is always the original, verbatim; n_expansions = 1 disables expansion. Expansion forces same-language queries so results match the user’s language.

The expander is told what the documents are about ({{context}}: their most common keywords and some file names, see expander_context). Without it, it guesses: “SABRE” in a set of CVs became the airline booking system.

User Query: "What is the pirate code?"
           ↓
Query Expansion (via expander_model)
├─ "What is the pirate code?"        ← original, verbatim
├─ "Explain the pirate code rules"
├─ "Pirate code of conduct"
└─ "Pirate laws and regulations"
           ↓
Search All Variations (4 × top_k candidates each) → Fuse ranks → Return top-k

Fusing. The results are merged by reciprocal rank fusion (fuse_and_rank): a document several queries find ranks high, whatever each query scored it (scores of different queries don’t compare). One more ranking counts the words of the user’s own query found verbatim in a candidate, weighing as much as all queries together, so an exact term the embeddings don’t know (an acronym, a code, a name) lifts the document that contains it. Words in most candidates (“the”, “is”) don’t count. Every search logs one line: the queries, hits per query, the exact terms and what was kept (RAG search: ...); one query means the expander fell back to the original.

Persistent Storage

Chroma and Qdrant storage configs derive their backend from settings via from_settings: in-memory when unset, AI_SDK_VECTOR_STORE_PATH for local persistence, and AI_SDK_VECTOR_STORE_URL (Qdrant server) when set:

from django_ai_sdk.rags.config import QdrantStorageConfig

config = QdrantStorageConfig.from_settings(memory_id="mem-1")

See the Settings Reference for both settings.

Remote Embeddings

Qdrant hybrid RAG embeds locally with FastEmbed by default. To send dense embeddings to an OpenAI-compatible endpoint instead (a hosted model, no local inference):

AI_SDK_EMBEDDINGS_MODEL = "Qwen/Qwen3-Embedding-8B"
AI_SDK_EMBEDDINGS_DIM = 4096

Vectors of different models don’t compare, so after changing the model run python manage.py reindex_memories (see CLI).

Documents

RagDocument

The framework-agnostic document used by all variants:

from django_ai_sdk.rags import RagDocument

RagDocument(
    id="doc-1",
    content="The Pirate Code includes: Every man has a vote...",
    metadata={"topic": "rules"},
    title="Pirate Code",
    source="manual.pdf",
    score=0.95,   # optional
)

From a Django QuerySet

  • queryset_to_rag_documents(): Django QuerySet → RagDocuments
  • to_document(): RagDocument → Haystack Document
  • RagDocument.from_document(): Haystack Document → RagDocument

The default Agent.get_rag_documents(memory_id) calls your get_rag_queryset(memory_id) and maps the results. Override either to control exactly what gets indexed.

Next: Storage, where conversations persist.