Files
immich-photo-flow/docs/superpowers/specs/2026-06-27-pgvector-embedding-findings.md
T
m038 4882f4ca5a feat(spike): read-only pgvector probe + live findings
scripts/pgvector_spike.py probes Immich's Postgres read-only (session-level
read-only guard) and answers the M1.5 prerequisites against the live DB:
catalog-discovered embedding table/column, vector dimension, distance operator
(from the index opclass), the embedding->asset FK join, and coverage over the
IMAGE population. Discovers the asset table name from the FK (asset, not the
legacy assets) rather than hardcoding it, so it survives Immich version drift.

Findings (this Immich version): smart_search.embedding, 1152-dim, cosine <=>
(vector_cosine_ops), assetId->asset.id join clean (0 orphans), ~44% image
coverage (CLIP re-run in progress). Doc is M1.5's version-pinned contract.
2026-06-27 20:39:31 +02:00

4.1 KiB
Raw Blame History

pgvector embedding feasibility spike — findings

Generated: 2026-06-27 18:39 UTC by scripts/pgvector_spike.py Status: machine-probed against the live Immich Postgres. This is M1.5's contract. See the design spec: docs/superpowers/specs/2026-06-27-pgvector-embedding-spike-design.md.

⚠️ Immich's undocumented, internal schema — no deprecation contract. Everything below is valid only for the model + Immich version observed and can be renamed/restructured on any Immich upgrade. M1.5 must re-run this probe (or version-guard) on every Immich upgrade. The "contract" is version-pinned, not durable.

Requirement 0 — does M1.5 need raw vectors (vs. a REST query)?

M1.5 clusters photos by visual similarity at trip level — it needs either the raw CLIP embedding vectors or an arbitrary asset→asset nearest-neighbour query. Immich's REST surface provides neither:

  • POST /api/search/smarttext→image CLIP search: takes a text query, returns assets. It never returns embedding vectors and cannot do asset→asset similarity without a text prompt. Insufficient.
  • POST /api/search/metadata, /api/search/random — metadata/random only; no embeddings, no similarity.
  • Duplicate detection (/api/duplicates) consumes embeddings internally but only surfaces near-duplicate groups above Immich's own threshold — not a tunable pairwise similarity usable for trip-level clustering. Insufficient.
  • No documented endpoint returns raw CLIP vectors or arbitrary k-NN neighbours.

Conclusion: the load-bearing "REST can't expose embeddings" premise holds for Immich's documented API → read-only Postgres access (below) is the viable path. (Re-confirm against the OpenAPI of the running version on upgrade.)

Requirements 14 — probed facts

# Question Answer
1 Embeddings readable? yes — read 19607 distinct embedding(s)
2 Join to asset table? 19607/19607 embeddings join to asset via assetId
3 Table / column public.smart_search / embedding
3 Vector dimension 1152 (live sample=1152, declared typmod=1152)
3 Distance operator <=> (cosine) — from index opclass vector_cosine_ops
3 Operator sanity check OK — nearest neighbour (a duplicate (identical-vector) asset) at distance 0
4 Coverage (raw) 42.8% (19607/45854 assets)
4 Coverage (image-only) 44.1% (19221/43601 IMAGE assets)
  • Asset-key join: FK smart_search.assetId -> asset.id
  • Orphan embeddings (no matching asset): 0
  • SQLite cross-check (optional): skipped — no SQLite store at data/trip-cluster.db (fresh checkout; not a failure)

The join M1.5 relies on

smart_search.assetIdasset.id → SQLite assets.immich_id (shared/photoflow/core). The Immich asset UUID is the same key our store uses, so embeddings index straight onto tracked photos.

⚠️ Immich's asset table is asset in the observed version (it was assets in older versions). The probe discovers this from the FK; M1.5's reader must not hardcode the name.

Environment observed

  • Postgres: 18.2 (Debian 18.2-1.pgdg12+1)
  • pgvector extension: 0.8.1
  • Schema age proxy (latest migration): unknown
  • Immich server version: record from the Immich UI / GET /api/server/version — not reliably available in the DB.
  • CLIP model: record the model from Immich's Machine-Learning settings — the user is re-running CLIP with a stronger model, so dimension + coverage above are a snapshot of whichever model was live at probe time.

The probe enforces read-only at the session layer (SET default_transaction_read_only = on + psycopg read_only), which neutralises write capability even with Immich's write/DDL-capable postgres user. For M1.5, provision a dedicated least-privilege role instead:

CREATE ROLE photoflow_ro LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE immich TO photoflow_ro;
GRANT USAGE ON SCHEMA public TO photoflow_ro;
GRANT SELECT ON public.smart_search, public.asset TO photoflow_ro;