xavier-ramirez.com
← How Google’s AI Overview works

Your own AI Overview · 01

Your own AI Overview: the spine

Clone it and run it — retrieval over your own private data, end to end.

Google’s AI Overview retrieves, grounds, and cites over the open web. This series rebuilds that same machinery over your private data — claims, policies, underwriting — the way a regulated insurer can actually ship it. This first post is the whole spine, minimal but real: a write path that ingests change data from an S3 data lake, and a read path that answers a question with citations. One repo, one git clone, runs on your laptop.

01 · Spine02 · Chunking03 · Retrieve & rerank04 · Access control05 · Grounding & citations06 · Evals & guardrails

↳ This post is the spine — the whole pipeline, thin. Each chip after it is one post that deepens that stage.

Worked exampleThis series uses property & casualty (P&C) insurance as its running example — policies, claims and underwriting notes. The pipeline itself is domain-agnostic; the data just happens to be an insurer’s.

Write path

How a claim gets into the index

Before you can retrieve anything, a change in your systems of record has to travel — safely — into a vector store.

Why the commit event, not the S3 event?

The data platform writes many Parquet objects per batch, then commits by updating manifest.json. Raw ObjectCreated events fire on each object before that commit — trigger on them and you read a half-written table. The batch-committed change event is the reliable signal: it only fires once the manifest is updated.

ingestion/change_events.py
class ChangeEvent(StrEnum):
    BATCH_TABLE_WRITTEN = "batchTableWrittenOut"
    BATCH_COMPLETED = "batchCompleted"
    STREAMING_BATCH_COMPLETED = "streamingBatchCompleted"
    SCHEMA_CHANGED = "tableSchemaChanged"

# Events that mean "a committed batch is ready to ingest for this table".
INGESTABLE = {ChangeEvent.STREAMING_BATCH_COMPLETED, ChangeEvent.BATCH_TABLE_WRITTEN}

And the fork that makes a delete a first-class outcome — the tombstone becomes an eviction, not a skipped row:

ingestion/merge.py
for key, row in latest.items():
    doc_id = f"{doc_type}/{key}"
    if _op(row) == "D":
        deletes.append(doc_id)     # tombstone → evict from the index
        continue
    upserts.append(Document(...))  # latest state → chunk, embed, upsert

Read path

Six nodes from question to citation

The query side is one explicit LangGraph graph — step through it and watch a real insurance question turn into a grounded, cited answer.

query graph
01 / 6

One question becomes several

"POL-55012 premium increase reason"
"POL-55012 recent claims"
"POL-55012 rating factors changed"
next: retrieve

That stepper isn’t a diagram of the code — it is the code. The graph is wired once, one node per phase:

own_overview/pipeline/graph.py
g = StateGraph(QueryState)

g.add_node("fan_out",    lambda st: fan_out.run(st, s))
g.add_node("retrieve",   lambda st: retrieve.run(st, s, embedder=embedder, store=store))
g.add_node("rerank",     lambda st: rerank.run(st, s, reranker=reranker))
g.add_node("ground",     lambda st: ground.run(st, s, llm=llm))
g.add_node("guardrails", lambda st: guardrails.run(st, s))
g.add_node("audit",      lambda st: audit.run(st, s))

g.add_edge(START, "fan_out")
g.add_edge("fan_out", "retrieve")
g.add_edge("retrieve", "rerank")
g.add_edge("rerank", "ground")
g.add_edge("ground", "guardrails")
g.add_edge("guardrails", "audit")
g.add_edge("audit", END)

Isolation

Why nothing crosses tenants

The same code serves every insurer and every environment — what keeps them apart is one key carried end to end. A TenantScope (tenant + env) rides on every document, chunk and query, and the vector index is namespaced tenant__env. Retrieval fails closed: a query for acme can only ever look inside acme__prod, so a globex chunk — or a dev chunk — is physically unreachable.

own_overview/contracts.py
@dataclass(frozen=True, slots=True)
class TenantScope:
    tenant_id: str
    env: str  # e.g. "dev", "qa", "prod"

    def namespace(self) -> str:
        return f"{self.tenant_id}__{self.env}"
Run it yourself · own-overview @ v1-spine

This whole post is one repo at one tag. Clone it, seed synthetic P&C insurance data, and ask it a question — grounded and cited, on your laptop, zero cloud.

terminal
git clone https://github.com/xaviramirezcom/open-ai-overview
cd open-ai-overview
git checkout v1-spine

uv sync --extra local
cp .env.example .env        # set the *_PROVIDER / VECTOR_STORE values to "local"

own-overview seed           # generate synthetic P&C data + ingest it
own-overview query "Why did the premium on POL-55012 go up?" --role adjuster
own-overview query "Why did the premium on POL-55012 go up?" --role underwriter

The answer changes with --role. The adjuster gets a cited answer from the claim and the policy; the underwriter additionally sees the restricted risk memo. Same question, same index — different access, enforced at retrieval. That’s the thread Post 4 pulls on.

own_overview/pipeline/graph.py
def build_query_graph(*, settings=None, embedder=None, store=None,
                      reranker=None, llm=None):
    """Compile the query graph. Components default from config but can be
    injected (tests, notebooks, alternate providers)."""
    s = settings or get_settings()
    embedder = embedder or build_embedder(s)
    store = store or build_vector_store(s, embedder=embedder)
    reranker = reranker or build_reranker(s)
    llm = llm or build_llm(s)

    g = StateGraph(QueryState)
    g.add_node("fan_out",    lambda st: fan_out.run(st, s))
    g.add_node("retrieve",   lambda st: retrieve.run(st, s, embedder=embedder, store=store))
    # ... rerank · ground · guardrails · audit, then edges wire them in order ...
    return g.compile()

Teaching-grade reference implementation, not a production insurance product. It reproduces the ideas and the S3-data-lake integration shape; bring your own data and keys. MIT-licensed.

Explain it back

Your team wires the ingestion Lambda to fire on S3 ObjectCreated so new data shows up “the instant it lands.” A week later, answers occasionally cite garbled, half-populated claims. What went wrong, and what should the trigger have been?

Reveal a model answer

The data platform writes a batch as many Parquet objects and only commits it by updating manifest.json. ObjectCreated fires per object, before the commit — so the Lambda sometimes reads a table still being written and indexes partial rows. Trigger instead on the batch-committed change event (streamingBatchCompleted / batchTableWrittenOut) delivered via EventBridge: that event is the commit watermark, so you only ingest a finished batch. Bonus — the same event path is where you honor DELETE tombstones and evict redacted records, something a per-file S3 trigger gives you no clean hook for.

Next in the series · 02
Chunking: split the document wrong and you lose the answer

The spine chunks with a naive splitter — good enough to run, not to trust. How you cut a claim file decides whether the water-damage detail and its policy number land in the same passage or get orphaned into two. Next we make chunking a retrieval decision and measure it.

Continue to Chunking →