GEMINI LABJP
PRO35 — July 17, the date reported as Gemini 3.5 Pro's target, has arrived with no official Google announcement or model card confirmed as of todayUNCONFIRMED — The 2M-token context window and pricing both remain unconfirmed by Google; reports place the model in limited enterprise preview on Vertex AIFLASH35 — gemini-3.5-flash is the current GA release, and is now the model that gemini-flash-latest points toAGENTS — Managed Agents enter public preview in the Gemini API, running stateful, autonomous agents inside isolated Google-hosted Linux sandboxesSEARCH — File Search adds multimodal search, letting you natively embed and query images with gemini-embedding-2WEBHOOKS — Event-driven webhooks arrive to replace polling workflows for the Batch API and long-running operationsPRO35 — July 17, the date reported as Gemini 3.5 Pro's target, has arrived with no official Google announcement or model card confirmed as of todayUNCONFIRMED — The 2M-token context window and pricing both remain unconfirmed by Google; reports place the model in limited enterprise preview on Vertex AIFLASH35 — gemini-3.5-flash is the current GA release, and is now the model that gemini-flash-latest points toAGENTS — Managed Agents enter public preview in the Gemini API, running stateful, autonomous agents inside isolated Google-hosted Linux sandboxesSEARCH — File Search adds multimodal search, letting you natively embed and query images with gemini-embedding-2WEBHOOKS — Event-driven webhooks arrive to replace polling workflows for the Batch API and long-running operations
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 API189gemini-embedding-27multilingual8vector searchevaluation4

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-07-15
A near-miss label won't fix itself on retry — a normalization layer for closed-vocabulary classification
When responseSchema enum returns an out-of-set label, retrying tends to return the same near-miss. From a wallpaper app's 30-category batch, here is the distribution of how labels miss, plus a normalization layer built on an alias table and gemini-embedding-2 nearest-neighbor, with measured results.
Advanced2026-07-10
The Day We Went From 30 Categories to 34 — Reclassifying 1,180 Assets Instead of 8,142
Adding categories to a taxonomy does not require reclassifying everything. Here is how embeddings and confidence margins narrowed a backfill from 8,142 assets to 1,180, with the numbers.
Advanced2026-07-16
What language should your system instruction be in? Measuring three approaches when most prompts arrive in the user's language
Keep the system instruction in English, or translate it into the user's language? I measured input tokens per language with countTokens, then lined up output-language match and schema compliance to find where nine tokens is enough.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →