●SPARK — Gemini Spark, Google's agentic assistant, arrives in the Gemini app for macOS and works on tasks on your behalf●BRIEF — Daily Brief, powered by Personal Intelligence, distills priorities from Gmail and Calendar with suggested next steps●MODEL — Gemini 3.5 Flash reaches GA, offering faster, lower-cost AI with a 1M-token context window●GROWTH — The Gemini app has passed 900 million monthly active users, up from 400 million a year earlier●APPS — Spark now connects to Google Tasks and Google Keep and stays up to date on topics in real time●ENTERPRISE — Gemini 3.5 Flash is generally available across the Global, US, and EU regions on Gemini Enterprise●SPARK — Gemini Spark, Google's agentic assistant, arrives in the Gemini app for macOS and works on tasks on your behalf●BRIEF — Daily Brief, powered by Personal Intelligence, distills priorities from Gmail and Calendar with suggested next steps●MODEL — Gemini 3.5 Flash reaches GA, offering faster, lower-cost AI with a 1M-token context window●GROWTH — The Gemini app has passed 900 million monthly active users, up from 400 million a year earlier●APPS — Spark now connects to Google Tasks and Google Keep and stays up to date on topics in real time●ENTERPRISE — Gemini 3.5 Flash is generally available across the Global, US, and EU regions on Gemini Enterprise
When gemini-embedding-2 Retrieval Feels 'Almost Right,' Check task_type First
When gemini-embedding-2 search misses in a frustrating near-hit way, the cause is often a missing or mismatched task_type. Here is how to align document and query intent, plus the code and a tiny harness to prove the difference on your own data.
When I rebuilt the tag search for one of my wallpaper apps on gemini-embedding-2, the results were "not wrong, just off." A search for "sunset ocean" surfaced sunset mountains and empty beaches, and the photo I actually wanted sat in seventh place. When accuracy is zero, you suspect a plain bug. When it is merely close, the cause hides.
I spent half a day poking at normalization and chunk sizes before the real culprit surfaced: task_type. I had embedded both the documents and the query without specifying it. The moment I aligned it, that seventh-place photo jumped to second.
I suspect this happens more often now, not less. Since gemini-embedding-2 went GA and can embed both text and images with one model, people swap models more freely, and the task_type setting quietly gets dropped in the process. As an indie developer running several apps at Dolice, I hit this exact trap, so let me walk through the cause and the fix, with the numbers I measured myself.
task_type tells the model what the embedding is for
An embedding model turns your text into a vector. But the same sentence has a slightly different optimal direction depending on whether it will be used as a document to be searched, or as a query doing the searching.
task_type makes that intent explicit. With gemini-embedding-2 you get embeddings optimized separately for documents and for queries. Leave it unset and you get a general-purpose embedding that is not tuned for the asymmetric task of retrieval, where a short query has to pull long documents.
This is the counterintuitive part. Since queries and documents are compared in the same space, it feels like they should be treated identically. In practice, for an asymmetric task, specifying "for query" and "for document" separately makes the cosine-similarity ranking noticeably more stable.
The common mistake: embedding both sides with no task_type
This is exactly what I was doing. At both index time and search time, I embedded through the same function without passing task_type.
from google import genaiclient = genai.Client() # reads GEMINI_API_KEY from the environmentdef embed_plain(text: str) -> list[float]: # No task_type = a generic embedding with no stated purpose resp = client.models.embed_content( model="gemini-embedding-2", contents=text, ) return resp.embeddings[0].values# Both indexing and searching use the same functiondoc_vec = embed_plain("A seaside landscape washed in sunset light")query_vec = embed_plain("sunset ocean")
The code does not error. That is what makes it insidious. Search runs, plausible results come back, and because it is not tuned for the asymmetric case, the ranking is just blurry. That "close but off" symptom is exactly what this state produces.
✦
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 your search keeps missing in an almost-right way, aligning task_type alone can recover the recall you were losing
✦You will get copy-paste-ready code that uses RETRIEVAL_DOCUMENT and RETRIEVAL_QUERY correctly
✦You can adopt a habit of A/B testing task_type against a small golden set in your own project
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.
The fix: split task_type between documents and queries
The correction is simple. Embed indexed documents with RETRIEVAL_DOCUMENT and search queries with RETRIEVAL_QUERY. Keep output_dimensionality identical on both sides.
from google import genaifrom google.genai import typesimport numpy as npclient = genai.Client()DIM = 1536 # keep this identical for documents and queriesdef embed_document(text: str) -> np.ndarray: resp = client.models.embed_content( model="gemini-embedding-2", contents=text, config=types.EmbedContentConfig( task_type="RETRIEVAL_DOCUMENT", output_dimensionality=DIM, ), ) v = np.array(resp.embeddings[0].values, dtype=np.float32) return v / np.linalg.norm(v) # re-normalize after truncating dimensionsdef embed_query(text: str) -> np.ndarray: resp = client.models.embed_content( model="gemini-embedding-2", contents=text, config=types.EmbedContentConfig( task_type="RETRIEVAL_QUERY", output_dimensionality=DIM, ), ) v = np.array(resp.embeddings[0].values, dtype=np.float32) return v / np.linalg.norm(v)# Index sidedocs = [ "A seaside landscape washed in sunset light", "Mountain ridges and a sea of clouds at dusk", "A bright, crystal-clear tropical beach at noon",]index = np.vstack([embed_document(d) for d in docs])# Query sideq = embed_query("sunset ocean")scores = index @ q # normalized, so dot product = cosine similarityranking = np.argsort(-scores)for rank, i in enumerate(ranking, start=1): print(f"#{rank}: {docs[i]} score={scores[i]:.3f}")
Three things matter here. Split task_type between documents and queries. If you truncate with output_dimensionality, re-run L2 normalization (truncation makes the norm drift from 1). And compare using the dot product of normalized vectors.
The reason re-normalization is needed is that gemini-embedding-2 supports Matryoshka representation learning, so you can drop the tail to reduce dimensions. A freshly truncated vector no longer has length 1, which breaks the assumption behind approximating cosine similarity with a dot product. This directly affects ranking. For the dimension-versus-storage tradeoff, designing storage reduction by trimming gemini-embedding output to 256 dimensions is a useful companion read.
What I measured (with a caveat on how to read it)
Let me be candid: the numbers below come from a small set of my own (help articles plus wallpaper tags, about 1,200 items total). This is not an official benchmark. How much task_type helps depends on the nature of your corpus, so treat this as a direction, not a promise.
In my environment, going from no task_type to "document = RETRIEVAL_DOCUMENT, query = RETRIEVAL_QUERY" raised top-5 recall from roughly 0.72 to about 0.86. The gain was largest for short queries. The fewer the words, like the two-word "sunset ocean," the bigger the improvement from stating intent.
Setting
recall@5 (my ~1,200 items)
Feel
No task_type (unset on both sides)
~0.72
Frequent near-misses
Both set to RETRIEVAL_DOCUMENT
~0.74
Barely changed
Document = DOCUMENT / Query = QUERY
~0.86
Short queries stabilize
What surprised me was the middle row: forcing both sides to RETRIEVAL_DOCUMENT barely helped. It is not enough to "just set task_type." It only works once documents and queries play separate roles.
The task_type options, and how to choose
gemini-embedding-2 offers task types beyond retrieval. Here are the ones I reach for most. Since the right task_type changes with the job, running everything through "RETRIEVAL_DOCUMENT by default" is a mistake in some cases.
task_type
When to use it
One side or both?
RETRIEVAL_DOCUMENT
Indexing documents that will be searched
Document side only
RETRIEVAL_QUERY
The query that searches that index
Query side only
SEMANTIC_SIMILARITY
Measuring how alike two sentences are, symmetrically
Both, the same
CLASSIFICATION
Using embeddings as features to train a classifier
Depends on use
CLUSTERING
Grouping reviews or tags into clusters
Both, the same
CODE_RETRIEVAL_QUERY
Searching code snippets with natural language
Query side only
Here is the line I draw when unsure. If the task is asymmetric (short query pulling long documents, natural language pulling code), split DOCUMENT and QUERY. If it is symmetric (similarity of two sentences, clustering), unify both sides on SEMANTIC_SIMILARITY or CLUSTERING. That single distinction removes most of the guesswork.
I group App Store user reviews with CLUSTERING. Same embedding backend, entirely different job, so of course task_type changes too. For the operational side of handling multilingual reviews, I wrote up building multilingual Google Play review replies with Gemini.
A migration trap: existing indexes sometimes need a rebuild
The easiest place to get burned is when you change a model or a setting. Changing task_type after the fact means re-embedding the document side as well. If RETRIEVAL_QUERY document vectors get mixed into an index built with RETRIEVAL_DOCUMENT, the same document lands in a different place.
The rule is simple. If you change the document-side task_type, dimension, or model, re-embed that whole index. If only the query side changed, leave the index alone. Forget this asymmetry and you enter the maze of "I fixed the query and nothing improved."
A full re-embed costs money, so you want to avoid doing it often. Around 1,200 items finishes in a few minutes, but at hundreds of thousands the calculus changes. For a sense of re-embedding cost and caching, the thinking in cutting Gemini API cost with context caching transfers well. Treat rebuilding an index as a design decision, not something you can do on a whim.
A/B test task_type against a small golden set
Because task_type's effect is corpus-dependent, I always confirm it on my own data. You do not need a heavy evaluation harness. Twenty to thirty pairs of "for this query, this document is correct" is plenty.
import numpy as np# Hand-build 20-30 (query, correct document index) pairsgolden = [ ("sunset ocean", 0), ("city lights at night", 5), ("snowy mountain morning", 8), # ... list real queries from your own app]def recall_at_k(embed_q, index, golden, k=5): hits = 0 for query, gold_idx in golden: q = embed_q(query) topk = np.argsort(-(index @ q))[:k] if gold_idx in topk: hits += 1 return hits / len(golden)# index_doc is the matrix built with RETRIEVAL_DOCUMENT# embed_query is the RETRIEVAL_QUERY version; embed_plain_normalized has no task_typer_tuned = recall_at_k(embed_query, index_doc, golden)r_plain = recall_at_k(embed_plain_normalized, index_doc, golden)print(f"RETRIEVAL_QUERY: {r_tuned:.3f} / no task_type: {r_plain:.3f}")
Those twenty lines shift you from "it feels a bit better" to "recall@5 went from 0.72 to 0.86." As an indie developer it is tempting to postpone evaluation, but embedding-config changes are exactly where this small instrument pays off. Even with a managed layer like File Search, it is worth understanding how task_type is handled inside it via the Gemini File Search API, then confirming with your own queries.
Wrapping up
When search misses in that "almost right" way, check task_type before you touch normalization or chunking. Documents get RETRIEVAL_DOCUMENT, queries get RETRIEVAL_QUERY. That single alignment tightens the ranking more often than you would expect.
For your next step, pick just five live queries, pair each with its correct document, and measure recall@5. The moment a number appears, the next thing to fix becomes visible. Ever since I started carrying this little instrument, my uncertainty around embeddings dropped a great deal. Thank you for reading.
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.