Industry

Pixeltable for ecommerce catalogs

An ecommerce catalog is SKUs, titles, and photos that get replaced. Exact duplicates share a normalized PNG hash. Near-duplicates and "find this bag in navy" are CLIP on the image column. S3 plus Qdrant plus a worker is the glue stack. One TableModel file can cover the same slots.

Updated · Matching docs cookbook

A catalog drop is thousands of studio photos, marketplace crops, and vendor dumps. Merchandising asks for near-duplicates of a hero shot. Shoppers search with a sentence or a phone picture. Ops wants to refuse a re-upload of the same pixels. The files already sit on S3. The missing piece is a row per SKU photo that hashes, embeds, and serves search when a new file lands.

This is the user-uploaded media architecture plus a SKU column. FiftyOne is the App for visual QA of a dump. Pixeltable is the production table the site and the PIM talk to. POST /images does not run the duplicate check; call keep_if_new yourself.

SKU photo pipeline

  1. Insert sku, title, and the image (upload or S3 URL).
  2. Hash a normalized RGBA PNG for exact duplicates.
  3. Index the image with CLIP for text-to-image and image-to-image search.
  4. Before insert, look up the digest, then CLIP only if that lookup is empty.
  5. Serve /products insert and /search. Image-to-image is POST multipart.

Worked example: SKU photos with exact-hash and CLIP search

This sketch follows the image-search and image-dedup how-tos. Tune the near-duplicate threshold on labelled pairs from your catalog. 0.95 is a starting point, not a measured result. Keep Qdrant when that engine is already the serving index.

import hashlib
from io import BytesIO
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')
def rgba_png_sha256(img) -> str:
buf = BytesIO()
img.convert('RGBA').save(buf, format='PNG')
return hashlib.sha256(buf.getvalue()).hexdigest()
@pxt.udf
def image_sha256(img: pxt.Image) -> str:
return rgba_png_sha256(img)
class Products(TableModel, name='products'):
sku: pxt.String
title: pxt.String
image: pxt.Image
file_sha256 = image_sha256(image)
__indexes__ = [
pxt.EmbeddingIndex(image, embedding=clip_embed),
]
@pxt.query
def search_catalog(query: str, limit: int = 8):
sim = Products.image.similarity(string=query)
return (
Products.order_by(sim, asc=False)
.limit(limit)
.select(Products.sku, Products.title, Products.image, sim)
)
def keep_if_new(candidate, sku: str, title: str, threshold: float = 0.95) -> None:
products = pxt.get_table('catalog.products')
digest = rgba_png_sha256(candidate)
exact = products.where(products.file_sha256 == digest).select(products.sku).collect()
if len(exact) > 0:
return
sim = products.image.similarity(image=candidate)
hits = (
products.where(sim > threshold)
.order_by(sim, asc=False)
.limit(10)
.select(products.sku, sim)
.collect()
)
if len(hits) == 0:
products.insert([{'image': candidate, 'sku': sku, 'title': title}])
api = FastAPIRouter(name='catalog')
api.add_insert_route(
Products,
path='/products',
inputs=[Products.sku, Products.title],
uploadfile_inputs=[Products.image],
)
api.add_query_route(path='/search', query=search_catalog)
# POST /products does not call keep_if_new.
# pxt init
# pxt schema update app.py catalog -f
# pxt service update app.py catalog -f

To run a sketch, install pixeltable[serve] and the packages its model functions import (such as sentence-transformers, openai, or openai-whisper), export the provider key a model call reads (such as OPENAI_API_KEY), save the file as app.py, and run the commands in its last lines. The tables start empty: insert a row, or POST to an insert route, then select the computed columns. Get started covers the CLI.

Honest alternatives per slot

Named alternatives for Pixeltable for ecommerce catalogs
SlotRolePixeltableNamed alternatives
Object storeStudio and marketplace photosImage column; insert an S3 URL if the bucket is the origin
  • Amazon S3. The CDN already reads that bucket
  • Cloudflare R2. S3-compatible storage with no egress fee on that vendor
Vector indexVisual search and near-duplicatesCLIP EmbeddingIndex on the image column
  • Qdrant. You already upsert catalog vectors from a worker
  • pgvector. Postgres already holds SKUs and inventory
  • Pinecone. A hosted ANN namespace is the serving contract
Visual QAReview a dump of vendor photosQuery results; not a CV visualizer
  • FiftyOne. Interactive visual curation of a dump before it is the catalog

Where Pixeltable fits

Pixeltable is the catalog photo table: SKU, title, image, a hash UDF, CLIP, and HTTP. Keep S3 as the CDN origin. Keep Qdrant or pgvector when that engine is already the index. Keep FiftyOne for a one-off dump. POST /products does not refuse duplicates; call keep_if_new. The how-tos are image similarity search and image dedup. The architecture is /reference-architecture/user-uploaded-media-pipeline.

Questions

Can shoppers search with a phone photo?

Yes. Declare a @pxt.query whose parameter is pxt.Image and pass that name in uploadfile_inputs. The route is POST multipart. GET cannot take uploads. The image-search how-to shows search_by_image.

Will this replace my PIM?

No. Title, price, and inventory stay in the PIM or Postgres. This table is the photos, the hash, and visual search. Join on sku.