Files
immich-photo-flow/docs/superpowers/specs/2026-06-27-pgvector-embedding-spike-design.md
T
m038andClaude Opus 4.8 e5923317de docs(spec): pgvector embedding feasibility spike (M1.5 dependency)
Defines the read-only Postgres/pgvector feasibility spike that the
roadmap claims ran in M1 but the M1 plan deferred. Pins the four
pass/fail requirements, the disposable-probe approach, and the
findings-doc contract that unblocks M1.5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 19:41:59 +02:00

6.0 KiB

pgvector embedding feasibility spike — design

Date: 2026-06-27 Milestone dependency: unblocks M1.5 (visual similarity). See docs/ROADMAP.md. Status: design

Why this exists

The roadmap narrative claims M1 "runs a read-only feasibility spike" and that M1.5's dependency is "verified in M1." It is not: the M1 plan explicitly deferred the spike (docs/superpowers/plans/2026-06-27-immich-photo-flow-m1.md:3710"pgvector spike → out of this plan by decision (tracked separately; M1.5 dependency)"). So M1.5 is blocked on a prerequisite that never ran. This spec defines that prerequisite.

M1.5 wants to cluster photos by visual similarity using the CLIP embeddings Immich already computes — the rescue signal for the GPS-poor old library where timestamp/GPS are weakest. Immich's REST API does not cleanly expose raw embedding vectors, so the viable path is read-only access to Immich's Postgres pgvector embeddings. Before M1.5 can build clustering on that path, this spike must prove the path exists and pin its shape.

Scope

Feasibility only. Read-only. Throwaway probe. The spike:

  • does not build clustering, a tested module, or any shared/ reader;
  • does not write to Immich or its database (only SELECT);
  • exists to turn unknowns into pinned facts and hand M1.5 a verified contract.

Requirements (pass/fail)

The spike succeeds when it answers all four against the live database and records the answers in the findings doc. Phrased as plain questions:

  1. Can we read the CLIP embeddings at all? Connect read-only over the LAN and read the embedding vectors Immich stores. This is the real go/no-go: if the embeddings are not readable, M1.5 needs a different plan.
  2. Can we join each embedding back to a photo we already track? Our SQLite store keys assets by Immich asset ID. Confirm each embedding row carries that same ID so an embedding can be matched to a known asset. Without the join the vectors are unusable.
  3. What are the exact shapes? Record the table name, embedding column, vector dimension, and the correct pgvector distance operator for similarity. These have drifted across Immich versions, so they must be read from this DB, not assumed.
  4. What is the coverage? What fraction of library assets currently have an embedding (the user is re-running CLIP with a stronger model — has it finished?). Tells M1.5 how much it can lean on the signal.

What we already know (to verify, not assume)

Immich historically stores CLIP vectors in a smart_search table with an embedding column of pgvector type vector, keyed by assetId referencing assets.id; similarity uses cosine distance (<=>); the legacy default model (ViT-B-32) produced 512-dim vectors. Names and dimension have changed across versions and the user's re-run uses a stronger model, so the probe discovers these rather than trusting them — the list above is only the set of candidates to probe first.

Approach

Disposable read-only probe + two durable artifacts (chosen over a throwaway-only or a build-the-module-now approach: feasibility-only honors the roadmap, but capturing the DSN and a written contract is the cheap part that saves M1.5 from guessing).

Connection

  • New optional env var IMMICH_DB_URL (a Postgres DSN), added to .env.example and loaded by config.py as an optional field (REST creds stay required; the DSN is only needed for the spike / M1.5).
  • Simplest credentials that work: Immich's existing Postgres user, kept read-only by the probe issuing only SELECT. A dedicated least-privilege read-only role is documented as recommended hardening for M1.5 but is not required to prove feasibility.
  • Driver: psycopg (psycopg3). Add to the spike's dependencies; M1.5 will formalize it.

The probe

A single disposable script, scripts/pgvector_spike.py, that is idempotent and read-only. It:

  1. connects using IMMICH_DB_URL;
  2. discovers candidate embedding tables/columns from the catalog (probe smart_search / embedding first, then fall back to scanning information_schema for vector-typed columns) — so it survives version drift;
  3. reads one embedding and reports its dimension and a sample of the distance operator working (e.g. a ... ORDER BY embedding <=> embedding LIMIT 5 self-similarity sanity check);
  4. verifies the asset-ID join: the embedding row's key matches assets.id, and that same ID is one we store in SQLite (shared/photoflow/core);
  5. computes coverage: count(embeddings) / count(assets) (and, if cheap, broken down by whether the asset is an image vs video);
  6. prints a human-readable report and writes/refreshes the findings doc.

It hardcodes nothing destructive, takes no write path, and is safe to re-run.

Deliverables

  1. scripts/pgvector_spike.py — disposable read-only probe (removed or left as a documented one-off after M1.5 internalizes its findings).
  2. IMMICH_DB_URL in .env.example; optional immich_db_url field in config.py.
  3. docs/superpowers/specs/2026-06-27-pgvector-embedding-findings.md — the one-page pinned schema that becomes M1.5's contract: table, embedding column, vector dimension, distance operator, the embedding → assetId → SQLite asset join, coverage %, Immich version, and the (optional) read-only-role recipe.
  4. Roadmap correction: M1.5's status note updated to "dependency spike pending/this spec" instead of "verified in M1."

Out of scope (explicitly M1.5)

CLIP clustering; a tested shared/photoflow/immich/db.py reader; nearest-neighbor queries at scale; any use of the embeddings beyond the feasibility checks above.

Risks

  • Embeddings unreadable / not joinable → M1.5 blocked; spike's whole point is to surface this early and cheaply.
  • Schema differs from the known shape → expected and handled by catalog discovery, not hardcoded names.
  • Coverage near zero (re-run unfinished) → not a spike failure; recorded as a fact so M1.5 can sequence around it.