<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Benjamin Trent - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Benjamin Trent - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/fr/search-labs/author/benjamin-trent</link>
    </image>
    <link>https://www.elastic.co/fr/search-labs/author/benjamin-trent</link>
    <atom:link href="https://www.elastic.co/fr/search-labs/rss/author/benjamin-trent.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[fr]]></language>
    <lastBuildDate>Tue, 29 Sep 2026 06:14:22 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Scaling des modèles d'interaction tardive dans Elasticsearch - 2e partie]]></title>
    <description><![CDATA[Cet article explore des techniques pour préparer les vecteurs d'interaction tardive aux charges de travail de production à grande échelle, notamment la réduction de l’espace disque et l’amélioration de l’efficacité des calculs.]]></description>
    <content:encoded><![CDATA[<p>Dans notre <a href="https://www.elastic.co/search-labs/blog/elastiacsearch-colpali-document-search">article précédent sur ColPali</a>, nous avons exploré comment créer des applications de recherche visuelle avec Elasticsearch. Nous nous sommes principalement concentrés sur la valeur que des modèles tels que ColPali apportent à nos applications, mais ils présentent des inconvénients en termes de performances par rapport à la recherche vectorielle avec des bi-encodeurs tels que E5.</p><p>S’appuyant sur les exemples de <a href="https://www.elastic.co/search-labs/blog/elastiacsearch-colpali-document-search">la première partie</a>, ce blog explore comment utiliser différentes techniques et la puissante boîte à outils de recherche vectorielle d’Elasticsearch afin de préparer les vecteurs d’interaction tardive pour des charges de production à grande échelle.</p><p>Les exemples complets de code sont disponibles sur <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/colpali">GitHub.</a></p><h2>Défis des modèles d’interaction tardive</h2><p>ColPali crée plus de 1000 vecteurs par page pour les documents dans notre index.</p><p>Travailler avec des vecteurs d'interaction tardive présente alors deux défis :</p><ol><li><p>Espace disque : l’enregistrement de tous ces vecteurs sur disque entraînera une consommation de stockage considérable, ce qui s’avérera coûteux à grande échelle..</p></li><li><p>Calcul : Lors du classement de nos documents avec la comparaison <code>maxSimDotProduct()</code> , nous devons comparer tous ces vecteurs pour chacun de nos documents avec les N vecteurs de notre requête.</p></li></ol><p>Examinons quelques techniques pour remédier à ces problèmes.</p><h2>Techniques pour optimiser les modèles d'interactions tardives</h2><h3>Vecteurs de bits</h3><p>Afin de réduire l'espace disque, nous pouvons compresser les images en vecteurs binaires. Nous pouvons utiliser une simple fonction Python pour transformer nos multi-vecteurs en vecteurs de bits :</p>def to_bit_vectors(embeddings: list) -&gt; list:
    return [
        np.packbits(np.where(np.array(embedding) &gt; 0, 1, 0))
        .astype(np.int8)
        .tobytes()
        .hex()
        for embedding in embeddings
    ]<p>Le concept de noyau de la fonction est simple : les valeurs supérieures à 0 deviennent 1, et les valeurs inférieures à 0. Cela aboutit à un tableau de 0 et de 1, que nous transformons ensuite en une chaîne hexadécimale représentant notre vecteur de bits.</p><p>Pour le mapping de notre index, nous avons défini le paramètre <code>element_type</code> sur <code>bit</code>:</p>mappings = {
    "mappings": {
        "properties": {
            "col_pali_vectors": {
                "type": "rank_vectors",
                "element_type": "bit"
            }
        }
    }
}

es.indices.create(index=INDEX_NAME, body=mappings)<p>Après avoir écrit tous nos nouveaux vecteurs de bits dans notre index, nous pouvons classer nos vecteurs de bits en utilisant le code suivant :</p>query = "What do companies use for recruiting?"
query_vector = to_bit_vectors(create_col_pali_query_vectors(query))
es_query = {
    "_source": False,
    "query": {
        "script_score": {
            "query": {
                "match_all": {}
            },
            "script": {
                "source": "maxSimInvHamming(params.query_vector, 'col_pali_vectors')",
                "params": {
                    "query_vector": query_vector
                }
            }
        }
    },
    "size": 5
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcaf0c8f999d68701/6a17f46696142a6f46eb1c56/51b989446e4099745971e1eac27d147a78d13e0a-1600x480.png" alt="" /><p>En échange d'un peu de précision, cela nous permet d'utiliser la distance de hamming (<code>maxSimInvHamming(...)</code>), qui est capable de tirer parti d'optimisations telles que les masques de bits, SIMD, etc. Pour en savoir plus sur les <a href="https://www.elastic.co/search-labs/blog/bit-vectors-in-elasticsearch">vecteurs binaires et la distance de hamming, consultez notre blog</a>.</p><p>Sinon, nous ne pouvons pas convertir notre vecteur de requête en vecteurs de bits et rechercher à l'aide du vecteur d'interaction tardive en pleine fidélité :</p>query = "What do companies use for recruiting?"
query_vector = create_col_pali_query_vectors(query)
es_query = {
    "_source": False,
    "query": {
        "script_score": {
            "query": {
                "match_all": {}
            },
            "script": {
                "source": "maxSimDotProduct(params.query_vector, 'col_pali_vectors')",
                "params": {
                    "query_vector": query_vector
                }
            }
        }
    },
    "size": 5
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2fa6c89fb451b6e0/6a17f468af47b65da9cde0d9/89f3c795c6dc44b2f7d46801320288316b47e1b2-1600x488.png" alt="Résultats de l'utilisation de vecteurs de bits pour optimiser les modèles d'interaction tardive" /><p>Cela comparera nos vecteurs en utilisant une fonction de similarité asymétrique.</p><p></p><p>Considérons une distance de Hamming standard entre deux vecteurs de bits.. Supposons que nous ayons un vecteur de document <em>D :</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd4b6ece1ec4dedac/6a17f4694b055d248143234e/366a70a23e37d403788327b7aefc873fd4482f5e-1235x86.png" alt="" /><p>Et un vecteur de requête <em>Q :</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7721df9c8f0d84f1/6a17f46b3e03d77cf54f2dcb/978ec31e0afbc0eaebf548a015b628c0cad84625-1247x84.png" alt="" /><p></p><p>La quantification binaire simple transformera les vecteurs <em>D</em> en <code>10101101</code> et <em>Q</em> en <code>11111011</code>. Pour trouver la distance de Hamming, il faut des calculs directs en bits — c’est extrêmement rapide. Dans ce cas, la distance de hamming est <code>01010110</code>, qui a un nombre de bits de 4. La notation devient donc l'inverse de cette distance de hamming. Rappelez-vous : plus les vecteurs sont similaires, plus la distance de Hamming est petite. L'inverser permet aux vecteurs les plus similaires d'être mieux notés. Plus précisément ici, le score serait 1/4 = <code>0.25</code>.</p><p>Cependant, remarquez comment nous perdons l'ampleur de chaque dimension. Un <code>1</code> est un <code>1</code>. Ainsi, pour <em>Q</em>, la différence entre <code>0.01</code> et <code>0.79</code> disparaît. Puisque nous quantifions simplement selon <code>&gt;0</code>, nous pouvons faire une petite astuce où le vecteur Q n'est pas quantifié. Cela ne permet pas des calculs bit à bit extrêmement rapides, mais cela maintient le coût de stockage bas car D est toujours quantifié.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blted50315518e2d599/6a17f46c414c6463709452d3/508e8498ba969534d2a0131d431c75735fc27cb9-1399x611.png" alt="" /><p>En résumé, cette méthode conserve les informations fournies dans <em>Q</em>, ce qui permet d’améliorer la qualité de l’estimation de la distance tout en limitant la consommation de stockage.</p><p>L'utilisation de vecteurs de bits nous permet d'économiser de manière significative de l'espace disque et de la charge de calcul au moment de la requête. Mais nous pouvons faire davantage.</p><h3>Vecteurs moyens</h3><p>Pour scaler notre recherche sur des centaines de milliers de documents, même les avantages de performance que nous apportent les vecteurs de bits ne seront pas suffisants. Afin de scaler à ces types de charges de travail, nous voudrons exploiter la structure d'index HNSW d'Elasticsearch pour la recherche vectorielle.</p><p>ColPali génère environ un millier de vecteurs par document, ce qui est trop pour les ajouter à notre graphe HNSW. Nous devons donc réduire le nombre de vecteurs. Pour cela, nous pouvons créer une représentation unique de la signification du document en prenant la moyenne de tous les vecteurs de documents produits par ColPali lors de l’intégration de notre image.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc053425fbfcf8de8/6a17f46f148009faf1b488aa/7c3b4dffb70bd95f67deb35f8c01e73c5286c2ab-1476x1102.png" alt="Vecteur moyen sur tous les vecteurs d’interaction tardive" /><p>Pour l’instant, cela n’est pas possible au sein d’Elastic. Nous devrons donc prétraiter les vecteurs avant de les ingérer dans Elasticsearch. </p><p>Nous pouvons le faire avec Logstash ou des pipelines d'ingestion, mais ici nous utiliserons une simple fonction Python :</p>def to_avg_vector(vectors):
    vectors_array = np.array(vectors)
    
    avg_vector = np.mean(vectors_array, axis=0)
    
    norm = np.linalg.norm(avg_vector)
    if norm &gt; 0:
        normalized_avg_vector = avg_vector / norm
    else:
        normalized_avg_vector = avg_vector

    return normalized_avg_vector.tolist()<p>Nous normalisons également le vecteur afin de pouvoir utiliser la similarité par produit scalaire.</p><p>Après avoir transformé tous nos vecteurs ColPali en vecteurs moyens, nous pouvons les indexer dans notre champ dense_vector :</p>mappings = {
    "mappings": {
        "properties": {
            "avg_vector": {
                "type": "dense_vector",
                "dims": 128,
                "index": True,
                "similarity": "dot_product"
            },
            "col_pali_vectors": {
                "type": "rank_vectors",
                "element_type": "bit"
            }
        }
    }
}

es.indices.create(index=INDEX_NAME, body=mappings)<p>Il faut considérer que cela accroîtra l’utilisation globale du disque, car nous sauvegardons des données supplémentaires parallèlement à nos vecteurs d'interaction tardive. En outre, nous utiliserons de la RAM supplémentaire pour contenir le graphe HNSW, ce qui nous permettra de scaler la recherche à des milliards de vecteurs. Pour réduire l’utilisation de la RAM, nous pouvons utiliser notre <a href="https://www.elastic.co/search-labs/blog/optimized-scalar-quantization-elasticsearch">fonctionnalité BBQ</a> populaire. À notre tour, nous obtenons des résultats de recherche rapides sur d'énormes ensembles de données qui ne seraient autrement pas possibles.</p><p>À présent, nous recherchons simplement à l'aide de la requête knn pour trouver nos documents les plus pertinents.</p>query = "What do companies use for recruiting?"
query_vector = to_avg_vector(create_col_pali_query_vectors(query))
es_query = {
    "_source": False,
    "knn": {
        "field": "avg_vector",
        "query_vector": query_vector,
        "k": 10,
        "num_candidates": 100
    },
    "size": 5
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf4ac6b7fd444f935/6a17f471a29299d76ad02dc0/a500f9907020f032c3b1c7a48ed8c759dc2a1dd4-1600x498.png" alt="" /><p>Le meilleur résultat précédent a malheureusement été relégué au troisième rang.</p><p>Pour résoudre ce problème, nous pouvons effectuer une récupération en plusieurs étapes. Dans notre première étape, nous utilisons la requête knn pour rechercher les meilleurs candidats pour notre requête parmi des millions de documents. Dans la deuxième étape, nous ne classons que les k premiers (ici : 10) avec la plus grande fidélité des vecteurs d'interaction tardive de ColPali. </p>query = "What do companies use for recruiting?"
col_pali_vector = create_col_pali_query_vectors(query)
avg_vector = to_avg_vector(col_pali_vector)
es_query = {
  "_source": False,
  "retriever": {
    "rescorer": {
      "retriever": {
        "knn": {
          "field": "avg_vector",
          "query_vector": avg_vector,
          "k": 10,
          "num_candidates": 100
        }
      },
      "rescore": {
        "window_size": 10,
        "query": {
          "rescore_query": {
            "script_score": {
              "query": {
                "match_all": {}
              },
              "script": {
                "source": "maxSimDotProduct(params.query_vector, 'col_pali_vectors')",
                "params": {
                  "query_vector": col_pali_vector
                }
              }
            }
          }
        }
      }
    }
  },
  "size": 5
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt265edd5f37be40b7/6a17f4733e9e45583dba15e6/797f490860af87fcf29f34668a5c4511419c6fd0-1600x501.png" alt="Résultats de l'utilisation des vecteurs moyens pour l'optimisation des modèles d'interaction tardive" /><p>Ici, nous utilisons l'outil <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.18/retriever.html#rescorer-retriever">rescore retriever</a> introduit en 8.18 pour reclasser nos résultats. Après un nouveau calcul des scores, nous constatons que notre meilleur match se retrouve à nouveau en première position. </p><p>Remarque : Dans une application de production, nous pouvons utiliser un k bien supérieur à 10 car la fonction Max Sim est toujours relativement performante.</p><h3>Regroupement de jetons</h3><p>Le pooling de tokens réduit la longueur de séquence des plongements multi-vectoriels en regroupant les informations redondantes, comme les zones de fond blanc. Cette technique réduit le nombre de plongements tout en préservant la majeure partie du signal de la page.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3d643e6ac908f640/6a17f4754b055d3783432352/09eae0b768f4b450e555d7e53e57561bd52de2c0-1412x1056.png" alt="Pooling de tokens pour optimiser les modèles d'interaction tardive" /><p>Le pooling de jetons fonctionne en regroupant des embeddings de jetons similaires au sein d'un document en clusters à l'aide d'un algorithme de clustering. Ensuite, la moyenne des vecteurs dans chaque groupe est calculée pour créer une représentation unique et agrégée. Ce vecteur agrégé remplace les tokens originaux dans le groupe, réduisant ainsi le nombre total de vecteurs sans perte significative du signal du document.</p><p>Le document ColPali propose une valeur initiale de facteur de pool de 3 pour la plupart des ensembles de données, ce qui assure la maintenance de 97,8 % des performances originales tout en réduisant le nombre total de vecteurs de 66,7 %. </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2d78a15c6944a1ea/6a17f477414c64a2929452d9/343cc9eaf54af8c7a125d4115838f3c7d5659de0-1600x1007.png" alt="Facteur pool pour optimiser les modèles d'interactions tardives" /><p>Mais attention : l’ensemble de données « Shift », qui contient des documents très denses et riches en texte avec peu d’espace blanc, voit ses performances se dégrader rapidement à mesure que les facteurs de regroupement augmentent.</p><p>Pour créer les vecteurs regroupés, nous pouvons utiliser la bibliothèque colpali_engine :</p>from colpali_engine.compression.token_pooling import HierarchicalTokenPooler

pooler = HierarchicalTokenPooler(pool_factor=3) # test on your data for a good pool_factor

def pool_vectors(embedding: list) -&gt; list:
    tensor = torch.tensor(embedding).unsqueeze(0)
    pooled = pooler.pool_embeddings(tensor)
    return pooled.squeeze(0).tolist()<p>Nous avons maintenant un vecteur dont les dimensions ont été réduites d’environ 66,7 %. Nous l'indexons comme d'habitude et nous pouvons rechercher dessus avec notre fonction <code>maxSimDotProduct()</code>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc460c14031814ede/6a17f4794202292bf629f72d/2412c5db7d79a590b96d42fe01f140c42f010612-1600x481.png" alt="Résultats des modèles à interaction tardive" /><p>Nous pouvons obtenir de bons résultats de recherche au détriment d’une légère précision dans les résultats.</p><p>Conseil : avec un pool_factor plus élevé (100-200), vous pouvez également obtenir un juste milieu entre la solution vectorielle moyenne et celle dont nous avons discuté ici. Avec environ 5 à 10 vecteurs par document, il devient possible de les indexer dans un champ imbriqué pour tirer parti de l'index HNSW.</p><h2>Encodeur Coss vs. encodeur à interaction tardive vs. bi-encodeur</h2><p>Compte tenu de ces éléments, quelle est la place des modèles d'interaction tardive (comme ColPali ou ColBERT) vis-à-vis des autres méthodes de récupération de données par IA ?</p><p>Bien que la fonction de simulation maximale soit moins chère que les encodeurs croisés, elle nécessite néanmoins beaucoup plus de comparaisons et de calculs que la recherche vectorielle avec des bi-encodeurs, où nous comparons simplement deux vecteurs pour chaque paire requête-document. </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbfd97f3bba3e5b17/6a17f47be31791b0052d5943/75e1fc9e601aa7a6e88f137565c1919166c7a71c-1480x458.png" alt="Cross-encodeur vs. modèles à interaction tardive vs. bi-encodeur" /><p>Pour cette raison, nous recommandons d'utiliser les modèles d'interaction tardive uniquement pour le reclassement des k premiers résultats de recherche. Nous le reflétons également dans le nom du type de champ : rank_vectors.</p><p>Mais qu'en est-il de l'encodeur croisé ? Les modèles d'interaction tardive sont-ils meilleurs parce qu'ils sont moins chers à exécuter au moment de la requête ? Comme c'est souvent le cas, la réponse est : cela dépend. Les encodeurs croisés produisent généralement des résultats de meilleure qualité, mais ils nécessitent beaucoup de calcul car les paires de documents de requête doivent effectuer un passage complet à travers le modèle transformateur. Ils bénéficient également du fait qu'ils ne nécessitent aucune indexation de vecteurs et peuvent fonctionner de manière apatride. Résultats obtenus :</p><ul><li><p>Moins d'espace disque utilisé</p></li><li><p>Un système plus simple</p></li><li><p>Qualité supérieure des résultats de recherche</p></li><li><p>Latence plus élevée, et donc impossible à repositionner aussi profondément</p></li></ul><p>D'autre part, les modèles d'interaction tardive peuvent décharger une partie de ce calcul sur l'index, ce qui rend la requête moins coûteuse. Le prix à payer est la nécessité d’indexer les vecteurs, ce qui complexifie nos pipelines d’ingestion et nécessite davantage d’espace disque pour sauvegarder ces données.</p><p>Dans le cas particulier de ColPali, l'analyse des informations contenues dans les images est très coûteuse car elles contiennent beaucoup de données. Dans ce cas, le compromis penche en faveur de l'utilisation d'un modèle d'interaction tardive tel que ColPali, car évaluer ces informations au moment de la requête serait trop gourmand en ressources/lent. </p><p>Pour un modèle d’interaction tardif comme ColBERT, qui fonctionne sur des données textuelles comme la plupart des encodeurs croisés (par exemple, elastic-rerank-v1), la décision pourrait plutôt pencher vers l’utilisation du codeur croisé pour bénéficier des économies et de la simplicité du disque.</p><p>Nous vous encourageons à peser le pour et le contre en fonction de votre cas d'utilisation et à expérimenter les différents outils qu'Elasticsearch met à votre disposition pour créer les meilleures applications de recherche.</p><h2>Conclusion</h2><p>Dans ce blog, nous avons exploré diverses techniques pour optimiser les modèles d'interaction tardive comme ColPali pour la recherche vectorielle à grande échelle dans Elasticsearch. Bien que les modèles d'interaction tardive offrent un bon équilibre entre l'efficacité de la récupération et la qualité du classement, ils présentent également des défis liés au stockage et au calcul.</p><p>Pour relever ces défis, nous avons examiné :</p><ul><li><p>Des <strong>vecteurs de bits</strong> pour réduire de manière significative l'espace disque tout en tirant parti de calculs de similarité efficaces, tels que la distance de Hamming ou la similarité maximale asymétrique.</p></li><li><p>Des <strong>vecteurs moyens</strong> pour compresser plusieurs intégrations en une seule représentation dense, permettant ainsi une récupération efficace grâce à l'indexation HNSW.</p></li><li><p>Le <strong>pooling de jetons</strong> permet de fusionner intelligemment les embeddings redondants tout en assurant la maintenance de l’intégrité sémantique, réduisant ainsi la surcharge de calcul au moment de la requête.</p></li></ul><p>Elasticsearch offre une boîte à outils puissante pour personnaliser et optimiser les applications de recherche selon vos besoins. Que vous privilégiez la vitesse de récupération, la qualité de classement ou l'efficacité de stockage, ces outils et techniques vous permettent d'équilibrer les performances et la qualité selon vos besoins pour vos applications du monde réel.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/late-interaction-model-colpali-scale</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/late-interaction-model-colpali-scale</guid>
    <category><![CDATA[Pertinence]]></category>
    <category><![CDATA[Base vectorielle]]></category>
    <dc:creator><![CDATA[Peter Straßer,Benjamin Trent]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt97a536033e6b0a56/6a17f47dfbc5f88c86491c0d/c780b78a07573f2df1cfef8b29a7109f839b0ab3-1200x628.png" length="0" type="image/png"/>
    <pubDate>Thu, 20 Mar 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Bugs de concurrence dans Lucene : Comment corriger les échecs de concurrence optimiste]]></title>
    <description><![CDATA[Grâce à Fray, un cadre de test de concurrence déterministe du PASTA Lab de CMU, nous avons découvert un bogue Lucene délicat et l'avons éliminé.]]></description>
    <content:encoded><![CDATA[<p>Yep, un autre blog sur la correction des bogues. Mais cette fois-ci, c'est un héros de l'open source qui intervient et sauve la situation. </p><p>Le débogage des bogues de concurrence n'est pas une sinécure, mais nous allons nous y atteler. C'est là qu'intervient Fray, un cadre de test de concurrence déterministe du laboratoire PASTA de l'Université de Californie du Sud, qui transforme les défaillances en défaillances reproductibles de manière fiable. Grâce à la conception astucieuse du shadow lock de Fray et au contrôle précis des threads, nous avons traqué un bug Lucene délicat et l'avons finalement éliminé. Ce billet explore la façon dont les héros et les outils open-source rendent le débogage de la concurrence moins pénible et le monde du logiciel bien meilleur.</p><h2>Les bogues de simultanéité : le fléau des ingénieurs en informatique</h2><p>Les bogues liés à la concomitance sont les pires. Non seulement ils sont difficiles à réparer, mais le plus difficile est de les faire tomber en panne de manière fiable. Prenons l'exemple de l'échec de ce test, <a href="https://github.com/apache/lucene/issues/13127"><code>TestIDVersionPostingsFormat#testGlobalVersions</code></a>. Il génère plusieurs fils d'écriture et de mise à jour de documents, ce qui remet en cause le modèle de concurrence optimiste de Lucene. Ce test a mis en évidence une condition de course dans le contrôle optimiste de la concurrence. En d'autres termes, une opération de document peut faussement prétendre être la dernière d'une séquence d'opérations 😱. Cela signifie que, dans certaines conditions, une opération de mise à jour ou de suppression peut réussir alors qu'elle aurait dû échouer compte tenu des contraintes de concurrence optimistes.</p><p></p>org.apache.lucene.sandbox.codecs.idversion.TestIDVersionPostingsFormat &gt; testGlobalVersions FAILED
    java.lang.AssertionError: maxSeqNo must be greater or equal to 7442 but was 7441
        at __randomizedtesting.SeedInfo.seed([B97A2BDBC7E40BF6:B4D76006D5101E6]:0)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.DocumentsWriterDeleteQueue.close(DocumentsWriterDeleteQueue.java:325)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.DocumentsWriter.flushAllThreads(DocumentsWriter.java:659)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.IndexWriter.getReader(IndexWriter.java:576)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.StandardDirectoryReader.doOpenFromWriter(StandardDirectoryReader.java:381)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.StandardDirectoryReader.doOpenIfChanged(StandardDirectoryReader.java:355)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.StandardDirectoryReader.doOpenIfChanged(StandardDirectoryReader.java:345)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.DirectoryReader.openIfChanged(DirectoryReader.java:170)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.search.SearcherManager.refreshIfNeeded(SearcherManager.java:144)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.search.SearcherManager.refreshIfNeeded(SearcherManager.java:52)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.search.ReferenceManager.doMaybeRefresh(ReferenceManager.java:167)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.search.ReferenceManager.maybeRefresh(ReferenceManager.java:213)<p>Toutes nos excuses à ceux qui détestent les traces de pile Java. Remarque : supprimer ne signifie pas nécessairement "effacer". Il peut également indiquer une "mise à jour" du document, car les segments de Lucene sont en lecture seule.
</p><p>Apache Lucene gère chaque thread qui écrit des documents par l'intermédiaire de la classe <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java"><code>DocumentsWriter</code></a>. Cette classe crée ou réutilise des fils de discussion pour l'écriture de documents et chaque action d'écriture contrôle ses informations dans la classe <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java"><code>DocumentsWriterPerThread</code></a> (DWPT). En outre, le rédacteur garde la trace des documents supprimés dans le site <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> (DWDQ). Ces structures conservent en mémoire toutes les actions de mutation des documents et se vident périodiquement, libérant ainsi les ressources en mémoire et transférant les structures sur le disque.</p><p></p><p>Afin d'éviter le <a href="https://en.wikipedia.org/wiki/Blocking_(computing)">blocage des threads</a> et d'assurer un débit élevé dans les systèmes concurrents, Apache Lucene tente de ne <a href="https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html">se synchroniser</a> que dans les sections très critiques. Bien que cela puisse être une bonne chose dans la pratique, comme dans tous les systèmes concurrents, il y a des dragons.</p><h2>
Un faux espoir</h2><p>Mon enquête initiale m'a permis d'identifier quelques sections critiques qui n'étaient pas correctement synchronisées. Toutes les interactions avec un site <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> donné sont contrôlées par le site <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java"><code>DocumentsWriter</code></a> qui l'entoure. Ainsi, même si les méthodes individuelles ne sont pas synchronisées de manière appropriée sur le site <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a>, leur accès au monde l'est (ou devrait l'être). (Ne nous attardons pas sur la confusion entre propriété et accès - il s'agit d'un projet de longue date rédigé par de nombreux contributeurs. Laissez-lui un peu de répit.)</p><p></p><p>Cependant, j'ai trouvé <a href="https://github.com/apache/lucene/blob/40060f8b7080d06a218518445a0a1dfc520c812a/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterFlushControl.java#L572-L576">un endroit</a> qui n'était pas synchronisé lors d'un rinçage.</p>// Advance the queue, meaning create a new one to keep track of deletes
// Since we have been flushed, let's starting tracking again
DocumentsWriterDeleteQueue newQueue = documentsWriter.deleteQueue.advanceQueue(perThreadPool.size());
// OK, get the current new maximum sequence number for optimistic concurrency
seqNo = documentsWriter.deleteQueue.getMaxSeqNo();
// Reset to the new queue
documentsWriter.resetDeleteQueue(newQueue);<p>Ces actions ne sont pas synchronisées en une seule opération atomique. Cela signifie qu'entre la création de <code>newQueue</code> et l'appel de <code>getMaxSeqNo</code>, un autre code a pu être exécuté, incrémentant le numéro de séquence dans la classe <code>documentsWriter</code>. J'ai trouvé l'insecte !</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbc55033280b45913/6a1706b4dc55dec27fe00d30/3f2617a102d28735e755a7539b047c28beda41be-1600x453.jpg" alt="" /><p>
Mais, comme pour la plupart des bogues complexes, il n'a pas été facile de trouver la cause première. C'est alors qu'un héros est intervenu.</p><h2>Un héros dans la mêlée</h2><p>Voici notre héros : <a href="https://aoli.al/">Ao Li</a> et ses collègues du laboratoire PASTA. Je le laisserai expliquer comment ils ont sauvé la situation avec Fray.</p><p><a href="https://github.com/cmu-pasta/fray">Fray</a> est un cadre de test de concurrence déterministe développé par des chercheurs du <a href="https://pastalab.org/">PASTA Lab de</a> l'Université Carnegie Mellon. La motivation derrière la création de Fray provient d'un écart notable entre le monde universitaire et l'industrie : alors que les tests déterministes de concurrence ont été largement étudiés dans la recherche universitaire depuis plus de 20 ans, les praticiens continuent de s'appuyer sur les tests de stress - une méthode largement reconnue comme peu fiable et peu fiable - pour tester leurs programmes simultanés. C'est pourquoi nous avons voulu concevoir et mettre en œuvre un cadre de test de concurrence déterministe dont l'objectif principal est la généralité et l'applicabilité pratique.</p><p></p><h2>L'idée maîtresse</h2><p>Fray s'appuie sur un principe simple mais puissant : l'exécution séquentielle. Le modèle de concurrence de Java offre une <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.3">propriété</a>essentielle: si un programme est exempt de course aux données, toutes les exécutions sembleront cohérentes d'un point de vue séquentiel. Cela signifie que le comportement du programme peut être représenté comme une séquence d'instructions.</p><p>Fray exécute le programme cible de manière séquentielle : à chaque étape, il met en pause tous les threads sauf un, ce qui permet à Fray de contrôler précisément l'ordonnancement des threads. Les fils sont sélectionnés au hasard pour simuler la concurrence, mais les choix sont enregistrés en vue d'une relecture déterministe ultérieure. Pour optimiser l'exécution, Fray n'effectue des changements de contexte que lorsqu'un thread est sur le point d'exécuter une instruction de synchronisation telle que le verrouillage ou l'accès atomique/volatile. Une propriété intéressante de la liberté de la course aux données est que ce changement de contexte limité est suffisant pour explorer tous les comportements observables dus à l'entrelacement des threads<a href="https://arxiv.org/abs/2501.12618">(notre article</a> contient une esquisse de preuve).</p><p></p><h2>Le défi : contrôler l'ordonnancement des threads</h2><p>Bien que l'idée de base semble simple, la mise en œuvre de Fray a présenté des défis importants. Pour contrôler l'ordonnancement des threads, Fray doit gérer l'exécution de chaque thread d'application. À première vue, cela peut sembler simple : remplacer les primitives de concurrence par des implémentations personnalisées. Cependant, le contrôle de la concurrence dans la JVM est complexe et implique un mélange d'<a href="https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-6.html">instructions de bytecode</a>, de <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html">bibliothèques de haut niveau</a> et de <a href="https://github.com/openjdk/jdk/blob/b720517cb33c2119ec6ed85504bce321de748228/src/java.base/share/classes/java/lang/Object.java#L394">méthodes natives.</a></p><p></p><p>Il s'est avéré qu'il s'agissait d'un trou de lapin :</p><p></p><ul><li><p>Par exemple, chaque instruction <code>MONITORENTER</code> doit avoir un correspondant <code>MONITOREXIT</code> dans la même méthode. Si Fray remplace <code>MONITORENTER</code> par un appel de méthode à un stub/mock, il doit également remplacer <code>MONITOREXIT</code>.</p></li><li><p>Dans le code qui utilise <code>object.wait/notify</code>, si <code>MONITORENTER</code> est remplacé, le <code>object.wait</code> correspondant doit également être remplacé. Cette chaîne de remplacement s'étend jusqu'à <code>object.notify</code> et au-delà.</p></li><li><p>La JVM invoque certaines méthodes liées à la concurrence (par exemple, <code>object.notify</code> lorsqu'un thread se termine) dans le code natif. Le remplacement de ces opérations nécessiterait de modifier la JVM elle-même.</p></li><li><p>Les fonctions de la JVM, telles que les chargeurs de classes et les threads de collecte de déchets (GC), utilisent également des primitives de concurrence. La modification de ces primitives peut créer des incohérences avec ces fonctions de la JVM.</p></li><li><p>Le remplacement des primitives de concurrence dans le JDK entraîne souvent des plantages de la JVM pendant sa phase d'initialisation.</p></li></ul><p></p><p>Ces défis ont clairement montré qu'un remplacement complet des primitives de concurrence n'était pas réalisable.</p><h2>
Notre solution : la conception d'une serrure à l'abri des regards</h2><p>Pour relever ces défis, Fray utilise un nouveau mécanisme de verrouillage fictif pour orchestrer l'exécution des threads sans remplacer les primitives de concurrence. Les Shadow locks agissent comme des intermédiaires qui guident l'exécution des threads. Par exemple, avant d'acquérir un verrou, un thread d'application doit interagir avec le verrou fantôme correspondant. Le verrou fantôme détermine si le thread peut acquérir le verrou. Si le thread ne peut pas continuer, le verrou fantôme le bloque et permet à d'autres threads de s'exécuter, ce qui évite les blocages et permet une concurrence contrôlée. Cette conception permet à Fray de contrôler l'entrelacement des threads de manière transparente tout en préservant l'exactitude de la sémantique de la concurrence. Chaque primitive de concurrence est soigneusement modélisée dans le cadre du verrouillage fictif afin d'en garantir la solidité et l'exhaustivité. Plus de détails techniques peuvent être trouvés dans notre document.</p><p></p><p>En outre, cette conception est conçue pour être à l'épreuve du temps. En n'exigeant que l'instrumentation des verrous fantômes autour des primitives de concurrence, il garantit la compatibilité avec les versions plus récentes de la JVM. Cela est possible parce que les interfaces des primitives de concurrence dans la JVM sont relativement stables et sont restées inchangées depuis des années.</p><h2>
Test Fray</h2><p>Après la construction de Fray, l'étape suivante a été l'évaluation. Heureusement, de nombreuses applications, comme Apache Lucene, intègrent déjà des tests de concurrence. Ces tests de concurrence sont des tests JUnit classiques qui génèrent plusieurs threads, effectuent un certain travail, attendent (généralement) que ces threads se terminent, puis affirment une certaine propriété. La plupart du temps, ces tests réussissent parce qu'ils n'exercent qu'un seul entrelacement. Pire encore, certains tests n'échouent qu'occasionnellement dans l'environnement CI/CD, comme décrit précédemment, ce qui rend ces échecs extrêmement difficiles à déboguer. Lorsque nous avons exécuté les mêmes tests avec Fray, nous avons découvert de nombreux bogues. Notamment, Fray a redécouvert des bogues précédemment signalés qui n'avaient pas été corrigés en raison de l'absence d'une reproduction fiable, y compris l'objet de ce blog : <a href="https://github.com/apache/lucene/issues/13127"><code>TestIDVersionPostingsFormat.testGlobalVersions</code></a>. Heureusement, avec Fray, nous pouvons les rejouer de manière déterministe et fournir aux développeurs des informations détaillées, ce qui leur permet de reproduire et de résoudre le problème de manière fiable.</p><p></p><h2>Prochaines étapes pour Fray</h2><p>Nous sommes ravis d'entendre les développeurs d'Elastic dire que Fray a été utile pour déboguer les bogues de concurrence. Nous continuerons à travailler sur Fray afin de le rendre accessible à un plus grand nombre de développeurs.</p><p>Nos objectifs à court terme sont d'améliorer la capacité de Fray à rejouer le programme de manière déterministe, même en présence d'autres opérations non déterministes telles qu'un générateur de valeurs aléatoires ou l'utilisation de <code>object.hashcode</code>. Nous visons également à améliorer la facilité d'utilisation de Fray, en permettant aux développeurs d'analyser et de déboguer les tests de concurrence existants sans aucune intervention manuelle. Plus important encore, si vous rencontrez des difficultés pour déboguer ou tester les problèmes de concurrence dans votre programme, nous aimerions avoir de vos nouvelles. N'hésitez pas à créer un problème dans le <a href="https://github.com/cmu-pasta/fray">dépôt Github de Fray</a>.</p><p></p><h2>Il est temps de corriger le bogue de la concurrence</h2><p>Grâce à Ao Li et au laboratoire PASTA, nous disposons désormais d'une instance de ce test qui échoue de manière fiable ! Nous pouvons enfin réparer cette chose. Le principal problème réside dans la manière dont <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThreadPool.java"><code>DocumentsWriterPerThreadPool</code></a> permet la réutilisation des fils et des ressources.</p>1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_0, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t0 20 getNextSequenceNumber 1 called from stack:
&lt;snip&gt;...&lt;/snip&gt;
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_1, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_5, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_2, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_6, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_3, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_4, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]<p>Ici, nous pouvons voir la création de chaque thread, qui fait référence à la file d'attente de suppression initiale de la génération 0.</p><p>Ensuite, l'avance dans la file d'attente se produira au moment de l'effacement, en voyant correctement les 7 actions précédentes dans la file d'attente.</p>1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t020 advanceQueue called from stack with maxSeq 9 lastSeqNo: 1 maxNumPendingOps: 7:
&lt;snip&gt;...&lt;/snip&gt;
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t525 getNextSequenceNumber 2 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t828 getNextSequenceNumber 3 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t727 getNextSequenceNumber 4 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t626 getNextSequenceNumber 5 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t424 getNextSequenceNumber 6 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t323 getNextSequenceNumber 7 called from stack:<p>Mais avant que tous les fils ne puissent finir de se vider, deux d'entre eux sont réutilisés pour un document supplémentaire :</p>1&gt; getAndLock: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_0, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; getAndLock: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_3, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]<p>Ces derniers incrémenteront alors le site <code>seqNo</code> au-delà du maximum supposé, qui a été calculé lors de la chasse d'eau comme étant 7. Notez le site supplémentaire <code>numDocsInRAM</code> pour les segments <code>_3</code> et <code>_0</code></p>1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_2, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_3, aborted=false, numDocsInRAM=2, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_6, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_4, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_5, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_1, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_0, aborted=false, numDocsInRAM=2, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove<p>Lucene tient donc compte de manière incorrecte de la séquence d'actions des documents au cours d'une vidange et déclenche l'échec de ce test.</p><p>Comme toutes les bonnes corrections de bogues, la correction proprement dite se résume à <a href="https://github.com/apache/lucene/pull/13627/files">une dizaine de lignes de code</a>. Mais il a fallu plusieurs jours à deux ingénieurs pour trouver la solution :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt71d78f22ce6ec215/6a1706b61949f7bcd0e7a94a/7915e5ba2feba0fb8e846da0af11bfdca470c467-1600x622.jpg" alt="" /><h2>Tous les héros ne portent pas de capes</h2><p>Oui, c'est un cliché, mais c'est vrai.</p><p></p><p>Le débogage de programmes simultanés est extrêmement important. Ces bogues de concurrence délicats prennent un temps fou à déboguer et à résoudre. Bien que les nouveaux langages comme Rust intègrent des mécanismes permettant d'éviter de telles conditions de course, la majorité des logiciels dans le monde sont déjà écrits, et écrits dans un autre langage que <a href="https://www.rust-lang.org/">Rust</a>. Java, même après toutes ces années, reste l'un des langages les plus utilisés. L'amélioration du débogage sur les langages basés sur la JVM permet d'améliorer le monde du génie logiciel. Et étant donné que certains pensent que le code sera écrit par de grands modèles de langage, peut-être que notre travail d'ingénieur consistera finalement à déboguer du mauvais code LLM au lieu de notre propre mauvais code. Mais quel que soit l'avenir du génie logiciel, le débogage simultané des programmes restera essentiel pour la maintenance et la création de logiciels.</p><p></p><p>Merci à Ao Li et à ses collègues du laboratoire PASTA de l'avoir rendu encore meilleur.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/concurrency-bugs-lucene-debugging</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/concurrency-bugs-lucene-debugging</guid>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Benjamin Trent,Ao Li]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf9fcbf843affc566/6a1706b8964ceaea3208bab9/7884e35f40c15fe349379ef7ae4648b21708962f-1070x712.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 07 Feb 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Aventures de bogues Lucene : Correction d'une exception d'index corrompu]]></title>
    <description><![CDATA[Parfois, il faut des jours pour écrire une seule ligne de code. Ici, nous avons un aperçu de la douleur et du débogage d'un ingénieur pendant plusieurs jours pour corriger une corruption potentielle de l'index Apache Lucene.]]></description>
    <content:encoded><![CDATA[<h2>Soyez prêts : </h2><p>Ce blog est différent des autres. Il ne s'agit pas d'une explication d'une nouvelle fonctionnalité ou d'un tutoriel. Il s'agit d'une simple ligne de code qui a pris trois jours à écrire. Nous allons corriger une corruption potentielle de l'index Apache Lucene. J'espère que vous en tirerez quelques enseignements :</p><ul><li><p>Tous les tests défectueux sont reproductibles, avec suffisamment de temps et les bons outils.</p></li><li><p>Pour que les systèmes soient robustes, il faut qu'il y ait plusieurs niveaux de tests. Cependant, les niveaux de tests plus élevés deviennent de plus en plus difficiles à déboguer et à reproduire.</p></li><li><p>Le sommeil est un excellent débogueur</p></li></ul><h2>Comment les tests d'Elasticsearch</h2><p>Chez Elastic, nous avons une pléthore de tests qui s'exécutent sur la base de code Elasticsearch. Certains sont des tests fonctionnels simples et ciblés, d'autres sont des tests d'intégration "happy path" sur un seul nœud, et d'autres encore tentent de casser le cluster pour s'assurer que tout se comporte correctement dans un scénario de défaillance. Lorsqu'un test échoue continuellement, un ingénieur ou un outil d'automatisation crée un problème sur github et le signale à une équipe particulière pour qu'elle l'étudie. Ce <a href="https://github.com/elastic/elasticsearch/issues/105122">bogue particulier</a> a été découvert lors d'un test du dernier type. Ces tests sont délicats et ne peuvent parfois être répétés qu'après de nombreux essais.</p><h2>Qu'est-ce que ce test teste réellement ?</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt58aab468587a7d35/6a17dd8e3e03d74b8d4f2b5f/63268f3b5714ebf9df8070ec2e3c4f0486ed822e-1600x1088.jpg" alt="Numéro github : https://github.com/elastic/elasticsearch/issues/105122" /><p>Ce test particulier est intéressant. Il créera un mappage particulier et l'appliquera à un groupe primaire. Ensuite, lors de la création d'une réplique. La principale différence est que lorsque la réplique tente d'analyser le document, le test injecte une exception, provoquant ainsi l'échec de la récupération d'une manière surprenante (mais attendue).</p><p></p><p>Tout fonctionnait comme prévu, à un détail près. Lors du nettoyage du test, nous avons validé la cohérence, et c'est là que ce test a rencontré un problème.</p><p>
Ce test n'a pas donné les résultats escomptés. Lors du contrôle de cohérence, nous vérifions que tous les fichiers de segments Lucene répliqués et primaires sont cohérents. C'est-à-dire non corrompu et entièrement répliqué. Avoir des données partielles ou corrompues est bien pire que d'avoir une défaillance totale. Voici l'effrayante et abrégée trace de pile de l'échec.</p><p></p>Caused by: org.apache.lucene.index.CorruptIndexException: Problem reading index from store(ByteSizeCachingDirectory(ElasticsearchMockDirectoryWrapper(HybridDirectory@/opt/buildkite-agent/builds/bk-agent-prod-gcp-1707109485745743789/elastic/elasticsearch-periodic/server/build/testrun/internalClusterTest/temp/org.elasticsearch.indices.recovery.IndexRecoveryIT_40853F21F419B395-001/tempDir-005/node_t0/indices/ZNwxG7VvShuwYV78RTjknA/0/index lockFactory=org.apache.lucene.store.NativeFSLockFactory@2c169f59))) (resource=store(ByteSizeCachingDirectory(ElasticsearchMockDirectoryWrapper(HybridDirectory@/opt/buildkite-agent/builds/bk-agent-prod-gcp-1707109485745743789/elastic/elasticsearch-periodic/server/build/testrun/internalClusterTest/temp/org.elasticsearch.indices.recovery.IndexRecoveryIT_40853F21F419B395-001/tempDir-005/node_t0/indices/ZNwxG7VvShuwYV78RTjknA/0/index lockFactory=org.apache.lucene.store.NativeFSLockFactory@2c169f59))))

    at org.apache.lucene.index.SegmentCoreReaders.&lt;init&gt;(SegmentCoreReaders.java:165)
    at org.apache.lucene.index.SegmentReader.&lt;init&gt;(SegmentReader.java:96)
    at org.apache.lucene.index.ReadersAndUpdates.getReader(ReadersAndUpdates.java:178)
    at org.apache.lucene.index.ReadersAndUpdates.getLatestReader(ReadersAndUpdates.java:243)
    at org.apache.lucene.index.SoftDeletesRetentionMergePolicy.keepFullyDeletedSegment(SoftDeletesRetentionMergePolicy.java:82)
    at org.apache.lucene.index.FilterMergePolicy.keepFullyDeletedSegment(FilterMergePolicy.java:118)
    at org.apache.lucene.index.FilterMergePolicy.keepFullyDeletedSegment(FilterMergePolicy.java:118)
    at org.apache.lucene.index.ReadersAndUpdates.keepFullyDeletedSegment(ReadersAndUpdates.java:822)
    at org.apache.lucene.index.IndexWriter.isFullyDeleted(IndexWriter.java:6078)
    &lt;snip&gt;

    Caused by: java.io.FileNotFoundException: No sub-file with id .kdi found in compound file "_0.cfs" (fileName=_0.kdi files: [_0.pos, .nvm, .fnm, _0.tip, _Lucene90_0.dvd, _0.doc, _0.tim, _Lucene90_0.dvm, _ES87BloomFilter_0.bfm, .fdm, .nvd, _ES87BloomFilter_0.bfi, _0.tmd, .fdx, .fdt])

      at org.apache.lucene.codecs.lucene90.Lucene90CompoundReader.openInput(Lucene90CompoundReader.java:170)
      at org.apache.lucene.codecs.lucene90.Lucene90PointsReader.&lt;init&gt;(Lucene90PointsReader.java:63)
      at org.apache.lucene.codecs.lucene90.Lucene90PointsFormat.fieldsReader(Lucene90PointsFormat.java:74)
      at org.apache.lucene.index.SegmentCoreReaders.&lt;init&gt;(SegmentCoreReaders.java:152)
      &lt;snip&gt;
<p>D'une manière ou d'une autre, lors de l'échec de la réplication forcée, le shard répliqué a fini par être corrompu ! Permettez-moi d'expliquer la partie essentielle de l'erreur en termes simples.</p><p></p><p>Lucene est une architecture basée sur les segments, ce qui signifie que chaque segment connaît et gère ses propres fichiers en lecture seule. Ce segment particulier était validé par ses <a href="https://github.com/apache/lucene/blob/add9c09c84ee66d4522c566c9f679035a0dfec13/lucene/core/src/java/org/apache/lucene/index/SegmentCoreReaders.java">SegmentCoreReaders</a> pour s'assurer que tout était en ordre. Chaque lecteur central contient des métadonnées qui indiquent quels types de champs et de fichiers existent pour un segment donné. Cependant, lors de la validation du <a href="https://github.com/apache/lucene/blob/add9c09c84ee66d4522c566c9f679035a0dfec13/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90PointsFormat.java">Lucene90PointsFormat</a>, certains fichiers attendus manquaient. Avec les segments du fichier <code>_0.cfs</code>, nous attendons un fichier en format point appelé <code>kdi</code>. <code>cfs</code> représente le système de fichiers composés "" dans lequel Lucene combinera parfois tous les types de champs et tous les petits fichiers en un seul fichier plus grand pour une réplication et une utilisation des ressources plus efficaces. En fait, les trois extensions de fichiers de points : <code>kdd</code>, <code>kdi</code>, et <code>kdm</code> manquaient. Comment pouvons-nous arriver au point où un segment Lucene s'attend à trouver un fichier de points mais où il n'y en a pas ! Cela ressemble à un bug de corruption effrayant !</p><p></p><h2>La première étape de la correction d'un bogue est de le reproduire.</h2><p></p><p>La reproduction de l'échec de ce bogue particulier a été extrêmement douloureuse. Bien que nous tirions parti des <a href="https://en.wikipedia.org/wiki/Random_testing">tests de valeurs aléatoires</a> dans Elasticsearch, nous veillons à fournir à chaque défaillance une graine aléatoire (espérons-le) reproductible afin de garantir que toutes les défaillances peuvent être étudiées. Cela fonctionne très bien pour toutes les défaillances, sauf celles qui sont dues à une <a href="https://en.wikipedia.org/wiki/Race_condition">condition de course</a>.</p>./gradlew ':server:internalClusterTest' --tests "org.elasticsearch.indices.recovery.IndexRecoveryIT.testDoNotInfinitelyWaitForMapping" -Dtests.seed=40853F21F419B395 -Dtests.jvm.argline="-Des.concurrent_search=true" -Dtests.locale=id-ID -Dtests.timezone=Asia/Jerusalem -Druntime.java=21<p>Peu importe le nombre de fois que j'ai essayé, la graine en question n'a jamais répété l'échec localement. Mais il existe des moyens d'exercer les tests et de tendre vers un échec plus reproductible.</p><p></p><p>Notre suite de tests particulière permet d'exécuter un test donné plus d'une fois dans la même commande via le paramètre <code>-Dtests.iters</code>. Mais ce n'était pas suffisant, je devais m'assurer que les fils d'exécution changeaient et augmentaient ainsi la probabilité que cette condition de course se produise. Un autre problème était que le test prenait tellement de temps à s'exécuter que le programme d'exécution du test se mettait en veilleuse. Finalement, j'ai utilisé le cauchemar bash suivant pour exécuter le test de manière répétée :</p>for run in {1..10}; do ./gradlew ':server:internalClusterTest' --tests "org.elasticsearch.indices.recovery.IndexRecoveryIT.testDoNotInfinitelyWaitForMapping" -Dtests.jvm.argline="-Des.concurrent_search=true" -Dtests.iters=10 ; done || exit 1<p>Le <a href="https://github.com/ColinIanKing/stress-ng">stress</a> fait son apparition. Cela vous permet de lancer rapidement un processus qui mangera les cœurs de l'unité centrale pour le déjeuner. L'envoi aléatoire de spam stress-ng tout en exécutant de nombreuses itérations du test défaillant m'a finalement permis de reproduire l'échec. Un pas de plus. Pour stresser le système, il suffit d'ouvrir une autre fenêtre de terminal et d'exécuter :</p>stress-ng --cpu 16<h2>
Révéler le bogue</h2><p>

Maintenant que l'échec du test révélant le bogue est en grande partie reproductible, il est temps d'essayer d'en trouver la cause. Ce qui rend ce test particulier étrange, c'est que Lucene le lance parce qu'il attend des valeurs de point, mais qu'aucune n'est ajoutée directement par le test. Uniquement les valeurs textuelles. Cela m'a poussé à examiner les changements récents apportés à nos champs de <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/optimistic-concurrency-control.html">contrôle optimiste de la concurrence</a>: <code>_seq_no</code> et <code>_primary_term</code>. Ces deux éléments sont indexés en tant que points et existent dans chaque document Elasticsearch.</p><p></p><p>En effet, un <a href="https://github.com/elastic/elasticsearch/pull/105036">commit</a> a modifié notre mappeur <code>_seq_no</code>! OUI ! Cela doit être la cause ! Mais mon excitation a été de courte durée. Cela ne fait que modifier l'ordre dans lequel les champs sont ajoutés au document. Avant cette modification, les champs <code>_seq_no</code> étaient ajoutés en dernier au document. Ensuite, ils ont été ajoutés en premier. Il est impossible que l'ordre d'ajout des champs dans un document Lucene soit à l'origine de cet échec...</p><p></p><p>Oui, la modification de l'ordre d'ajout des champs est à l'origine de l'échec. Ceci était surprenant et s'avère être un bogue dans Lucene lui-même ! Le fait de modifier l'ordre dans lequel les champs sont analysés ne devrait pas modifier le comportement de l'analyse d'un document.</p><p></p><h2>Le bogue dans Lucene</h2><p>En effet, le bogue dans Lucene se concentre sur les conditions suivantes :</p><ul><li><p>Indexation d'un champ de valeurs de points (par ex. <code>_seq_no</code>)</p></li><li><p>Essai d'indexation d'un champ de texte lancé lors de l'analyse</p></li><li><p>Dans cet état étrange, nous ouvrons un <a href="https://blog.mikemccandless.com/2011/06/lucenes-near-real-time-search-is-fast.html">lecteur en temps quasi réel</a> de l'auteur qui fait l'expérience de l'exception d'analyse de l'index du texte.</p></li></ul><p>Mais j'ai eu beau essayer, je n'ai pas réussi à le reproduire entièrement. J'ai directement ajouté des points de pause pour le débogage dans la base de code Lucene. J'ai tenté d'ouvrir des lecteurs de manière aléatoire pendant le parcours d'exception. J'ai même imprimé des mégaoctets et des mégaoctets de journaux en essayant de trouver le chemin exact où cette défaillance s'est produite. Je n'ai pas pu le faire. J'ai passé une journée entière à me battre et à perdre.</p><p></p><p>Puis j'ai dormi.</p><p></p><p>Le lendemain, j'ai relu la trace de pile originale et j'ai découvert la ligne suivante :</p><p>
</p>    at org.apache.lucene.index.SoftDeletesRetentionMergePolicy.keepFullyDeletedSegment(SoftDeletesRetentionMergePolicy.java:82)<p>Dans toutes mes tentatives de recréation, je n'ai jamais défini spécifiquement la politique de fusion de la rétention. La <a href="https://github.com/apache/lucene/blob/5f0fa2b291ff9e7d878642f025a70c15b788a470/lucene/core/src/java/org/apache/lucene/index/SoftDeletesRetentionMergePolicy.java">politique SoftDeletesRetentionMergePolicy</a> est utilisée par Elasticsearch pour répliquer avec précision les suppressions dans les répliques et garantir que tous nos contrôles de concurrence sont en charge du moment où les documents sont effectivement supprimés. Dans le cas contraire, Lucene a le contrôle total et les supprimera à chaque fusion.</p><p></p><p>Une fois que j'ai ajouté cette politique et que j'ai répliqué les étapes les plus élémentaires mentionnées ci-dessus, la panne s'est immédiatement répliquée.</p><p>
Je n'ai jamais été aussi heureux d'ouvrir un <a href="https://github.com/apache/lucene/issues/13353">bogue dans Lucene</a>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt89d37c3454ec298b/6a17dd90ec0f8993c75a6511/c7d9ce3de5278923a8454097e2bdf487c861168a-1600x920.jpg" alt="Question Github https://github.com/apache/lucene/issues/13353" /><p>
Alors qu'il se présentait comme une condition de course dans Elasticsearch, il était simple d'écrire un test échouant de manière répétée dans Lucene une fois que toutes les conditions étaient remplies.</p><p></p><p>En fin de compte, comme tous les bons bogues, il a été corrigé avec une seule ligne de code. Plusieurs jours de travail pour une seule ligne de code.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdab62db3402b27f7/6a17dd92dbb4ff23e7fb55ad/2c10a02eb41da181b1dceee47186c076c9b14a90-1600x539.jpg" alt="Correction d'une ligne de code" /><p>Mais cela en valait la peine.</p><h2>
Pas la fin</h2><p>J'espère que vous avez apprécié cette aventure avec moi ! Écrire des logiciels, en particulier des logiciels aussi répandus et complexes qu'Elasticsearch et Apache Lucene, est gratifiant. Cependant, il est parfois exceptionnellement frustrant. J'aime et je déteste à la fois les logiciels. La correction des bogues n'est jamais terminée !</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/lucene-corrupted-index-exception</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/lucene-corrupted-index-exception</guid>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Benjamin Trent]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbc25119c4add97ba/6a17dd94420229633929f4f7/2c918bad62530ff6fbe419092b2ca44bfe9408c4-944x612.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 27 Dec 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Comprendre la quantification scalaire dans Lucene]]></title>
    <description><![CDATA[Découvrez comment Elastic a introduit la quantification scalaire dans Lucene, y compris la quantification automatique par octet, la quantification par segment &amp; performance insights.]]></description>
    <content:encoded><![CDATA[<h2>Quantification automatique des octets dans Lucene</h2><p>Bien que HNSW soit un moyen puissant et flexible de stocker et de rechercher des vecteurs, il nécessite une quantité importante de mémoire pour fonctionner rapidement. Par exemple, l'interrogation de 1MM float32 vectors de 768 dimensions nécessite environ  de ram. Dès que vous commencez à rechercher un nombre important de vecteurs, cela devient coûteux. La quantification des octets est un moyen d'utiliser environ  moins de mémoire. Lucene et, par conséquent, Elasticsearch prennent en charge l'indexation des vecteurs d' depuis un certain temps, mais la construction de ces vecteurs relève de la responsabilité de l'utilisateur. Cela est sur le point de changer, car nous avons introduit la quantification scalaire  dans Lucene.</p><h2>Quantification scalaire 101</h2><p>Toutes les techniques de quantification sont considérées comme des transformations avec perte des données brutes. Cela signifie que certaines informations sont perdues pour des raisons d'espace. Pour une explication approfondie de la quantification scalaire, voir : <a href="https://www.elastic.co/search-labs/scalar-quantization-101">Quantification scalaire 101</a>. À un niveau élevé, la quantification scalaire est une technique de compression avec perte. Un simple calcul permet de réaliser des économies d'espace significatives avec très peu d'impact sur le rappel.</p><h2>Explorer l'architecture</h2><p>Les personnes habituées à travailler avec Elasticsearch sont peut-être déjà familiarisées avec ces concepts, mais voici un aperçu rapide de la distribution des documents pour la recherche.</p><p>Chaque index Elasticsearch est composé de <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/size-your-shards.html#size-your-shards">plusieurs tiroirs (shards</a>). Bien que chaque nuage ne puisse être affecté qu'à un seul nœud, plusieurs nuages par index permettent un parallélisme de calcul entre les nœuds.</p><p>Chaque tesson est composé d'un seul <a href="https://lucene.apache.org/core/9_8_0/core/org/apache/lucene/index/package-summary.html">index Lucene</a>. Un index Lucene se compose de plusieurs segments en lecture seule. Pendant l'indexation, les documents sont mis en mémoire tampon et périodiquement vidés dans un segment en lecture seule. Lorsque certaines conditions sont remplies, ces segments peuvent être fusionnés en arrière-plan en un segment plus large. Tout cela est configurable et comporte son lot de complexités. Mais lorsque nous parlons de segments et de fusion, nous parlons de segments Lucene en lecture seule et de la fusion périodique automatique de ces segments. <a href="https://www.elastic.co/search-labs/vector-search-elasticsearch-rationale">Voici une analyse plus approfondie</a> de la fusion des segments et des décisions en matière de conception.</p><h2>Quantification par segment dans Lucene</h2><p>Chaque segment dans Lucene stocke les éléments suivants : les vecteurs individuels, les indices du graphe HNSW, les vecteurs quantifiés et les quantiles calculés. Par souci de concision, nous nous concentrerons sur la manière dont Lucene stocke les vecteurs quantifiés et bruts. Pour chaque segment, nous conservons la trace des  bruts dans le fichier vec, des vecteurs quantifiés et d'un seul multiplicateur correctif dans le  ainsi que les métadonnées relatives à la quantification  fichier vemq.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1861d0af970fd096/6a17d9887f6f15a45dc099c0/507a4d578f8a5d2225b11d16ac1fc0b7dd39eaa1-1207x228.png" alt="Le fichier .vec Fichier" /><p>Figure 1 : Présentation simplifiée d'un fichier de stockage de vecteurs bruts. Occupe la  de l'espace disque puisque les valeurs  sont de 4 octets. Étant donné que nous quantifions, ces données ne seront pas chargées lors de la recherche HNSW. Ils ne sont utilisés que sur demande expresse (par ex. secondaire par force brute via le <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/filter-search-results.html#query-rescorer">rescore</a>), ou pour la re-quantification lors de la fusion de segments.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8c52b51939357280/6a17d9897b54f9130f8b3761/ffc6e8bffefc5cd36f14aae315184c89e04a6587-1440x207.png" alt="Le fichier .veq" /><p>Figure 2 : Présentation simplifiée du fichier  fichier. Occupe un espace de  et sera chargé en mémoire lors de la recherche. Les  octets sont destinés à prendre en compte le multiplicateur de correction, utilisé pour ajuster la notation afin d'améliorer la précision et la mémorisation.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt28e007988f81e5c0/6a17d98b25daabe23208a0d9/555cc92d7d179d8c9711cefbaaac3e0b815d2e42-1440x182.png" alt="Le fichier .vemq" /><p>Figure 3 : Présentation simplifiée du fichier de métadonnées. C'est ici que nous gardons trace de la quantification et de la configuration du vecteur, ainsi que des quantiles calculés pour ce segment.</p><p>Ainsi, pour chaque segment, nous stockons non seulement les vecteurs quantifiés, mais aussi les quantiles utilisés pour créer ces vecteurs quantifiés et les vecteurs bruts originaux. Mais pourquoi conserver les vecteurs bruts ?</p><h2>Une quantification qui évolue avec vous</h2><p>Étant donné que Lucene se concentre périodiquement sur les segments en lecture seule, chaque segment n'a qu'une vue partielle de l'ensemble des données. Cela signifie que les quantiles calculés ne s'appliquent directement qu'à cet échantillon de l'ensemble de vos données. Ce n'est pas très grave si votre échantillon représente correctement l'ensemble de votre corpus. Mais Lucene vous permet de trier votre index de différentes manières. Ainsi, vous pourriez indexer des données triées d'une manière qui ajoute un biais pour les calculs de quantile par segment. De plus, vous pouvez effacer les données quand vous le souhaitez ! Votre échantillon peut être minuscule, ne serait-ce qu'un seul vecteur. Un autre avantage est que vous avez le contrôle sur le moment où les fusions ont lieu. Bien qu'Elasticsearch ait configuré des valeurs par défaut et une fusion périodique, vous pouvez demander une fusion quand vous le souhaitez via l'API <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.10/indices-forcemerge.html">_force_merge</a>. Alors, comment permettre cette flexibilité tout en assurant une bonne quantification et un bon rappel ?</p><p>La quantification vectorielle de Lucene s'adaptera automatiquement au fil du temps. Lucene étant conçu avec une architecture de segments en lecture seule, nous avons la garantie que les données de chaque segment n'ont pas changé et des démarcations claires dans le code pour savoir quand les choses peuvent être mises à jour. Cela signifie que lors de la fusion des segments, nous pouvons ajuster les quantiles si nécessaire et éventuellement ré-équantifier les vecteurs.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0d9a2390958c3e82/6a17d98cbe6086e4fd0045d5/646baa8279719d06c05c1a4fdb80a0c060e0dd94-1440x446.png" alt="Quantiles de segments multiples" /><p>Figure 4 : Trois exemples de segments avec différents quantiles.</p><p>Mais la requantification n'est-elle pas coûteuse ? Il y a un certain surcoût, mais Lucene gère les quantiles intelligemment et ne les quantifie complètement que lorsque c'est nécessaire. Prenons l'exemple des segments de la figure 4. Donnons aux segments  et   documents chacun et au segment  seulement  documents. Lucene prend une moyenne pondérée des quantiles et si le quantile fusionné qui en résulte est suffisamment proche des quantiles originaux du segment, nous n'avons pas besoin de quantifier à nouveau ce segment et nous utiliserons les quantiles nouvellement fusionnés.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte2596c10ee616cf3/6a17d98eec0f8903b85a6497/58fa20d60dc337ca7108eb01e84a07c330e87ee6-1440x447.png" alt="Quantiles fusionnés" /><p>Figure 5 : Exemple de quantiles fusionnés lorsque les segments  et  ont  documents et que le segment  n'en a que .</p><p>Dans la situation représentée à la figure 5, nous pouvons voir que les quantiles fusionnés qui en résultent sont très similaires aux quantiles originaux en  et  Ils ne justifient donc pas la quantification des vecteurs. Le segment  semble s'écarter trop de la réalité. Par conséquent, les vecteurs de  seront quantifiés à nouveau avec les valeurs de quantile nouvellement fusionnées.</p><p>Il existe en effet des cas extrêmes où les quantiles fusionnés diffèrent considérablement des quantiles initiaux. Dans ce cas, nous prendrons un échantillon de chaque segment et recalculerons entièrement les quantiles.</p><h2>Performance de quantification &amp; numbers</h2><p>Est-il rapide et offre-t-il toujours un bon rappel ? Les chiffres suivants ont été recueillis lors de l'exécution de l'expérience sur une instance GCP <code>c3-standard-8</code>. Pour garantir une comparaison équitable avec , nous avons utilisé une instance suffisamment grande pour contenir des vecteurs bruts en mémoire. Nous avons indexé  vecteurs <a href="https://huggingface.co/datasets/Cohere/wikipedia-22-12-simple-embeddings">Cohere Wiki</a> en utilisant le produit intérieur maximal.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfa48e3724ad97fed/6a17d98f445de96aca4cff88/81958f99b8a1397f669db48de00f264cacc6b49f-576x455.png" alt="Rappel de quantification" /><p>Figure 6 : Rappel@10 pour les vecteurs quantifiés par rapport aux vecteurs bruts. La performance de recherche des vecteurs quantifiés est nettement plus rapide que celle des vecteurs bruts, et le rappel est rapidement récupérable en rassemblant seulement 5 vecteurs supplémentaires ; visible par .</p><p>La figure 6 illustre l'histoire. Bien qu'il y ait une différence de rappel, comme on peut s'y attendre, elle n'est pas significative. Et la différence de rappel disparaît en rassemblant seulement 5 vecteurs supplémentaires. Tout cela avec des fusions de segments  rapides et 1/4 de la mémoire des vecteurs .</p><h2>Conclusion</h2><p>Lucene apporte une solution unique à un problème difficile. La quantification ne nécessite aucune étape de "formation" ou d'"optimisation". Dans Lucene, cela fonctionnera simplement. Il n'y a pas d'inquiétude à avoir quant à la nécessité de "ré-entraîner" votre index vectoriel si vos données changent. Lucene détectera les changements significatifs et s'en chargera automatiquement pendant toute la durée de vie de vos données. Nous attendons avec impatience le moment où nous intégrerons cette fonctionnalité dans Elasticsearch !</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/scalar-quantization-in-lucene</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/scalar-quantization-in-lucene</guid>
    <category><![CDATA[Lucene]]></category>
    <category><![CDATA[Recherche ML]]></category>
    <dc:creator><![CDATA[Benjamin Trent]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb382d99ffac9992c/6a17d991b1e113640179f12c/dc07f16d347a68476bded5df9adb831452f61278-1024x1024.jpg" length="0" type="image/jpeg"/>
    <pubDate>Sat, 11 Nov 2023 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>