# SQLite-индекс архива писем ## Назначение Быстрый поиск и трекинг по архиву email.md без grep-а по всем папкам. Основа для: contacts_extractor (знает какие письма обработаны), sqlite_search (FTS5), digest (выборка по дате). ## Компоненты | Компонент | Путь | Назначение | |-----------|------|------------| | Индексатор | `/opt/hermes/email-assistant/scripts/mail_index.py` | Сканирует email.md → SQLite | | Поиск | `/opt/hermes/email-assistant/scripts/sqlite_search.py` | FTS5-поиск по индексу | | База | `/opt/hermes/email/mail_index.db` | SQLite (WAL mode) | ## Схема БД ### emails — основная таблица ```sql CREATE TABLE IF NOT EXISTS emails ( path TEXT PRIMARY KEY, -- относительный путь от EMAIL_ROOT uid INTEGER, -- числовой UID из пути folder TEXT, -- INBOX, INBOX/!Scan, Sent... date TEXT, -- дата из frontmatter (ISO) from_addr TEXT, -- отправитель to_addrs TEXT, -- получатели subject TEXT, -- тема body_preview TEXT, -- первые 500 символов тела (без HTML) contacts_extracted INTEGER DEFAULT 0, -- 0/1 — обработано contacts_extractor contacts_skipped INTEGER DEFAULT 0, -- 0/1 — нет подписи / LLM error first_seen TEXT, -- когда проиндексировано last_scanned TEXT, -- последняя проверка contacts file_mtime REAL -- mtime файла для инкрементальной проверки ); ``` ### email_fts — FTS5 virtual table ```sql CREATE VIRTUAL TABLE IF NOT EXISTS email_fts USING fts5( subject, from_addr, to_addrs, body_preview, content='emails', content_rowid='rowid', tokenize='unicode61' ); ``` Синхронизация через триггеры INSERT/UPDATE/DELETE + FTS5 rebuild. ### Индексы - `idx_emails_folder` — быстрая фильтрация по папке - `idx_emails_uid` — lookup по UID - `idx_emails_contacts` — необработанные письма (contacts_extracted=0) - `idx_emails_date` — сортировка по дате ## Контракт между mail_index.py и contacts_extractor.py **mail_index.py** владеет схемой и создаёт таблицы. **contacts_extractor.py** — только читает/пишет поля `contacts_extracted`, `contacts_skipped`, `last_scanned`. ```sql -- contacts_extractor берёт необработанные письма: SELECT rowid, path, uid, folder, from_addr, subject FROM emails WHERE contacts_extracted = 0 AND contacts_skipped = 0 ORDER BY folder, uid LIMIT ? -- После обработки: UPDATE emails SET contacts_extracted=1, last_scanned=? WHERE rowid=? UPDATE emails SET contacts_skipped=1, last_scanned=? WHERE rowid=? ``` ## Быстродействие - 2073 письма → полная индексация ~9 секунд - FTS5-поиск — мгновенно (<100ms) - WAL mode — конкурентные чтения не блокируют запись - Инкрементальная индексация по mtime — доли секунды ## Использование sqlite_search.py ```bash # Простой поиск (AND по умолчанию) python3 sqlite_search.py 'Стороженко' python3 sqlite_search.py 'битрикс OR контрагент' python3 sqlite_search.py '"точечная фраза"' # Фильтры python3 sqlite_search.py --folder 'INBOX/!Отчеты' python3 sqlite_search.py --limit 20 # Специальные префиксы (точно в поле from_addr/subject через LIKE) python3 sqlite_search.py 'from:example@mail' python3 sqlite_search.py 'subject:отчёт' # Вкл. тело письма в FTS5 (медленнее, но находит больше) python3 sqlite_search.py --body ``` ## Операторы FTS5 - `AND` — по умолчанию между словами - `OR` — `'битрикс OR контрагент'` - `"точная фраза"` — кавычки для точного совпадения - `-исключить` — минус перед словом - `prefix*` — wildcard (звёздочка на конце) ## mail_index.py — ключевые параметры ```bash python3 mail_index.py # полная переиндексация python3 mail_index.py --incremental # только новые (по mtime) python3 mail_index.py --search "..." # поиск (встроенный, без FTS5) python3 mail_index.py --stats # статистика ``` ## Важные детали - **body_preview** — только первые 500 символов. Для full-text search с телом используй `sqlite_search.py --body` (FTS5 на preview). - **contacts_extracted/contacts_skipped** — взаимоисключающие флаги. Если ни один не 1 — письмо не обработано. - **file_mtime** — для инкрементальной индексации. Если mtime файла > last_mtime в БД — переиндексировать. - **FTS5 rebuild** — вызывается после каждой полной индексации для согласованности. - Исключает папки `contacts/` и `state/` из сканирования.