Skip to content

ameghcoder/qindex-rag

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

QIndex-RAG

A retrieval-augmented generation (RAG) system that indexes questions, not chunks.

Instead of embedding raw document text and hoping a user's query semantically matches it, QIndex uses an LLM to generate the questions each chunk answers during ingest, embeds those questions, then at query time matches the user's question against the stored question vectors. This produces dramatically better cosine similarity scores and more grounded answers.


The Core Idea

Standard RAG embeds chunk text → matches against a user query. The gap: a user asking "command to install the package" rarely matches a chunk that says "Run the following npm command" — the vocabulary is different even though the intent is identical.

QIndex closes this gap:

Ingest:
  chunk → LLM → ["how do I install the package?", "what is the npm install command?", ...]
                        ↓ embed each question
               Qdrant stores (question_vector → chunk_text as payload)

Query:
  user question → embed → cosine search against question_vectors
                       ↓ top-k matched questions → retrieve chunk payloads
                   LLM generates answer from chunks

The user's question is matched against a question, not a prose chunk. Semantically similar phrasings align far better in embedding space.


Why Context-Aware Chunking (Not Token-Based)

Most RAG implementations use token-based chunking: split every N tokens with an overlap. This breaks content at arbitrary positions — mid-sentence, mid-list, mid-code block. The result is chunks that lose their structural context.

QIndex uses a heading → paragraph → sentence cascade:

  1. H2/H3 boundaries first — each section becomes its own chunk, preserving topic coherence
  2. Paragraph split next — if a section is too large, split on double newlines (list blocks stay atomic)
  3. Sentence split last — only when a single paragraph exceeds the word limit
  4. Full-page shortcut — pages under 500 words are stored as a single chunk; no split needed
  5. Page summary prepended — every chunk gets Source: URL / Page: H1 title / first paragraph prepended, so the chunk is self-contained even out of context

Why this matters for question generation: An LLM generating questions from a coherent section produces grounded, specific questions. A token-split chunk that starts mid-sentence often produces generic questions that fail the grounding check.

Grounding check: Every generated question must share ≥ 2 meaningful words (>4 chars) with the chunk it came from. Questions that don't pass are discarded. This prevents hallucinated generic questions from polluting the index.

Nav-heavy page detection: Raw markdown is preferred (preserves code blocks and install commands). Pages where raw word count exceeds 4× the fit word count are considered nav-heavy and use the filtered fit_markdown instead.


Observed Performance

Tested against stl.fysk.dev (19 pages, ~80 indexed questions).

Standard RAG vs QIndex — same queries, same corpus

Query Standard RAG top score QIndex top score
"how write a button in stl syntax" 0.753 (wrong page — matched link docs) 0.694 (correct page — button docs)
"what is the use of action attribute in button" 0.873
"what is the use of targetId in button" 0.889
"why should I use STL over other table plugins" 0.853 (correct) 0.854 (correct)

Standard RAG example failure — query "how write a button in stl syntax" returned chunks about links, not buttons, because the link documentation page contains more link-related prose that happened to score higher. QIndex matched the question "how do I create a button element in STL syntax?" directly.

QIndex scores across query types (context-aware chunking)

Query Top score Answer quality
"command to install the structured-table package" 0.807 Correct: npm i structured-table
"how to install the structured-table" 0.771 Correct
"what is the use of targetId in button in STL syntax" 0.889 Correct
"what is the use of action attribute in button" 0.873 Correct
"how to add a button in the stl syntax" 0.694 Correct
"how can I use this in my project" 0.724 Correct (multi-framework answer)

Confidence threshold (0.60): Results below threshold are shown but excluded from LLM generation. When all results fall below threshold, the system returns "I don't have enough information about that" rather than hallucinating.

Known gap

Keyword/command queries still underperform vs. natural language questions:

"command to install the stl package"  →  top score: 0.560 (below threshold)
"how to install the structured-table" →  top score: 0.771 (answers correctly)

Both ask the same thing. The fix (not yet implemented): a normalize_query() step that rewrites the user's query to a canonical question form before embedding.


Project Structure

qindex/
├── qindex_rag.py          # API version (OpenAI + Anthropic Haiku)
├── qindex_rag_local.py    # Local version (Ollama, no API keys)
├── chunker/
│   ├── __init__.py
│   └── context_chunker.py # Heading-aware chunker with page summary
└── requirements.txt

Installation

pip install -r requirements.txt
playwright install chromium

Qdrant (local vector store):

docker run -p 6333:6333 qdrant/qdrant

Usage — Local (Ollama, no API keys)

Requires Ollama running locally with the models pulled:

ollama pull nomic-embed-text
ollama pull qwen2.5:14b
# Ingest a documentation site
python qindex_rag_local.py ingest https://docs.yoursite.com 30

# Ask a question
python qindex_rag_local.py ask 'how do I install the package'

# Filter to a specific site
python qindex_rag_local.py ask 'how do I install the package' --site docs.yoursite.com

Usage — API (OpenAI + Anthropic)

Set environment variables:

export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-...
# Ingest a documentation site
python qindex_rag.py ingest https://docs.yoursite.com 30

# Ask a question
python qindex_rag.py ask 'how do I install the package'

# Filter to a specific site
python qindex_rag.py ask 'how do I install the package' --site docs.yoursite.com

Models used:

  • Embedding: text-embedding-3-small (1536-dim, ~$0.02/1M tokens)
  • LLM: claude-haiku-4-5-20251001 (question generation + answer generation)

Configuration

All constants are at the top of each file:

Constant Default Description
COLLECTION_NAME qindex_docs_v1 / qindex_local_v1 Qdrant collection name
EMBED_DIM 1536 (API) / 768 (local) Must match embedding model
CHUNK_SIZE 500 Chunker uses chunk_size // 4 as max words
CHUNK_OVERLAP 100 Overlap in characters between consecutive chunks
TOP_K 3 Number of results to retrieve
CONFIDENCE_THRESHOLD 0.60 Min cosine similarity to pass to LLM

How It Compares to Standard RAG

Standard RAG QIndex
What is embedded Chunk text LLM-generated questions
Query match Query ↔ prose Question ↔ question
Chunking Token-based Heading/paragraph cascade
Hallucination guard Prompt only Confidence threshold + grounding check
Cost Low Higher (LLM call per chunk at ingest)

QIndex trades higher ingest cost (one LLM call per chunk to generate questions) for higher retrieval precision. For a support knowledge base where ingest happens once and queries happen thousands of times, this is the right tradeoff.


Independent Discovery

I built QIndex while developing SupportGPT, a support chatbot that answers questions from your documentation. The core frustration was that standard RAG kept failing on exact same intent expressed with different words — "install command" vs "how to install" would return completely different results.

The solution felt obvious in retrospect: if you want question-to-question matching, index questions. An LLM can generate all the ways a user might ask about a chunk far better than any heuristic.

After I shared this publicly, my friend sent me a link to a 2025 Seoul National University research paper that describes a nearly identical approach — LLM-generated questions as the index representation. I had not seen the paper before building QIndex. This kind of parallel discovery is actually a strong signal: when two independent paths arrive at the same answer, the intuition is probably right.

I'm sharing QIndex openly because the idea deserves to be used, not hoarded.


License

MIT License — use it, build on it, ship it.

See LICENSE.


Built with

About

Q-Index: Ingest generates questions per chunk, embeds questions (not chunks), stores chunk as payload. Query matches question-to-question.

Resources

License

Stars

4 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages