← back to Lifestyle Asset Intel

db/migrations/0003_image_intake.sql

67 lines

-- 0003_image_intake.sql — prep for v0.3 image-intake pipeline.
--
-- BLUEPRINT.md calls for CLIP-style multimodal embeddings as the launch
-- choice for image-based candidate retrieval. This migration lays in:
--
--   image_assets       — provenance + raw metadata for each image we hold
--   image_embeddings   — vector representations + provenance (model + dim)
--
-- Embeddings stored as real[] (PG native) for v0. When pgvector lands,
-- migrate to `vector(dim)` and add an HNSW index. The ORM/pg client
-- doesn't need to change — real[] reads back as a JS Number[].

CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- ---------------------------------------------------------------------------
-- image_assets
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS image_assets (
  id                  uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  canonical_asset_id  uuid REFERENCES canonical_assets(id) ON DELETE SET NULL,
  raw_observation_id  uuid REFERENCES raw_observations(id) ON DELETE SET NULL,
  -- 'upload' = direct user/console upload; 'auction_lot' / 'marketplace' /
  -- 'crawl' = auto-captured from a connector. 'reference' = curated set
  -- (used as a similarity anchor for new uploads).
  origin              text NOT NULL CHECK (origin IN ('upload','auction_lot','marketplace','crawl','reference')),
  source_url          text,
  storage_url         text,                       -- e.g. s3://… or local file://…
  sha256              text NOT NULL,              -- content-addressed dedupe
  mime                text,
  width               int,
  height              int,
  attribution         text,                       -- credit string per BLUEPRINT.md
  captured_at         timestamptz NOT NULL DEFAULT now(),
  created_at          timestamptz NOT NULL DEFAULT now(),
  UNIQUE (sha256)
);
CREATE INDEX IF NOT EXISTS image_assets_canonical_idx
  ON image_assets(canonical_asset_id);
CREATE INDEX IF NOT EXISTS image_assets_origin_idx
  ON image_assets(origin);

-- ---------------------------------------------------------------------------
-- image_embeddings
-- ---------------------------------------------------------------------------
-- One image can have multiple embeddings if we run more than one model
-- (e.g., CLIP ViT-B/32 baseline + a finer SigLIP model for re-ranking).
-- Vector dimensionality varies by model: 512 for CLIP ViT-B/32, 768 for
-- ViT-L/14, 768 for SigLIP base, 1024 for ViT-H/14. Store as real[] so PG
-- doesn't care; enforce dim consistency in app code.
CREATE TABLE IF NOT EXISTS image_embeddings (
  id              uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  image_asset_id  uuid NOT NULL REFERENCES image_assets(id) ON DELETE CASCADE,
  model           text NOT NULL,                  -- e.g. 'clip-vit-base-patch32'
  dim             int  NOT NULL CHECK (dim BETWEEN 64 AND 4096),
  vector          real[] NOT NULL,
  created_at      timestamptz NOT NULL DEFAULT now(),
  UNIQUE (image_asset_id, model)                  -- one embedding per (image, model) pair
);
CREATE INDEX IF NOT EXISTS image_embeddings_model_idx
  ON image_embeddings(model);

-- ---------------------------------------------------------------------------
-- migrations bookkeeping
-- ---------------------------------------------------------------------------
INSERT INTO schema_migrations(filename) VALUES ('0003_image_intake.sql')
  ON CONFLICT (filename) DO NOTHING;