Cache sémantique

Le cache sémantique fait correspondre les requêtes par signification plutôt que par comparaison exacte de chaînes. Lorsqu’une requête est sémantiquement similaire à une requête précédemment mise en cache, le cache renvoie les résultats stockés, ce qui évite le passage par l’intégration, la recherche, le prétraitement et le réordonnancement.

Configuration de base

Configuration de base

Installation : Bibliothèque principale (aucune dépendance supplémentaire requise)

Exemple :

from mistralai.search.toolkit.retrieval.cache import (
    CachedQueryEngine,
    InMemoryCacheBackend,
    SemanticCache,
    EvictionPolicy,
)

# 1. Create a cache backend
backend = InMemoryCacheBackend(
    dim=1024,                          # Embedding dimensionality
    max_entries=500,                   # Max cached queries
    ttl_seconds=3600,                  # 1-hour expiration
    eviction_policy=EvictionPolicy.LRU,
)

# 2. Create the semantic cache
cache = SemanticCache(
    backend=backend,
    similarity_threshold=0.95,  # 95% cosine similarity = cache hit
)

# 3. Wrap your QueryEngine
cached_engine = CachedQueryEngine(
    engine=query_engine,
    cache=cache,
    embedder=embedder,  # Used to embed incoming queries
)

# 4. Use normally — caching is transparent
result = await cached_engine.search("What is RAG?", top_k=10)
# First call: embedding + retrieval → cached
# Second call with similar query: cache hit → instant

Fonctionnement :

  1. La requête arrive
  2. Le module d’intégration convertit la requête en vecteur
  3. Le cache recherche des vecteurs de requêtes similaires déjà mis en cache
  4. Si la similarité > seuil : renvoie les résultats en cache (rapide)
  5. Si aucune correspondance : exécute le pipeline de recherche complet et met les résultats en cache (lent mais mis en cache pour une utilisation ultérieure)
Configuration

Configuration

Options backend :

OptionValeur par défautObjectif
dim(obligatoire)Dimensionnalité de l’intégration (1024 pour mistral-embed)
max_entries1000Nombre maximal de requêtes en cache avant éviction
ttl_secondsAucunDurée de validité des entrées en secondes (Aucun = pas d’expiration)
eviction_policyLRULRU, LFU ou FIFO

Sensibilité du cache :

ParamètreEffetCas d’usage
similarity_threshold=0.99Très strict, moins de correspondancesCorrespondance exacte des réponses
similarity_threshold=0.95Équilibré (par défaut)La plupart des cas d’usage
similarity_threshold=0.90Permissif, plus de correspondancesRéponses approximatives acceptables
# High sensitivity (strict matching)
cache = SemanticCache(
    backend=InMemoryCacheBackend(dim=1024, max_entries=500),
    similarity_threshold=0.99,
)

# Low sensitivity (broad matching)
cache = SemanticCache(
    backend=InMemoryCacheBackend(dim=1024, max_entries=500),
    similarity_threshold=0.90,
)

Stratégies d’éviction :

  • LRU (Least Recently Used) — Évite les entrées les moins récemment consultées
  • LFU (Least Frequently Used) — Évite les entrées les moins fréquemment consultées
  • FIFO (First In, First Out) — Évite les entrées les plus anciennes
backend = InMemoryCacheBackend(
    dim=1024,
    max_entries=500,
    eviction_policy=EvictionPolicy.LFU,  # Cache frequently asked queries
)
Surveillance avec métriques

Surveillance avec métriques

Suivez les performances du cache :

from mistralai.search.toolkit.retrieval.cache import CacheMetrics

# Create metrics tracker
metrics = CacheMetrics()

cached_engine = CachedQueryEngine(
    engine=query_engine,
    cache=cache,
    embedder=embedder,
    metrics=metrics,
)

# Run queries
for query in queries:
    result = await cached_engine.search(query)

# Check performance
snapshot = metrics.snapshot()
print(f"Hit rate: {snapshot.hit_rate:.1%}")
print(f"Avg hit similarity: {snapshot.avg_hit_similarity:.3f}")
print(f"Total requests: {snapshot.total_requests}")
print(f"Evictions: {snapshot.evictions}")

Métriques disponibles :

MétriqueDescription
hit_ratePart des requêtes traitées depuis le cache
avg_hit_similaritySimilarité cosinus moyenne pour les correspondances dans le cache
total_requestsNombre total de requêtes traitées
avg_embed_time_msTemps moyen d’intégration par requête
avg_lookup_time_msTemps moyen de recherche dans le cache par requête
avg_retrieval_time_msTemps moyen de recherche en cas d’absence dans le cache
evictionsNombre total d’entrées évictées en raison de la capacité
errorsNombre total d’erreurs de cache ou d’intégration

Exemple de surveillance :

# Track performance over time
if snapshot.hit_rate > 0.5:
    print(f"✓ Good hit rate: {snapshot.hit_rate:.1%}")
    print(f"  Avg similarity: {snapshot.avg_hit_similarity:.3f}")
else:
    print(f"⚠ Low hit rate: {snapshot.hit_rate:.1%}")
    print(f"  Consider lowering similarity_threshold")

if snapshot.evictions > max_entries * 0.1:
    print(f"⚠ High eviction rate")
    print(f"  Consider increasing max_entries or using LFU policy")
Tolérance aux pannes

Tolérance aux pannes

Toutes les opérations de cache sont non fatales. Si le cache ou le module d’intégration génère une exception, la requête est traitée par le chemin sans cache :

# Even if cache fails, retrieval continues
cached_engine = CachedQueryEngine(
    engine=query_engine,
    cache=cache,
    embedder=embedder,
)

# If embedder or cache throws exception:
# - Exception is logged
# - Query runs through normal pipeline
# - Result is NOT cached
# - Pipeline reliability is unaffected

result = await cached_engine.search(query="...", top_k=10)
# Always returns a result, cache is best-effort

Surveillance des erreurs :

snapshot = metrics.snapshot()
if snapshot.errors > 0:
    print(f"Cache errors: {snapshot.errors}")
    print("Check logs for details — retrieval pipeline is unaffected")
Gestion du cache

Gestion du cache

Invalidation par namespace :

# Invalidate cache for a specific namespace
await cache.invalidate(namespace="query_engine")

# All entries in that namespace are removed

Effacer l’intégralité du cache :

# Remove all cached entries
await cache.clear()

Expiration avec TTL :

# Entries expire automatically after ttl_seconds
backend = InMemoryCacheBackend(
    dim=1024,
    max_entries=500,
    ttl_seconds=3600,  # 1-hour expiration
)

# Expired entries are removed on next access or eviction

Gestion manuelle (pour les backends personnalisés) :

from mistralai.search.toolkit.retrieval.cache import CacheBackend

class CustomCacheBackend(CacheBackend):
    async def invalidate_old_entries(self, before_timestamp: float):
        """Remove entries older than timestamp."""
        ...

    async def get_cache_size(self) -> int:
        """Return current cache size in entries."""
        ...
Bonnes pratiques

Bonnes pratiques

1. Surveillez le taux de correspondances :

# Track performance to validate cache effectiveness
if metrics.snapshot().hit_rate < 0.2:
    # Cache not being utilized effectively
    # Consider:
    # - Lowering similarity_threshold
    # - Increasing max_entries
    # - Analyzing query patterns
    ...

2. Ajustez la taille du cache :

# Based on expected queries and memory
# 1024-dim embeddings ≈ 4KB per entry
backend = InMemoryCacheBackend(
    dim=1024,
    max_entries=1000,  # ~4MB cache
    ttl_seconds=3600,
)

3. Utilisation avec une recherche multi-étapes :

# Caching is most valuable when downstream is expensive
cached_engine = CachedQueryEngine(
    engine=QueryEngine(
        retriever=vector_retriever,
        rerankers=[
            LLMReRanker(llm_provider=llm, top_k=10),  # Expensive!
        ],
    ),
    cache=cache,
    embedder=embedder,
)

# Cache avoids expensive LLM reranking on cache hits

4. Seuil pour différents cas d’usage :

# Conservative (exact answers)
similarity_threshold=0.98

# Balanced (most Q&A systems)
similarity_threshold=0.95

# Permissive (approximate answers ok)
similarity_threshold=0.90
Voir aussi

Voir aussi