Back to Home

DRAG with KNEE for RAG: dynamic pruning

DRAG with KNEE — algorithm for adaptive RAG with hierarchical trees in Qdrant. Uses RRF and knee-point pruning for dynamic context selection. Reduces overflow and increases relevance.

DRAG with KNEE: revolution in RAG search
Advertisement 728x90

DRAG with KNEE: Dynamic Chunking for High-Performance RAG

DRAG with KNEE is a dynamic retrieval-augmented generation algorithm that uses knee-point pruning to optimize context selection. It builds hierarchical vector trees in Qdrant and leverages hybrid search with Reciprocal Rank Fusion (RRF) to adaptively retrieve relevant context—solving the limitations of static top_k approaches, such as context overflow or information loss.

Building Hierarchical Indexes

Documents are split into pages. For each page, an LLM generates a concise summary and key terms via structured output—these become leaf nodes in the tree.

Leaves are grouped into parent nodes (3–5 per group). Each parent inherits child keywords and generates its own summary by aggregating child content. This process repeats until reaching the root node (parent_id = -1).

Google AdInline article slot

In Qdrant, data is stored as a flat list of points with metadata for filtering. This enables flexible manipulation of the hierarchical structure through payload fields.

Hybrid Search & RRF

Point comparison combines dense and sparse embeddings using Reciprocal Rank Fusion:

$$RRFscore(d \in D) = \sum_{r \in R} \frac{1}{k + r(d)}$$

Google AdInline article slot

where $D$ is the set of documents, $R$ represents rankings (dense, sparse), and $k=60$.

Search Algorithms

Branch Search

Finds top-level roots, then recursively compares parents against children. If children score better, it dives deeper; otherwise, it locks onto the parent.

def parent_vs_children(query: str, parent: ScoredPoint) -> list[ScoredPoint]:
    file_name = parent.payload["file_name"]
    parent_id = parent.payload["id"]
    child_ids = parent.payload["child_ids"]
    all_ids = [parent_id] + child_ids

    if len(child_ids) == 0:
        console.print(
            f"Parent with id {parent_id} is leave!", style="bold underline violet"
        )
        return []

    console.print()
    console.print("#" * 20, style="red")
    console.print(
        f"Children (ids: {child_ids}) [underline]VS[/underline] Parent (id: {parent_id}) [ FILE: {file_name} ]\nFor query: {query[:20]}...",
        style="bold violet",
    )

    with RAGalicClient() as client: # artifacts from a previous life :)
        dense_model: str = client.client.embedding_model_name  # type: ignore
        sparse_model: str = client.client.sparse_embedding_model_name  # type: ignore
        children_and_parent_filter = Filter(
            must=FieldCondition(key="id", match=MatchAny(any=all_ids))
        )
        sorted_points = client.client.query_points(
            collection_name="ragalic",
            prefetch=[
                Prefetch(
                    query=Document(text=query, model=dense_model),
                    using="dense",
                    limit=len(all_ids),
                    filter=children_and_parent_filter,
                ),
                Prefetch(
                    query=Document(text=query, model=sparse_model),
                    using="sparse",
                    limit=len(all_ids),
                    filter=children_and_parent_filter,
                ),
            ],
            query=FusionQuery(fusion=Fusion.RRF),
            query_filter=children_and_parent_filter,
            limit=len(all_ids),
        ).points

    children_better_then_parent = []
    for point in sorted_points:
        if point.payload["id"] == parent_id:
            break
        children_better_then_parent.append(point)

    ids = [child.payload["id"] for child in children_better_then_parent]
    console.print(
        f"Child ids which is better than parent: {ids if ids else 'N/A'}",
        style="italic purple",
    )
    console.print("#" * 20, style="red")
    console.print()

    return children_better_then_parent


def branch_search(query: str, num_roots: int = 3) -> list[Chunk]:
    roots = find_roots(query=query, num_to_find=num_roots)

    final_points = []
    points_to_process = roots
    while len(points_to_process) > 0:
        new_point_to_process = []
        for point in points_to_process:
            new_points = parent_vs_children(query=query, parent=point)
            if len(new_points) == 0:
                console.print(
                    f"--- NEW FINAL POINT (id: {point.payload['id']} | file: {point.payload['file_name']} | pages: {point.payload['page_start']} - {point.payload['page_end']}) ---",
                    style="bold green",
                )
                final_points.append(point)
            else:
                new_point_to_process.extend(new_points)
        points_to_process = new_point_to_process

    console.print(
        f"--- WAS FOUND {len(final_points)} POINTS FOR QUERY: '{query[:20]}...' ---",
        style="bold green on white",
    )

    return prepare_chunks(final_points)

Usage:

Google AdInline article slot
from rag_lib.search import branch_search

results = branch_search(query="your_query", num_roots=3)
for chunk in results:
    print(f"{chunk.file_name} (pages {chunk.page_start}-{chunk.page_end}):\n{chunk.text[:200]}...")

Pros: High recall. Cons: May include irrelevant branches.

Beam Search (Fixed)

  • Take current generation of parents and their children.
  • Winners among parents eliminate their children; winners among children eliminate their parents.
  • Survivors compete for a fixed beam width.

Reduces noise but risks cutting off relevant content or including irrelevant ones.

Adaptive Beam with Knee-Point

Dynamic beam width using the knee-point method. Identifies the inflection point on the RRF curve (downward convexity).

Dynamically prunes noise: at each iteration, apply knee-pruning first, then generate the next beam. Discards ~62% of irrelevant documents.

Key Takeaways

  • Hierarchical trees in Qdrant with inherited keywords.
  • Hybrid search + RRF for effective node comparison.
  • Knee-point pruning for dynamic relevance-based chunking.
  • Branch search for completeness, adaptive beam for efficiency.
  • Structured LLM output minimizes hallucinations.

— Editorial Team

Advertisement 728x90

Read Next