mail/app/services/search.py
deeily 5024bf9a8d init: full mail stack — phases 0..8 (web client, admin, IMAP/SMTP,
sieve, search, sessions, dramatiq, deploy/install, ELK, monitoring)
2026-04-29 16:30:43 +03:00

67 lines
2.0 KiB
Python

"""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}"