advanced15 min· python · siftq · anthropic

Visual product matching with image scope + Claude vision

Find images, identify them, structure the metadata — all in one script.

Prerequisites

  • $pip install anthropic requests
  • $export SIFTQ_API_KEY=mk-...
  • $export ANTHROPIC_API_KEY=sk-ant-...

How it works

Most search APIs treat images as a separate product with its own auth and shape. SiftQ's image scope returns canonical image URLs alongside dimensions; Claude's vision capability handles the identification half. Chain them and you have a complete "describe-to-classified-metadata" pipeline in 50 lines — useful for product matching, asset discovery, moderation pipelines, and visual research agents.

The recipe

multimodal_extraction.py
import os
import json
import base64
import requests
import anthropic

claude = anthropic.Anthropic()
SIFTQ_KEY = os.environ["SIFTQ_API_KEY"]


def find_images(description: str, k: int = 8) -> list[dict]:
    r = requests.post(
        "https://api.siftq.com/v1/search",
        headers={"Authorization": f"Bearer {SIFTQ_KEY}"},
        json={"q": description, "scope": "image", "size": str(k)},
        timeout=15,
    )
    r.raise_for_status()
    return r.json().get("images", [])


def identify(image_url: str) -> dict:
    """Claude vision: classify and structure metadata from one image."""
    try:
        img = requests.get(image_url, timeout=10).content
    except Exception as e:
        return {"error": f"fetch failed: {e}"}

    msg = claude.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=600,
        messages=[{
            "role": "user",
            "content": [
                {"type": "image", "source": {
                    "type": "base64",
                    "media_type": "image/jpeg",
                    "data": base64.b64encode(img).decode(),
                }},
                {"type": "text", "text": (
                    "Return ONLY a JSON object with these keys: "
                    "subject (string), dominant_colors (array of 3 hex), "
                    "style (string), era (string), confidence (0-1)."
                )},
            ],
        }],
    )
    try:
        return json.loads(msg.content[0].text)
    except json.JSONDecodeError:
        return {"error": "non-json response", "raw": msg.content[0].text}


def pipeline(query: str) -> list[dict]:
    hits = find_images(query)
    results = []
    for h in hits:
        meta = identify(h["imageUrl"])
        results.append({
            "url": h["imageUrl"],
            "dims": f"{h.get('imageWidth')}×{h.get('imageHeight')}",
            **meta,
        })
    return results


if __name__ == "__main__":
    out = pipeline("vintage 1970s minimalist serif typography poster")
    print(json.dumps(out, indent=2))

Variations

Reverse: image → matches

Upload an image you have, ask Claude to describe it, feed that description into SiftQ image scope. Effectively reverse-image search.

Batch with asyncio

Run all identify() calls concurrently with anyio or asyncio.gather() — 10x speedup on a 10-image batch.

Add embedding clustering

After identification, embed the subject strings (Cohere / Voyage / Jina) and cluster — auto-discovers categories in the result set.

Keep cooking

SiftQ as a LangChain agent tool
Wrap SiftQ in @tool, hand it to a ReAct agent, watch it search autonomously.
Test this query in the SiftQ API playground →