Files
HealthLog/prisma/migrations/0228_document_content_index/migration.sql
T
Marc-André Bombeck 7c5d1a264e feat(documents): add DocumentContentIndex table for blind content search
P2-1-T1. Migration 0228 adds the 1:1 document_content_index sibling of
inbound_documents: AES-256-GCM ciphertext of the extracted text plus a
deduped HMAC-SHA256 token array (search_tokens) with a GIN index for the
array-overlap match the list search will union with title/filename ILIKE.
Both foreign keys cascade. Nothing readable sits at rest; the index subkey
is HKDF-derived at runtime, so tokens are opaque without the server key.
2026-07-07 18:08:12 +02:00

57 lines
2.5 KiB
SQL

-- Document vault P2: the blind content-search index.
--
-- `document_content_index` — a 1:1 sibling of `inbound_documents` that makes
-- a document searchable INSIDE its body without ever storing the body in the
-- clear. Two artefacts per row:
-- - `text_encrypted` — AES-256-GCM ciphertext of the normalised extracted
-- text (the `encrypt()`-string-as-UTF-8 BYTEA shape every other PHI
-- column uses). Recoverable server-side so key rotation can re-tokenise.
-- - `search_tokens` — deduped HMAC-SHA256 (truncated hex) of the normalised
-- tokens, under an HKDF-derived index subkey. One-way / opaque at rest.
-- A GIN index over `search_tokens` accelerates the array-overlap (`&&`)
-- predicate the list search unions with the title/filename ILIKE. Prisma
-- cannot express a GIN index, so it lives here as raw SQL.
--
-- Both foreign keys cascade: a deleted document (or user) takes its index with
-- it. Additive; applies clean on a 0227 database and on a fresh database.
-- CreateTable
CREATE TABLE "document_content_index" (
"id" TEXT NOT NULL,
"document_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"text_encrypted" BYTEA NOT NULL,
"search_tokens" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
"source" TEXT NOT NULL,
"provider_type" TEXT,
"tokenizer_version" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "document_content_index_pkey" PRIMARY KEY ("id")
);
-- 1:1 with the document.
CREATE UNIQUE INDEX "document_content_index_document_id_key"
ON "document_content_index" ("document_id");
-- Owner-scoped queries (the usage gauge + the backfill discovery).
CREATE INDEX "document_content_index_user_id_idx"
ON "document_content_index" ("user_id");
-- Blind-token search: GIN over the token array for the array-overlap (`&&`)
-- match the list search runs. Documented in schema.prisma — Prisma cannot
-- express a GIN index, so it lives here as raw SQL.
CREATE INDEX "document_content_index_search_tokens_gin"
ON "document_content_index" USING GIN ("search_tokens");
ALTER TABLE "document_content_index"
ADD CONSTRAINT "document_content_index_document_id_fkey"
FOREIGN KEY ("document_id") REFERENCES "inbound_documents" ("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "document_content_index"
ADD CONSTRAINT "document_content_index_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users" ("id")
ON DELETE CASCADE ON UPDATE CASCADE;