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
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
textsen entrée → embeddings en sortie. - Synchrone (utilise
requests, pashttpx). - 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 var | Type | Notes |
|---|---|---|
INFOMANIAK_PID | string | Product ID du service Infomaniak |
INFOMANIAK_TOKEN | string | Bearer token associé |
(EMBEDDING_URL) | string | Pré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 :
travel.nametravel.subtitletravel.descriptiontravel.servicesIncludedtravel.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 :
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 responseComportement :
- 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 = 30s surrequests.post. Pas de retry. - Pas de circuit breaker. Si Infomaniak est down :
- Côté reindex : la page courante lève →
_do_reindexlè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_embeddinglève → la requête/travelsplante en 500. Pas de fallback FTS pur. Le client doit gérer (afficher un message, retry, ou retomber sur une URL/travelssanssearch).
- Côté reindex : la page courante lève →
- 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 :
- Réimplémenter
get_embedding/get_embeddingsderrière la même signature. - Si dimensions ≠ 3584 : migration
ALTER TABLE travels ALTER COLUMN embedding TYPE vector(<new_dim>). Migration coûteuse, requiert un recalcul complet. - Vider la table (
TRUNCATE travels). - 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.

