Ces derniers temps, mon travail ressemble davantage à celui d'un administrateur de bases de données qu'à celui d'un ingénieur logiciel, passant mes journées à optimiser Elasticsearch (ES). Qu'il s'agisse de diagnostiquer des problèmes d'E/S, de gérer la mémoire, d'affiner les requêtes ou d'améliorer le débit, mon quotidien est rythmé par ES. L'essor des modèles d'IA générative (RAG) dépend fortement des bases de connaissances, et notre choix s'est porté sur ES. Par conséquent, j'ai passé une année entière à interagir avec diverses facettes d'ES. Je vais donc partager quelques astuces que j'ai découvertes récemment, dans l'espoir que cela puisse aider d'autres personnes évitant ainsi les mêmes écueils.
La version d'ES que nous utilisons est la 8.13, qui présente des différences notables par rapport à la version 7. Si vous utilisez la série 7.x, certains aspects de cette discussion pourraient ne pas s'appliquer.
Cette section aborde les points suivants :
- Différentes stratégies pour les requêtes vectorielles multiples : utilisation de scripts et de l'API
knn(msearch). - Diagnostic des problèmes d'utilisation élevée des E/S lors des requêtes KNN.
- Comment utiliser les clauses
filterde manière performante.
Stratégies pour les requêtes vectorielles multiples
Les bases de connaissances pour les grands modèles linguistiques reposent sur des requêtes vectorielles. Les approches RAG actuelles impliquent souvent la réécriture et la diversification des requêtes utilisateur sous plusieurs angles, ce qui nécessite l'exécution simultanée de plusieurs requêtes vectorielles. Dans ES, plusieurs approches sont couramment utilisées pour implémenter cela :
Requêtes par script avec agrégation de vecteurs multiples (Pooling)
Cette méthode consiste à agréger les vecteurs de plusieusr requêtes (par exemple, en calculant leur moyenne) avant d'effectuer une seule requête vectorielle. Bien que cela puisse améliorer l'efficacité en réduisant la charge de calcul et la pression sur les E/S en condensant plusieurs vecteurs en un seul, la qualité des résultats en souffre souvent considérablement. L'agrégation peut entraîner une perte d'informations significative. Cependant, cela pourrait être une option viable pour les utilisateurs d'ES 7.x.
{
"size": 10,
"query": {
"bool": {
"must": [
{
"script_score": {
"query": {"match_all": {}},
"script": {
"source": "cosineSimilarity(params.query_vector, 'vectors')",
"params": {
"query_vector": "avg_embedding"
}
}
}
}
]
}
}
}
Calcul du maximum ou de la moyenne des similarités vectorielles via script
Une approche courante pour les utilisateurs d'ES 7.x, en particulier ceux qui n'ont pas accès à des fonctionnalités KNN natives, consiste à utiliser des scripts pour parcourir linéairement les vecteurs et calculer la similarité cosinus. Pour éviter de rapatrier et de retrier un grand nombre de documents, on calcule la similarité directement dans le script, ce qui réduit la pression sur les E/S et la taille des données transférées. L'exemple Python suivant illustre comment traiter une liste de vecteurs de requêtes (embedding_list) :
{
"query": {
"function_score": {
"script_score": {
"script": {
"source": """
double max_score = -1.0;
if (doc[params.field].size() == 0) return 0;
for (int i = 0; i < params.length; i++) {
double similarity = cosineSimilarity(params.query_vectors[i], doc[params.field][0]);
max_score = Math.max(max_score, similarity);
}
return max_score;
""",
"params": {
"length": "list_length",
"query_vectors": "embedding_list",
"field": "vectors"
}
}
},
"boost_mode": "replace"
}
}
}
Note : L'exemple de code ci-dessus est une représentation conceptuelle. Le remplacement de list_length, embedding_list et la manière dont vectors est accédé devraient être adaptés à votre contexte spécifique et à la structure de vos données.
Requêtes KNN multiples avec msearch
Pour les utilisatuers d'ES 8.x, l'utilisation de l'API knn est généralement recommandée. Lorsqu'il s'agit d'exécuter plusieurs requêtes KNN sur le même index, il est conseillé d'utiliser l'API msearch. Cela permet de grouper plusieurs requêtes KNN en une seule requête, ce qui améliore l'efficacité.
{
"query": {
"bool": {
"must": [
{
"knn": {
"field": "vectors",
"query_vector": "embedding_item",
"num_candidates": 10
}
}
]
}
}
}
Diagnostic des problèmes d'E/S lors des requêtes KNN
Lors de l'utilisation des requêtes KNN, nous avons rencontré un problème où les indicateurs d'utilisation des E/S atteignaient leur maximum. Cela entraînait des ralentissements généralisés, bloquant d'autres tâches de requêtes et provoquant des interruptions du service. L'image ci-dessous illustre cette situation.
Étape 1 : Optimisation de la taille du graphe
Initialement, nos graphes utilisaient le stockage Float32. ES 8.15 a introduit la prise en charge du stockage int4 et int8, et la version 8.17 a même proposé des méthodes de compression plus efficaces. Nous avons donc réindexé nos données en utilisant le stockage INT8. Cependant, cette optimisation n'a pas entraîné une réduction notable de l'utilisation des E/S.
Étape 2 : Prélèvement des données (Preloading)
L'explication fournie par les experts était que l'algorithme HNSW implique de nombreuses lectures aléatoires. Outre le graphe HNSW lui-même, les données vectorielles brutes ou quantifiées doivent également être chargées en mémoire pour l'interrogation. La solution à ce problème est le prélèvement des données. Pour un graphe int8_hnsw, la procédure de prélèvement est la suivante. L'index doit d'abord être fermé, le prélèvement effectué, puis l'index rouvert. Cette opération doit impérativement être réalisée en dehors des heures de pointe d'utilisation. Pour des index de plusieurs millions de documents, cela prend généralement quelques minutes. Notez que dans certains cas, le prélèvement a pu entraîner une instabilité du cluster, il est donc conseillé de l'effectuer pendant les périodes de faible activité.
# Fermer l'index
POST votre_index/_close
# Configurer le paramètre de prélèvement
PUT votre_index/_settings
{
"index.store.preload": ["vex", "veq"]
}
# Ouvrir l'index
POST votre_index/_open
Il est important de noter que le prélèvement des données maintient les données en cache en permanence, ce qui représente une stratégie d'échange temps contre espace. Si plusieurs index nécessitent un prélèvement, il pourrait y avoir une contention pour les ressources. Cependant, dans notre configuration, ce problème ne s'est pas encore manifesté. Par conséquent, lors de la mise en œuvre du prélèvement, surveillez attentivement les métriques telles que l'utilisation de la mémoire.
Trois manières d'utiliser les clauses filter
Dans le contexte de la recherche vectorielle, nous utilisons fréquemment des filtres temporels. En fonction de la fraîcheur des données, nous pouvons partitionner les index ou sélectionner des plages de temps spécifiques pour les requêtes. Bien que les filtres temporels puissent réduire considérablement la portée de l'index par rapport à sa taille totale, la manière dont le filtre est appliqué a un impact significatif sur les performances de la requête.
1. Pré-filtrage (Pre-filter)
Le filtre est appliqué directement dans la clause knn.
{
"knn": {
"field": "vectors",
"query_vector": [-0.0574951171875, 0.0222320556640625, ...],
"num_candidates": 3,
"filter": [
{
"range": {
"publishDate": {
"lte": "2025-04-22",
"gte": "2023-04-22",
"format": "yyyy-MM-dd"
}
}
}
]
}
}
2. Post-filtrage (Post-filter)
Le filtre est appliqué dans une clause bool séparée, après l'exécution de la requête KNN.
{
"bool": {
"must": [
{
"knn": {
"field": "vectors",
"query_vector": [-0.0574951171875, 0.0222320556640625, ...],
"num_candidates": 3
}
}
],
"filter": [
{
"range": {
"publishDate": {
"lte": "2025-04-22",
"gte": "2023-04-22",
"format": "yyyy-MM-dd"
}
}
}
]
}
}
3. Filtre dans la clause must
La condition de filtre est incluse dans la clause must aux côtés de la requête KNN.
{
"bool": {
"must": [
{
"range": {
"publishDate": {
"lte": "2025-04-22",
"gte": "2023-04-22",
"format": "yyyy-MM-dd"
}
}
},
{
"knn": {
"field": "vectors",
"query_vector": [-0.0574951171875, 0.0222320556640625, ...],
"num_candidates": 3
}
}
]
}
}
Comparaison des trois méthodes de filtrage
| Méthode de filtrage | Efficacité de la requête |
|---|---|
| Pré-filtrage (Pre-filter) | La plus élevée. Le filtrage préalable réduit la portée de la recherche KNN, améliorant ainsi l'efficacité. |
| Post-filtrage (Post-filter) | Moins efficace. Bien que ce soit la méthode la plus performante pour les requêtes non-KNN, dans le contexte KNN, elle est plus lente car le filtrage s'applique aux résultats KNN déjà obtenus. De plus, le nombre de résultats finaux peut être inférieur au nombre de candidats spécifiés. |
Filtre dans la clause must |
La moins efficace. Dans une clause filter, ES ignore directement les documents ne satisfaisant pas les critères. En revanche, dans une clause must, tous les documents participent au calcul du score. De plus, ES met en cache les conditions de filter pour accélérer les requêtes ultérieures, ce que ne fait pas must. |