Code-review follow-ups on the pgvector spike: - self-similarity sanity check now compares to the seed's own self-distance (0 for cosine/L2, ~-1 for inner product <#>) instead of a hardcoded ~0, so it no longer misfires if Immich ever uses a vector_ip_ops index (correctness P3). - mark the deliberate raw-SQLite read in the probe as a spike-only exception and add an M1.5 TODO that the real pgvector reader belongs in shared/photoflow/immich and SQLite access in shared/photoflow/core (project-standards P2 x2). - document join_and_coverage's return shape; extract a _pct() helper to dedupe the coverage-percentage formatting (maintainability P3 x2). Findings doc refreshed from the latest live run (coverage now ~46%, re-run ongoing).
4.1 KiB
pgvector embedding feasibility spike — findings
Generated: 2026-06-27 18:49 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/smart— text→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 1–4 — probed facts
| # | Question | Answer |
|---|---|---|
| 1 | Embeddings readable? | yes — read 20727 distinct embedding(s) |
| 2 | Join to asset table? | 20727/20727 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 (seed self-distance 0) |
| 4 | Coverage (raw) | 45.2% (20727/45854 assets) |
| 4 | Coverage (image-only) | 46.6% (20323/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.assetId → asset.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
assetin the observed version (it wasassetsin 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.
Read-only access (recommended hardening for M1.5)
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;