◉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-17Advanced

A Japanese query won't surface its English twin — when embeddings notice language before meaning

Embed a translation pair with gemini-embedding-2 and the two halves won't be nearest neighbours, because language itself inflates similarity. Here is how I measured cross-lingual recall using translation pairs as ground truth, and what happened when I subtracted the language centroid.

Gemini API249gemini-embedding-27multilingual8vector searchevaluation5

✦ Premium Article

Across four sites I publish every article in Japanese and English as a matched pair. If one half is missing, switching languages returns a 404, so the counts always match. One site currently sits at 725 JA and 725 EN. Which means I hold 725 documents whose meaning is identical and whose language is the only difference — ground truth, already labelled, for free.

I wanted to use those pairs to build a pre-publication duplicate check. Embed a new Japanese draft, retrieve the closest existing articles, and since both languages live in the same index, catch cross-language overlap too.

The first query returned ten Japanese articles. Every one of them. And the draft I queried with had an English counterpart that already existed — one whose content matched almost exactly. It lost to Japanese articles about entirely different topics.

My task_type values were correct. My vectors were normalised. The problem sat further upstream than either.

Cosine similarity is also measuring what language you wrote in

An embedding model turns text into a vector. You would like that vector to carry meaning. In practice it also carries the language the text was written in.

Multilingual models are trained to bring meanings closer across languages, true. They are not trained to align them perfectly. What you get instead is a space where Japanese text loosely clusters in one region and English text clusters in another.

PairSemantic distanceSame language?Observed similarity
Japanese draft ↔ its English translationnear-identicalnomedium
Japanese draft ↔ unrelated Japanese articlefaryesmedium to high

The bonus that language agreement pays out is large enough to cover the semantic gap. Those ten Japanese results were not a model failure. I had been measuring language and meaning together while believing I measured only meaning.

Whether that matters depends entirely on your usage. If you only ever search within one language, the language component loads onto every vector equally and rankings are unaffected. It bites at exactly one moment: when Japanese and English share an index and you query across both. That is the moment I walked into.

Translation pairs are already an evaluation set

Measurement first. The nice property of translation pairs is that nobody has to label anything. When I query with the Japanese version of gemini-3-multitool-agent-guide.mdx, the correct answer is the English file with the same slug. Decided in advance.

A hundred pairs is enough to see the shape. The harness below embeds both halves and reports JA→EN recall and MRR.

import os, json, math, random
from google import genai
from google.genai import types
 
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
MODEL = "gemini-embedding-2"
 
def embed(texts, task_type):
    """Embed a list of texts, batching to stay under the API limit."""
    out = []
    for i in range(0, len(texts), 32):
        chunk = texts[i:i + 32]
        res = client.models.embed_content(
            model=MODEL,
            contents=chunk,
            config=types.EmbedContentConfig(task_type=task_type),
        )
        out.extend([e.values for e in res.embeddings])
    return out
 
def l2_normalize(v):
    n = math.sqrt(sum(x * x for x in v))
    return [x / n for x in v] if n else v
 
def cosine(a, b):
    return sum(x * y for x, y in zip(a, b))
 
# pairs: [{"slug": "...", "ja": "first 2000 chars of the JA body", "en": "English body ..."}, ...]
pairs = json.load(open("pairs.json"))
random.seed(42)
pairs = random.sample(pairs, min(100, len(pairs)))
 
ja_vecs = [l2_normalize(v) for v in embed([p["ja"] for p in pairs], "RETRIEVAL_QUERY")]
en_vecs = [l2_normalize(v) for v in embed([p["en"] for p in pairs], "RETRIEVAL_DOCUMENT")]
 
def evaluate(queries, docs):
    """queries[i] should retrieve docs[i]. Collect rank statistics."""
    hits = {1: 0, 5: 0, 10: 0}
    rr_sum = 0.0
    for i, q in enumerate(queries):
        scored = sorted(
            ((cosine(q, d), j) for j, d in enumerate(docs)),
            reverse=True,
        )
        rank = next(r for r, (_, j) in enumerate(scored, start=1) if j == i)
        for k in hits:
            if rank <= k:
                hits[k] += 1
        rr_sum += 1.0 / rank
    n = len(queries)
    return {
        "recall@1": hits[1] / n,
        "recall@5": hits[5] / n,
        "recall@10": hits[10] / n,
        "MRR": rr_sum / n,
    }
 
print("baseline:", evaluate(ja_vecs, en_vecs))

At this point the candidate pool holds English documents only. The language bonus applies uniformly to every candidate, so recall comes out clean. On my corpus baseline recall@1 landed around 0.9.

The interesting part is the next step. Add the Japanese articles to the pool.

# Put the Japanese articles into the same index (excluding the query itself)
ja_docs = [l2_normalize(v) for v in embed([p["ja"] for p in pairs], "RETRIEVAL_DOCUMENT")]
 
def evaluate_mixed(queries, en_docs, ja_docs):
    hits = {1: 0, 5: 0, 10: 0}
    for i, q in enumerate(queries):
        cands = [(cosine(q, d), ("en", j)) for j, d in enumerate(en_docs)]
        cands += [(cosine(q, d), ("ja", j)) for j, d in enumerate(ja_docs) if j != i]
        cands.sort(reverse=True)
        rank = next(r for r, (_, key) in enumerate(cands, start=1) if key == ("en", i))
        for k in hits:
            if rank <= k:
                hits[k] += 1
    n = len(queries)
    return {f"recall@{k}": v / n for k, v in hits.items()}
 
print("mixed:", evaluate_mixed(ja_vecs, en_vecs, ja_docs))

recall@1 collapsed the moment Japanese candidates entered the pool. The gap between the two runs is the size of the language component in your data. An index that looked healthy on a single-language benchmark became a different animal once it went multilingual. Being able to put a number on that drop was the real payoff of the harness.

✦

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
✦Use translation pairs you already own as a labelled evaluation set and measure your index's cross-lingual recall today
✦Restore cross-lingual ranking with language-centroid removal — vector math only, zero extra API calls at query time
✦Choose between centroid removal, a translation bridge, and per-language indexes on latency and cost grounds
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-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.
⬡ Advanced2026-09-14
When BLOCK_NONE Changes Nothing: Telling Adjustable Gemini Blocks from the Ones You Can't Move
Some safety blocks go away when you loosen a threshold. Others never will. Here is how to read the response and decide which kind you are looking at before you touch a single setting.
⬡ 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.