Les embeddings constituent le fondement de nombreuses applications d'intelligence artificielle moderne, des moteurs de recherche sémantique aux systèmes de recommandation. Cependant, le coût computationnel du génération d'embedding peut rapidement devenir prohibitif lorsqu'il s'agit de traiter des millions de requêtes quotidiennes. Dans ce tutoriel, nous explorons les stratégies de mise en cache permettant d'optimiser les performances et de réduire les coûts.

Comparatif des Solutions d'Embedding

CritèreHolySheep AIAPI OfficielleServices Relais
Latence moyenne<50ms80-150ms60-120ms
Coût par million de tokensÀ partir de ¥0.50$0.13-0.20$0.10-0.18
Méthodes de paiementWeChat/Alipay/CarteCarte internationaleVariable
Crédits gratuitsOui (offerts à l'inscription)NonVariable

Principe Fondamental du Cache d'Embeddings

Le principe est simple : au lieu de recalculer l'embedding d'un texte déjà traité, nous le stockons et le récupérons lors des requêtes ultérieures. Cette approche répond particulièrement aux scénarios où un petit nombre de requêtes représente une grande partie du trafic total.

Implémentation en Python

Architecture de Base avec Redis

import hashlib
import redis
import json
from typing import List, Optional

class EmbeddingCache:
    def __init__(self, redis_host: str = "localhost", redis_port: int = 6379):
        self.redis_client = redis.Redis(
            host=redis_host,
            port=redis_port,
            db=0,
            decode_responses=True
        )
        self.cache_ttl = 86400  # 24 heures par défaut
    
    def _generate_cache_key(self, text: str, model: str) -> str:
        """Génère une clé de cache unique basée sur le hash du texte."""
        text_hash = hashlib.sha256(text.encode('utf-8')).hexdigest()
        return f"embedding:{model}:{text_hash}"
    
    def get_cached_embedding(self, text: str, model: str) -> Optional[List[float]]:
        """Récupère un embedding depuis le cache."""
        cache_key = self._generate_cache_key(text, model)
        cached = self.redis_client.get(cache_key)
        
        if cached:
            return json.loads(cached)
        return None
    
    def cache_embedding(self, text: str, model: str, embedding: List[float], ttl: int = None):
        """Stocke un embedding dans le cache."""
        cache_key = self._generate_cache_key(text, model)
        ttl = ttl or self.cache_ttl
        
        self.redis_client.setex(
            cache_key,
            ttl,
            json.dumps(embedding)
        )

Utilisation

cache = EmbeddingCache() cached = cache.get_cached_embedding("Quelle est la capitale de la France?", "text-embedding-3-small") print(f"Cache hit: {cached is not None}")

Client API Compatible avec HolySheep

import requests
import hashlib
from typing import List, Dict
import json

class HolySheepEmbeddingClient:
    def __init__(self, api_key: str, base_url: str = "https://api.holysheep.ai/v1"):
        self.api_key = api_key
        self.base_url = base_url
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        })
    
    def generate_embedding(self, text: str, model: str = "text-embedding-3-small") -> List[float]:
        """Génère un embedding via l'API HolySheep."""
        response = self.session.post(
            f"{self.base_url}/embeddings",
            json={
                "input": text,
                "model": model
            }
        )
        response.raise_for_status()
        data = response.json()
        return data["data"][0]["embedding"]
    
    def batch_generate(self, texts: List[str], model: str = "text-embedding-3-small") -> Dict[str, List[float]]:
        """Génère des embeddings par lots avec mise en cache."""
        results = {}
        for text in texts:
            cache_key = hashlib.md5(text.encode()).hexdigest()
            results[cache_key] = self.generate_embedding(text, model)
        return results

Initialisation du client

client = HolySheepEmbeddingClient(api_key="YOUR_HOLYSHEEP_API_KEY") embedding = client.generate_embedding("Bonjour le monde") print(f"Embedding généré: {len(embedding)} dimensions")

Système de Cache Multi-Niveaux

from functools import lru_cache
import hashlib
import time
from collections import OrderedDict
from threading import Lock

class LRUCacheEmbedding:
    """Cache LRU en mémoire pour les embeddings très fréquemment utilisés."""
    
    def __init__(self, capacity: int = 1000):
        self.cache = OrderedDict()
        self.capacity = capacity
        self.lock = Lock()
        self.stats = {"hits": 0, "misses": 0}
    
    def _hash_text(self, text: str) -> str:
        return hashlib.sha256(text.encode()).hexdigest()
    
    def get(self, text: str) -> tuple:
        """Retourne (embedding, hit)"""
        key = self._hash_text(text)
        
        with self.lock:
            if key in self.cache:
                self.cache.move_to_end(key)
                self.stats["hits"] += 1
                return self.cache[key], True
            
            self.stats["misses"] += 1
            return None, False
    
    def put(self, text: str, embedding: list):
        key = self._hash_text(text)
        
        with self.lock:
            if key in self.cache:
                self.cache.move_to_end(key)
            self.cache[key] = embedding
            
            if len(self.cache) > self.capacity:
                self.cache.popitem(last=False)
    
    def get_stats(self) -> dict:
        total = self.stats["hits"] + self.stats["misses"]
        hit_rate = self.stats["hits"] / total if total > 0 else 0
        return {
            **self.stats,
            "total_requests": total,
            "hit_rate": f"{hit_rate:.2%}"
        }

Démonstration

cache = LRUCacheEmbedding(capacity=100) cache.put("Paris", [0.1, 0.2, 0.3]) embedding, hit = cache.get("Paris") print(f"Cache hit: {hit}, Stats: {cache.get_stats()}")

Stratégies d'Invalidation du Cache

Optimisation pour les Charges de Travail Élevées

Pour les applications manipulant des millions de requêtes, une architecture distribuée devient nécessaire. Le pattern suivant combine un cache local L1 (Redis) avec un cache partagé L2 (Memcached) pour maximiser les performances tout en minimisant la charge sur l'API sous-jacente.

import asyncio
import aioredis
from typing import List, Optional
import numpy as np

class DistributedEmbeddingCache:
    """Cache distribué multi-niveaux pour embeddings."""
    
    def __init__(self, redis_url: str = "redis://localhost:6379"):
        self.redis: Optional[aioredis.Redis] = None
        self.redis_url = redis_url
        self.local_cache: LRUCacheEmbedding = LRUCacheEmbedding(capacity=500)
    
    async def connect(self):
        self.redis = await aioredis.create_redis_pool(self.redis_url)
    
    async def get_embedding(self, text: str, model: str) -> Optional[List[float]]:
        # Niveau L1: Cache local
        embedding, hit = self.local_cache.get(text)
        if hit:
            return embedding
        
        # Niveau L2: Redis
        cache_key = f"emb:{model}:{hashlib.sha256(text.encode()).hexdigest()}"
        cached = await self.redis.get(cache_key)
        
        if cached:
            embedding = json.loads(cached)
            self.local_cache.put(text, embedding)  # Populate L1
            return embedding
        
        return None
    
    async def close(self):
        if self.redis:
            self.redis.close()

Exemple d'utilisation asynchrone

async def main(): cache = DistributedEmbeddingCache() await cache.connect() result = await cache.get_embedding("Ma requête", "text-embedding-3-small") print(f"Résultat: {result}") await cache.close() asyncio.run(main())

Erreurs courantes et solutions

Erreur 1 : Clé de cache non unique pour textes similaires

# PROBLÈME : Hachage direct sans normalisation
def bad_hash(text):
    return hashlib.md5(text.encode())  # Sensible aux espaces supplémentaires

SOLUTION : Normaliser le texte avant hachage

def good_hash(text): normalized = ' '.join(text.lower().split()) # Normalise les espaces normalized = normalized.strip() # Supprime les espaces bords return hashlib.sha256(normalized.encode()).hexdigest()

Test

print(good_hash(" Bonjour le monde ")) # Égal à print(good_hash("Bonjour le monde")) # Ces deux produisent le même hash

Erreur 2 : Fuite mémoire dans le cache local

# PROBLÈME : Cache sans limite de capacité
class MemoryLeakCache:
    def __init__(self):
        self.cache = {}  # Grandit indéfiniment

SOLUTION : Implémenter une politique d'éviction

class SafeCache: def __init__(self, max_size: int = 10000): self.cache = OrderedDict() self.max_size = max_size def put(self, key, value): if len(self.cache) >= self.max_size: # Élimine les 10% les plus anciens for _ in range(self.max_size // 10): self.cache.popitem(last=False) self.cache[key] = value self.cache.move_to_end(key)

Erreur 3 : Latence excessive avec Redis non optimisé

# PROBLÈME : Connexion créée pour chaque requête
def bad_approach(texts):
    results = []
    for text in texts:
        r = redis.Redis(host='localhost')  # Nouvelle connexion à chaque fois
        results.append(r.get(text))
    return results

SOLUTION : Pool de connexions et pipeline

import redis from redis.connection import ConnectionPool pool = ConnectionPool(host='localhost', max_connections=20) def good_approach(texts): client = redis.Redis(connection_pool=pool) pipe = client.pipeline() # Batch les opérations for text in texts: pipe.get(text) return pipe.execute() # Exécute tout en une seule commande réseau

Erreur 4 : Incohérence des dimensions d'embedding

# PROBLÈME : Mixing de modèles avec dimensions différentes
cache = {}
cache[" texte"] = [0.1, 0.2]  # embedding-ada-002 (1536 dims)
cache["texte2"] = [0.1]*1536  # text-embedding-3-small (1536 dims)

SOLUTION : Inclure le nom du modèle dans la clé de cache

def get_embedding_safe(text: str, model: str, client) -> list: cache_key = f"emb:{model}:{hashlib.sha256(text.encode()).hexdigest()}" cached = redis_client.get(cache_key) if cached: return json.loads(cached) embedding = client.generate_embedding(text, model) redis_client.setex(cache_key, 86400, json.dumps(embedding)) return embedding

Bonnes Pratiques de Production

Les stratégies de mise en cache des embeddings constituent un levier majeur d'optimisation des coûts et des performances. En combinant cache local L1, cache distribué L2 et invalidation intelligente, il est possible de réduire la latence de 80% tout en diminuant significativement les appels API.

👉 Inscrivez-vous sur HolySheep AI — crédits offerts