Map-Reduce Summarization

Map, Then Reduce

long document ──► chunk 1 ──► summary 1 ─┐
              ├─► chunk 2 ──► summary 2 ─┼──► combine ──► final summary
              └─► chunk 3 ──► summary 3 ─┘
                  (map: parallel, small model)   (reduce: one call, stronger model)

Map-Reduce with a Stand-in Summarizer

Example

fake_summarize stands in for a model call (it keeps sentences containing numbers or key words). The structure (chunk, parallel map, reduce) is the real pattern:

import re
from concurrent.futures import ThreadPoolExecutor

report = """Q3 sales grew 18% to Rs 42 crore. Festive season orders doubled in October.
Most growth came from tier-2 cities. The team also redesigned the app home screen.
Return rates fell from 9% to 6% after better size charts. Customer support tickets dropped 12%.
Delivery times in the North-East are still slow at 7 days. A new warehouse in Guwahati opens in Q4.
The office moved to a new floor. Marketing spend rose 25% for the Diwali campaign."""

KEY = ("grew", "fell", "dropped", "rose", "opens", "doubled", "slow")

def fake_summarize(text, max_sentences=2):
    """Stand-in for a model call: keep the sentences with numbers or key verbs."""
    sentences = re.split(r"(?<=\.)\s+", text.strip())
    scored = sorted(sentences, key=lambda s: -(bool(re.search(r"\d", s)) + any(k in s for k in KEY)))
    keep = set(scored[:max_sentences])
    return " ".join(s for s in sentences if s in keep)

chunks = report.splitlines()
with ThreadPoolExecutor() as pool:                                   # map (in parallel)
    partials = list(pool.map(lambda c: fake_summarize(c, max_sentences=1), chunks))
for i, s in enumerate(partials, 1):
    print(f"chunk {i}: {s}")

final = fake_summarize(" ".join(partials), max_sentences=4)          # reduce
print("\nFINAL:", final)

Output

chunk 1: Q3 sales grew 18% to Rs 42 crore.
chunk 2: Most growth came from tier-2 cities.
chunk 3: Return rates fell from 9% to 6% after better size charts.
chunk 4: Delivery times in the North-East are still slow at 7 days.
chunk 5: Marketing spend rose 25% for the Diwali campaign.

FINAL: Q3 sales grew 18% to Rs 42 crore. Return rates fell from 9% to 6% after better size charts. Delivery times in the North-East are still slow at 7 days. Marketing spend rose 25% for the Diwali campaign.

With Real Calls

import asyncio, anthropic

client = anthropic.AsyncAnthropic()

async def summarize(text: str, instruction: str, model: str) -> str:
    r = await client.messages.create(
        model=model, max_tokens=800,
        messages=[{"role": "user", "content": f"{instruction}\n\n<text>\n{text}\n</text>"}],
    )
    return next(b.text for b in r.content if b.type == "text")

async def map_reduce(chunks: list[str]) -> str:
    partials = await asyncio.gather(*[
        summarize(c, "Summarize this part of a business report in 5 bullets. Keep all numbers.", "claude-haiku-5-5")
        for c in chunks
    ])
    joined = "\n\n".join(f"<part n='{i}'>{p}</part>" for i, p in enumerate(partials, 1))
    return await summarize(joined, "Combine these part summaries into one executive summary: "
                                   "8 bullets max, most important first, keep numbers.", "claude-sonnet-5-5")

print(asyncio.run(map_reduce(chunks)))

Tip: Limit concurrency (for example with an asyncio.Semaphore(5)) so a 300-chunk document does not hit your rate limit all at once.