Skip to content

Module — Couche d'embeddings

Petit wrapper qui isole le service d'embeddings derrière deux fonctions Python. Code dans src/infomaniak.py (40 lignes).

Cette page décrit l'interface du module et son rôle dans le pipeline. Les détails du fournisseur (auth, endpoints) sont volontairement omis. Pour les opérer en prod, voir le coffre-fort équipe et architecture/deployment.md (section secrets).

Surface publique

python
def get_embedding(text: str) -> list[float]:
    """Embedding pour un seul texte. Retourne 3584 floats."""

def get_embeddings(texts: list[str]) -> list[list[float]]:
    """Embeddings pour un batch (jusqu'à ~10 textes / page reindex)."""
  • Préserve l'ordre des texts en entrée → embeddings en sortie.
  • Synchrone (utilise requests, pas httpx).
  • Lève si l'API répond non-2xx (raise_for_status()).

Modèle utilisé

bge_multilingual_gemma2 — BGE Multilingual Gemma2, 3584 dimensions. Rationale : ADR 0003.

Configuration

Env varTypeNotes
INFOMANIAK_PIDstringProduct ID du service Infomaniak
INFOMANIAK_TOKENstringBearer token associé
(EMBEDDING_URL)stringPrésent dans app.py (assert non vide) mais non utilisé par infomaniak.py — relique d'un ancien design

Provisioning des credentials : hors documentation publique. Demander à l'équipe d'infra Spektrum.

Où c'est appelé

┌─────────────────────────────┐    ┌────────────────────────────┐
│  src/reindex.py             │    │  src/db.py                 │
│  _ingest_source(...)        │    │  BuchardDatabase.search(): │
│  ─ embedding_texts = […]    │    │  ─ if search_query :        │
│  ─ get_embeddings(texts)    │    │     get_embedding(query)   │
│    (1 appel par page)       │    │     (1 appel par recherche)│
└─────────────────────────────┘    └────────────────────────────┘
  • Côté reindex : batch de 10 textes à la fois (= une page amont). Un appel par page, donc ~30 appels par cycle de reindex pour un catalogue de 300 voyages.
  • Côté recherche : un appel par requête utilisateur avec ?search= non vide. Sur le path critique de la latence — cf. architecture/search-tuning.md § « latence ».

Construction du texte à embedder (reindex)

src/_utils.get_embedding_text(travel) concatène, après nettoyage HTML :

  1. travel.name
  2. travel.subtitle
  3. travel.description
  4. travel.servicesIncluded
  5. travel.highLights

Séparés par \n. Asymétrie avec la FTS : get_embedding_text inclut servicesIncluded et highLights que la tsvector search_vector ne couvre pas. Voir domain/travel-record.md.

Le nettoyage HTML utilise BeautifulSoup(input_str, 'html.parser').get_text(). Pas de strip explicite ; les espaces et sauts de ligne issus du HTML sont conservés (acceptable pour un embedding).

Stockage en DB

Une seule colonne : embedding vector(3584) NOT NULL dans travels. Pas de timestamp / hash / metadata. Si on bascule sur un autre modèle, toute la table doit être re-embeddée (cf. ADR 0003).

Indexation : aucun index ANN (hnsw, ivfflat). Seq scan à chaque recherche, suffisant à l'échelle actuelle.

Tests

mock_infomaniak_api (fixture pytest dans tests/conftest.py) patch src.infomaniak.requests.post pour retourner des embeddings fixtures :

python
def _mock_post(url, **kwargs):
    response = MagicMock()
    payload = kwargs.get('json', {})
    inputs = payload.get('input', [])
    if isinstance(inputs, str):
        inputs = [inputs]
    embeddings = []
    for i, text in enumerate(inputs):
        if text in test_query_embeddings:
            emb = test_query_embeddings[text]
        else:
            np.random.seed(hash(text) % (2**32))
            emb = np.random.rand(3584).tolist()
        embeddings.append({"index": i, "embedding": emb})
    response.json.return_value = {"data": embeddings}
    response.status_code = 200
    return response

Comportement :

  • Textes dans tests/test_query_embeddings.json → embedding stockée (utilisée pour les tests de pertinence où l'embedding cible matche un voyage spécifique).
  • Autres textes → embedding pseudo-aléatoire mais déterministe (seed = hash(text)).

Pas d'appel réseau dans la suite de tests.

Échecs et résilience

  • Timeout : _TIMEOUT = 30 s sur requests.post. Pas de retry.
  • Pas de circuit breaker. Si Infomaniak est down :
    • Côté reindex : la page courante lève → _do_reindex lève → le DELETE final n'est pas exécuté → l'ancien contenu reste interrogeable. Au prochain cycle (15 min plus tard), retry.
    • Côté recherche avec ?search= : infomaniak.get_embedding lève → la requête /travels plante en 500. Pas de fallback FTS pur. Le client doit gérer (afficher un message, retry, ou retomber sur une URL /travels sans search).
  • Réponse mal formée (manque data, ordre cassé, dimension ≠ 3584) : la fonction crash. Pas de validation explicite.

Pistes d'amélioration (non implémentées) :

  • Retry avec backoff (3 essais, 1s/3s/9s).
  • Cache LRU côté Python sur get_embedding(query) pour les recherches répétées.
  • Fallback FTS pur en cas d'échec embedding.

Si l'on change de fournisseur ou de modèle

Étapes :

  1. Réimplémenter get_embedding / get_embeddings derrière la même signature.
  2. Si dimensions ≠ 3584 : migration ALTER TABLE travels ALTER COLUMN embedding TYPE vector(<new_dim>). Migration coûteuse, requiert un recalcul complet.
  3. Vider la table (TRUNCATE travels).
  4. Déclencher un reindex complet pour repeupler.

Voir ADR 0003.

Sécurité

  • Token Infomaniak monté via secret Docker (/run/secrets/infomaniak_token → env var ou directement, selon le déploiement).
  • Aucun log applicatif ne contient le header Authorization.
  • Aucune URL ou identifiant ne ressort en réponse HTTP (pas exposé via /, /travels, ou /metrics).
  • Si jamais le token fuite, le rotation se fait côté Infomaniak (panel admin) + mise à jour du secret côté infra.

Contributors

No contributors

Changelog

No recent changes