---
title: "How do I store images with embeddings and serve similarity search?"
description: "Store images with CLIP embeddings and serve similarity search over HTTP: insert by upload, query by text or image, and index each new file on insert."
url: "https://www.pixeltable.com/learn/how-to/serve-image-similarity-search"
updated: "2026-10-23"
doc: "https://docs.pixeltable.com/howto/cookbooks/search/search-similar-images"
---

# 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: 2026-10-23
Part of [How do I ... in Python?](https://www.pixeltable.com/learn/how-to).

## 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

## On this page

- [Before you start](https://www.pixeltable.com/learn/how-to/serve-image-similarity-search#before-you-start)
- [Store pictures as an image column](https://www.pixeltable.com/learn/how-to/serve-image-similarity-search#step-1)
- [Declare CLIP on that column](https://www.pixeltable.com/learn/how-to/serve-image-similarity-search#step-2)
- [Rank by a sentence or by an uploaded still](https://www.pixeltable.com/learn/how-to/serve-image-similarity-search#step-3)
- [Serve insert and both searches over HTTP](https://www.pixeltable.com/learn/how-to/serve-image-similarity-search#step-4)
- [Know where the bytes live](https://www.pixeltable.com/learn/how-to/serve-image-similarity-search#step-5)
- [Comparison](https://www.pixeltable.com/learn/how-to/serve-image-similarity-search#comparison)
- [Where Pixeltable fits](https://www.pixeltable.com/learn/how-to/serve-image-similarity-search#where-pixeltable-fits)
- [Questions](https://www.pixeltable.com/learn/how-to/serve-image-similarity-search#questions)

## 1. Store pictures as an image column {#step-1}

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.

```python
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 {#step-2}

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.

```python
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 {#step-3}

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

```python
@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 {#step-4}

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.

```python
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 {#step-5}

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.

```python
# 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 {#comparison}

| Job | Glue stack | One schema | FiftyOne similarity |
| --- | --- | --- | --- |
| Files | MinIO or S3 plus a path table | An Image column; media under PIXELTABLE_HOME | Samples in a Dataset; the App browses them |
| Vector index | Qdrant as a second store | EmbeddingIndex on the image column | compute_similarity with a backend you pick |
| Embeddings | OpenCLIP or CLIP in a worker | CLIP on the row at insert | Zoo model, a Model instance, or precomputed vectors |
| Job runner | Prefect between object store, embedder, and index | Computed index on insert | You re-run when the set changes |
| Serving | FastAPI you write against those copies | FastAPIRouter insert, text search, image-upload search | The FiftyOne App and sort_by_similarity, not an app backend |
| New image added | Rerun the flow or upsert by hand | Insert a row; only that file is indexed | Add samples, then update or rebuild the brain index |
| Better choice when | Those services are already the contract | The same file must store, index, and serve | Interactive visual curation and labeling QA |

## Where Pixeltable fits {#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.

```python
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
```

## Concepts

- [What is an embedding index?](https://www.pixeltable.com/learn/what-is-an-embedding-index)
- [What is visual search?](https://www.pixeltable.com/learn/what-is-visual-search)
- [How does an embedding index differ from a vector database?](https://www.pixeltable.com/learn/embedding-index-vs-vector-database)

## Use case

- [Computer Vision Pipeline: Object Detection, Classification, and Search](https://www.pixeltable.com/use-cases/computer-vision-pipeline-optimization)

## Questions {#questions}

### Do I need Qdrant or Pinecone? {#faq-1}

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? {#faq-2}

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? {#faq-3}

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? {#faq-4}

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? {#faq-5}

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.

## Related

- [What is an embedding index?](https://www.pixeltable.com/learn/what-is-an-embedding-index)
- [What is visual search?](https://www.pixeltable.com/learn/what-is-visual-search)
- [Embedding index vs a vector database](https://www.pixeltable.com/learn/embedding-index-vs-vector-database)
- [Computer vision pipeline](https://www.pixeltable.com/use-cases/computer-vision-pipeline-optimization)
- [How do I embed images for search in Python?](https://www.pixeltable.com/learn/how-to/embed-images-for-search)
- [How do I deduplicate a large image dataset and keep embeddings for near-duplicate search?](https://www.pixeltable.com/learn/how-to/deduplicate-images-with-embeddings)
- [Pixeltable vs Voxel51](https://www.pixeltable.com/compare/pixeltable-vs-voxel51)
- [Pixeltable vs Pinecone](https://www.pixeltable.com/compare/pixeltable-vs-pinecone)
- [Find similar images with CLIP](https://docs.pixeltable.com/howto/cookbooks/search/search-similar-images)
- [Infrastructure and PIXELTABLE_HOME](https://docs.pixeltable.com/howto/deployment/infrastructure)
