-- Migration p2-002: pgvector und Ablage der Item-Embeddings -- -- psql "$ADMIN_DATABASE_URL" -v ON_ERROR_STOP=1 -f migrations/p2-002-embeddings.sql -- -- Setzt p2-001 voraus und ein Image, das pgvector mitbringt (siehe README): -- docker.io/pgvector/pgvector:pg18-trixie. Ohne die Erweiterung bricht die -- Migration in der ersten Zeile ab und hinterlaesst nichts. -- -- Die Embeddings sind Vorfilter fuer die Kandidatengenerierung in Phase 3b, -- kein Selbstzweck. Phase 2 clustert nicht. begin; create extension if not exists vector; -- -------------------------------------------------------------------------- -- Der Vektor liegt bewusst nicht in codings.nutzlast: JSONB-Vektoren sind -- weder indizierbar noch platzsparend. Die Coding-Zeile der Schicht -- 'embedding' haelt nur die Kennung, der Vektor steht hier. -- -- 1024 Dimensionen deckt beide in der Spezifikation genannten Modelle ab -- (multilingual-e5-large, jina-embeddings-v3). Ein Modell mit anderer -- Dimension braucht eine eigene Migration - das ist gewollt, denn es waere -- ohnehin eine neue coder_version. -- -------------------------------------------------------------------------- create table if not exists item_embeddings ( raw_item_id bigint not null references raw_items(id) on delete cascade, coder_version text not null, erzeugt_am timestamptz not null default now(), vektor vector(1024) not null, primary key (raw_item_id, coder_version) ); comment on table item_embeddings is 'Satz-Embedding je Item und Kodierer-Version. Vorfilter fuer Phase 3b'; comment on column item_embeddings.coder_version is 'Ohne Fremdschluessel auf coder_versionen, wie codings auch - die Herkunft ' 'steht dort, erzwungen wird sie nicht'; -- Der Fremdschluessel auf raw_items ist nicht Kosmetik: ohne ihn bleiben -- Vektoren als Waisen liegen, wenn ein Item aus raw_items verschwindet. -- -------------------------------------------------------------------------- -- Kein globaler HNSW-Index. -- -- Ein Index ueber alle coder_version hinweg legt Vektoren verschiedener -- Modelle in dieselbe Nachbarschaftsstruktur. Eine Suche muss dann nach -- Version filtern, und pgvector filtert bei HNSW erst *nach* dem Durchlauf -- der Kandidatenliste - das kostet Treffer, nicht nur Zeit. -- -- Stattdessen ein partieller Index je Version, angelegt wenn die Version in -- Betrieb geht: -- -- select p2_embedding_index('p2-2026-09-07-a3f1b2c4'); -- -- Der Aufbau ist speicherhungrig; bei grossen Bestaenden vorher -- set maintenance_work_mem = '2GB'; -- Und die Version wieder loswerden (Invariante 4, verwerfbar): -- drop index if exists item_embeddings_hnsw_; -- delete from item_embeddings where coder_version = '...'; -- -------------------------------------------------------------------------- create or replace function p2_embedding_index(version text) returns text language plpgsql as $$ declare idx text; begin -- Nicht-alphanumerisches im Versionsnamen wird zu _, damit der -- Indexname ohne Anfuehrungszeichen auskommt. idx := 'item_embeddings_hnsw_' || regexp_replace(version, '[^a-zA-Z0-9]+', '_', 'g'); if to_regclass(idx) is not null then return idx || ' (bestand bereits)'; end if; execute format( 'create index %I on item_embeddings using hnsw (vektor vector_cosine_ops) where coder_version = %L', idx, version); return idx; end; $$; comment on function p2_embedding_index is 'Legt den HNSW-Index fuer genau eine coder_version an. Idempotent'; insert into schema_migrationen (name) values ('p2-002-embeddings') on conflict (name) do nothing; commit;