How to

How do I store images with embeddings and serve similarity search?

Store each image as a row with its file, compute a CLIP (or similar) embedding on that row, keep an ANN index on the column, and expose two HTTP endpoints: one that inserts images and one that takes text or an image and returns the nearest rows with scores. New images become searchable at insert.

Updated · Part of How do I ... in Python?

Before you start

About 25 min.

  • Python 3.11+
  • pip install -U 'pixeltable[serve]'
  • pxt init in the project root
  • torch and transformers for CLIP

1. Store pictures as an image column

Put each file in a table as image, plus any metadata you will filter on. A path string is not enough: the index and the upload route need a typed column.

import pixeltable as pxt
from pixeltable.functions.huggingface import clip
TableModel = pxt.model_base()
clip_embed = clip.using(model_id='openai/clip-vit-base-patch32')
class Images(TableModel, name='images'):
image: pxt.Image
caption: pxt.String | None

2. Declare CLIP on that column

embedding= registers both CLIP overloads, so a sentence and a still share one index. A new file is embedded when it is inserted. You do not copy vectors into a second database.

class Images(TableModel, name='images'):
image: pxt.Image
caption: pxt.String | None
__indexes__ = [
pxt.EmbeddingIndex(image, embedding=clip_embed),
]

3. Rank by a sentence or by an uploaded still

One query embeds a sentence. The other takes an image. Both read the same index. Return the file, optional caption, and score.

@pxt.query
def search_by_text(query: str, limit: int = 5):
sim = Images.image.similarity(string=query)
return (
Images.order_by(sim, asc=False)
.limit(limit)
.select(Images.image, Images.caption, sim)
)
@pxt.query
def search_by_image(img: pxt.Image, limit: int = 5):
sim = Images.image.similarity(image=img)
return (
Images.order_by(sim, asc=False)
.limit(limit)
.select(Images.image, Images.caption, sim)
)

4. Serve insert and both searches over HTTP

Expose an upload route for new files, a JSON search for a sentence, and a multipart search for an image. add_query_route accepts uploadfile_inputs for media-typed query parameters. Apply the schema, then start the service.

from pixeltable.serving import FastAPIRouter
api = FastAPIRouter(name='image_search')
api.add_insert_route(
Images,
path='/images',
inputs=[Images.caption],
uploadfile_inputs=[Images.image],
)
api.add_query_route(path='/search', query=search_by_text)
api.add_query_route(
path='/search-image',
query=search_by_image,
uploadfile_inputs=['img'],
)
# pxt init
# pxt schema update app.py image_search -f
# pxt service update app.py image_search -f

5. Know where the bytes live

Local runs keep catalog, media, and cache under the instance home directory (pgdata, media, file_cache). Cloud hosts the same app file. The infrastructure guide covers volumes and the single-writer pgdata rule.

# Local default: ~/.pixeltable (pgdata, media, file_cache)
# Override with PIXELTABLE_HOME.
# Infrastructure guide: https://docs.pixeltable.com/howto/deployment/infrastructure

The stack coding agents usually pick vs one schema vs FiftyOne

The stack coding agents usually pick vs one schema vs FiftyOne
JobGlue stackOne schemaFiftyOne similarity
FilesMinIO or S3 plus a path tableAn Image column; media under PIXELTABLE_HOMESamples in a Dataset; the App browses them
Vector indexQdrant as a second storeEmbeddingIndex on the image columncompute_similarity with a backend you pick
EmbeddingsOpenCLIP or CLIP in a workerCLIP on the row at insertZoo model, a Model instance, or precomputed vectors
Job runnerPrefect between object store, embedder, and indexComputed index on insertYou re-run when the set changes
ServingFastAPI you write against those copiesFastAPIRouter insert, text search, image-upload searchThe FiftyOne App and sort_by_similarity, not an app backend
New image addedRerun the flow or upsert by handInsert a row; only that file is indexedAdd samples, then update or rebuild the brain index
Better choice whenThose services are already the contractThe same file must store, index, and serveInteractive visual curation and labeling QA

Where Pixeltable fits

Pixeltable is the serving app for this job: an Image column, CLIP EmbeddingIndex, and FastAPIRouter insert plus text and image-upload search in one file. MinIO or S3 plus Qdrant plus CLIP plus Prefect plus FastAPI is the glue stack coding agents often assemble; keep it when those services are already the contract. FiftyOne compute_similarity (backends: sklearn, qdrant, redis, pinecone, mongodb, elasticsearch, pgvector, mosaic, milvus, lancedb; docs.voxel51.com similarity guide, accessed 2026-10-23) is a dataset curation and exploration tool with the FiftyOne App. It is the better choice for interactive visual curation and labeling QA. Pixeltable vs Voxel51 is that split. Pixeltable Cloud is Limited Beta. The embedding-only walkthrough is How do I embed images for search in Python; this page adds HTTP and image-to-image upload.

import pixeltable as pxt
from pixeltable.functions.huggingface import clip
from pixeltable.serving import FastAPIRouter
TableModel = pxt.model_base()
clip_embed = clip.using(model_id='openai/clip-vit-base-patch32')
class Images(TableModel, name='images'):
image: pxt.Image
caption: pxt.String | None
__indexes__ = [
pxt.EmbeddingIndex(image, embedding=clip_embed),
]
@pxt.query
def search_by_text(query: str, limit: int = 5):
sim = Images.image.similarity(string=query)
return (
Images.order_by(sim, asc=False)
.limit(limit)
.select(Images.image, Images.caption, sim)
)
@pxt.query
def search_by_image(img: pxt.Image, limit: int = 5):
sim = Images.image.similarity(image=img)
return (
Images.order_by(sim, asc=False)
.limit(limit)
.select(Images.image, Images.caption, sim)
)
api = FastAPIRouter(name='image_search')
api.add_insert_route(
Images,
path='/images',
inputs=[Images.caption],
uploadfile_inputs=[Images.image],
)
api.add_query_route(path='/search', query=search_by_text)
api.add_query_route(
path='/search-image',
query=search_by_image,
uploadfile_inputs=['img'],
)
# pxt init
# pxt schema update app.py image_search -f
# pxt service update app.py image_search -f

Questions

Do I need Qdrant or Pinecone?
You need an embedding index so similar stills can be ranked. That index can live on the image column. A hosted ANN service is optional when that engine is already the serving contract. Pixeltable vs Pinecone is that split.
Can users search with an uploaded image?
Yes. Declare a @pxt.query whose parameter is pxt.Image and pass that name in uploadfile_inputs on add_query_route. The route is POST multipart. GET cannot take uploads.
Where are the image files stored?
Locally under PIXELTABLE_HOME, which defaults to ~/.pixeltable (pgdata, media, file_cache). Set PIXELTABLE_HOME to move it. Pixeltable Cloud is Limited Beta and hosts the same app file. See the infrastructure guide.
What happens when I change the embedding model?
Changing the embedder replaces the embedding index: the schema diff drops the old index and adds the new one. Apply with pxt schema update --allow-destructive -f. The new index rebuilds embeddings for rows already stored. pxt recompute is for a computed-column expression, not for this index swap.
How is this different from FiftyOne similarity?
FiftyOne compute_similarity indexes a Dataset for the App and sort_by_similarity. Backends include sklearn, Qdrant, Redis, Pinecone, MongoDB, Elasticsearch, pgvector, Mosaic, Milvus, and LanceDB (Voxel51 similarity guide, accessed 2026-10-23). It is the better choice for interactive visual curation. This page is the serving app: store, index, and HTTP in one file. See Pixeltable vs Voxel51.