-- ================================================================ -- Migration 006: Face Recognition Tables -- Version: 006 -- Date: 2026-03-30 -- Description: Add tables for face recognition feature storage -- Includes face embeddings, identities, and clusters -- ================================================================ -- 6.1: Enable pgvector extension if not already enabled CREATE EXTENSION IF NOT EXISTS vector; -- 6.2: Create face_identities table CREATE TABLE IF NOT EXISTS face_identities ( id SERIAL PRIMARY KEY, face_id VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(255), embedding VECTOR(512), -- InsightFace default embedding dimension attributes JSONB, metadata JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, is_active BOOLEAN DEFAULT TRUE, -- Indexes for performance CONSTRAINT face_identities_face_id_key UNIQUE (face_id) ); -- 6.3: Create face_detections table CREATE TABLE IF NOT EXISTS face_detections ( id SERIAL PRIMARY KEY, video_uuid VARCHAR(255) NOT NULL, frame_number BIGINT NOT NULL, timestamp_secs DOUBLE PRECISION NOT NULL, face_id VARCHAR(255), x INTEGER NOT NULL, y INTEGER NOT NULL, width INTEGER NOT NULL, height INTEGER NOT NULL, confidence DOUBLE PRECISION NOT NULL, embedding VECTOR(512), attributes JSONB, identity_id INTEGER REFERENCES face_identities(id) ON DELETE SET NULL, identity_confidence DOUBLE PRECISION, cluster_id VARCHAR(255), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Ensure unique detection per frame CONSTRAINT unique_detection_per_frame UNIQUE (video_uuid, frame_number, x, y, width, height) ); -- 6.4: Create face_clusters table CREATE TABLE IF NOT EXISTS face_clusters ( id SERIAL PRIMARY KEY, cluster_id VARCHAR(255) NOT NULL, video_uuid VARCHAR(255) NOT NULL, centroid VECTOR(512), size INTEGER NOT NULL DEFAULT 0, representative_face_id VARCHAR(255), metadata JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Constraints CONSTRAINT face_clusters_cluster_id_key UNIQUE (cluster_id) ); -- 6.5: Create face_recognition_results table CREATE TABLE IF NOT EXISTS face_recognition_results ( id SERIAL PRIMARY KEY, video_uuid VARCHAR(255) NOT NULL UNIQUE, frame_count BIGINT NOT NULL DEFAULT 0, fps DOUBLE PRECISION NOT NULL DEFAULT 0.0, total_faces INTEGER NOT NULL DEFAULT 0, recognized_faces INTEGER NOT NULL DEFAULT 0, clusters_count INTEGER NOT NULL DEFAULT 0, result_data JSONB NOT NULL, processing_time_secs DOUBLE PRECISION, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Constraints CONSTRAINT face_recognition_results_video_uuid_key UNIQUE (video_uuid) ); -- 6.6: Create face_similarity_search function CREATE OR REPLACE FUNCTION find_similar_faces( query_embedding VECTOR(512), similarity_threshold DOUBLE PRECISION DEFAULT 0.6, limit_count INTEGER DEFAULT 10 ) RETURNS TABLE ( face_id VARCHAR(255), name VARCHAR(255), similarity DOUBLE PRECISION, attributes JSONB, metadata JSONB ) AS $$ BEGIN RETURN QUERY SELECT fi.face_id, fi.name, 1 - (fi.embedding <=> query_embedding) AS similarity, fi.attributes, fi.metadata FROM face_identities fi WHERE fi.is_active = TRUE AND fi.embedding IS NOT NULL AND 1 - (fi.embedding <=> query_embedding) >= similarity_threshold ORDER BY fi.embedding <=> query_embedding LIMIT limit_count; END; $$ LANGUAGE plpgsql; -- 6.7: Create function to update face cluster centroids CREATE OR REPLACE FUNCTION update_cluster_centroid(cluster_uuid VARCHAR(255)) RETURNS VOID AS $$ DECLARE new_centroid VECTOR(512); BEGIN -- Calculate new centroid from all face embeddings in the cluster SELECT AVG(embedding) INTO new_centroid FROM face_detections WHERE cluster_id = cluster_uuid AND embedding IS NOT NULL; -- Update cluster centroid UPDATE face_clusters SET centroid = new_centroid, size = (SELECT COUNT(*) FROM face_detections WHERE cluster_id = cluster_uuid) WHERE cluster_id = cluster_uuid; END; $$ LANGUAGE plpgsql; -- 6.8: Create function to find or create face identity CREATE OR REPLACE FUNCTION find_or_create_face_identity( p_face_id VARCHAR(255), p_name VARCHAR(255) DEFAULT NULL, p_embedding VECTOR(512) DEFAULT NULL, p_attributes JSONB DEFAULT NULL, p_metadata JSONB DEFAULT '{}'::jsonb ) RETURNS INTEGER AS $$ DECLARE v_id INTEGER; BEGIN -- Try to find existing face identity SELECT id INTO v_id FROM face_identities WHERE face_id = p_face_id; IF v_id IS NULL THEN -- Create new face identity INSERT INTO face_identities (face_id, name, embedding, attributes, metadata) VALUES (p_face_id, p_name, p_embedding, p_attributes, p_metadata) RETURNING id INTO v_id; ELSE -- Update existing face identity UPDATE face_identities SET name = COALESCE(p_name, name), embedding = COALESCE(p_embedding, embedding), attributes = COALESCE(p_attributes, attributes), metadata = COALESCE(p_metadata, metadata), updated_at = CURRENT_TIMESTAMP WHERE id = v_id; END IF; RETURN v_id; END; $$ LANGUAGE plpgsql; -- 6.9: Create indexes for performance CREATE INDEX IF NOT EXISTS idx_face_detections_video_uuid ON face_detections(video_uuid); CREATE INDEX IF NOT EXISTS idx_face_detections_face_id ON face_detections(face_id); CREATE INDEX IF NOT EXISTS idx_face_detections_frame ON face_detections(video_uuid, frame_number); CREATE INDEX IF NOT EXISTS idx_face_detections_identity ON face_detections(identity_id); CREATE INDEX IF NOT EXISTS idx_face_detections_cluster ON face_detections(cluster_id); CREATE INDEX IF NOT EXISTS idx_face_clusters_video_uuid ON face_clusters(video_uuid); CREATE INDEX IF NOT EXISTS idx_face_recognition_results_created_at ON face_recognition_results(created_at); -- 6.10: Create indexes for vector similarity search CREATE INDEX IF NOT EXISTS idx_face_identities_embedding ON face_identities USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); CREATE INDEX IF NOT EXISTS idx_face_detections_embedding ON face_detections USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); -- 6.11: Add comments COMMENT ON TABLE face_identities IS 'Stores registered face identities with embeddings'; COMMENT ON TABLE face_detections IS 'Stores individual face detections from videos'; COMMENT ON TABLE face_clusters IS 'Stores face clusters from video analysis'; COMMENT ON TABLE face_recognition_results IS 'Stores face recognition processing results'; COMMENT ON FUNCTION find_similar_faces IS 'Finds similar faces based on embedding similarity'; COMMENT ON FUNCTION update_cluster_centroid IS 'Updates cluster centroid from member embeddings'; COMMENT ON FUNCTION find_or_create_face_identity IS 'Finds or creates a face identity record'; -- 6.12: Create trigger to update updated_at timestamp CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Create triggers only if they don't exist DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_trigger WHERE tgname = 'update_face_identities_updated_at' AND tgrelid = 'face_identities'::regclass ) THEN CREATE TRIGGER update_face_identities_updated_at BEFORE UPDATE ON face_identities FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); END IF; IF NOT EXISTS ( SELECT 1 FROM pg_trigger WHERE tgname = 'update_face_recognition_results_updated_at' AND tgrelid = 'face_recognition_results'::regclass ) THEN CREATE TRIGGER update_face_recognition_results_updated_at BEFORE UPDATE ON face_recognition_results FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); END IF; END $$;