Skip to content
Dev ResourcesTested on EmbeddingGemma 2 with sentence-transformers 6.1.0, October 19, 2026

Add Search to Your Product for $0 With EmbeddingGemma 2

Google's open embedding model runs on a laptop and puts text and screenshots in one search space. Install it, index your catalog, search by meaning, find a screenshot from a sentence and serve it from your app. Tested on 2,218 real product listings.

EmbeddingGemma 2 · search by meaning
Your catalog, your screenshots, one search box, no API bill.
EmbeddingGemma 2EmbeddingsSemantic SearchGoogleOpen SourcePythonsentence-transformersSelf-HostingTutorial
Cost
$0 (Apache 2.0, runs locally)
Time
~25 min
Steps
7
Stack
Python 3.12 · sentence-transformers
What you'll get
  • ✓EmbeddingGemma 2 running on your machine, with only the encoders you need loaded
  • ✓Your product catalog or docs indexed with the prompts the model was trained on
  • ✓A search that finds 'check if my idea has demand' when no listing contains those words
  • ✓Screenshots you can find by describing them in a sentence
  • ✓A small search API your app can call from a CPU-only server
Share

Most product search is a keyword match. Someone types "check if my idea has demand", your listing says "validate your startup with market data", and the search box comes back empty. Semantic search fixes that by comparing meaning instead of words, and until recently it meant an embeddings API, a bill per request and your catalog leaving your server.

EmbeddingGemma 2 is Google's open embedding model, released on October 6, 2026 under Apache 2.0. It has 740M parameters in total, but the text part alone is 270M, small enough for a laptop or a cheap server. It puts text, code, images, video and audio into one space of 768 numbers per item, so a sentence can find a screenshot.

I tested every step below on a MacBook Pro (M3 Pro, 18 GB), and the index is real data: the 2,218 live product listings on Nick Launches. The search results, timings and sizes in this guide are what I got, not what the model card promises.

Step 1 of 7
3 min

Install

At a glance
Python
3.12
Package
sentence-transformers[image]
Download
1.4 GB, no token needed

A clean virtual environment, then one package with its image extra:

Terminal
uv venv -p 3.12 .venv && source .venv/bin/activate
uv pip install "sentence-transformers[image]" numpy

# without uv:
# python3.12 -m venv .venv && source .venv/bin/activate
# pip install "sentence-transformers[image]" numpy

That pulled sentence-transformers 6.1.0, transformers 5.20.1 and PyTorch 2.14.1 for me. The model downloads on first use: one 1.4 GB file with every encoder in it, whichever ones you load. It came down without a Hugging Face account or token.

Install the image extra even for text-only search
Without it, loading the model fails before you embed a single word: ImportError: EmbeddingGemma2VideoProcessor requires the Torchvision library. The processor imports torchvision even when the vision encoder is switched off. The [image] extra installs it.
Step 2 of 7
2 min

Load only the encoders you need

At a glance
Text only
270M parameters
Load time
4.7 s on an M3 Pro
Precision
float32 or bfloat16

The model is built from parts: a text backbone and embedder, an optional vision encoder and an optional audio encoder. Switch off what you do not use and the model in memory shrinks to match:

load.py
import torch
from sentence_transformers import SentenceTransformer

model = SentenceTransformer(
    "google/embeddinggemma-2",
    model_kwargs={"torch_dtype": torch.float32},
    config_kwargs={"vision_config": None, "audio_config": None},  # text only
)
print(model.device)                                   # mps:0 on a Mac, cuda:0 on NVIDIA, else cpu
print(sum(p.numel() for p in model.parameters()))     # 271002624
You needconfig_kwargsSize
Text and code{"vision_config": None, "audio_config": None}270M
Text and images{"audio_config": None}440M
Text and audio{"vision_config": None}570M
Everything, video included{}740M
Not float16
The model card warns that float16 can return NaN or quietly degraded embeddings with no error. Use float32, or bfloat16 on hardware that supports it.
Step 3 of 7
5 min

Embed your catalog

At a glance
Document format
title: … | text: …
2,218 listings
42 s on GPU, ~2.3 min on CPU
Index size
6.8 MB

EmbeddingGemma 2 was trained with short prompts in front of every input, and it searches best when you use them. Documents get title: {title} | text: {content}, or title: none when there is no title. My catalog is one JSON line per product with a name, a tagline and a description:

build_index.py
import json
import numpy as np
import torch
from sentence_transformers import SentenceTransformer

rows = [json.loads(line) for line in open("catalog.jsonl")]
docs = [f"title: {r['name']} | text: {r['tagline']}. {r['description']}" for r in rows]

model = SentenceTransformer(
    "google/embeddinggemma-2",
    model_kwargs={"torch_dtype": torch.float32},
    config_kwargs={"vision_config": None, "audio_config": None},
)
vectors = model.encode(docs, prompt="", batch_size=32, normalize_embeddings=True)
np.save("catalog-768.npy", vectors)   # shape (2218, 768)

prompt="" matters: the documents already carry their own prefix with the real product name, so this stops the library from adding its default title: none prefix on top. normalize_embeddings=True lets a plain dot product act as cosine similarity later.

On the M3 Pro's GPU the 2,218 listings (about 420 characters each) took 42 seconds. On CPU only it ran at about 16 listings a second, so roughly 2.3 minutes. The saved index is 6.8 MB.

Re-embed only what changed
Keep the vectors next to an id per row. When a listing is added or edited, embed that one row and replace it. A full rebuild is only needed when you change the document format or the model.
Step 5 of 7
2 min

Shrink the vectors, carefully

At a glance
Sizes
768, 512, 256, 128
256 dims
6.8 MB → 2.3 MB
My result
5 of 6 searches held up

The model is trained so the first 512, 256 or 128 numbers of each vector still work on their own. Cut, then normalize again:

shrink.py
small = vectors[:, :256]
small = small / np.linalg.norm(small, axis=1, keepdims=True)

# or at encode time:
# model.encode(docs, prompt="", truncate_dim=256, normalize_embeddings=True)

Google's benchmark puts 256 dimensions close to lossless, and on my six searches the top five stayed the same or lost one result in five cases. The ebook search kept only two of its top five. At 2,218 rows the full index is 6.8 MB, so I keep 768. Shrink when you have millions of rows and the memory bill is real, and test your own searches when you do.

Step 6 of 7
5 min

Find a screenshot from a sentence

At a glance
Encoders
Text + vision, 440M
16 screenshots
9.5 s to embed
Result
5 of 5 found first

Load the vision encoder too, and images go into the same space as your text. An image is an input with an <|image|> placeholder and a file:

screenshots.py
import glob
model = SentenceTransformer(
    "google/embeddinggemma-2",
    model_kwargs={"torch_dtype": torch.float32},
    config_kwargs={"audio_config": None},   # text + images
)
files = sorted(glob.glob("screenshots/*"))
shots = np.stack([
    model.encode({"text": "<|image|>", "image": [f]}, normalize_embeddings=True)
    for f in files
])

q = model.encode("a table of sales leads with deal size", prompt_name="query", normalize_embeddings=True)
print(files[int(np.argmax(shots @ q))])

I indexed the 16 screenshots from two of our guides and described five of them in plain words. Every description found the right screenshot first:

DescriptionTop resultScore
Google search results showing Reddit threadsGoogle's Forums tab with r/SaaS threads0.723
a character counter for SEO titles and descriptionsThe Meta Title and Description tool0.746
approve a change an AI agent made to my dataA Busabase change request diff0.690
list of launch directories with how often AI cites themThe Launch Directories tracker sorted by AI cited0.765
a table of sales leads with deal sizeA Busabase table of leads0.739

That is a help center where "where do I approve changes" lands on the right screenshot, or a changelog you can search by what the feature looks like. The text and image process peaked at about 2.1 GB of memory on my Mac.

Mixed inputs work too
One input can mix text and images, such as a product description with its screenshot: {"text": "Leads table. <|image|>", "image": ["leads.png"]}. Images take no text prompt in front.
Step 7 of 7
5 min

Serve it from your app

At a glance
Server
Python standard library
CPU query
70 to 280 ms end to end
Box
2 GB RAM minimum

Your web app probably is not written in Python. Keep the model in one small process and call it over HTTP. This server uses only the standard library, runs on CPU, and is what I timed:

search_server.py
import json
import numpy as np
import torch
from http.server import BaseHTTPRequestHandler, HTTPServer
from sentence_transformers import SentenceTransformer

model = SentenceTransformer(
    "google/embeddinggemma-2",
    device="cpu",
    model_kwargs={"torch_dtype": torch.float32},
    config_kwargs={"vision_config": None, "audio_config": None},
)
rows = [json.loads(line) for line in open("catalog.jsonl")]
vectors = np.load("catalog-768.npy")

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        q = model.encode(body["q"], prompt_name="query", normalize_embeddings=True)
        scores = vectors @ q
        top = np.argsort(-scores)[: body.get("k", 5)]
        out = [{"slug": rows[i]["slug"], "name": rows[i]["name"],
                "score": round(float(scores[i]), 3)} for i in top]
        data = json.dumps(out).encode()
        self.send_response(200)
        self.send_header("content-type", "application/json")
        self.end_headers()
        self.wfile.write(data)

HTTPServer(("127.0.0.1", 8092), Handler).serve_forever()
Terminal
python search_server.py &

curl -s -X POST localhost:8092 -H 'content-type: application/json' \
  -d '{"q":"an offline ebook reader that reads aloud","k":3}'
# [{"slug": "strophe-reader", "name": "Strophe Reader", "score": 0.832}, ...]

From your app's server code, it is one fetch:

search.ts
export async function semanticSearch(q: string, k = 5) {
    const res = await fetch("http://127.0.0.1:8092", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ q, k }),
    });
    return (await res.json()) as Array<{ slug: string; name: string; score: number }>;
}

On CPU, with the GPU unused, three test searches came back in 70 to 280 ms each, the slowest being the first one after start. Loading the text model takes about 1.5 GB of memory, so give it a box with at least 2 GB of RAM, or 4 GB if the box also runs your app. Keep it bound to 127.0.0.1 and put your own auth in front if anything outside the box needs it.

llama.cpp needs a build from October 6 or later
The llama.cpp team publishes a GGUF build, ggml-org/embeddinggemma-2-GGUF (310 MB at Q8_0), for running it under llama.cpp instead of Python. Support for the architecture landed in llama.cpp on October 6, 2026. The older build I had installed refused to load it with unknown model architecture: 'gemma-embedding2', so update llama.cpp before you try that route.

What it actually costs

  • The model: $0. Apache 2.0, per the model card, and the weights are a free download.
  • Indexing 2,218 listings: 42 seconds on a Mac's GPU, about 2.3 minutes on CPU. No API bill.
  • Serving: whatever a box with 2 GB or more of RAM costs you, or $0 if it runs next to your app on a server you already pay for.

When a hosted embedding API is still the better call

  • Your app runs only on serverless functions. A 1.4 GB model does not fit in a cold start. You need one process that stays up, or an API.
  • You are indexing tens of millions of rows. At 16 rows a second on CPU, that is weeks of compute. Rent a GPU for the backfill or pay per token.
  • You already pay for a vector database with built-in embeddings. Then the saving is small and the extra moving part is not worth it.

For a catalog, a docs site or a help center in the thousands of rows, local wins.

Troubleshooting

  • ImportError about Torchvision on load. Install sentence-transformers[image], even for text only. See step 1.
  • A warning that image decoding with torchvision is deprecated. It still works. Installing torchcodec 0.16 or newer makes the warning go away.
  • Scores that look random, or NaN. Check you are not loading in float16, and that queries use prompt_name="query" while documents use the title: … | text: … format.
  • Search quality dropped after shrinking. Go back to 768 dimensions or try 512. See step 5.
  • llama.cpp says unknown model architecture. Your build predates October 6, 2026. Update it.

You're set

You have a search box that understands what people mean, an index you can rebuild in minutes, and screenshots you can find by describing them. Start with your docs or your catalog, check the top three results for twenty real searches by hand, and only then think about shrinking or scaling.

Built search into your product? Launch it on the weekly board: launching is free, every launch is reviewed by a person, and it gets a permanent listing page. Submit it here.

Disclaimer: Nick Launches is not affiliated with Google. Tested on October 19, 2026 on macOS with Python 3.12, sentence-transformers 6.1.0, transformers 5.20.1 and PyTorch 2.14.1, against the 2,218 live listings on Nick Launches that day. Scores and timings will differ on your data and hardware; check the model card if something does not match.

Share

Get the weekly recap

New dev-resource guides, top launches, and what's worth a look. One email a week.