Parent Document and Contextual Retrieval

Small Chunks for Search, Big Chunks for the Answer

Small chunks match queries precisely, but they lack context. Parent-document retrieval searches small child chunks, then sends the whole parent section to the model.

Example: parent-document retrieval

import re
SECTIONS = {
    "Returns > Electronics": "Electronics have a 7 day replacement only policy. They must be unused and in the "
                             "original box. Earbuds and phones need the IMEI or serial number on the invoice.",
    "Returns > Fashion":     "Clothes and shoes can be returned within 10 days. Tags must be attached.",
}
children = []  # (child_text, parent_id)
for parent, text in SECTIONS.items():
    for sentence in re.split(r"(?<=\.)\s+", text):
        children.append((sentence, parent))

def words(t): return set(re.findall(r"[a-z]+", t.lower()))
query = "serial number for earbuds"
best_child, parent = max(children, key=lambda c: len(words(query) & words(c[0])))
print("matched child :", best_child)
print("sent to model :", parent, "->", SECTIONS[parent])

Output

matched child : Earbuds and phones need the IMEI or serial number on the invoice.
sent to model : Returns > Electronics -> Electronics have a 7 day replacement only policy. They must be unused and in the original box. Earbuds and phones need the IMEI or serial number on the invoice.

Contextual Retrieval

A chunk like "They can be replaced within 7 days." is useless on its own: what is "they"? Contextual retrieval (described by Anthropic in 2024) asks a model to write one or two sentences of context for each chunk, and indexes context + chunk for both BM25 and embeddings.

Example: adding context to a chunk

import re
def words(t): return set(re.findall(r"[a-z]+", t.lower()))

chunk = "They can be replaced within 7 days if unused."
context = "From the ShopEase returns policy, section on electronics replacement for earbuds and phones."  # model-written
query = "earbuds replacement days"

print("plain chunk overlap      :", sorted(words(query) & words(chunk)))
print("contextual chunk overlap :", sorted(words(query) & words(context + " " + chunk)))

Output

plain chunk overlap      : ['days']
contextual chunk overlap : ['days', 'earbuds', 'replacement']

Generating the Context Cheaply

def chunk_context(full_doc: str, chunk: str) -> str:
    resp = client.messages.create(
        model="claude-haiku-5-5",
        max_tokens=150,
        system=[{"type": "text", "text": f"<document>\n{full_doc}\n</document>",
                 "cache_control": {"type": "ephemeral"}}],          # same document for every chunk -> cached
        messages=[{"role": "user", "content":
                   f"<chunk>{chunk}</chunk>\nGive a short context that situates this chunk in the document. "
                   "Answer only with the context."}],
    )
    return next(b.text for b in resp.content if b.type == "text")

Tip: Because the whole document is the same for every chunk, prompt caching makes this step much cheaper: only the first call pays full price for the document.