-- Trouve-KA — migration 001 : schéma initial -- Author: Simon-Pierre Boucher -- Contact: contact@spboucher.ai BEGIN; CREATE TABLE IF NOT EXISTS schema_migrations ( version INTEGER PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- --------------------------------------------------------------------------- -- Domaines connus du web québécois (et non québécois, pour mémoire) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS domains ( id BIGSERIAL PRIMARY KEY, domain TEXT NOT NULL UNIQUE, first_seen TIMESTAMPTZ NOT NULL DEFAULT now(), last_crawled_at TIMESTAMPTZ, robots_fetched_at TIMESTAMPTZ, robots_body TEXT, robots_status TEXT, -- ok | not_found | error | forbidden crawl_delay_ms INTEGER, -- délai imposé par robots.txt (Crawl-delay) quebec_score REAL NOT NULL DEFAULT 0, authority_score REAL NOT NULL DEFAULT 0, page_count INTEGER NOT NULL DEFAULT 0, inlink_domains INTEGER NOT NULL DEFAULT 0, outlink_domains INTEGER NOT NULL DEFAULT 0, language_stats JSONB NOT NULL DEFAULT '{}'::jsonb, content_change_rate REAL, blocked BOOLEAN NOT NULL DEFAULT FALSE, is_seed BOOLEAN NOT NULL DEFAULT FALSE ); CREATE INDEX IF NOT EXISTS idx_domains_quebec ON domains (quebec_score DESC); -- --------------------------------------------------------------------------- -- URLs canoniques connues -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS urls ( id BIGSERIAL PRIMARY KEY, url TEXT NOT NULL UNIQUE, canonical_url TEXT, domain_id BIGINT NOT NULL REFERENCES domains(id), first_seen TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_urls_domain ON urls (domain_id); -- --------------------------------------------------------------------------- -- Frontier : la file d'URLs à crawler, avec priorités et scheduling -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS frontier_items ( id BIGSERIAL PRIMARY KEY, url_id BIGINT NOT NULL UNIQUE REFERENCES urls(id), priority REAL NOT NULL DEFAULT 0.5, depth INTEGER NOT NULL DEFAULT 0, source_url_id BIGINT REFERENCES urls(id), discovered_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_crawled_at TIMESTAMPTZ, next_crawl_at TIMESTAMPTZ NOT NULL DEFAULT now(), status TEXT NOT NULL DEFAULT 'pending', -- pending|in_progress|done|failed|blocked retries INTEGER NOT NULL DEFAULT 0, error_code TEXT, locked_by TEXT, locked_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_frontier_ready ON frontier_items (next_crawl_at, priority DESC) WHERE status = 'pending'; CREATE INDEX IF NOT EXISTS idx_frontier_status ON frontier_items (status); -- --------------------------------------------------------------------------- -- Historique des tentatives de crawl (l'échec est normal; il est traqué) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS crawl_attempts ( id BIGSERIAL PRIMARY KEY, url_id BIGINT NOT NULL REFERENCES urls(id), fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(), status_code INTEGER, error_code TEXT, -- dns|timeout|tls|http_4xx|http_5xx|robots_denied|parse_failed| -- unsupported_content|too_large|duplicate|spam|not_quebec|ssrf_blocked|ok outcome TEXT NOT NULL, -- indexed|duplicate|error|not_quebec|robots_blocked|redirect|unchanged content_hash TEXT, bytes INTEGER, duration_ms INTEGER, redirect_url TEXT, title TEXT, quebec_score REAL ); CREATE INDEX IF NOT EXISTS idx_attempts_time ON crawl_attempts (fetched_at DESC); CREATE INDEX IF NOT EXISTS idx_attempts_url ON crawl_attempts (url_id); -- --------------------------------------------------------------------------- -- Documents indexés (métadonnées; le contenu cherchable vit dans l'index) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS documents ( id BIGSERIAL PRIMARY KEY, url_id BIGINT NOT NULL UNIQUE REFERENCES urls(id), content_hash TEXT NOT NULL, etag TEXT, last_modified TEXT, title TEXT, description TEXT, language TEXT, page_quebec_score REAL NOT NULL DEFAULT 0, published_at TIMESTAMPTZ, last_changed_at TIMESTAMPTZ, first_indexed_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_indexed_at TIMESTAMPTZ NOT NULL DEFAULT now(), enrichment_stage INTEGER NOT NULL DEFAULT 1 ); CREATE INDEX IF NOT EXISTS idx_documents_hash ON documents (content_hash); -- --------------------------------------------------------------------------- -- Graphe de liens agrégé au niveau domaine (page→page viendra plus tard) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS domain_links ( from_domain_id BIGINT NOT NULL REFERENCES domains(id), to_domain_id BIGINT NOT NULL REFERENCES domains(id), link_count INTEGER NOT NULL DEFAULT 1, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (from_domain_id, to_domain_id) ); CREATE INDEX IF NOT EXISTS idx_domain_links_to ON domain_links (to_domain_id); -- --------------------------------------------------------------------------- -- Analytics de recherche agrégées et respectueuses de la vie privée -- (les requêtes zéro-résultat sont de l'or : elles pilotent le crawl) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS search_queries ( id BIGSERIAL PRIMARY KEY, query TEXT NOT NULL, language TEXT, results_total INTEGER NOT NULL, took_ms INTEGER NOT NULL, zero_result BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_queries_zero ON search_queries (created_at) WHERE zero_result; -- --------------------------------------------------------------------------- -- Soumissions publiques d'URL (soumission ≠ inclusion) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS submissions ( id BIGSERIAL PRIMARY KEY, url TEXT NOT NULL, submitted_at TIMESTAMPTZ NOT NULL DEFAULT now(), status TEXT NOT NULL DEFAULT 'queued' -- queued|accepted|rejected ); INSERT INTO schema_migrations (version) VALUES (1) ON CONFLICT DO NOTHING; COMMIT;