claude-os/app/core/schema_pgvector.sql
2025-10-24 16:17:47 -05:00

232 lines
8.4 KiB
PL/PgSQL

-- ============================================================================
-- Code-Forge PGVectorStore-Compatible Schema
-- ============================================================================
-- This schema is designed to work with llama_index's PGVectorStore
-- while maintaining multi-tenancy support for multiple knowledge bases.
--
-- Strategy: Separate table per KB (data_{kb_name})
-- - Clean isolation between KBs
-- - Easy to delete entire KB (DROP TABLE)
-- - Matches PGVectorStore's expected structure
-- ============================================================================
-- Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- ============================================================================
-- METADATA TABLE: knowledge_bases
-- ============================================================================
-- Tracks all knowledge bases in the system
-- Each KB gets its own data_{kb_name} table created dynamically
-- ============================================================================
-- Create or alter knowledge_bases table
DO $$
BEGIN
-- Create table if it doesn't exist
CREATE TABLE IF NOT EXISTS knowledge_bases (
id SERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL,
kb_type VARCHAR(50) DEFAULT 'GENERIC',
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
metadata JSONB DEFAULT '{}'::jsonb
);
-- Add new columns if they don't exist
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name='knowledge_bases' AND column_name='table_name') THEN
ALTER TABLE knowledge_bases ADD COLUMN table_name VARCHAR(255) UNIQUE;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name='knowledge_bases' AND column_name='embed_dim') THEN
ALTER TABLE knowledge_bases ADD COLUMN embed_dim INTEGER NOT NULL DEFAULT 768;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name='knowledge_bases' AND column_name='updated_at') THEN
ALTER TABLE knowledge_bases ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
END IF;
-- Add constraints if they don't exist
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'valid_kb_type') THEN
ALTER TABLE knowledge_bases ADD CONSTRAINT valid_kb_type
CHECK (kb_type IN ('GENERIC', 'CODE', 'DOCUMENTATION', 'AGENT_OS'));
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'valid_table_name') THEN
ALTER TABLE knowledge_bases ADD CONSTRAINT valid_table_name
CHECK (table_name IS NULL OR table_name ~ '^data_[a-z0-9_]+$');
END IF;
END$$;
-- Index for fast lookups
CREATE INDEX IF NOT EXISTS idx_kb_name ON knowledge_bases(name);
CREATE INDEX IF NOT EXISTS idx_kb_table_name ON knowledge_bases(table_name);
CREATE INDEX IF NOT EXISTS idx_kb_type ON knowledge_bases(kb_type);
-- ============================================================================
-- VECTOR STORE TABLES: data_{kb_name}
-- ============================================================================
-- These tables are created dynamically per KB using the template below.
-- PGVectorStore expects this exact structure.
--
-- Example for KB named "pistn":
-- ============================================================================
-- TEMPLATE (not executed, just for reference):
/*
CREATE TABLE data_{kb_name} (
id BIGSERIAL PRIMARY KEY,
text VARCHAR NOT NULL, -- Document text content
metadata_ JSON, -- Document metadata (JSONB for better performance)
node_id VARCHAR, -- llama_index node identifier
embedding vector({embed_dim}) -- Vector embedding (768 for nomic-embed-text)
);
-- Index for metadata filtering (ref_doc_id is commonly used)
CREATE INDEX idx_{kb_name}_metadata_ref_doc
ON data_{kb_name} ((metadata_ ->> 'ref_doc_id'));
-- Vector similarity index (IVFFlat for fast approximate search)
CREATE INDEX idx_{kb_name}_embedding
ON data_{kb_name}
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- Optional: HNSW index for even faster search (requires more memory)
-- CREATE INDEX idx_{kb_name}_embedding_hnsw
-- ON data_{kb_name}
-- USING hnsw (embedding vector_cosine_ops)
-- WITH (m = 16, ef_construction = 64);
*/
-- ============================================================================
-- HELPER FUNCTIONS
-- ============================================================================
-- Function to create a new KB table
CREATE OR REPLACE FUNCTION create_kb_table(
p_kb_name VARCHAR,
p_table_name VARCHAR,
p_embed_dim INTEGER DEFAULT 768
) RETURNS VOID AS $$
DECLARE
v_index_suffix VARCHAR;
BEGIN
-- Create safe index suffix from table name (remove data_ prefix)
v_index_suffix := REPLACE(p_table_name, 'data_', '');
-- Create the data table
EXECUTE format('
CREATE TABLE IF NOT EXISTS %I (
id BIGSERIAL PRIMARY KEY,
text VARCHAR NOT NULL,
metadata_ JSON,
node_id VARCHAR,
embedding vector(%s)
)', p_table_name, p_embed_dim);
-- Create metadata index
EXECUTE format('
CREATE INDEX IF NOT EXISTS %I
ON %I ((metadata_ ->> ''ref_doc_id''))',
'idx_' || v_index_suffix || '_metadata_ref_doc',
p_table_name);
-- Create vector index (IVFFlat)
EXECUTE format('
CREATE INDEX IF NOT EXISTS %I
ON %I USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100)',
'idx_' || v_index_suffix || '_embedding',
p_table_name);
RAISE NOTICE 'Created table % with vector dimension %', p_table_name, p_embed_dim;
END;
$$ LANGUAGE plpgsql;
-- Function to drop a KB table
CREATE OR REPLACE FUNCTION drop_kb_table(p_table_name VARCHAR) RETURNS VOID AS $$
BEGIN
EXECUTE format('DROP TABLE IF EXISTS %I CASCADE', p_table_name);
RAISE NOTICE 'Dropped table %', p_table_name;
END;
$$ LANGUAGE plpgsql;
-- Function to get KB stats
CREATE OR REPLACE FUNCTION get_kb_stats(p_table_name VARCHAR)
RETURNS TABLE(
total_documents BIGINT,
avg_text_length NUMERIC,
total_size_bytes BIGINT
) AS $$
BEGIN
RETURN QUERY EXECUTE format('
SELECT
COUNT(*)::BIGINT as total_documents,
AVG(LENGTH(text))::NUMERIC as avg_text_length,
pg_total_relation_size(%L)::BIGINT as total_size_bytes
FROM %I',
p_table_name, p_table_name);
END;
$$ LANGUAGE plpgsql;
-- ============================================================================
-- MIGRATION NOTES
-- ============================================================================
-- To migrate from old schema (documents table) to new schema:
--
-- 1. For each KB in knowledge_bases:
-- - Create new data_{kb_name} table using create_kb_table()
-- - Copy data from documents WHERE kb_id = X to data_{kb_name}
-- - Transform columns: content -> text, metadata -> metadata_
-- - Generate node_id if not present
--
-- 2. Update knowledge_bases table:
-- - Add table_name column
-- - Add embed_dim column
--
-- 3. Drop old documents table after verification
--
-- See scripts/migrate_to_pgvector.py for automated migration
-- ============================================================================
-- ============================================================================
-- EXAMPLE USAGE
-- ============================================================================
/*
-- Create a new KB
INSERT INTO knowledge_bases (name, kb_type, description, table_name, embed_dim)
VALUES ('pistn', 'CODE', 'Pistn appointment system documentation', 'data_pistn', 768);
-- Create the table
SELECT create_kb_table('pistn', 'data_pistn', 768);
-- Insert a document (normally done via PGVectorStore)
INSERT INTO data_pistn (text, metadata_, node_id, embedding)
VALUES (
'Sample document text',
'{"filename": "test.md", "source": "upload"}'::json,
'node_abc123',
'[0.1, 0.2, ...]'::vector(768)
);
-- Query similar documents
SELECT
text,
metadata_,
1 - (embedding <=> '[0.1, 0.2, ...]'::vector(768)) as similarity
FROM data_pistn
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector(768)
LIMIT 10;
-- Get KB stats
SELECT * FROM get_kb_stats('data_pistn');
-- Delete a KB
DELETE FROM knowledge_bases WHERE name = 'pistn';
SELECT drop_kb_table('data_pistn');
*/