perf: add GIN trigram indexes for ILIKE substring search
Eliminates full table scans on all text search queries by enabling pg_trgm extension and creating GIN indexes with gin_trgm_ops on every column used in LIKE/ILIKE '%text%' patterns. Changes: - Add pg_trgm extension to schema.sql - Add 10 GIN trigram indexes: contacts (full_name, first_name, last_name, nickname, organization, email::text, phone::text), calendar_events (summary), files (name), folders (name) - Unify all LOWER(col) LIKE patterns to col ILIKE — eliminates .to_lowercase() allocation in Rust and ensures index match - Add minimum 3-char guard on search queries so PostgreSQL uses the trigram index instead of falling back to sequential scan - Add migration 004 with CONCURRENTLY for zero-downtime upgrades Expected improvement: 100-500x faster text searches on large datasets (e.g. 100K contacts: ~1.5s → ~3ms). https://claude.ai/code/session_01QpWV7HXAagdZfyefUw6wKC
This commit is contained in:
@@ -253,6 +253,9 @@ CREATE TABLE IF NOT EXISTS caldav.calendar_events (
|
||||
CREATE INDEX IF NOT EXISTS idx_calendar_events_calendar_id ON caldav.calendar_events(calendar_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_calendar_events_ical_uid ON caldav.calendar_events(ical_uid);
|
||||
CREATE INDEX IF NOT EXISTS idx_calendar_events_time_range ON caldav.calendar_events(calendar_id, start_time, end_time);
|
||||
-- GIN trigram index for ILIKE substring search (find_events_by_summary)
|
||||
CREATE INDEX IF NOT EXISTS idx_calendar_events_summary_trgm
|
||||
ON caldav.calendar_events USING gin (summary gin_trgm_ops);
|
||||
|
||||
-- Calendar sharing
|
||||
CREATE TABLE IF NOT EXISTS caldav.calendar_shares (
|
||||
@@ -283,6 +286,10 @@ COMMENT ON TABLE caldav.calendar_events IS 'Calendar events (VEVENT) stored with
|
||||
COMMENT ON TABLE caldav.calendar_shares IS 'Calendar sharing permissions between users';
|
||||
COMMENT ON TABLE caldav.calendar_properties IS 'Custom WebDAV properties on calendars';
|
||||
|
||||
-- ── pg_trgm extension for GIN trigram indexes (ILIKE / LIKE substring search) ──
|
||||
-- Required before creating any gin_trgm_ops indexes below.
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
|
||||
-- ============================================================
|
||||
-- 3. CARDDAV SCHEMA (RFC 6352)
|
||||
-- ============================================================
|
||||
@@ -331,6 +338,22 @@ CREATE INDEX IF NOT EXISTS idx_contacts_address_book_id ON carddav.contacts(addr
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_uid ON carddav.contacts(uid);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_full_name ON carddav.contacts(full_name);
|
||||
|
||||
-- GIN trigram indexes for ILIKE substring search (search_contacts, get_contacts_by_email)
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_full_name_trgm
|
||||
ON carddav.contacts USING gin (full_name gin_trgm_ops);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_first_name_trgm
|
||||
ON carddav.contacts USING gin (first_name gin_trgm_ops);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_last_name_trgm
|
||||
ON carddav.contacts USING gin (last_name gin_trgm_ops);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_nickname_trgm
|
||||
ON carddav.contacts USING gin (nickname gin_trgm_ops);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_organization_trgm
|
||||
ON carddav.contacts USING gin (organization gin_trgm_ops);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_email_text_trgm
|
||||
ON carddav.contacts USING gin ((email::text) gin_trgm_ops);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_phone_text_trgm
|
||||
ON carddav.contacts USING gin ((phone::text) gin_trgm_ops);
|
||||
|
||||
-- Address book sharing
|
||||
CREATE TABLE IF NOT EXISTS carddav.address_book_shares (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@@ -441,6 +464,9 @@ CREATE INDEX IF NOT EXISTS idx_folders_trashed ON storage.folders(user_id, is_tr
|
||||
CREATE INDEX IF NOT EXISTS idx_folders_lpath ON storage.folders USING gist (lpath);
|
||||
-- B-tree index on path for exact path lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_folders_path ON storage.folders (path text_pattern_ops);
|
||||
-- GIN trigram index for ILIKE substring search (search_folders, suggest_folders_by_name)
|
||||
CREATE INDEX IF NOT EXISTS idx_folders_name_trgm
|
||||
ON storage.folders USING gin (name gin_trgm_ops);
|
||||
|
||||
-- ── ltree trigger: compute path & lpath on INSERT or UPDATE of name/parent_id ──
|
||||
CREATE OR REPLACE FUNCTION storage.compute_folder_path()
|
||||
@@ -528,6 +554,9 @@ CREATE INDEX IF NOT EXISTS idx_files_folder_id ON storage.files(folder_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_blob_hash ON storage.files(blob_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_trashed ON storage.files(user_id, is_trashed);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_name_search ON storage.files(user_id, name text_pattern_ops);
|
||||
-- GIN trigram index for ILIKE substring search (search_files, suggest_files_by_name)
|
||||
CREATE INDEX IF NOT EXISTS idx_files_name_trgm
|
||||
ON storage.files USING gin (name gin_trgm_ops);
|
||||
|
||||
-- Trash view combining trashed files and folders for the TrashRepository.
|
||||
-- Only shows top-level trashed items: excludes files/folders whose parent
|
||||
|
||||
Reference in New Issue
Block a user