Context
First implementation phase of the storage-philosophy roadmap (#645). Generalizes the per-model caching pattern PR #636 established for detections/predictions to the classifier embedding slot.
Problem
photos.embedding is currently a single BLOB slot tagged by photos.embedding_model. Re-running a different classifier overwrites the previous embedding. After PR #636 made classifier_runs a workspace-shared skip set, switching models in workspace B silently destroys workspace A's embedding even though A's predictions remain cached — A then renders with B's embedding under the hood.
What to build
New table:
CREATE TABLE photo_embeddings (
photo_id INTEGER NOT NULL REFERENCES photos(id) ON DELETE CASCADE,
model TEXT NOT NULL, -- e.g. "bioclip-2"
variant TEXT NOT NULL DEFAULT '',
embedding BLOB NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (photo_id, model, variant)
);
Migration
Backfill from existing photos.embedding + embedding_model (where embedding IS NOT NULL), then drop both columns.
Readers to update
vireo/classify_job.py:store_photo_embedding → upsert_photo_embedding(pid, model, embedding)
vireo/db.py:get_photo_embedding(photo_id) → take optional model param
vireo/db.py:get_embeddings_by_model(model) — already filters by model name, just point at new table
/api/species/<n>/clusters (vireo/app.py)
/api/photos/<id>/similar (vireo/app.py)
Tests
Mirror PR #636 pattern: db-level tests for the upsert/get helpers + an integration test that classifies with two different models and asserts both embeddings survive.
Reference
Full design at docs/plans/2026-04-24-storage-philosophy-design.md (Phase 1 section).
Tracker: #645
Context
First implementation phase of the storage-philosophy roadmap (#645). Generalizes the per-model caching pattern PR #636 established for detections/predictions to the classifier embedding slot.
Problem
photos.embeddingis currently a single BLOB slot tagged byphotos.embedding_model. Re-running a different classifier overwrites the previous embedding. After PR #636 made classifier_runs a workspace-shared skip set, switching models in workspace B silently destroys workspace A's embedding even though A's predictions remain cached — A then renders with B's embedding under the hood.What to build
New table:
Migration
Backfill from existing
photos.embedding+embedding_model(whereembedding IS NOT NULL), then drop both columns.Readers to update
vireo/classify_job.py:store_photo_embedding→upsert_photo_embedding(pid, model, embedding)vireo/db.py:get_photo_embedding(photo_id)→ take optionalmodelparamvireo/db.py:get_embeddings_by_model(model)— already filters by model name, just point at new table/api/species/<n>/clusters(vireo/app.py)/api/photos/<id>/similar(vireo/app.py)Tests
Mirror PR #636 pattern: db-level tests for the upsert/get helpers + an integration test that classifies with two different models and asserts both embeddings survive.
Reference
Full design at
docs/plans/2026-04-24-storage-philosophy-design.md(Phase 1 section).Tracker: #645