◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/Advanced
⬡ Advanced/2026-07-08Advanced

One File Search Store for Many Apps: Splitting Retrieval With customMetadata and Chunk Config

Put several apps' FAQs in a single Gemini File Search store and metadataFilter can silently return empty grounding, or answers get split across chunk boundaries. Here is the customMetadata design, the AIP-160 filter-syntax trap, and measured chunkingConfig tuning.

Gemini API249File Search7RAG14customMetadataindie development28

✦ Premium Article

When I first moved my apps' help FAQs into Gemini's File Search, I gave each app its own store. Wallpaper app, relaxation app, one store each. It felt like the obvious layout.

In practice, every extra store meant another re-indexing job and another thing to track. Fixing a single FAQ line meant first remembering which store it lived in. As an indie developer running several apps alone, that small tax adds up.

So I switched to putting every app's FAQ in one store and narrowing by app only at query time. File Search offers customMetadata and metadataFilter. Tag each document with app_id, filter on app_id at query time, and a single store should never bleed one app's answers into another.

Should. I say "should" because metadataFilter kept returning empty answers with no exception and no warning. This article walks through how I isolated that, plus a second issue I found along the way: answers getting dropped when they split across a chunk boundary, and how I tuned it with real numbers.

Why merge into one store — an indie developer's call

Split stores or one shared store? For a solo developer, that decision is settled less by "what is correct" and more by "what I can keep maintaining."

A File Search store is a container that persists embeddings. Raw files are deleted after 48 hours, but the embeddings you import stay until you delete them manually. So a store is an object where every new one adds another target for deletion and re-import.

The docs note that a store's size is roughly three times the input (input plus generated embeddings), and recommend keeping each store under 20 GB. For indie-scale FAQs, size is never the problem. The count is.

My rule became: split by update frequency and isolation requirements, not by data volume. Things I edit constantly and must never mix between apps — like FAQs — go into one shared store and are separated logically by app_id. Blocks that need a different embedding model or a different re-indexing strategy get their own physical store. "Physical split only when forced, logical split by metadata" is the line I found easiest to sustain.

Store staleness itself is a separate topic; I wrote up drift detection in a note on File Search stores quietly going stale in production. This article stays focused on the shared-store case.

Separating apps with customMetadata

Isolation starts with the customMetadata you attach at import time — key/value pairs, up to 20 per document.

from google import genai
import time
 
client = genai.Client()
 
# Use the multimodal embedding model so images can live here too
store = client.file_search_stores.create(
    config={
        "display_name": "app-faqs-shared",
        "embedding_model": "models/gemini-embedding-2",
    }
)
 
def import_faq(path: str, app_id: str, kind: str, lang: str):
    # Upload via the Files API, then import into the store
    src = client.files.upload(file=path, config={"name": f"faq-{app_id}"})
    op = client.file_search_stores.import_file(
        file_search_store_name=store.name,
        file_name=src.name,
        # The key to separation: attach app_id / kind / lang
        custom_metadata=[
            {"key": "app_id", "string_value": app_id},
            {"key": "kind", "string_value": kind},
            {"key": "lang", "string_value": lang},
        ],
    )
    while not op.done:
        time.sleep(5)
        op = client.operations.get(op)
    return op
 
import_faq("faq_wallpaper_en.md", app_id="wallpaper", kind="faq", lang="en")
import_faq("faq_healing_en.md",   app_id="healing",   kind="faq", lang="en")
# Expect: two documents in one store, each chunk tied to an app_id

One design note that saves regret later: decide the value type up front. Whether app_id is a string or a number changes how you write the metadataFilter below. My rule is that identifiers are always string_value, and only thresholds I want to compare — a year or a version number — use numeric_value. Mix them and you will forget which type a key is when you write the filter.

✦

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
✦If you have been spinning up one File Search store per app and drowning in upkeep, you can switch to a single shared store with metadata-based isolation
✦You will be able to diagnose why metadataFilter returns empty grounding with no exception, using an AIP-160 syntax isolation procedure
✦You can stop FAQ answers from being dropped when they straddle a chunk boundary, by tuning max_tokens_per_chunk and overlap against measured results
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

Gemini 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

⬡ Advanced2026-08-31
Define What Counts as a Duplicate Before Adding Embedding Search to Your Image Pipeline
My dedup check flagged two prints with identical composition but different colors as one image. Here is how I split duplicate detection into three layers, measured where perceptual hashing ends, and decided what embedding search is actually for.
⬡ Advanced2026-09-05
Two Kinds of Video Questions: Why I Send "Find It" and "Prove It Isn't There" Down Separate Paths
Agentic video understanding lets the model decide which parts of a video to watch. That works beautifully for finding things, and it quietly breaks when you need to prove something never appears. Here is how I split my questions, and the coverage check I now run first.
⬡ Advanced2026-09-24
I Stopped Asking Gemini 'Is This Right?' and Started Hiding My Answer Instead
Ask Gemini to confirm a label you already chose and it tends to agree. Here is why I compared three ways of asking, chose blind re-classification, how my answer still leaked in, and the routing code that sends only disagreements to a human.