88 lines
3.6 KiB
PL/PgSQL
88 lines
3.6 KiB
PL/PgSQL
-- 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_<version>;
|
|
-- 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;
|