# Trouve-KA — client OpenSearch # Author: Simon-Pierre Boucher # Contact: contact@spboucher.ai """Client OpenSearch async : création d'index, indexation immédiate, recherche, statut.""" import hashlib from typing import Any from opensearchpy import AsyncOpenSearch from opensearchpy.exceptions import NotFoundError from .mapping import INDEX_SETTINGS def doc_id_for_url(url: str) -> str: """ID de document stable dérivé de l'URL canonique.""" return hashlib.sha256(url.encode("utf-8")).hexdigest()[:32] class SearchCore: def __init__(self, search_url: str, index: str): self.index = index self.client = AsyncOpenSearch(hosts=[search_url], timeout=15, max_retries=2, retry_on_timeout=True) async def close(self) -> None: await self.client.close() async def ensure_index(self) -> None: if not await self.client.indices.exists(index=self.index): await self.client.indices.create(index=self.index, body=INDEX_SETTINGS) async def index_document(self, doc: dict[str, Any]) -> str: """Indexation immédiate d'un document (cherchable au prochain refresh, ~1 s).""" _id = doc_id_for_url(doc["canonical_url"] or doc["url"]) await self.client.index(index=self.index, id=_id, body=doc) return _id async def update_document(self, url: str, partial: dict[str, Any]) -> None: """Mise à jour partielle (enrichissement asynchrone, étapes 2-3).""" try: await self.client.update(index=self.index, id=doc_id_for_url(url), body={"doc": partial}) except NotFoundError: pass async def delete_document(self, url: str) -> None: try: await self.client.delete(index=self.index, id=doc_id_for_url(url)) except NotFoundError: pass async def search(self, body: dict[str, Any]) -> dict[str, Any]: return await self.client.search(index=self.index, body=body) async def count(self) -> int: try: res = await self.client.count(index=self.index) return res["count"] except NotFoundError: return 0 async def ping(self) -> bool: try: return await self.client.ping() except Exception: return False