"""Meilisearch wrapper — index mail bodies and run search queries. Phase 6 scaffolding: connect, ensure index, simple add/search. Indexing is triggered from IMAP IDLE pushes (manager) and a nightly reconcile. """ from __future__ import annotations import logging from typing import Any, Iterable from flask import current_app log = logging.getLogger(__name__) INDEX_NAME = "messages" def _client(): from meilisearch import Client url = current_app.config.get("MEILI_URL", "http://localhost:7700") key = current_app.config.get("MEILI_KEY", "") return Client(url, key) if key else Client(url) def ensure_index(): try: c = _client() try: c.get_index(INDEX_NAME) except Exception: c.create_index(INDEX_NAME, {"primaryKey": "id"}) idx = c.index(INDEX_NAME) idx.update_searchable_attributes(["subject", "from_email", "from_name", "to", "preview", "body"]) idx.update_filterable_attributes(["owner_email", "folder_key", "uid"]) return True except Exception: log.warning("Meili ensure_index failed", exc_info=True) return False def add_documents(docs: Iterable[dict[str, Any]]): try: c = _client() c.index(INDEX_NAME).add_documents(list(docs)) return True except Exception: log.warning("Meili add_documents failed", exc_info=True) return False def search(owner_email: str, query: str, limit: int = 30) -> list[dict]: try: c = _client() res = c.index(INDEX_NAME).search( query, {"filter": f"owner_email = '{owner_email}'", "limit": limit}, ) return list(res.get("hits", [])) except Exception: log.warning("Meili search failed", exc_info=True) return [] def doc_id(owner_email: str, folder_key: str, uid: int) -> str: # Meili allows only [a-zA-Z0-9_-] in primary keys safe_owner = owner_email.replace("@", "_at_").replace(".", "_") return f"{safe_owner}__{folder_key}__{uid}"