◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/API & SDK
⬡ API & SDK/2026-06-19Advanced

Grounding Claude on Your Own Knowledge Base with search_result Blocks

How to stop your own RAG setup from losing track of which article it cited, using Claude's search_result content block and structured citations — with real numbers from running it across four sites.

Claude API128CitationsRAG4search_resultindie developer21

✦ Premium Article

This starts with an internal agent I built on the Claude API to pick related articles and fact-check claims across the content I publish. I run four technical blogs as an indie developer, and on the Japanese side alone I have more than 600 articles on hand. The usual self-hosted RAG shape: concatenate the search hits into a prompt and ask, "Which of these articles supports this claim?"

The first version worked well enough. But a habit surfaced over time that I could not ignore. Claude would answer "this is described in article X" — and sometimes article X was not among the candidates I had passed in. I also could not trace which part of the concatenated text it had actually read. The provenance dissolved into prose and could not be verified after the fact. For an internal tool, that lack of honesty still bothered me.

The mechanism that forces "which source, which passage" to come back as structured data is Claude's Citations feature, specifically the search_result content block. Here are the notes from swapping it into production and running it for a few weeks, including the potholes along the way.

Why prose citations can't be verified

When you concatenate body text yourself, Claude sees one long string. Headers like ## Article A convey a semantic boundary, but they are not machine-readable reference handles. So the grounding comes back as natural language — "according to Article A" — and you need post-processing to map that back to the source with regular expressions.

That post-processing was brittle. When Claude paraphrased a title slightly, or merged several articles into a vague "based on these," matching failed. In my setup the mechanical match rate plateaued around 70%. The remaining 30% had to be checked by hand, which halved the point of automating it.

The root cause is simply that I was not passing reference IDs. If you don't send them, you can't get them back. The search_result block exists precisely to pass documents in as search results that carry an ID.

Anatomy of the search_result block

search_result is a dedicated block you can place in a message's content array. A single result is expressed as three parts: a source identifier, a title, and an array of text fragments.

search_result_block = {
    "type": "search_result",
    "source": "https://claudelab.net/articles/api-sdk/claude-api-prompt-caching-monthly-cost-half-guide",
    "title": "Halving monthly cost with prompt caching",
    "content": [
        {"type": "text", "text": "Putting a 5-minute TTL cache breakpoint at the end of system..."},
        {"type": "text", "text": "A 1-hour TTL suits large static context. Pricing is..."},
    ],
    "citations": {"enabled": True},
}

The key point is that content is an array of text blocks, not a string. Claude returns the index of which entry it relied on, so how you split the fragments becomes the granularity of your citations. Splitting per paragraph makes it easy to link back to "this paragraph of the article" later. Note too that only blocks with citations.enabled set to true are eligible to be cited.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Make Claude attach which document and which passage it relied on as structured data, every time, via search_result blocks
✦Move off hand-rolled context concatenation and cut request tokens by ~40% while reducing missed citations
✦Wire the citations array back into article links, plus three search_result gotchas I hit in production
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ API & SDK2026-09-06
The translation read perfectly and still crashed at runtime
When you translate app strings with the Claude API, the meaning can be right while the format specifiers quietly break. Here is a severity-aware acceptance check and a repair loop that only re-translates the broken lines.
⬡ API & SDK2026-10-16
Sending 40 Rows From Make to the Claude API Stops at Row 14 — Avoiding the 429 With One Subtraction
Why a no-code automation that sends spreadsheet rows to the Claude API one by one hits a 429 partway through, and how to fix it. Seven spacing strategies tested on a mock bucket, a formula for the Sleep delay, and settings that stop failed rows from vanishing.
⬡ API & SDK2026-10-13
Haiku 5.5 is cheaper per token, but your bill jumps fivefold past about 77k Haiku 4.5 tokens
On the price sheet, Haiku 5.5 costs roughly a tenth of Haiku 4.5. But the same text becomes about 30% more tokens, and a prompt over 100k tokens is billed at five times the rate. Here is the boundary in Haiku 4.5 terms, a script I actually ran, and the changes that return a 400.