◉GEMINI LABJP
●VEO 3.1 — The three veo-3.1 preview models shut down Oct 22, 14 days left. Move to gemini-omni-1.1-flash●OMNI — gemini-omni-flash-preview also shuts down Oct 22, not Sep 30. Check the deprecations table●NANO 2.1 — gemini-nano-banana-2.1 is GA. gemini-3.1-flash-image is deprecated with no shutdown date yet●CLI 0.63 — Gemini CLI stable is v0.63.0, with clearer retry progress on reconnect●TTS/LIVE — 2.5-era audio and TTS previews shut down Nov 17, 40 days left●NEW — Veo 3.1 previews stop Oct 22: take inventory before you swap●VEO 3.1 — The three veo-3.1 preview models shut down Oct 22, 14 days left. Move to gemini-omni-1.1-flash●OMNI — gemini-omni-flash-preview also shuts down Oct 22, not Sep 30. Check the deprecations table●NANO 2.1 — gemini-nano-banana-2.1 is GA. gemini-3.1-flash-image is deprecated with no shutdown date yet●CLI 0.63 — Gemini CLI stable is v0.63.0, with clearer retry progress on reconnect●TTS/LIVE — 2.5-era audio and TTS previews shut down Nov 17, 40 days left●NEW — Veo 3.1 previews stop Oct 22: take inventory before you swap
Articles/API / SDK
◈ API / SDK/2026-06-19Advanced

Catch Near-Duplicate Images Before You Publish with gemini-embedding-2

This is about removing near-duplicates, not image search. Use gemini-embedding-2 multimodal embeddings to vectorize images, cluster them, and build a pre-publish gate — with working code and threshold guidance.

gemini116gemini-embedding-27embeddings12image5deduplication

✦ Premium Article

When you run several sites, image assets bite you later not because you have too few, but because near-identical ones quietly pile up. As an indie developer I keep the OGP images for four blogs plus a set of wallpaper apps under Dolice, and for the last six months I've increasingly paused on "wait, haven't I already published this pale blue abstract background?" Checking one by one stops being realistic once the count crosses three digits.

In June 2026, File Search gained multimodal search with gemini-embedding-2, which adds a clean tool for this problem. But what we want here is not search. Instead of finding and pulling back similar images, we want to reject images that are too similar before they ship. These two goals differ in both intent and implementation, and conflating them lets the gate pass everything through.

Why image search can't reject near-duplicates

Retrieval returns "the top N closest to a query." It always returns something, and a loose threshold still works. Near-duplicate detection needs something else: a binary judgment of "are these two images close enough to be considered effectively the same?"

If you repurpose retrieval directly, the single closest item is always returned, so completely unrelated images still line up as "similar candidates." Conversely, if you leave the threshold tuned for search, you miss the recolors and crops you actually want to catch. A near-duplicate gate has to switch to a design where the score itself is the decision boundary.

I underestimated this difference at first and judged "no duplicates" just by glancing at the top File Search results. In reality, similarly composed gradient backgrounds had grown into three lineages; the search context simply treated them as separate hits.

Vectorize the images

First, convert each image into an embedding vector with gemini-embedding-2. Because it's multimodal, you pass an image part to the same endpoint you'd use for text.

import os
from pathlib import Path
from google import genai
from google.genai import types
 
client = genai.Client(api_key="YOUR_API_KEY")
 
EMBED_MODEL = "gemini-embedding-2"  # multimodal (GA, 2026-06)
 
def embed_image(path: Path) -> list[float]:
    data = path.read_bytes()
    mime = "image/png" if path.suffix.lower() == ".png" else "image/jpeg"
    resp = client.models.embed_content(
        model=EMBED_MODEL,
        contents=[types.Part.from_bytes(data=data, mime_type=mime)],
    )
    return resp.embeddings[0].values

If you L2-normalize the vectors up front, the dot product is the cosine similarity, which keeps the later math simple.

import math
 
def l2_normalize(v):
    norm = math.sqrt(sum(x * x for x in v)) or 1.0
    return [x / norm for x in v]
 
def cosine(a, b):
    # a, b are normalized -> dot product is the cosine similarity
    return sum(x * y for x, y in zip(a, b))
 
def build_index(paths):
    index = {}
    for p in paths:
        index[str(p)] = l2_normalize(embed_image(p))
    return index

Since embedding hits the API once per image, a naive implementation pays that cost every run as assets grow. In my own setup, I store vectors locally keyed by the file hash and skip re-embedding any image whose content hasn't changed.

✦

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 keep hesitating because 'I think I've published something like this before,' you can now flag near-duplicates automatically before publishing
✦You get working code that vectorizes images with gemini-embedding-2 multimodal embeddings and isolates only the near-duplicates via cosine similarity and threshold clustering
✦You'll understand how to choose a threshold and how to handle crops and recolors, so you can grow OGP and wallpaper assets across multiple sites without drift into redundancy
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 $15 for lifetime access
View Membership →

Related Articles

◈ API / SDK2026-09-06
Two Ways to Make a Longer Shot in Gemini Omni 1.1, and Two Different Ways They Break
Omni 1.1 extends scenes in 10-second steps up to 40 seconds cumulative, and interpolates between a pinned first and last frame. Which one you use is decided before your first generation, not after. Here is the planner I run first and the drift check I run last.
◈ API / SDK2026-09-05
Gemini 3.8 Flash Costs the Same Per Token and Can Still Raise Your Bill
Gemini 3.8 Flash carries the same per-token price as 3.7 Flash, but it is designed to work harder, so output tokens can grow and your bill with them. Here is what to measure before you switch, and how to price it across the January boundary.
◈ API / SDK2026-09-04
Screen Loop Seam Clicks With Numbers Before You Hand the Audio to Gemini
An ambient loop that clicks only at the wrap point. Here is the numeric prescreen I run before sending anything to audio understanding, why an absolute threshold fails, and how AAC encoding quietly rebuilt the seam I had just repaired.
📚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