<?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[Lucene - 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[Lucene - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/de/search-labs/blog/category/lucene</link>
    </image>
    <link>https://www.elastic.co/de/search-labs/blog/category/lucene</link>
    <atom:link href="https://www.elastic.co/de/search-labs/rss/category/lucene.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[de]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 09:26:47 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Vektorsuchfilterung: Relevanz beibehalten]]></title>
    <description><![CDATA[Es reicht nicht aus, eine Vektorsuche durchzuführen, um die ähnlichsten Ergebnisse zu einer Suchanfrage zu finden. Um die Suchergebnisse einzugrenzen, ist häufig das Filtern erforderlich. Dieser Artikel erklärt, wie die Filterung bei der Vektorsuche in Elasticsearch und Apache Lucene funktioniert.]]></description>
    <content:encoded><![CDATA[<p>Eine Vektorsuche reicht nicht aus, um relevante Ergebnisse zu finden. Es ist sehr üblich, Filterkriterien zu verwenden, die dabei helfen, die Suchergebnisse einzugrenzen und irrelevante Ergebnisse herauszufiltern.</p><p>Das Verständnis der Funktionsweise von Filtern bei der Vektorsuche hilft Ihnen, die Kompromisse zwischen Leistung und Trefferquote auszubalancieren und einige der Optimierungen zu entdecken, die verwendet werden, um die Vektorsuche bei Verwendung von Filtern leistungsfähig zu gestalten.</p><h2>Warum filtern?</h2><p>Die Vektorsuche hat die Art und Weise, wie wir relevante Informationen in großen Datensätzen finden, revolutioniert und ermöglicht es uns, Elemente zu entdecken, die einer Suchanfrage semantisch ähnlich sind.</p><p>Es genügt jedoch nicht, einfach nur ähnliche Artikel zu finden. Oftmals müssen wir die Suchergebnisse anhand spezifischer Kriterien oder Attribute eingrenzen.</p><p>Stellen Sie sich vor, Sie suchen in einem Online-Shop nach einem Produkt. Eine reine Vektorsuche zeigt Ihnen möglicherweise visuell ähnliche Artikel an, aber Sie möchten vielleicht auch nach Preisspanne, Marke, Verfügbarkeit oder Kundenbewertungen filtern. Ohne Filterfunktion würden Ihnen unzählige ähnliche Produkte präsentiert, was es schwierig macht, genau das zu finden, was Sie suchen.</p><p>Durch die Filterung wird eine präzise Kontrolle über die Suchergebnisse ermöglicht, sodass sichergestellt wird, dass die abgerufenen Elemente nicht nur semantisch übereinstimmen, sondern auch alle notwendigen Anforderungen erfüllen. Dies führt zu einem wesentlich genaueren, effizienteren und benutzerfreundlicheren Sucherlebnis.</p><p>Hier liegt die Stärke von Elasticsearch und Apache Lucene – die effektive Filterung über verschiedene Datentypen hinweg ist einer der Hauptunterschiede zu anderen Vektordatenbanken.</p><h2>Filterung für exakte Vektorsuche</h2><p>Es gibt zwei Hauptmethoden zur Durchführung exakter Vektorsuchen:</p><ul><li><p>Verwendung des Indextyps <code>flat</code> für Ihr dense_vector-Feld. Dies bewirkt, dass <code>knn</code> -Suchen eine exakte Suche anstelle einer approximativen Suche verwenden.</p></li><li><p>Die Punktzahl wird mithilfe einer <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-script-score-query#vector-functions">script_score-Abfrage</a> berechnet, die Vektorfunktionen verwendet. Dies kann mit jedem Indextyp verwendet werden.</p></li></ul><p>Bei der Ausführung einer exakten Vektorsuche werden alle Vektoren mit der Suchanfrage verglichen. In diesem Szenario verbessert das Filtern die Leistung, da nur die Vektoren verglichen werden müssen, die den Filter passieren.</p><p>Dies hat keinen Einfluss auf die Ergebnisqualität, da ohnehin alle Vektoren berücksichtigt werden. Wir filtern bereits im Voraus die Ergebnisse heraus, die nicht interessant sind, um die Anzahl der Operationen zu reduzieren.</p><p>Dies ist sehr wichtig, da eine exakte Suche gegenüber einer approximativen Suche performanter sein kann, wenn die angewendeten Filter zu einer geringen Anzahl von Dokumenten führen.</p><p>Als Faustregel gilt: Verwenden Sie die exakte Suche, wenn weniger als 10.000 Dokumente den Filter passieren. <a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">BBQ</a> -Indizes sind für Vergleiche wesentlich schneller, daher ist es sinnvoll, bei weniger als 100.000 Einträgen für die Basisindizes die exakte Suche zu verwenden. Weitere Details finden Sie in <a href="https://www.elastic.co/search-labs/blog/knn-exact-vs-approximate-search">diesem Blogbeitrag</a> .</p><p>Falls Ihre Filter immer sehr restriktiv sind, könnten Sie erwägen, die Indizierung auf die exakte Suche anstatt auf die ungefähre Suche auszurichten, indem Sie einen <code>flat</code> -Indextyp anstelle eines HNSW-basierten Index verwenden. Weitere Details finden Sie in <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector#dense-vector-params">den Eigenschaften von index_options</a>.</p><h2>Filterung für die approximative Vektorsuche</h2><p>Bei der Durchführung einer approximativen Vektorsuche tauschen wir Ergebnisgenauigkeit gegen Leistung ein. Vektorsuchdatenstrukturen wie HNSW suchen effizient nach ungefähren nächsten Nachbarn in Millionen von Vektoren. Sie konzentrieren sich darauf, die ähnlichsten Vektoren zu finden, indem sie die geringste Anzahl an Vektorvergleichen durchführen, deren Berechnung aufwändig ist.</p><p>Dies bedeutet, dass andere Filterattribute nicht Teil der Vektordaten sind. Verschiedene Datentypen verfügen über eigene Indexierungsstrukturen, die effizient zum Auffinden und Filtern dieser Daten sind, wie beispielsweise Termwörterbücher, Beitragslisten und Dokumentwerte.</p><p>Da diese Datenstrukturen vom Vektorsuchmechanismus getrennt sind, wie wenden wir Filter auf die Vektorsuche an? Es gibt zwei Möglichkeiten: Filter nach der Vektorsuche (Nachfilterung) oder vor der Vektorsuche (Vorfilterung) anwenden.</p><p>Jede dieser Optionen hat Vor- und Nachteile. Lasst uns tiefer in diese Materie eintauchen!</p><h3>Nachfilterung</h3><p>Die Nachfilterung wendet Filter an, nachdem die Vektorsuche durchgeführt wurde. Dies bedeutet, dass die Filter angewendet werden, nachdem die k ähnlichsten Vektorergebnisse gefunden wurden.</p><p>Offensichtlich können wir nach Anwendung der Filter auf die Ergebnisse unter Umständen weniger als k Ergebnisse erhalten. Wir könnten natürlich mehr Ergebnisse durch eine Vektorsuche erhalten (höherer k-Wert), aber wir können nicht sicher sein, dass wir nach Anwendung der Filter k oder mehr Ergebnisse erhalten.</p><p>Der Vorteil der Nachfilterung besteht darin, dass sie das Laufzeitverhalten der Vektorsuche nicht verändert – die Vektorsuche ist sich der Filterung nicht bewusst. Dies ändert jedoch die endgültige Anzahl der abgerufenen Ergebnisse.</p><p>Nachfolgend ein Beispiel für die Nachfilterung mithilfe der <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query">knn-Abfrage</a>. Prüfen Sie, ob die Filterklausel von der knn-Abfrage getrennt ist:</p>{
  "query": {
    "bool": {
      "must": {
        "knn": {
          "field": "image-vector",
          "query_vector": [54, 10, -2],
          "k": 5,
          "num_candidates": 50
        }
      },
      "filter": {
        "term": {
          "file-type": "png"
        }
      }
    }
  }
}<p>Für die KNN-Suche ist auch eine Nachfilterung mit <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/filter-search-results#post-filter">dem Postfilter</a> möglich:</p>{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, 2],
    "k": 5,
    "num_candidates": 50
  },
  "post_filter": {
    "term": {
      "file-type": "png"
    }
  }
}<p>Beachten Sie, dass Sie bei der knn-Suche einen expliziten Post-Filter-Abschnitt verwenden müssen. Wenn Sie keinen Nachfilter verwenden, <a href="https://www.elastic.co/docs/solutions/search/vector/knn#_combine_approximate_knn_with_other_features">kombiniert die kNN-Suche die Ergebnisse der nächsten Nachbarn</a> mit anderen Abfragen oder Filtern, anstatt einen Nachfilter anzuwenden.</p><h3>Vorfilterung</h3><p>Durch das Anwenden von Filtern vor der Vektorsuche werden zunächst die Dokumente abgerufen, die den Filtern entsprechen, und diese Informationen werden dann an die Vektorsuche weitergegeben.</p><p>Lucene verwendet <a href="https://github.com/apache/lucene/blob/7a60d7ce92392181e137361336e5196bd486cdd9/lucene/core/src/java/org/apache/lucene/util/BitSet.java">BitSets</a> , um die Dokumente, die die Filterbedingung erfüllen, effizient zu speichern. Anschließend durchläuft die Vektorsuche den HNSW-Graphen und berücksichtigt dabei die Dokumente, die die Bedingung erfüllen. Bevor ein Kandidat zu den Ergebnissen hinzugefügt wird, wird geprüft, ob er im BitSet gültiger Dokumente enthalten ist.</p><p>Allerdings muss der Kandidat untersucht und mit der Anfrage verglichen werden, selbst wenn es sich nicht um ein gültiges Dokument handelt. Die Effektivität von HNSW beruht auf der Verbindung zwischen den Vektoren im Graphen – wenn wir die Untersuchung eines Kandidaten abbrechen würden, hieße das, dass wir möglicherweise auch seine Nachbarn überspringen würden.</p><p>Stellen Sie es sich so vor, als würden Sie zu einer Tankstelle fahren. Wenn Sie alle Straßen ausschließen, auf denen sich keine Tankstelle befindet, ist es unwahrscheinlich, dass Sie Ihr Ziel erreichen. Andere Straßen sind vielleicht nicht das, was Sie brauchen, aber sie <em>verbinden</em> Sie mit Ihrem Ziel. Gleiches gilt für Vektoren in einem HNSW-Diagramm!</p><p>Daraus folgt, dass die Anwendung von Vorfiltern weniger effizient ist als der Verzicht auf Filter. Wir müssen die Arbeit an <em>allen</em> Vektoren durchführen, die wir bei unserer Suche besuchen, und diejenigen verwerfen, die nicht dem Filter entsprechen. Wir investieren mehr Arbeit und nehmen uns mehr Zeit, um unsere Top-K-Ergebnisse zu erzielen.</p><p>Nachfolgend ein Beispiel für Vorfilterung in der Elasticsearch Query DSL. Prüfen Sie, ob die Filterklausel nun Teil des knn-Abschnitts ist:</p>{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "filter": {
      "term": {
        "file-type": "png"
      }
    }
  }
}<p>Eine Vorfilterung ist sowohl für <a href="https://www.elastic.co/docs/solutions/search/vector/knn#knn-search-filter-example">die KNN-Suche</a> als auch für <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query#knn-query-filtering">die KNN-Abfrage</a> verfügbar:</p>{
  "query": {
    "knn": {
      "field": "image-vector",
      "query_vector": [-5, 9, -12],
      "k": 5,
      "filter": {
        "term": {
          "file-type": "png"
        }
      }
    }
  }
}<h4>Vorfilteroptimierungen</h4><p>Es gibt einige Optimierungen, die wir anwenden können, um eine effiziente Vorfilterung zu gewährleisten.</p><p>Wir können auf die exakte Suche umschalten, wenn der Filter sehr restriktiv ist. Wenn nur wenige Vektoren zu vergleichen sind, ist es schneller, eine exakte Suche in den wenigen Dokumenten durchzuführen, die dem Filter entsprechen.</p><p>Dies ist eine Optimierung, die in <a href="https://github.com/apache/lucene/blob/eb876b618da5d04c1ad14b04a48321638318493a/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L218">Lucene</a> und Elasticsearch automatisch angewendet wird.</p><p>Eine weitere Optimierungsmethode besteht darin, die Vektoren zu ignorieren, die den Filter nicht erfüllen. Stattdessen prüft diese Methode die Nachbarn der gefilterten Vektoren, die den Filter passieren. Dieser Ansatz reduziert effektiv die Anzahl der Vergleiche, da die gefilterten Vektoren nicht berücksichtigt werden, und untersucht weiterhin Vektoren, die mit dem aktuellen Pfad verbunden sind.</p><p>Dieser Algorithmus heißt ACORN-1, und der Prozess wird in <a href="https://www.elastic.co/search-labs/blog/filtered-hnsw-knn-search">diesem Blogbeitrag</a> ausführlich beschrieben.</p><h2>Filtern mithilfe der Dokumentensicherheit</h2><p><a href="https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/controlling-access-at-document-field-level#document-level-security">Document Level Security (DLS)</a> ist eine Elasticsearch-Funktion, die festlegt, welche Dokumente Benutzerrollen abrufen können.</p><p>DLS wird mittels Abfragen durchgeführt. Einer Rolle kann eine Abfrage zugeordnet sein, die die Dokumente einschränkt, die ein Benutzer dieser Rolle aus den Indizes abrufen kann.</p><p>Die Rollenabfrage dient als Filter, um <a href="https://github.com/elastic/elasticsearch/blob/c3a1cb34294e902a9f46d7e840ea09965019f456/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexReaderWrapper.java#L92">die Dokumente abzurufen, die ihr entsprechen</a>, und wird als BitSet zwischengespeichert. Dieses BitSet wird dann verwendet, um den zugrunde liegenden Lucene-Reader zu umschließen, sodass nur die Dokumente, die von der Abfrage zurückgegeben wurden, als <em>aktiv</em>gelten – das heißt, sie existieren im Index und wurden nicht gelöscht.</p><p>Da die Live-Dokumente <a href="https://github.com/apache/lucene/blob/a211d30097a8e3264d3ef073a054bd31eb847231/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L196">vom Reader abgerufen</a> werden, um die kNN-Abfrage durchzuführen, werden nur die dem Benutzer zur Verfügung stehenden Dokumente berücksichtigt. Falls ein Vorfilter vorhanden ist, werden die DLS-Dokumente <a href="https://github.com/apache/lucene/blob/a211d30097a8e3264d3ef073a054bd31eb847231/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L204">diesem hinzugefügt</a>.</p><p>Dies bedeutet, dass die DLS-Filterung als Vorfilter für die approximative Vektorsuche fungiert, mit denselben Auswirkungen auf die Leistung und den gleichen Optimierungen.</p><p>Die DLS-Suche mit exakter Suche bietet die gleichen Vorteile wie die Anwendung eines beliebigen Filters – je weniger Dokumente aus der DLS abgerufen werden, desto effizienter ist die exakte Suche. Berücksichtigen Sie auch die Anzahl der von DLS zurückgegebenen Dokumente – wenn die DLS-Rollen sehr restriktiv sind, sollten Sie die Verwendung einer exakten Suche anstelle einer ungefähren Suche in Betracht ziehen.</p><h2>Benchmarking</h2><p>Wir bei Elasticsearch möchten sicherstellen, dass die Vektorsuchfilterung effizient ist. Wir haben <a href="https://elasticsearch-benchmarks.elastic.co/#tracks/so_vector/nightly/default/90d">einen speziellen Benchmark für die Vektorfilterung</a> , der approximative Vektorsuchen mit unterschiedlichen Filtern durchführt, um sicherzustellen, dass die Vektorsuche relevante Ergebnisse so schnell wie möglich liefert.</p><p>Prüfen Sie die <a href="https://elasticsearch-benchmark-analytics.elastic.co/app/dashboards#/view/43b63e80-5ba2-11ed-aede-a742809feed4?_g=(refreshInterval:(pause:!t,value:60000),time:(from:'2025-05-28T01:27:58.456Z',to:'2025-06-30T13:53:26.430Z'))&amp;_a=()">Verbesserungen,</a> die mit der Einführung von ACORN-1 einhergingen. Bei Tests, bei denen nur 2 % der Vektoren den Filter passieren, reduziert sich die Abfragelatenz auf 55 % der ursprünglichen Dauer:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt820cb0b715cf291c/6a17e227dbb4ff8d49fb5615/3eac3748a33376fc97d957364a5c1f5108d5c58b-1023x896.png" alt="" /><h2>Fazit</h2><p>Filtern ist ein integraler Bestandteil der Suche. Die Gewährleistung einer leistungsfähigen Filterung bei der Vektorsuche sowie das Verständnis der damit verbundenen Kompromisse und Optimierungsmöglichkeiten entscheiden darüber, ob eine effiziente und genaue Suche gelingt oder scheitert.</p><p>Die Filterung beeinflusst die Leistung bei der Vektorsuche:</p><ul><li><p>Die exakte Suche ist schneller, wenn Filter verwendet werden. Sie sollten die Verwendung einer exakten Suche anstelle einer ungefähren Suche in Betracht ziehen, wenn Ihre Filterkriterien ausreichend restriktiv sind. Dies ist eine automatische Optimierung in Elasticsearch.</p></li><li><p>Die ungefähre Suche ist langsamer, wenn Vorfilter verwendet werden. Durch die Vorfilterung erhalten wir die ersten k Ergebnisse, die dem Filter entsprechen, allerdings auf Kosten einer langsameren Suche.</p></li><li><p>Die Nachfilterung liefert nicht unbedingt die obersten k Ergebnisse, da diese bereits beim Anwenden des Filters herausgefiltert worden sein können.</p></li></ul><p>Viel Spaß beim Filtern!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/vector-search-filtering</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/vector-search-filtering</guid>
    <category><![CDATA[Vektordatenbank]]></category>
    <category><![CDATA[Lucene]]></category>
    <category><![CDATA[Inside Elastic]]></category>
    <dc:creator><![CDATA[Carlos Delgado]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt39ede1736e0f1456/6a17e2282f4a5c5031fa8843/03b1dd4c7bda4fbabd8e374bc2e4f12d5be6ef5f-1600x1150.png" length="0" type="image/png"/>
    <pubDate>Wed, 03 Sep 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Beschleunigung der Zusammenführung von HNSW-Diagrammen]]></title>
    <description><![CDATA[Informieren Sie sich über unsere Arbeit zur Reduzierung des Aufwands beim Erstellen mehrerer HNSW-Diagramme, insbesondere zur Reduzierung der Kosten für das Zusammenführen von Diagrammen.]]></description>
    <content:encoded><![CDATA[<p>In der Vergangenheit <a href="https://www.elastic.co/de/search-labs/blog/multi-graph-vector-search">haben wir einige der Herausforderungen erörtert</a> , die mit der Suche in mehreren <a href="https://www.elastic.co/de/search-labs/blog/hnsw-graph">HNSW-Graphen</a> einhergehen, und wie wir diese bewältigen konnten. Damals deuteten wir einige weitere Verbesserungen an, die wir geplant hatten. Dieser Beitrag ist der Höhepunkt dieser Arbeit.</p><p>Sie fragen sich vielleicht, warum Sie überhaupt mehrere Diagramme verwenden? Dies ist ein Nebeneffekt einer Architekturentscheidung in Lucene: unveränderliche Segmente. Wie bei den meisten architektonischen Entscheidungen gibt es Vor- und Nachteile. Beispielsweise haben wir vor Kurzem Serverless Elasticsearch allgemein zugänglich gemacht. In diesem Zusammenhang haben wir durch unveränderliche Segmente erhebliche Vorteile erzielt, darunter eine effiziente Indexreplikation und die Möglichkeit, die Index- und Abfrageberechnung zu entkoppeln und unabhängig voneinander automatisch zu skalieren. Bei der Vektorquantisierung bieten uns Segmentzusammenführungen die Möglichkeit, Parameter zu aktualisieren, um sie an die Dateneigenschaften anzupassen. In diesem Sinne sind wir der Meinung, dass die Möglichkeit, Datenmerkmale zu messen und Indexierungsentscheidungen zu überprüfen, noch weitere Vorteile mit sich bringt.</p><p>In diesem Beitrag besprechen wir die Arbeit, die wir geleistet haben, um den Aufwand für die Erstellung mehrerer HNSW-Diagramme deutlich zu reduzieren und insbesondere die Kosten für das Zusammenführen von Diagrammen zu senken.</p><h3>Hintergrund</h3><p>Um eine überschaubare Anzahl von Segmenten beizubehalten, prüft Lucene regelmäßig, ob Segmente zusammengeführt werden sollen. Dies läuft darauf hinaus, zu prüfen, ob die aktuelle Segmentanzahl eine Zielsegmentanzahl überschreitet, die durch die Basissegmentgröße und die Zusammenführungsrichtlinie bestimmt wird. Wenn die Anzahl überschritten wird, führt Lucene Segmentgruppen zusammen, während die Einschränkung verletzt wird. Dieser Vorgang wurde <a href="https://blog.mikemccandless.com/2011/02/visualizing-lucenes-segment-merges.html">an anderer Stelle</a> ausführlich beschrieben.</p><p>Lucene entscheidet sich für die Zusammenführung ähnlich großer Segmente, da dadurch ein logarithmisches Wachstum der Schreibverstärkung erreicht wird. Im Fall eines Vektorindex ist die Schreibverstärkung die Anzahl der Male, die ein Vektor in ein Diagramm eingefügt wird. Lucene versucht, Segmente in Gruppen von etwa 10 zusammenzuführen. Folglich werden Vektoren in einen Graphen eingefügt, der ungefähr beträgt.mal, wobei  die Anzahl der Indexvektoren und  die erwartete Anzahl der Basissegmentvektoren ist. Aufgrund des logarithmischen Wachstums liegt die Schreibverstärkung selbst bei großen Indizes im einstelligen Bereich. Die Gesamtzeit, die zum Zusammenführen von Graphen benötigt wird, ist jedoch linear proportional zur Schreibverstärkung.</p><p>Beim Zusammenführen von HNSW-Graphen nehmen wir bereits eine kleine Optimierung vor: Wir behalten den Graphen für das größte Segment bei und fügen Vektoren aus den anderen Segmenten darin ein. Dies ist der Grund für den oben genannten 9/10-Faktor. Im Folgenden zeigen wir, wie wir durch die Verwendung von Informationen aus allen Diagrammen, die wir zusammenführen, deutlich bessere Ergebnisse erzielen können.</p><h3>HNSW-Graphzusammenführung</h3><p>Bisher haben wir den größten Graphen beibehalten und Vektoren aus den anderen eingefügt, wobei wir die Graphen, die sie enthalten, ignoriert haben. Die zentrale Erkenntnis, die wir im Folgenden nutzen, ist, dass jeder HNSW-Graph, den wir verwerfen, wichtige Näheinformationen über die darin enthaltenen Vektoren enthält. Wir möchten diese Informationen nutzen, um das Einfügen zumindest einiger Vektoren zu beschleunigen.</p><p>Wir konzentrieren uns auf das Problem des Einfügens eines kleineren Graphen  in einen größeren Graphen , da dies eine atomare Operation ist, die wir zum Erstellen beliebiger Zusammenführungsrichtlinien verwenden können.</p><p>Die Strategie besteht darin, eine Teilmenge von Eckpunkten von  zu finden, die in den großen Graphen eingefügt werden soll. Wir verwenden dann die Konnektivität dieser Knoten im kleinen Graphen, um das Einfügen der verbleibenden Knoten  zu beschleunigen. Im Folgenden verwenden wir  und  um die Nachbarn eines Knotens  im kleinen bzw. großen Graphen zu bezeichnen. Schematisch ist der Ablauf wie folgt.</p><p><code>MERGE-HNSW</code></p><p><code>Inputs </code><code> and </code></p><p><code>1</code><code>Find </code><code> to insert into </code><code> using COMPUTE-JOIN-SET</code>
<code>2</code><code>Insert each vertex </code><code> into </code>
<code>3</code><code>for </code><code> do</code>
<code>4</code>
<code>5</code>
<code>6</code><code>FAST-SEARCH-LAYER</code>
<code>7</code><code>SELECT-NEIGHBORS-HEURISTIC</code>
<code>8</code></p><p>Wir berechnen die Menge  mithilfe eines Verfahrens, das wir unten besprechen (Zeile 1). Dann fügen wir jeden Knoten in  mithilfe des Standard-HNSW-Einfügeverfahrens in den großen Graphen ein (Zeile 2). Für jeden Knoten, den wir nicht eingefügt haben, finden wir seine Nachbarn, die wir eingefügt haben, und deren Nachbarn im großen Graphen (Zeilen 4 und 5). Wir verwenden ein mit diesem Set (Zeile 6) gesätes <code>FAST-SEARCH-LAYER</code> -Verfahren, um die Kandidaten für <code>SELECT-NEIGHBORS-HEURISTIC</code> aus dem HNSW- <a href="https://arxiv.org/pdf/1603.09320">Papier</a> (Zeile 7) zu finden. Tatsächlich ersetzen wir <code>SEARCH-LAYER</code> , um den Kandidatensatz in der Methode <code>INSERT</code> (Algorithmus 1 aus dem Dokument) zu finden, die ansonsten unverändert bleibt. Schließlich fügen wir den Scheitelpunkt hinzu, den wir gerade in  eingefügt haben (Zeile 8).</p><p>Damit dies funktioniert, ist klar, dass jeder Scheitelpunkt in  mindestens einen Nachbarn in  haben muss. Tatsächlich verlangen wir, dass für jeden Scheitelpunkt in   für ein gewisses , die maximale Schichtkonnektivität, gilt. Wir beobachten, dass in realen HNSW-Diagrammen eine ziemliche Streuung der Scheitelpunktgrade zu sehen ist. Die folgende Abbildung zeigt eine typische kumulative Dichtefunktion des Scheitelpunktgrads für die unterste Ebene eines Lucene HNSW-Diagramms.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc815e1a9c3bb0d06/6a17e178ec0f89308a5a6564/44f001b3a1bc6627e172fed5c52a02cfdcb4cd66-1324x898.png" alt="HNSW-Graph: Beispielhafte Knotengradverteilung" /><p>Wir haben die Verwendung eines festen Werts für  sowie die Möglichkeit untersucht, ihn zu einer Funktion des Scheitelpunktgrads zu machen. Diese zweite Wahl führt zu größeren Beschleunigungen bei minimalen Auswirkungen auf die Grafikqualität, daher habe ich mich für Folgendes entschieden</p><p>Beachten Sie, dass | per Definition gleich dem Grad des Scheitelpunkts  im kleinen Graphen ist. Eine Untergrenze von zwei bedeutet, dass wir jeden Scheitelpunkt einfügen, dessen Grad kleiner als zwei ist.</p><p>Ein einfaches Zählargument legt nahe, dass wir bei sorgfältiger Wahl von  nur etwa  direkt in  einfügen müssen. Konkret färben wir eine Kante des Graphen, wenn wir genau einen ihrer Endknoten in  einfügen. Dann wissen wir, dass wir für jeden Knoten in  mindestens  Kanten einfärben müssen, um mindestens  Nachbarn in  Darüber hinaus erwarten wir, dass</p><p>Dabei ist  der durchschnittliche Knotengrad im kleinen Graphen. Für jeden Knoten  färben wir höchstens  Kanten. Daher beträgt die Gesamtzahl der Kanten, die wir voraussichtlich einfärben werden, höchstens . Wir hoffen, dass wir durch sorgfältige Wahl von  annähernd diese Anzahl von Kanten einfärben können. Um alle Eckpunkte abzudecken, muss  folgende Bedingung erfüllen:</p><p>Dies impliziert, dass .</p><p>Vorausgesetzt, <code>SEARCH-LAYER</code> dominiert die Laufzeit, lässt dies darauf schließen, dass wir eine bis zu  Beschleunigung der Zusammenführungszeit erreichen könnten. Angesichts des logarithmischen Wachstums der Schreibverstärkung bedeutet dies, dass wir selbst bei sehr großen Indizes die Erstellungszeit im Vergleich zum Erstellen eines Diagramms normalerweise nur verdoppeln würden.</p><p>Das Risiko bei dieser Strategie besteht darin, dass wir die Grafikqualität beeinträchtigen. Wir haben es zunächst mit einem No-Op <code>FAST-SEARCH-LAYER</code> versucht. Wir haben festgestellt, dass dies die Qualität des Diagramms in dem Maße verschlechtert, dass die Rückrufrate als Funktion der Latenz beeinträchtigt wird, insbesondere beim Zusammenführen auf ein einzelnes Segment. Anschließend haben wir mithilfe einer eingeschränkten Suche im Diagramm verschiedene Alternativen untersucht. Am Ende war die effektivste Wahl die einfachste. Verwenden Sie <code>SEARCH-LAYER</code> , aber mit einem niedrigen <code>ef_construction</code>. Mit dieser Parametrisierung konnten wir Grafiken von hervorragender Qualität erzielen und die Zusammenführungszeit dennoch durchschnittlich um etwas mehr als 30 % verkürzen.</p><h3>Berechnung der Join-Menge</h3><p>Die Suche nach einer guten Join-Menge kann als HNSW-Graphüberdeckungsproblem formuliert werden. Eine Greedy-Heuristik ist eine einfache und effektive Heuristik zur Annäherung an optimale Graphüberdeckungen. Bei unserem Ansatz werden die Knotenpunkte nacheinander in absteigender Reihenfolge der Verstärkung zu  hinzugefügt. Der Gewinn ist wie folgt definiert:</p><p>Dabei bezeichnet  die Anzahl der Nachbarn eines Vektors  in  und  ist die Indikatorfunktion. Der Gewinn beinhaltet die Änderung der Anzahl der Scheitelpunkte, die wir zu  hinzugefügt haben, also , da wir unserem Ziel durch das Hinzufügen eines weniger abgedeckten Scheitelpunkts näher kommen. Die Gewinnberechnung wird in der folgenden Abbildung für den zentralen orangefarbenen Scheitelpunkt veranschaulicht.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7a353d56fa09af5f/6a17e17a2f4a5c33f7fa8825/fe11cd55a7d94d9e0b0f4d47ea309a99075c355d-540x474.png" alt="Knotengewinn, der der Join-Menge J im HNSW-Graphen hinzugefügt werden soll" /><p>Wir behalten für jeden Knoten  den folgenden Zustand bei:</p><ol><li><p>Ob es abgestanden ist,</p></li><li><p>Sein Gewinn ,</p></li><li><p>Die Anzahl der benachbarten Eckpunkte in  wird  bezeichnet,</p></li><li><p>Eine Zufallszahl im Bereich [0,1], die zum Auflösen eines Gleichstands verwendet wird.</p></li></ol><p>Der Pseudocode zum Berechnen des Join-Sets lautet wie folgt.</p><p><code>COMPUTE-JOIN-SET</code></p><p><code>Inputs </code></p><p><code>1</code>
<code>2</code>
<code>3</code><code>for </code><code> do</code>
<code>4</code>
<code>5</code>
<code>6</code>
<code>7</code><code>while </code><code> do
8</code><code> maximum gain vertex in </code>
<code>9</code><code>Remove the state for </code><code> from </code>
<code>10</code><code>if </code><code> is not stale then</code>
<code>11</code>
<code>12</code>
<code>13</code><code>for </code><code> do</code>
<code>14</code><code>mark </code><code> as stale if </code>
<code>15</code><code>mark neighbors of </code><code> stale if </code><code>
16</code><code>
17</code><code>else
18</code><code>
19</code><code>if </code><code> then
20</code><code>
21</code><code>return </code></p><p>Wir initialisieren zunächst den Status in den Zeilen 1-5.</p><p>In jeder Iteration der Hauptschleife extrahieren wir zunächst den Scheitelpunkt mit der maximalen Verstärkung (Zeile 8) und lösen Gleichstände nach dem Zufallsprinzip auf. Bevor wir Änderungen vornehmen, müssen wir prüfen, ob die Verstärkung des Scheitelpunkts veraltet ist. Insbesondere beeinflussen wir jedes Mal, wenn wir einen Scheitelpunkt zu  hinzufügen, den Gewinn anderer Scheitelpunkte:</p><ol><li><p>Da alle seine Nachbarn einen zusätzlichen Nachbarn in  haben, können sich ihre Gewinne ändern (Zeile 14).</p></li><li><p>Wenn einer seiner Nachbarn jetzt vollständig gedeckt ist, können sich die Gewinne aller seiner Nachbarn ändern (Zeilen 14-16).</p></li></ol><p>Wir berechnen die Gewinne auf verzögerte Weise neu, d. h. wir berechnen den Gewinn eines Scheitelpunkts nur dann neu, wenn wir ihn in  einfügen möchten (Zeilen 18–20). Da die Gewinne immer nur abnehmen, können wir nie einen Scheitelpunkt verpassen, den wir einfügen sollten.</p><p>Beachten Sie, dass wir lediglich den Gesamtgewinn der zu  hinzugefügten Scheitelpunkte im Auge behalten müssen, um zu bestimmen, wann wir aussteigen müssen. Darüber hinaus wird, obwohl  mindestens ein Scheitelpunkt einen von Null verschiedenen Gewinn aufweisen, sodass wir immer Fortschritte machen.</p><h3>Ergebnisse</h3><p>Wir haben Experimente mit vier Datensätzen durchgeführt, die zusammen unsere drei unterstützten Distanzmetriken (euklidisch, Kosinus und inneres Produkt) abdecken:</p><ol><li><p>quora-E5-small: 522931 Dokumente, 384 Dimensionen und verwendet Kosinusähnlichkeit,</p></li><li><p>cohere-wikipedia-v2: 1 Million Dokumente, 768 Dimensionen und verwendet Kosinusähnlichkeit,</p></li><li><p>Kern: 1 Million Dokumente, 960 Dimensionen und verwendet euklidische Distanz und</p></li><li><p>cohere-wikipedia-v3: 1 Mio. Dokumente, 1024 Dimensionen und verwendet das maximale innere Produkt.</p></li></ol><p>Für jeden Datensatz bewerten wir zwei Quantisierungsstufen:</p><ol><li><p>int8 – verwendet eine 1-Byte-Ganzzahl pro Dimension und</p></li><li><p>BBQ – das ein einzelnes Bit pro Dimension verwendet.</p></li></ol><p>Schließlich haben wir für jedes Experiment die Suchqualität bei zwei Abruftiefen bewertet und nach dem Erstellen des Index und anschließend nach der erzwungenen Zusammenführung zu einem einzelnen Segment untersucht.</p><p>Zusammenfassend lässt sich sagen, dass wir durchgängig erhebliche Beschleunigungen bei der Indizierung und Zusammenführung erzielen und dabei in allen Fällen die Grafikqualität und damit die Suchleistung beibehalten.</p><h4>Experiment 1: int8-Quantisierung</h4><p>Die durchschnittlichen Beschleunigungen von der Basislinie bis zum Kandidaten, die vorgeschlagenen Änderungen, sind:</p><p>Beschleunigung der Indexzeit: <strong>1,28</strong></p><p>Beschleunigung der Force Merge: <strong>1,72</strong></p><p>Dies entspricht folgender Laufzeitaufteilung</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8422be122ef1a9c9/6a17e17bbe6086522e00467d/5f9f6d487b4c74c4998aa47465912c1a9743e928-734x479.png" alt="Index- und Zusammenführungszeiten für die Baseline- und Kandidatenzusammenführungsstrategien" /><p>Der Vollständigkeit halber sind die genauen Zeiten</p><p></p><p>Index</p><p></p><p>Verschmelzen</p><p></p><p>Datensatz</p><p>Basislinie</p><p>Kandidat</p><p>Entwickeln</p><p>Kandidat</p><p>Quora-E5-klein</p><p>112,41 s</p><p>81,55 s</p><p>113,81 s</p><p>70,87 s</p><p>wiki-cohere-v2</p><p>158,1 s</p><p>122,95 s</p><p>425,20 s</p><p>239,28 s</p><p>Kern</p><p>141,82 s</p><p>119,26 s</p><p>536,07 s</p><p>279,05 s</p><p>wiki-cohere-v3</p><p>211,86 s</p><p>168,22 s</p><p>654,97 s</p><p>414,12 s</p><p>Unten zeigen wir die Diagramme „Rückruf vs. Latenz“, die den Kandidaten (gestrichelte Linien) mit der Basislinie bei zwei Abruftiefen vergleichen: Rückruf@10 und Rückruf@100 für Indizes mit mehreren Segmenten (das Endergebnis unserer Standard-Zusammenführungsstrategie nach der Indizierung aller Vektoren) und nach der erzwungenen Zusammenführung zu einem einzelnen Segment. Eine Kurve, die höher und weiter links liegt, ist besser, da sie eine höhere Rückrufrate bei geringerer Latenz bedeutet.</p><p>Wie Sie sehen, ist der Kandidat für mehrere Segmentindizes für den Cohere v3-Datensatz besser und für alle anderen Datensätze etwas schlechter, aber fast vergleichbar. Nach der Zusammenführung zu einem einzigen Segment sind die Erinnerungskurven in allen Fällen nahezu identisch.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf674622469bf6a90/6a17e17dfbc5f874a84919c9/9195297f19f90b172d832ba56bdada7b0d8a768b-985x392.png" alt="Rückruf @10 und @100 im Vergleich zur Latenz nach dem Erstellen des Index" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltde89f7e94ac823f4/6a17e17fe9ea876429a9c507/c866f32d579ae2b841e92ca733dd29c0e214ac6b-986x386.png" alt="Rückruf @10 und @100 im Vergleich zur Latenz nach der Zusammenführung zu einem einzelnen Segment" /><h4>Experiment 2: BBQ-Quantisierung</h4><p>Die durchschnittlichen Beschleunigungen von der Basislinie zum Kandidaten sind:</p><p>Beschleunigung der Indexzeit: <strong>1,33</strong></p><p>Beschleunigung der Force Merge: <strong>1,34</strong></p><p>Dies entspricht folgender Laufzeitaufteilung</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5b0eb305222fa5c2/6a17e18125daab514f08a18c/11be0cf6b63480dc703409329357ea4847b55051-740x415.png" alt="Index- und Zusammenführungszeit für die Baseline- und Kandidatenzusammenführungsstrategien" /><p>Der Vollständigkeit halber sind die genauen Zeiten</p><p></p><p>Index</p><p></p><p>Verschmelzen</p><p></p><p>Datensatz</p><p>Basislinie</p><p>Kandidat</p><p>Entwickeln</p><p>Kandidat</p><p>Quora-E5-klein</p><p>70,71 s</p><p>58,25 s</p><p>59,38 s</p><p>40,15 s</p><p>wiki-cohere-v2</p><p>203,08 s</p><p>142,27 s</p><p>107,27 s</p><p>85,68 s</p><p>Kern</p><p>110,35 s</p><p>105,52 s</p><p>323,66 s</p><p>202,2 s</p><p>wiki-cohere-v3</p><p>313,43 s</p><p>190,63 s</p><p>165,98 s</p><p>159,95 s</p><p>Bei Indizes mit mehreren Segmenten ist der Kandidat für fast alle Datensätze besser, mit Ausnahme von Cohere v2, wo die Basislinie etwas besser ist. Für die einzelnen Segmentindizes sind die Recall-Kurven in allen Fällen nahezu identisch.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb12174abeadbcdd1/6a17e1822f4a5c4cc2fa8829/7da1be27bab5a7f5f9c9a1882ea35761a4a0ba5c-973x383.png" alt="Rückruf @10 und @100 im Vergleich zur Latenz nach dem Erstellen des Index" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf1b88d4f7fec9905/6a17e184414c64a8de9450b4/a26af30a56c55a12addee7e4fb7e8f606f366668-979x386.png" alt="Erinnern Sie sich an @10 und @100 im Vergleich zur Latenz nach der Zusammenführung zu einem einzigen Segment" /><h3>Fazit</h3><p>Der in diesem Blog besprochene Algorithmus wird im kommenden Lucene 10.2 und in der darauf basierenden Elasticsearch-Version verfügbar sein. Benutzer können in diesen neuen Versionen von der verbesserten Zusammenführungsleistung und der verkürzten Indexerstellungszeit profitieren. Diese Änderung ist Teil unserer kontinuierlichen Bemühungen, Lucene und Elasticsearch für die Vektor- und Hybridsuche schnell und effizient zu machen.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/hnsw-graphs-speed-up-merging</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/hnsw-graphs-speed-up-merging</guid>
    <category><![CDATA[Vektordatenbank]]></category>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Thomas Veasey,Mayya Sharipova]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9fca6a01d0541cfb/6a17e187033c8d46486bb0e1/49a6c880f5dedd0fa502ece5be124824ee218cc0-1792x1024.png" length="0" type="image/png"/>
    <pubDate>Mon, 07 Apr 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Parallelitätsfehler in Lucene: Wie man optimistische Parallelitätsfehler behebt]]></title>
    <description><![CDATA[Dank Fray, einem Framework für deterministische Parallelitätstests aus dem PASTA Lab der Carnegie Mellon University, konnten wir einen kniffligen Lucene-Fehler aufspüren und beheben.]]></description>
    <content:encoded><![CDATA[<p>Ja, noch ein Blogbeitrag zur Fehlerbehebung. Doch diese Geschichte hat eine Wendung: Ein Open-Source-Held eilt herbei und rettet die Situation. </p><p>Das Debuggen von Parallelitätsfehlern ist kein Zuckerschlecken, aber wir werden uns damit befassen. Hier kommt Fray ins Spiel, ein deterministisches Framework für Parallelitätstests aus dem PASTA Lab der CMU, das unzuverlässige Fehler in zuverlässig reproduzierbare Fehler verwandelt. Dank Frays cleverem Shadow-Lock-Design und präziser Gewindesteuerung konnten wir einen kniffligen Fehler in Lucene aufspüren und ihn schließlich beheben. Dieser Beitrag untersucht, wie Open-Source-Helden und -Tools das Debuggen von Parallelverarbeitung weniger schmerzhaft machen – und die Softwarewelt um einiges besser.</p><h2>Parallelitätsfehler: Der Fluch von Softwareentwicklern</h2><p>Parallelitätsfehler sind das Schlimmste. Sie sind nicht nur schwer zu reparieren, sondern es ist schon die größte Herausforderung, sie zuverlässig zum Ausfall zu bringen. Nehmen wir diesen Testfehler, <a href="https://github.com/apache/lucene/issues/13127"><code>TestIDVersionPostingsFormat#testGlobalVersions</code></a>, als Beispiel. Es erzeugt mehrere Threads zum Schreiben und Aktualisieren von Dokumenten und stellt damit Lucenes optimistisches Parallelitätsmodell in Frage. Dieser Test deckte eine Race Condition in der optimistischen Parallelitätskontrolle auf. Das heißt, eine Dokumentenoperation kann fälschlicherweise als die letzte in einer Abfolge von Operationen ausgegeben werden 😱. Das bedeutet, dass unter bestimmten Bedingungen eine Aktualisierungs- oder Löschoperation tatsächlich erfolgreich sein kann, obwohl sie aufgrund optimistischer Parallelitätsbeschränkungen eigentlich hätte fehlschlagen müssen.</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>Entschuldigung an alle, die Java-Stacktraces hassen. Hinweis: Löschen bedeutet nicht unbedingt „löschen“. Es kann auch auf eine Dokumentenaktualisierung hinweisen, da die Segmente von Lucene schreibgeschützt sind.
</p><p>Apache Lucene verwaltet jeden Thread, der Dokumente schreibt, über die Klasse <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java"><code>DocumentsWriter</code></a> . Diese Klasse erstellt oder verwendet Threads zum Schreiben von Dokumenten wieder, wobei jede Schreibaktion ihre Informationen innerhalb der Klasse <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java"><code>DocumentsWriterPerThread</code></a> (DWPT) steuert. Darüber hinaus verfolgt der Autor, welche Dokumente in <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> (DWDQ) gelöscht werden. Diese Strukturen halten alle Dokumentänderungsaktionen im Speicher und werden periodisch geleert, wodurch Speicherressourcen freigegeben und Strukturen auf der Festplatte gespeichert werden.</p><p></p><p>Um <a href="https://en.wikipedia.org/wiki/Blocking_(computing)">blockierende Threads</a> zu vermeiden und einen hohen Durchsatz in parallelen Systemen zu gewährleisten, versucht Apache Lucene, nur in sehr kritischen Abschnitten zu <a href="https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html">synchronisieren</a> . Das kann in der Praxis zwar gut sein, aber wie bei jedem System mit parallelen Prozessen gibt es auch hier Tücken.</p><h2>
Eine falsche Hoffnung</h2><p>Meine ersten Nachforschungen führten mich zu einigen kritischen Abschnitten, die nicht ordnungsgemäß synchronisiert waren. Alle Interaktionen eines gegebenen <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> werden durch das ihn umschließende <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java"><code>DocumentsWriter</code></a> gesteuert. Während also einzelne Methoden in <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> möglicherweise nicht angemessen synchronisiert sind, ist ihr Zugriff auf die Welt (oder sollte es sein) gewährleistet. (Wir wollen uns nicht näher damit befassen, wie dies die Eigentums- und Zugriffsrechte verkompliziert – es handelt sich um ein langjähriges Projekt, an dem viele Mitwirkende beteiligt sind.) Seid etwas nachsichtiger.)</p><p></p><p>Ich habe jedoch <a href="https://github.com/apache/lucene/blob/40060f8b7080d06a218518445a0a1dfc520c812a/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterFlushControl.java#L572-L576">während eines Spülvorgangs eine Stelle</a> gefunden, die nicht synchronisiert war.</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>Diese Aktionen werden nicht zu einer einzigen atomaren Operation synchronisiert. Das heißt, zwischen der Erstellung <code>newQueue</code> und dem Aufruf von <code>getMaxSeqNo</code> könnte anderer Code ausgeführt worden sein, der die Sequenznummer in der Klasse <code>documentsWriter</code> inkrementiert. Ich habe den Fehler gefunden!</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbc55033280b45913/6a1706b4dc55dec27fe00d30/3f2617a102d28735e755a7539b047c28beda41be-1600x453.jpg" alt="" /><p>
Doch wie bei den meisten komplexen Fehlern war die Ursachenfindung nicht einfach. Da trat ein Held auf den Plan.</p><h2>Ein Held im Kampf</h2><p>Auftritt unseres Helden: <a href="https://aoli.al/">Ao Li</a> und seine Kollegen im PASTA-Labor. Ich lasse ihn erklären, wie sie mit Fray die Situation gerettet haben.</p><p><a href="https://github.com/cmu-pasta/fray">Fray</a> ist ein deterministisches Framework für Parallelitätstests, das von Forschern des <a href="https://pastalab.org/">PASTA Lab</a> an der Carnegie Mellon University entwickelt wurde. Die Motivation für die Entwicklung von Fray liegt in einer deutlich erkennbaren Kluft zwischen Wissenschaft und Industrie: Während deterministisches Concurrency-Testing in der akademischen Forschung seit über 20 Jahren intensiv untersucht wird, verlassen sich Praktiker weiterhin auf Stresstests – eine Methode, die allgemein als unzuverlässig und fehleranfällig gilt – um ihre Concurrency-Programme zu testen. Unser Hauptziel war daher die Entwicklung und Implementierung eines deterministischen Frameworks für Parallelitätstests, bei dem Allgemeingültigkeit und praktische Anwendbarkeit im Vordergrund standen.</p><p></p><h2>Die Kernidee</h2><p>Im Kern nutzt Fray ein einfaches, aber wirkungsvolles Prinzip: die sequentielle Ausführung. Das Nebenläufigkeitsmodell von Java bietet eine <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.3">Schlüsseleigenschaft</a> : Wenn ein Programm frei von Datenkonflikten ist, erscheinen alle Ausführungen sequenziell konsistent. Das bedeutet, dass das Verhalten des Programms als eine Folge von Programmanweisungen dargestellt werden kann.</p><p>Fray arbeitet, indem es das Zielprogramm sequenziell ausführt: Bei jedem Schritt werden alle Threads außer einem angehalten, wodurch Fray die Thread-Planung präzise steuern kann. Um Parallelität zu simulieren, werden die Threads zufällig ausgewählt, die Auswahl wird jedoch für die spätere deterministische Wiedergabe aufgezeichnet. Um die Ausführung zu optimieren, führt Fray Kontextwechsel nur dann durch, wenn ein Thread im Begriff ist, eine Synchronisierungsanweisung wie Sperren oder atomaren/flüchtigen Zugriff auszuführen. Eine schöne Eigenschaft der Data-Race-Freiheit ist, dass dieser begrenzte Kontextwechsel ausreicht, um alle beobachtbaren Verhaltensweisen aufgrund von Thread-Interleaving zu untersuchen (<a href="https://arxiv.org/abs/2501.12618">in unserem Paper</a> findet sich eine Beweisskizze).</p><p></p><h2>Die Herausforderung: die Thread-Planung steuern</h2><p>Obwohl die Grundidee einfach erscheint, stellte die Implementierung von Fray erhebliche Herausforderungen dar. Um die Thread-Planung zu steuern, muss Fray die Ausführung jedes Anwendungsthreads verwalten. Auf den ersten Blick mag dies unkompliziert erscheinen – die grundlegenden Mechanismen zur Parallelverarbeitung werden einfach durch maßgeschneiderte Implementierungen ersetzt. Die Steuerung der Parallelverarbeitung in der JVM ist jedoch komplex und beinhaltet eine Mischung aus <a href="https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-6.html">Bytecode-Anweisungen</a>, <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html">High-Level-Bibliotheken</a> und <a href="https://github.com/openjdk/jdk/blob/b720517cb33c2119ec6ed85504bce321de748228/src/java.base/share/classes/java/lang/Object.java#L394">nativen Methoden</a>.</p><p></p><p>Das entpuppte sich als Sackgasse:</p><p></p><ul><li><p>Beispielsweise muss zu jeder <code>MONITORENTER</code> -Anweisung eine entsprechende <code>MONITOREXIT</code> in derselben Methode gehören. Wenn Fray <code>MONITORENTER</code> durch einen Methodenaufruf an einen Stub/Mock ersetzt, muss es auch <code>MONITOREXIT</code> ersetzen.</p></li><li><p>In Code, der <code>object.wait/notify</code> verwendet, muss, wenn <code>MONITORENTER</code> ersetzt wird, auch das entsprechende <code>object.wait</code> ersetzt werden. Diese Ersatzkette erstreckt sich bis <code>object.notify</code> und darüber hinaus.</p></li><li><p>Die JVM ruft bestimmte Methoden im Zusammenhang mit Parallelität auf (z. B. <code>object.notify</code> , wenn ein Thread beendet wird) innerhalb des nativen Codes. Um diese Operationen zu ersetzen, müsste die JVM selbst modifiziert werden.</p></li><li><p>JVM-Funktionen wie Klassenlader und Garbage-Collection-Threads (GC-Threads) nutzen ebenfalls Parallelitätsprimitive. Durch die Modifizierung dieser Grundfunktionen können Inkompatibilitäten mit den entsprechenden JVM-Funktionen entstehen.</p></li><li><p>Das Ersetzen von Parallelitätsprimitiven im JDK führt oft zu JVM-Abstürzen während der Initialisierungsphase.</p></li></ul><p></p><p>Diese Herausforderungen machten deutlich, dass ein umfassender Ersatz der Parallelverarbeitungsprimitive nicht realisierbar war.</p><h2>
Unsere Lösung: Schattenverriegelungsdesign</h2><p>Um diese Herausforderungen zu bewältigen, verwendet Fray einen neuartigen Schattensperrmechanismus, um die Thread-Ausführung zu orchestrieren, ohne die Parallelitätsprimitive zu ersetzen. Schattensperren fungieren als Vermittler, die die Thread-Ausführung steuern. Bevor ein Anwendungsthread beispielsweise eine Sperre erwerben kann, muss er mit der entsprechenden Schatten-Sperre interagieren. Die Schattensperre bestimmt, ob der Thread die Sperre erwerben kann. Kann der Thread nicht fortfahren, blockiert die Schattensperre ihn und ermöglicht anderen Threads die Ausführung, wodurch Deadlocks vermieden und eine kontrollierte Parallelität ermöglicht wird. Dieses Design ermöglicht es Fray, die Thread-Verschachtelung transparent zu steuern und gleichzeitig die Korrektheit der Parallelitätssemantik zu erhalten. Jedes Parallelitätsprimitiv wird innerhalb des Shadow-Lock-Frameworks sorgfältig modelliert, um Korrektheit und Vollständigkeit zu gewährleisten. Weitere technische Details finden Sie in unserem Artikel.</p><p></p><p>Darüber hinaus ist dieses Design zukunftssicher ausgelegt. Da lediglich die Instrumentierung von Schattensperren um Parallelitätsprimitive herum erforderlich ist, wird die Kompatibilität mit neueren JVM-Versionen sichergestellt. Dies ist möglich, da die Schnittstellen der Parallelverarbeitungsprimitive in der JVM relativ stabil sind und seit Jahren unverändert geblieben sind.</p><h2>
Testkampf</h2><p>Nach der Entwicklung von Fray folgte der nächste Schritt: die Evaluierung. Zum Glück beinhalten viele Anwendungen, wie beispielsweise Apache Lucene, bereits Tests zur Parallelverarbeitung. Bei solchen Parallelitätstests handelt es sich um reguläre JUnit-Tests, die mehrere Threads erzeugen, eine bestimmte Arbeit verrichten, dann (in der Regel) warten, bis diese Threads fertig sind, und dann eine bestimmte Eigenschaft überprüfen. In den meisten Fällen bestehen diese Tests, weil sie nur eine einzige Verschachtelung durchführen. Noch schlimmer ist, dass einige Tests in der CI/CD-Umgebung nur gelegentlich fehlschlagen, wie bereits erwähnt, was die Fehlersuche extrem erschwert. Als wir die gleichen Tests mit Fray durchführten, entdeckten wir zahlreiche Fehler. Besonders bemerkenswert ist, dass Fray zuvor gemeldete Fehler wiederentdeckte, die aufgrund des Fehlens einer zuverlässigen Reproduktion ungelöst geblieben waren, einschließlich des Themas dieses Blogs: <a href="https://github.com/apache/lucene/issues/13127"><code>TestIDVersionPostingsFormat.testGlobalVersions</code></a>. Zum Glück können wir mit Fray diese deterministisch wiedergeben und den Entwicklern detaillierte Informationen liefern, sodass sie das Problem zuverlässig reproduzieren und beheben können.</p><p></p><h2>Nächste Schritte für Fray</h2><p>Wir freuen uns sehr über die Rückmeldung der Entwickler von Elastic, dass Fray bei der Fehlersuche in Parallelverarbeitungsproblemen hilfreich war. Wir werden weiterhin an Fray arbeiten, um es mehr Entwicklern zugänglich zu machen.</p><p>Zu unseren kurzfristigen Zielen gehört es, die Fähigkeit von Fray zu verbessern, den Zeitplan deterministisch wiederzugeben, selbst bei Vorhandensein anderer nicht-deterministischer Operationen wie einem Zufallswertgenerator oder der Verwendung von <code>object.hashcode</code>. Wir streben außerdem eine höhere Benutzerfreundlichkeit von Fray an, um Entwicklern die Analyse und das Debuggen bestehender Parallelitätstests ohne manuelle Eingriffe zu ermöglichen. Am wichtigsten ist jedoch, dass wir uns freuen, von Ihnen zu hören, wenn Sie Schwierigkeiten beim Debuggen oder Testen von Parallelitätsproblemen in Ihrem Programm haben. Bitte zögern Sie nicht, ein Problem im <a href="https://github.com/cmu-pasta/fray">Fray GitHub-Repository</a> zu melden.</p><p></p><h2>Es ist an der Zeit, den Parallelitätsfehler zu beheben.</h2><p>Dank Ao Li und dem PASTA-Labor haben wir nun eine zuverlässig fehlschlagende Instanz dieses Tests! Wir können das Problem endlich lösen. Die Kernfrage bestand darin, wie <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThreadPool.java"><code>DocumentsWriterPerThreadPool</code></a> die Wiederverwendung von Threads und Ressourcen ermöglichte.</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>Hier können wir sehen, wie jeder Thread erstellt wird, wobei auf die anfängliche Löschwarteschlange in Generation 0 verwiesen wird.</p><p>Beim Leeren der Warteschlange erfolgt dann ein Fortschrittsschritt, wobei die vorherigen 7 Aktionen in der Warteschlange korrekt angezeigt werden.</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>Bevor jedoch alle Threads vollständig verarbeitet werden können, werden zwei für ein zusätzliches Dokument wiederverwendet:</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>Dadurch wird der Wert <code>seqNo</code> über den angenommenen Maximalwert, der während des Spülvorgangs mit 7 berechnet wurde, erhöht. Beachten Sie die zusätzlichen <code>numDocsInRAM</code> für die Segmente <code>_3</code> und <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>Dadurch wird die Reihenfolge der Dokumentaktionen während eines Flush-Vorgangs von Lucene falsch berücksichtigt, was zu diesem Testfehler führt.</p><p>Wie bei allen guten Bugfixes besteht die eigentliche Korrektur aus etwa <a href="https://github.com/apache/lucene/pull/13627/files">10 Codezeilen</a>. Doch zwei Ingenieure brauchten mehrere Tage, um das herauszufinden:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt71d78f22ce6ec215/6a1706b61949f7bcd0e7a94a/7915e5ba2feba0fb8e846da0af11bfdca470c467-1600x622.jpg" alt="" /><h2>Nicht alle Helden tragen Umhänge.</h2><p>Ja, es ist ein Klischee – aber es stimmt.</p><p></p><p>Das Debuggen von parallelen Programmen ist unglaublich wichtig. Diese kniffligen Parallelitätsfehler erfordern einen unverhältnismäßig hohen Zeitaufwand für die Fehlersuche und -behebung. Während neue Sprachen wie Rust eingebaute Mechanismen zur Vermeidung solcher Race Conditions bieten, ist der Großteil der Software weltweit bereits geschrieben – und zwar in einer anderen Sprache als <a href="https://www.rust-lang.org/">Rust</a>. Auch nach all den Jahren ist Java immer noch eine der meistgenutzten Programmiersprachen. Eine verbesserte Fehlersuche in JVM-basierten Sprachen macht die Welt der Softwareentwicklung besser. Und angesichts der Tatsache, dass manche Leute glauben, Code werde von großen Sprachmodellen geschrieben, werden unsere Aufgaben als Ingenieure vielleicht irgendwann nur noch darin bestehen, schlechten LLM-Code zu debuggen, anstatt nur unseren eigenen schlechten Code. Doch unabhängig von der Zukunft der Softwareentwicklung wird das Debuggen paralleler Programme für die Wartung und Entwicklung von Software weiterhin von entscheidender Bedeutung sein.</p><p></p><p>Vielen Dank an Ao Li und seine Kollegen vom PASTA Lab, die das Ganze noch viel besser gemacht haben.</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[Lucene Wrapped 2024]]></title>
    <description><![CDATA[2024 war ein weiteres wichtiges Jahr für Apache Lucene. In diesem Blogbeitrag gehen wir auf die wichtigsten Punkte ein.]]></description>
    <content:encoded><![CDATA[<p>Apache Lucene hat im Jahr 2024 eine rege Aktivität erfahren, mit zahlreichen Veröffentlichungen, darunter das erste große Update seit drei Jahren, das mit spannenden Verbesserungen und neuen Funktionen aufwartet. Lassen Sie uns einige der wichtigsten Highlights näher betrachten.</p><h2>Lucene und die Gemeinschaft</h2><p>Ein Projekt ist nur so stark wie die Gemeinschaft, die es unterstützt. Trotz seiner mehr als 20-jährigen Entwicklungsgeschichte ist das Lucene-Projekt nach wie vor lebendig und floriert dank seiner leidenschaftlichen und aktiven Mitwirkenden.</p><p>Im Jahr 2024 verzeichnete das Lucene-Projekt mehr als 2.000 Commits von 98 verschiedenen Mitwirkenden und fast 800 Pull Requests. Die Zahl der Mitwirkenden wächst stetig, neue Committer und PMC-Mitglieder schließen sich dem Projekt an und tragen zu seinem Erfolg bei.</p><h2>Lucene 10</h2><p>Im Jahr 2024 erschien die erste größere Version seit fast 3 Jahren – Lucene 10 – mit mehr als 2.000 Commits von 185 verschiedenen Mitwirkenden. Während das Entwicklungsmodell von Lucene es ermöglicht, viele Verbesserungen und Funktionen in kleineren Releases bereitzustellen, bietet ein Major-Release die Möglichkeit, größere Funktionen und Modernisierungen einzuführen. Beispielsweise benötigt Lucene 10 mindestens Java 21. Durch die Erhöhung der minimalen Java-Version wird sichergestellt, dass Lucene weiterhin von den Verbesserungen moderner Java-Versionen profitieren kann.</p><p>Der Hauptfokus von Lucene 10 liegt auf der besseren Ausnutzung der Hardware, auf der es läuft. Werfen wir einen kurzen Blick auf einige der wichtigsten Punkte:</p><ul><li><p><strong>Mehr Suchparallelität</strong> – während die Suchausführung bereits segmentübergreifend parallelisiert ist, gehen wir jetzt noch einen Schritt weiter und parallelisieren innerhalb der Segmente. Dadurch wird die Darstellung auf der Festplatte von der Ausführungsleistung entkoppelt, sodass selbst einzelne Segmente von der Anzahl der Kerne auf modernen Systemen profitieren können.</p></li><li><p><strong>Verbesserte I/O-Parallelität</strong> – das einfache synchrone I/O-Modell, das Lucene verwendet, wurde um eine Vorabrufphase erweitert. Dadurch wird dem Betriebssystem mitgeteilt, dass ein Bereich einer Indexdatei in naher Zukunft benötigt wird, ohne den aufrufenden Thread zu blockieren.</p></li><li><p><strong>Bessere CPU- und Speichereffizienz durch Sparse-Indexierung</strong> – Lucene 10 führt die Unterstützung für Sparse-Indexierung ein, die in anderen Datenspeichern auch als Primärschlüssel-Indexierung oder Zonenindexierung bezeichnet wird.</p></li></ul><p>Weitere Informationen zu Lucene 10 finden Sie im entsprechenden <a href="https://www.elastic.co/search-labs/blog/apache-lucene-10-release-highlights">Artikel</a> zu Lucene 10.</p><h2>Lucene Forschung und Innovation</h2><p>Im Jahr 2024 erlebte Lucene einen Aufschwung in Forschung und Innovation, insbesondere in den Bereichen maschinelles Lernen Integration, Vektorsuche und Optimierung für große Datensätze, mit Referenzen aus 10 separaten <a href="https://scholar.google.com/scholar?as_ylo=2024&amp;q=lucene&amp;hl=en&amp;as_sdt=0,5">Forschungsarbeiten und Veröffentlichungen</a>. Zu den wichtigsten Forschungsbereichen und Entwicklungen gehören:</p><ul><li><p><strong>Vektorsuche und Einbettungsunterstützung</strong> – Lucene bietet eine leistungsstarke und skalierbare Lösung für die vektorbasierte Suche und ermöglicht so die semantische Suche in großem Umfang. Durch die Nutzung der robusten Indexierungs- und Suchinfrastruktur von Lucene können Anwender die Vorteile der traditionellen Textsuche mit den fortschrittlichen Möglichkeiten der modernen Vektorsuche kombinieren. Dadurch wird Lucene zu einer umfassenden Lösung für eine breite Palette von Such- und Informationsabrufaufgaben.</p></li><li><p><strong>Hybride Suchmodelle</strong> – Die Forschung hat sich auch mit hybriden Suchtechniken befasst, bei denen Lucene die traditionelle schlüsselwortbasierte Suche mit der modernen vektorbasierten Suche kombiniert. Durch die Verknüpfung von termbasierten Indizes mit dichten Vektordarstellungen kann Lucene genauere und kontextbezogenere Suchergebnisse liefern und so die Lücke zwischen der Präzision traditioneller Suchmaschinen und der Flexibilität der semantischen Suche schließen.</p></li></ul><p>Die laufenden Forschungsarbeiten im Jahr 2024 demonstrieren die Anpassungsfähigkeit von Lucene an die sich wandelnden Bedürfnisse moderner Suchtechnologien, insbesondere im Kontext von KI, semantischer Suche und Big-Data-Anwendungen. Das Projekt entwickelt sich stetig weiter und ist zu einer leistungsstarken, flexiblen und effizienten Plattform für sowohl traditionelle als auch innovative Suchanwendungen geworden.</p><h2>Lucene-Veröffentlichungen 2024</h2><p>Auch wenn es kein exaktes Abbild darstellt, unterstreicht die schiere Anzahl der Veröffentlichungen doch das anhaltende Engagement und die Energie der Community. Diese Aktualisierungen beinhalten wesentliche Verbesserungen der Vektorsuchleistung und -effizienz, Unterstützung für madvise, Optimierungen für die Dekodierung von Postinglisten, weitere Geschwindigkeitsverbesserungen durch SIMD und vieles mehr.</p><p>Hier ist die vollständige Liste der Veröffentlichungen:</p><ul><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-1010-available">10.1.0</a> (2024-12-20)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9121-available">9.12.1</a> (13.12.2024)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-1000-available">10.0.0</a> (14.10.2024)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9120-available">9.12.0</a> (2024-09-28)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-8114-available">8.11.4</a> (2024-09-24)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9111-available">9.11.1</a> (2024-06-27)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9110-available">9.11.0</a> (2024-06-06)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9100-available">9.10.0</a> (2024-02-20)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-8113-available">8.11.3</a> (2024-02-08)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-992-available">9.9.2</a> (2024-01-29)</p></li></ul><p>Weitere Informationen und Versionshinweise finden Sie auf der <a href="https://projects.apache.org/project.html?lucene-core">Lucene Core-</a> Seite. Zusätzlich gibt es entsprechende <a href="https://projects.apache.org/project.html?lucene-pylucene">PyLucene-</a> Versionen.</p><h2>Zusammenfassung</h2><p>Mit zunehmender Reife floriert Lucene dank seiner engagierten und dynamischen Community weiterhin. Wie wir gesehen haben, war 2024 ein unglaublich produktives Jahr, und wir freuen uns nun auf die spannenden Entwicklungen, die 2025 bringen wird.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/apache-lucene-wrapped-2024</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/apache-lucene-wrapped-2024</guid>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Chris Hegarty]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt901211870015335c/6a17ddf6a292998b08d02b93/29a02c89b3c5adb37a5f900de634ff09cb63fdd9-1792x1024.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 03 Jan 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Lucene-Bug-Abenteuer: Behebung einer beschädigten Index-Ausnahme]]></title>
    <description><![CDATA[Manchmal dauert es Tage, eine einzige Zeile Code zu schreiben. Hier erhalten wir einen Einblick in die Mühen und die mehrtägige Fehlersuche eines Ingenieurs, der versucht, eine mögliche Beschädigung des Apache Lucene-Index zu beheben.]]></description>
    <content:encoded><![CDATA[<h2>Sei vorbereitet: </h2><p>Dieser Blog ist anders als üblich. Es handelt sich weder um die Erklärung einer neuen Funktion noch um ein Tutorial. Es geht hier um eine einzige Codezeile, deren Schreiben drei Tage gedauert hat. Wir werden eine mögliche Beschädigung des Apache Lucene-Index beheben. Einige Erkenntnisse, die Sie hoffentlich mitnehmen können:</p><ul><li><p>Alle unzuverlässigen Tests sind bei ausreichend Zeit und den richtigen Werkzeugen wiederholbar.</p></li><li><p>Für robuste Systeme sind viele Testebenen unerlässlich. Höhere Testebenen werden jedoch zunehmend schwieriger zu debuggen und zu reproduzieren.</p></li><li><p>Sleep ist ein ausgezeichneter Debugger.</p></li></ul><h2>Wie Elasticsearch testet</h2><p>Bei Elastic verfügen wir über eine Vielzahl von Tests, die gegen die Elasticsearch-Codebasis ausgeführt werden. Manche sind einfache und fokussierte Funktionstests, andere sind Integrationstests für einen einzelnen Knoten im „Happy Path“-Modus, und wieder andere versuchen, den Cluster zum Absturz zu bringen, um sicherzustellen, dass sich in einem Fehlerszenario alles korrekt verhält. Wenn ein Test wiederholt fehlschlägt, erstellt ein Ingenieur oder die Tool-Automatisierung ein GitHub-Issue und kennzeichnet es zur Untersuchung durch ein bestimmtes Team. Dieser <a href="https://github.com/elastic/elasticsearch/issues/105122">spezielle Fehler</a> wurde bei einem Test der letztgenannten Art entdeckt. Diese Tests sind knifflig und manchmal erst nach vielen Durchläufen wiederholbar.</p><h2>Was genau wird mit diesem Test geprüft?</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt58aab468587a7d35/6a17dd8e3e03d74b8d4f2b5f/63268f3b5714ebf9df8070ec2e3c4f0486ed822e-1600x1088.jpg" alt="GitHub-Problem: https://github.com/elastic/elasticsearch/issues/105122" /><p>Dieser Test ist besonders interessant. Es wird eine bestimmte Zuordnung erstellen und diese auf einen primären Shard anwenden. Dann beim Versuch, eine Replik zu erstellen. Der entscheidende Unterschied besteht darin, dass beim Versuch der Replik, das Dokument zu parsen, der Test eine Ausnahme auslöst, was dazu führt, dass die Wiederherstellung auf eine überraschende (aber erwartete) Weise fehlschlägt.</p><p></p><p>Alles funktionierte wie erwartet, allerdings mit einem entscheidenden Haken. Bei der Bereinigung der Testergebnisse haben wir die Konsistenz überprüft, und dabei ist dieser Test auf ein Problem gestoßen.</p><p>
Dieser Test schlug erwartungsgemäß fehl. Im Rahmen der Konsistenzprüfung würden wir überprüfen, ob alle replizierten und primären Lucene-Segmentdateien konsistent sind. Das heißt, unverfälscht und vollständig repliziert. Teilweise oder beschädigte Daten sind viel schlimmer als ein vollständiger Ausfall. Hier ist der beängstigende und abgekürzte Stacktrace des Fehlers.</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>Irgendwie wurde der replizierte Shard während des erzwungenen Replikationsfehlers beschädigt! Ich möchte Ihnen den Kern des Fehlers in einfachen Worten erklären.</p><p></p><p>Lucene ist eine segmentbasierte Architektur, das heißt, jedes Segment kennt und verwaltet seine eigenen schreibgeschützten Dateien. Dieses spezielle Segment wurde über seine <a href="https://github.com/apache/lucene/blob/add9c09c84ee66d4522c566c9f679035a0dfec13/lucene/core/src/java/org/apache/lucene/index/SegmentCoreReaders.java">SegmentCoreReaders</a> validiert, um sicherzustellen, dass alles in Ordnung war. Jeder Core-Reader speichert Metadaten, die angeben, welche Feldtypen und Dateien für ein bestimmtes Segment vorhanden sind. Bei der Validierung des <a href="https://github.com/apache/lucene/blob/add9c09c84ee66d4522c566c9f679035a0dfec13/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90PointsFormat.java">Lucene90PointsFormat</a> fehlten jedoch bestimmte erwartete Dateien. Mit der Segmentdatei <code>_0.cfs</code> erwarteten wir eine Punktformatdatei namens <code>kdi</code>. <code>cfs</code> steht für "zusammengesetztes Dateisystem", in dem Lucene manchmal alle Feldtypen und alle kleinen Dateien zu einer einzigen größeren Datei kombiniert, um eine effizientere Replikation und Ressourcennutzung zu ermöglichen. Tatsächlich fehlten alle drei Punktdateierweiterungen: <code>kdd</code>, <code>kdi</code>, und <code>kdm</code> . Wie konnten wir an eine Stelle gelangen, an der ein Lucene-Segment eine Punktdatei erwartet, diese aber fehlt?! Das sieht nach einem schwerwiegenden Datenkorruptionsfehler aus!</p><p></p><h2>Der erste Schritt bei jeder Fehlerbehebung: den Fehler reproduzieren.</h2><p></p><p>Es war äußerst mühsam, den Fehler bei diesem speziellen Bug zu reproduzieren. Obwohl wir in Elasticsearch die Möglichkeit des <a href="https://en.wikipedia.org/wiki/Random_testing">randomisierten Wertetests</a> nutzen, stellen wir sicher, dass jeder Fehler mit einem (hoffentlich) reproduzierbaren Zufallswert versehen wird, um zu gewährleisten, dass alle Fehler untersucht werden können. Das funktioniert hervorragend für alle Fehler, außer für solche, die durch eine <a href="https://en.wikipedia.org/wiki/Race_condition">Race Condition</a> verursacht werden.</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>Egal wie oft ich es versucht habe, der Fehler konnte mit diesem speziellen Seed lokal nie reproduziert werden. Es gibt jedoch Möglichkeiten, die Tests durchzuführen und ein reproduzierbareres Scheitern zu erzielen.</p><p></p><p>Unsere spezielle Testsuite ermöglicht es, einen bestimmten Test über den Parameter <code>-Dtests.iters</code> im selben Befehl mehr als einmal auszuführen. Das reichte mir aber nicht, ich musste sicherstellen, dass die Ausführungsthreads wechselten und dadurch die Wahrscheinlichkeit des Auftretens dieser Race Condition erhöht wurde. Ein weiteres Problem im System war, dass der Test am Ende so lange dauerte, dass der Test-Runner eine Zeitüberschreitung meldete. Am Ende verwendete ich den folgenden albtraumhaften Bash-Code, um den Test reproduzierbar auszuführen:</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>Und da kommt <a href="https://github.com/ColinIanKing/stress-ng">Stress ins Spiel</a>. Dies ermöglicht es Ihnen, schnell einen Prozess zu starten, der die CPU-Kerne im Handumdrehen belegt. Durch wiederholtes, wahlloses Ausführen von stress-ng während zahlreicher Iterationen des fehlgeschlagenen Tests konnte ich den Fehler schließlich reproduzieren. Ein Schritt näher. Um das System zu belasten, öffnen Sie einfach ein weiteres Terminalfenster und führen Sie folgenden Befehl aus:</p>stress-ng --cpu 16<h2>
Aufdeckung des Fehlers</h2><p>

Da der Testfehler, der den Bug aufdeckt, nun größtenteils reproduzierbar ist, ist es an der Zeit, die Ursache zu finden. Das Merkwürdige an diesem speziellen Test ist, dass Lucene eine Ausnahme auslöst, weil es Punktwerte erwartet, aber vom Test direkt keine hinzugefügt werden. Nur Textwerte. Dies veranlasste mich, die jüngsten Änderungen an unseren <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/optimistic-concurrency-control.html">optimistischen Parallelitätskontrollfeldern</a> <code>_seq_no</code> und <code>_primary_term</code> genauer zu betrachten. Beide werden als Punkte indexiert und sind in jedem Elasticsearch-Dokument vorhanden.</p><p></p><p>Tatsächlich hat ein <a href="https://github.com/elastic/elasticsearch/pull/105036">Commit</a> unseren <code>_seq_no</code> Mapper verändert! JA! Das muss die Ursache sein! Doch meine Begeisterung war nur von kurzer Dauer. Dies änderte lediglich die Reihenfolge, in der Felder zum Dokument hinzugefügt wurden. Vor dieser Änderung wurden <code>_seq_no</code> Felder zuletzt zum Dokument hinzugefügt. Anschließend wurden sie zuerst hinzugefügt. Die Reihenfolge beim Hinzufügen von Feldern zu einem Lucene-Dokument kann unmöglich diesen Fehler verursachen...</p><p></p><p>Ja, die Änderung der Reihenfolge beim Hinzufügen der Felder hat den Fehler verursacht. Das war überraschend und entpuppte sich als Fehler in Lucene selbst! Die Änderung der Reihenfolge, in der die Felder analysiert werden, sollte das Verhalten beim Parsen eines Dokuments nicht verändern.</p><p></p><h2>Der Fehler in Lucene</h2><p>Der Fehler in Lucene bezog sich tatsächlich auf folgende Bedingungen:</p><ul><li><p>Indizieren eines Punktewertfeldes (z. B. <code>_seq_no</code>)</p></li><li><p>Beim Versuch, ein Textfeld zu indizieren, trat während der Analyse ein Fehler auf.</p></li><li><p>In diesem ungewöhnlichen Zustand öffnen wir einen <a href="https://blog.mikemccandless.com/2011/06/lucenes-near-real-time-search-is-fast.html">Near Real Time Reader</a> des Autors, der die Textindexanalyse-Ausnahme erlebt.</p></li></ul><p>Aber egal, wie viele Wege ich auch versucht habe, ich konnte es nicht vollständig nachbilden. Ich habe direkt Haltepunkte zum Debuggen im gesamten Lucene-Quellcode eingefügt. Ich habe versucht, während des Ausnahmepfads zufällig Reader zu öffnen. Ich habe sogar Megabytes über Megabytes an Protokolldateien ausgedruckt, um den genauen Pfad zu finden, an dem dieser Fehler aufgetreten ist. Ich konnte es einfach nicht. Ich habe einen ganzen Tag lang gekämpft und verloren.</p><p></p><p>Dann habe ich geschlafen.</p><p></p><p>Am nächsten Tag las ich den ursprünglichen Stacktrace erneut und entdeckte die folgende Zeile:</p><p>
</p>    at org.apache.lucene.index.SoftDeletesRetentionMergePolicy.keepFullyDeletedSegment(SoftDeletesRetentionMergePolicy.java:82)<p>Bei all meinen Nachbildungsversuchen habe ich die Aufbewahrungs- und Zusammenführungsrichtlinie nie explizit festgelegt. Die <a href="https://github.com/apache/lucene/blob/5f0fa2b291ff9e7d878642f025a70c15b788a470/lucene/core/src/java/org/apache/lucene/index/SoftDeletesRetentionMergePolicy.java">SoftDeletesRetentionMergePolicy</a> wird von Elasticsearch verwendet, um Löschungen in Replikaten genau zu replizieren und sicherzustellen, dass alle unsere Parallelitätskontrollen dafür zuständig sind, wann Dokumente tatsächlich entfernt werden. Andernfalls hat Lucene die volle Kontrolle und entfernt sie bei jedem Merge.</p><p></p><p>Nachdem ich diese Richtlinie hinzugefügt und die oben genannten grundlegendsten Schritte wiederholt hatte, trat der Fehler sofort wieder auf.</p><p>
Ich habe mich noch nie so sehr gefreut, einen <a href="https://github.com/apache/lucene/issues/13353">Fehler in Lucene</a> zu melden.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt89d37c3454ec298b/6a17dd90ec0f8993c75a6511/c7d9ce3de5278923a8454097e2bdf487c861168a-1600x920.jpg" alt="GitHub-Problem https://github.com/apache/lucene/issues/13353" /><p>
Während sich dies in Elasticsearch als Race Condition darstellte, war es einfach, in Lucene einen wiederholt fehlschlagenden Test zu schreiben, sobald alle Bedingungen erfüllt waren.</p><p></p><p>Am Ende wurde es, wie alle guten Bugs, mit nur einer einzigen Codezeile behoben. Mehrere Tage Arbeit für nur eine einzige Codezeile.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdab62db3402b27f7/6a17dd92dbb4ff23e7fb55ad/2c10a02eb41da181b1dceee47186c076c9b14a90-1600x539.jpg" alt="Eine Zeile Code behebt das Problem" /><p>Aber es hat sich gelohnt.</p><h2>
Nicht das Ende</h2><p>Ich hoffe, ihr hattet Spaß an dieser turbulenten Reise mit mir! Software zu entwickeln, insbesondere so weit verbreitete und komplexe Software wie Elasticsearch und Apache Lucene, ist sehr lohnend. Manchmal ist es jedoch außerordentlich frustrierend. Ich liebe und hasse Software gleichermaßen. Die Fehlerbehebung hört nie auf!</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[Elasticsearch vs. OpenSearch: Vergleich der Leistung bei der Vektorsuche]]></title>
    <description><![CDATA[Elasticsearch ist standardmäßig bei der Vektorsuche 2 bis 12 Mal schneller als OpenSearch]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-opensearch-vector-search-performance-comparison#up-to-12x-faster-out-of-the-box">TLDR: Elasticsearch ist bis zu 12 Mal schneller</a> – Wir bei Elastic haben zahlreiche Anfragen aus unserer Community erhalten, um die Leistungsunterschiede zwischen Elasticsearch und OpenSearch zu klären, insbesondere im Realm der semantischen Suche/Vektorsuche. Daher haben wir diesen Leistungstest durchgeführt, um einen klaren, datengesteuerten Vergleich bereitzustellen – keine Mehrdeutigkeiten, nur einfache Fakten zur Information unserer Nutzer. Die Ergebnisse zeigen, dass <strong>Elasticsearch bei der Vektorsuche bis zu 12 Mal schneller ist</strong> als OpenSearch und daher weniger Rechenressourcen benötigt. Dies spiegelt das Bestreben von Elastic wider, Lucene als beste Vektordatenbank für Such- und Abruffälle zu konsolidieren.</p><p>Die Vektorsuche revolutioniert die Art und Weise, wie wir Ähnlichkeitssuchen durchführen, insbesondere in Bereichen wie KI und Machine Learning. Mit der zunehmenden Verbreitung von Vektoreinbettungsmodellen wird die Fähigkeit, Millionen von hochdimensionalen Vektoren effizient zu durchsuchen, immer wichtiger.</p><p>Bei der Energieversorgung von Vektordatenbanken haben Elastic und OpenSearch bemerkenswert unterschiedliche Ansätze gewählt. Elastic investierte stark in die Optimierung von Apache Lucene mit Elasticsearch, um sie zur erstklassigen Wahl für Vektorsuchanwendungen zu machen. Im Gegensatz dazu erweiterte OpenSearch seinen Fokus, indem es andere Implementierungen der Vektorsuche integriert und über den Rahmen von Lucene hinausgeht. Unser Fokus auf Lucene ist strategisch und ermöglicht es uns, in unserer Version von Elasticsearch hochintegrierten Support bereitzustellen, was zu einem erweiterten Funktionsumfang führt, bei dem jede Komponente die Fähigkeiten der anderen ergänzt und erweitert.</p><p>Dieser Blog bietet einen detaillierten Vergleich zwischen Elasticsearch 8.14 und OpenSearch 2.14, wobei verschiedene Konfigurationen und Vektor-Engines berücksichtigt werden. In dieser Leistungsanalyse hat sich Elasticsearch als die überlegene Plattform für Vektorsuchen erwiesen, und kommende <a href="https://www.elastic.co/search-labs/blog/vector-similarity-computations-ludicrous-speed">Features</a> werden die Unterschiede noch <a href="https://www.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains">deutlicher</a> machen. Als es gegen OpenSearch antrat, schnitt es in jedem Benchmark-Track hervorragend ab und <strong>bot im Durchschnitt eine 2 bis 12 Mal schnellere Leistung</strong>. Dies geschah in Szenarien mit unterschiedlichen Vektormengen und Dimensionen, einschließlich <code>so_vector</code> (2 Mio. Vektoren, 768D), <code>openai_vector</code> (2,5 Mio. Vektoren, 1536D) und <code>dense_vector</code> (10 Mio. Vektoren, 96D), die alle in <a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance">diesem Repository</a> neben den Terraform-Skripten zum Bereitstellen der gesamten erforderlichen Infrastruktur in Google Cloud und Kubernetes-Manifesten für die Ausführung der Tests verfügbar sind.</p><p>Die in diesem Blog beschriebenen Ergebnisse ergänzen die Ergebnisse einer <a href="https://www.elastic.co/blog/elasticsearch-opensearch-performance-gap">zuvor veröffentlichten und von Dritten validierten Studie</a>, die zeigt, dass Elasticsearch bei den gängigsten Suchanalyseoperationen um 40%–140% schneller ist als OpenSearch: bei Textabfrage, Sortierung, Bereich, Datumshistogramm und Begriffsfilterung. Jetzt können wir ein weiteres Unterscheidungsmerkmal hinzufügen: die Vektorsuche.</p><h2>Bis zu 12 Mal schneller einsatzbereit</h2><p>Unsere gezielten Benchmarks über die vier Vektordatensätze hinweg umfassten sowohl approximative kNN- als auch exakte kNN-Suchen, wobei unterschiedliche Größen, Dimensionen und Konfigurationen berücksichtigt wurden, was insgesamt <code>40.189.820</code> nicht zwischengespeicherte Suchanfragen ergab. Das Ergebnis: <strong>Elasticsearch ist bei der Vektorsuche bis zu 12 Mal schneller</strong> als OpenSearch und benötigt daher weniger Rechenressourcen.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt34b83c6eba3bcb6e/6a17d727dbb4ff18d6fb54fb/cdb26e91f085b90e9b12aeb8fee53b04d365ecae-1440x1156.webp" alt="p90-Durchschnitt" /><p>Abbildung 1: Gruppierte Aufgaben für ANN und exaktes kNN in verschiedenen Kombinationen in Elasticsearch und OpenSearch.</p><p>Die Gruppen wie <code>knn-10-100</code> bezeichnen eine kNN-Suche mit  und . Bei der HNSW-Vektorsuche bestimmt  die Anzahl der nächsten Nachbarn, die für einen Abfragevektor abgerufen werden sollen. Es wird angegeben, wie viele ähnliche Vektoren als Ergebnis gefunden werden sollen.  legt die Anzahl der Kandidatenvektoren fest, die in jedem Segment abgerufen werden sollen. Mehr Kandidaten können die Genauigkeit erhöhen, erfordern jedoch mehr Rechenressourcen.</p><p>Wir haben auch mit verschiedenen Quantisierungstechniken getestet und Engine-spezifische Optimierungen genutzt. Die detaillierten Ergebnisse für jeden Track und jede Aufgabe und Vektor-Engine sind unten verfügbar.</p><h2>Exaktes kNN und approximatives kNN</h2><p>Beim Umgang mit unterschiedlichen Daten und Anwendungsfällen variiert der richtige Ansatz für die Vektorsuche. In diesem Blog verwenden alle als <code>knn-*</code> angegebenen Aufgaben wie <code>knn-10-100</code> <strong>approximatives kNN</strong> und <code>script-score-*</code> beziehen sich auf <strong>exaktes kNN</strong>, aber was ist der Unterschied zwischen ihnen und warum ist das wichtig?</p><p>Im Wesentlichen ist bei der Verarbeitung umfangreicherer Datensätze die Methode des Approximativen K-Nearest-Neighbor (ANN) aufgrund ihrer überlegenen Skalierbarkeit die bevorzugte Methode. Für bescheidenere Datensätze, die möglicherweise einen Filterprozess erfordern, ist die exakte kNN-Methode ideal.</p><p>Exaktes kNN verwendet eine Brute-Force-Methode, die den Abstand zwischen einem Vektor und jedem anderen Vektor im Datensatz berechnet. Anschließend werden diese Abstände in eine Rangfolge gebracht, um die  nächsten Nachbarn zu finden. Diese Methode gewährleistet zwar eine exakte Übereinstimmung, leidet aber bei großen, hochdimensionalen Datensätzen unter Problemen der Skalierbarkeit. Es gibt jedoch viele Fälle, in denen exaktes kNN benötigt wird:</p><ul><li><p><strong>Neubewertung</strong>: In Szenarien mit lexikalischen oder semantischen Suchen, gefolgt von einer vektorbasierten Neubewertung, ist exaktes kNN unerlässlich. In einer Produktsuchmaschine können erste Suchergebnisse auf der Grundlage von Textabfragen (z. B. Schlüsselwörtern, Kategorien) gefiltert werden. Anschließend werden Vektoren, die mit den gefilterten Elementen verknüpft sind, für eine genauere Ähnlichkeitsbewertung verwendet.</p></li><li><p><strong>Personalisierung</strong>: Bei einer großen Anzahl von Nutzern, die jeweils durch eine relativ kleine Anzahl (z. B. 1 Million) unterschiedlicher Vektoren repräsentiert werden, wird die Sortierung des Indexes nach nutzerspezifischen Metadaten (z. B. user_id) und die Brute-Force-Bewertung mit Vektoren effizient. Dieser Ansatz ermöglicht personalisierte Empfehlungen oder die Bereitstellung von Inhalten basierend auf präzisen Vektorvergleichen, die auf die individuellen Nutzerpräferenzen zugeschnitten sind.</p></li></ul><p>Exaktes kNN stellt sicher, dass das endgültige Ranking und die Empfehlungen basierend auf der Vektorähnlichkeit präzise und auf die Nutzerpräferenzen zugeschnitten sind.</p><p>Approximatives kNN (oder ANN) verwendet hingegen Methoden, um die Datensuche schneller und effizienter als exaktes kNN zu gestalten, insbesondere in großen, hochdimensionalen Datensätzen. Anstelle eines Brute-Force-Ansatzes, der den genauen kürzesten Abstand zwischen einer Abfrage und allen Punkten misst, was zu Berechnungs- und Skalierungsproblemen führt, verwendet ANN bestimmte Techniken, um die Indizes und Dimensionen durchsuchbarer Vektoren im Datensatz effizient neu zu strukturieren. Dies kann zwar zu einer leichten Ungenauigkeit führen, erhöht jedoch die Geschwindigkeit des Suchvorgangs erheblich und macht ihn zu einer effektiven Alternative für den Umgang mit großen Datensätzen.</p><p>In diesem Blog verwenden alle als <code>knn-*</code> bezeichneten Aufgaben wie <code>knn-10-100</code> <strong>approximatives kNN</strong> und <code>script-score-*</code> beziehen sich auf <strong>exaktes kNN</strong>.</p><h2>Testmethodik</h2><p>Während Elasticsearch und OpenSearch hinsichtlich der API für BM25-Suchvorgänge ähnlich sind, da letzteres ein Fork von ersterem ist, trifft dies auf die Vektorsuche, die nach dem Fork eingeführt wurde, nicht zu. OpenSearch verfolgte einen anderen Ansatz als Elasticsearch in Bezug auf Algorithmen, indem es neben <code>lucene</code> zwei weitere Engines – <code>nmslib</code> und <code>faiss</code> – einführte, die jeweils über spezifische Konfigurationen und Einschränkungen verfügen (z. B. erlaubt <code>nmslib</code> in OpenSearch keine Filter, ein wesentliches Feature für viele Anwendungsfälle).</p><p>Alle drei Engines verwenden den Hierarchical Navigable Small World (HNSW)-Algorithmus, der effizient für die approximative Suche nach dem nächsten Nachbarn und besonders leistungsfähig beim Umgang mit hochdimensionalen Daten ist. Es ist wichtig zu beachten, dass <code>faiss</code> auch einen zweiten Algorithmus, <code>ivf</code>, unterstützt, aber da dafür ein Vortraining am Datensatz erforderlich ist, werden wir uns ausschließlich auf HNSW konzentrieren. Die Kernidee von HNSW besteht darin, die Daten in mehreren Schichten verbundener Graphen zu organisieren, wobei jede Schicht eine andere Granularität des Datensatzes darstellt. Das Suchen beginnt auf der obersten Ebene mit der gröbsten Ansicht und schreitet zu immer feineren Schichten fort, bis es die unterste Ebene erreicht.</p><p>Beide Suchmaschinen wurden unter identischen Bedingungen in einer kontrollierten Umgebung getestet, um faire Testbedingungen zu gewährleisten. Die angewandte Methode ähnelt <a href="https://www.elastic.co/blog/elasticsearch-opensearch-performance-gap#testing-methodology">diesem zuvor veröffentlichten Leistungsvergleich</a>, mit dedizierten Node-Pools für Elasticsearch, OpenSearch und Rally. Das <a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/blob/main/terraform/main.tf">Terraform-Skript</a> ist (neben allen Quellen) verfügbar, um einen Kubernetes-Cluster mit Folgendem bereitzustellen:</p><ul><li><p>1 Node-Pool für Elasticsearch mit 3 <code>e2-standard-32</code>-Maschinen (128 GB RAM und 32 CPUs)</p></li><li><p>1 Node-Pool für OpenSearch mit 3 <code>e2-standard-32</code>-Maschinen (128 GB RAM und 32 CPUs)</p></li><li><p>1 Node-Pool für Rally mit 2 <code>t2a-standard-16</code> Maschinen (64 GB RAM und 16 CPUs)</p></li></ul><p>Jeder „Track“ (oder Test) wurde 10 Mal für jede Konfiguration ausgeführt, die unterschiedliche Engines, Konfigurationen und Vektortypen umfasste. Die Tracks haben Aufgaben, die sich je nach Track zwischen 1.000 und 10.000 Mal wiederholen. Wenn eine der Aufgaben in einem Track z. B. aufgrund einer Netzwerk-Zeitüberschreitung fehlschlug, wurden alle Aufgaben verworfen, sodass alle Ergebnisse Tracks darstellen, die ohne Probleme gestartet und beendet wurden. Alle Testergebnisse sind statistisch validiert, um sicherzustellen, dass Verbesserungen nicht zufällig sind.</p><h2>Detaillierte Ergebnisse</h2><p>Warum das 99. Perzentil und nicht die durchschnittliche Latenz vergleichen? Betrachten Sie ein hypothetisches Beispiel für die durchschnittlichen Hauspreise in einem bestimmten Viertel. Der Durchschnittspreis mag auf eine teure Gegend hindeuten, aber bei näherer Betrachtung stellt sich womöglich heraus, dass die meisten Häuser viel niedriger bewertet sind und nur einige wenige Luxusimmobilien den Durchschnittswert in die Höhe treiben. Dies verdeutlicht, dass der Durchschnittspreis das gesamte Spektrum der Hauswerte in der Gegend nicht unbedingt genau wiedergibt. Man kann das mit der Untersuchung von Reaktionszeiten vergleichen, bei denen der Durchschnittswert möglicherweise kritische Probleme verbirgt.</p><h4>Aufgaben</h4><ul><li><p>Approximatives kNN mit k:10, n:50</p></li><li><p>Approximatives kNN mit k:10, n:100</p></li><li><p>Approximatives kNN mit k:100, n:1000</p></li><li><p>Approximatives kNN mit k:10, n:50 und Keyword-Filtern</p></li><li><p>Approximatives kNN mit k:10, n:100 und Keyword-Filtern</p></li><li><p>Approximatives kNN mit k:100, n:1000 und Keyword-Filtern</p></li><li><p>Approximatives kNN mit k:10, n:100 in Verbindung mit Indizierung</p></li><li><p>Exaktes kNN (Skriptergebnis)</p></li></ul><h4>Vektor-Engines</h4><ul><li><p><code>lucene</code> in Elasticsearch und OpenSearch, beide in Version 9.10</p></li><li><p><code>faiss</code> in OpenSearch</p></li><li><p><code>nmslib</code> in OpenSearch</p></li></ul><h4>Vektortypen</h4><ul><li><p><code>hnsw</code> in Elasticsearch und OpenSearch</p></li><li><p><code>int8_hnsw</code> in Elasticsearch (HNSW mit automatischer 8-Bit-Quantisierung: <a href="https://www.elastic.co/search-labs/blog/evaluating-scalar-quantization">Link</a>)</p></li><li><p><code>sq_fp16 hnsw </code>in OpenSearch (HNSW mit automatischer 16-Bit-Quantisierung: <a href="https://opensearch.org/docs/2.14/search-plugins/knn/knn-vector-quantization#faiss-16-bit-scalar-quantization">Link</a>)</p></li></ul><h4>Vorkonfigurierte und gleichzeitige Segmentsuche</h4><p>Wie Sie wahrscheinlich wissen, ist Lucene eine hochleistungsfähige Text-Suchmaschinen-Bibliothek, die in Java geschrieben wurde und als Rückgrat für viele Suchplattformen wie Elasticsearch, OpenSearch und Solr dient. Im Kern organisiert Lucene Daten in Segmente, die im Wesentlichen in sich geschlossene Indizes sind, mit denen Lucene Suchvorgänge effizienter ausführen kann. Wenn Sie also eine auf Lucene basierende Suchmaschine mit einer Suche beauftragen, wird Ihre Suche in diesen Segmenten entweder sequentiell oder parallel ausgeführt.</p><p>OpenSearch hat die gleichzeitige Segmentsuche als optionales Flag eingeführt und verwendet sie standardmäßig nicht. Sie müssen sie mit einer speziellen Indexeinstellung <code>index.search.concurrent_segment_search.enabled</code>, wie <a href="https://opensearch.org/docs/latest/search-plugins/concurrent-segment-search/">hier</a> beschrieben, aktivieren und es gibt einige <a href="https://opensearch.org/docs/latest/search-plugins/concurrent-segment-search/#other-considerations">Einschränkungen</a>.</p><p>Elasticsearch hingegen sucht <a href="https://github.com/elastic/elasticsearch/pull/101230">standardmäßig</a> gleichzeitig in Segmenten, daher berücksichtigen die Vergleiche, die wir in diesem Blog anstellen, neben den verschiedenen Vektor-Engines und Vektortypen auch die unterschiedlichen Konfigurationen:</p><ul><li><p>Elasticsearch ootb: Elasticsearch sofort einsatzbereit, mit gleichzeitiger Segmentsuche;</p></li><li><p>OpenSearch ootb: ohne aktivierte gleichzeitige Segmentsuche;</p></li><li><p>OpenSearch css: mit aktivierter gleichzeitiger Segmentsuche</p></li></ul><p>Kommen wir nun zu einigen detaillierten Ergebnissen für jeden getesteten Vektordatensatz:</p><h2>2,5 Millionen Vektoren, 1536 Dimensionen (openai_vector)</h2><p>Beginnen wir mit dem einfachsten, aber hinsichtlich der Dimensionen auch größten Track, <a href="https://github.com/elastic/rally-tracks/edit/master/openai_vector">openai_vector</a> – er verwendet den <a href="https://huggingface.co/datasets/BeIR/nq">NQ-Datensatz</a>, der mit Einbettungen angereichert ist, die mit <a href="https://openai.com/blog/new-and-improved-embedding-model">dem Modell „text-embedding-ada-002“</a> von OpenAI generiert wurden. Dieser Track ist der einfachste, da er nur approximatives kNN testet und nur 5 Aufgaben hat. Er testet sowohl im Standalone-Modus (ohne Indizierung) als auch parallel zur Indizierung und verwendet sowohl einen einzelnen Client als auch 8 gleichzeitige Clients.</p><h3>Aufgaben</h3><ul><li><p><strong>standalone-search-knn-10-100-multiple-clients</strong>: Suche in 2,5 Millionen Vektoren mit 8 Clients gleichzeitig, k:10 und n:100</p></li><li><p><strong>standalone-search-knn-100-1000-multiple-clients</strong>: Suche in 2,5 Millionen Vektoren mit 8 Clients gleichzeitig, k:100 und n:1000</p></li><li><p><strong>standalone-search-knn-10-100-single-client</strong>: Suche in 2,5 Millionen Vektoren mit einem einzigen Client, k:10 und n:100</p></li><li><p><strong>standalone-search-knn-100-1000-single-client</strong>: Suche in 2,5 Millionen Vektoren mit einem einzigen Client, k:100 und n:1000</p></li><li><p><strong>parallel-documents-indexing-search-knn-10-100</strong>: Suche in 2,5 Millionen Vektoren bei gleichzeitigem Indexieren von weiteren 100.000 Dokumenten, k:10 und n:100</p></li></ul><p>Die durchschnittliche p99-Leistung ist unten aufgeführt:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf5791848eb0c12fb/6a17d72925daab9cae08a09e/eea0b2b49c690baada3e09d6968e513bfffe51a9-1440x318.webp" alt="openai_vector-Tabelle" /><p>Hier haben wir beobachtet, dass Elasticsearch zwischen <strong>3 und 8 Mal schneller</strong> ist als OpenSearch, wenn es neben dem Indexieren auch eine Vektorsuche (d. h. Lesen und Schreiben) mit :10 und :100 durchführt, und ohne Indizierung für denselben k- und n-Wert <strong>2 bis 3 Mal schneller</strong> ist. Bei :100 und :1000 (<em>standalone-search-knn-100-1000-single-client</em> und <em>standalone-search-knn-100-1000-multiple-clients</em>) ist Elasticsearch im Durchschnitt <strong>2 bis 7 Mal</strong> schneller als OpenSearch.</p><p>Die detaillierten Ergebnisse zeigen die exakten Tickets und Vektor-Engines im Vergleich:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdbe41d3187ced7ec/6a17d72a445de951c44cff4c/a7a761ed631d3e6211beb83d9d93d752d10123c9-1440x1728.webp" alt="openai_vector" /><h4>Recall</h4><p></p><p>knn-recall-10-100</p><p>knn-recall-100-1000</p><p>Elasticsearch-8.14.0@lucene-hnsw</p><p>0,969485</p><p>0,995138</p><p>Elasticsearch-8.14.0@lucene-int8_hnsw</p><p>0,781445</p><p>0,784817</p><p>OpenSearch-2.14.0@lucene-hnsw</p><p>0,96519</p><p>0,995422</p><p>OpenSearch-2.14.0@faiss</p><p>0,984154</p><p>0,98049</p><p>OpenSearch-2.14.0@faiss-sq_fp16</p><p>0,980012</p><p>0,97721</p><p>OpenSearch-2.14.0@nmslib</p><p>0,982532</p><p>0,99832</p><h2>10 Millionen Vektoren, 96 Dimensionen (dense_vector)</h2><p>In <a href="https://github.com/elastic/rally-tracks/tree/master/dense_vector">dense_vector</a> mit 10 Millionen Vektoren und 96 Dimensionen. Dies basiert auf dem Bilddatensatz <a href="https://big-ann-benchmarks.com/">DEEP1B von Yandex</a>. Der Datensatz wird aus den ersten 10 Millionen Vektoren der „Beispieldaten“-Datei mit dem Namen <code>learn.350M.fbin</code> erstellt. Die Suchvorgänge verwenden Vektoren aus der Abfrage der Datei „Abfragedaten“<code>public.10K.fbin</code>.</p><p>Sowohl Elasticsearch als auch OpenSearch funktionieren bei diesen Daten sehr gut, insbesondere nach einer <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html">erzwungenen Zusammenführung</a>, die normalerweise bei schreibgeschützten Indizes durchgeführt wird und der Defragmentierung des Index ähnelt, um eine einzige „Tabelle“ zu haben, in der gesucht werden kann.</p><h3>Aufgaben</h3><p>Jede Aufgabe wird mit 100 Anfragen aufgewärmt und dann werden 1000 Anfragen gemessen</p><ul><li><p><strong>knn-search-10-100</strong>: Suche in 10 Millionen Vektoren, k:10 und n:100</p></li><li><p><strong>knn-search-100-1000</strong>: Suche in 10 Millionen Vektoren, k:100 und n:1000</p></li><li><p><strong>knn-search-10-100-force-merge</strong>: Suche in 10 Millionen Vektoren nach einer erzwungenen Zusammenführung, k:10 und n:100</p></li><li><p><strong>knn-search-100-1000-force-merge</strong>: Suche in 10 Millionen Vektoren nach einer erzwungenen Zusammenführung, k:100 und n:1000</p></li><li><p><strong>knn-search-100-1000-concurrent-with-indexing</strong>: Suche in 10 Millionen Vektoren bei gleichzeitiger Aktualisierung <a href="https://github.com/elastic/rally-tracks/blob/master/dense_vector/challenges/default.json#L76C36-L76C37">von 5 % des Datensatzes</a>, k:100 und n:1000</p></li><li><p><strong>script-score-query</strong>: Exakte kNN-Suchen in <a href="https://github.com/elastic/rally-tracks/blob/master/dense_vector/queries.json">2000 spezifischen Vektoren</a>.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4629d06af85eb96c/6a17d72c6864a423e7b685dc/174995e0a2156d86359cdb7aa446dfaae6312ea4-1440x316.webp" alt="dense_vector" /><p>Sowohl Elasticsearch als auch OpenSearch haben sich bei der ANN-Suche gut bewährt. Wenn der Index in <em>knn-search-100-1000-force-merge</em> und <em>knn-search-10-100-force-merge</em> zusammengeführt wird (d. h. nur ein Segment hat), schneidet OpenSearch bei der Verwendung von <code>nmslib</code> und <code>faiss</code> besser ab als die anderen, obwohl sie alle um die 15 ms und dicht beieinander liegen.</p><p>Wenn der Index in <em>knn-search-10-100</em> und <em>knn-search-100-1000</em> jedoch mehrere Segmente hat (eine typische Situation, in der ein Index Aktualisierungen seiner Dokumente erhält), hält Elasticsearch die Latenz bei etwa ~7 ms und ~16 ms, während alle anderen OpenSearch-Engines langsamer sind.</p><p>Auch wenn der Index gleichzeitig gesucht und beschrieben wird (<em>knn-search-100-1000-concurrent-with-indexing</em>), hält Elasticsearch die Latenz unter 15 ms (bei 13,8 ms) und ist damit fast <strong>4 Mal schneller</strong> als OpenSearch im Standardmodus (49,3 ms) und auch dann noch schneller, wenn die gleichzeitige Segmentsuche aktiviert ist (17,9 ms). Dies ist aber zu nah, um von Bedeutung zu sein.</p><p>Bei exaktem kNN ist der Unterschied viel größer: Elasticsearch <strong>ist 6 Mal schneller</strong> als OpenSearch (~260 ms gegenüber ~1600 ms).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt254f43bcaa3dbfc2/6a17d72ddbb4ffc780fb54ff/17aec6be31117440bc4d1f99984aed95df1c4f6b-1440x1728.webp" alt="dense_vector" /><h4>Recall</h4><p></p><p>knn-recall-10-100</p><p>knn-recall-100-1000</p><p>Elasticsearch-8.14.0@lucene-hnsw</p><p>0,969843</p><p>0,996577</p><p>Elasticsearch-8.14.0@lucene-int8_hnsw</p><p>0,775458</p><p>0,840254</p><p>OpenSearch-2.14.0@lucene-hnsw</p><p>0,971333</p><p>0,996747</p><p>OpenSearch-2.14.0@faiss</p><p>0,9704</p><p>0,914755</p><p>OpenSearch-2.14.0@faiss-sq_fp16</p><p>0,968025</p><p>0,913862</p><p>OpenSearch-2.14.0@nmslib</p><p>0,9674</p><p>0,910303</p><h2>2 Millionen Vektoren, 768 Dimensionen (so_vector)</h2><p>Dieser <a href="https://github.com/elastic/rally-tracks/tree/master/so_vector">Track</a>,<code>so_vector</code>, stammt aus einem <a href="https://archive.org/download/stackexchange/stackoverflow.com-Posts.7z">Bündel von StackOverflow-Beiträgen, die</a> am 21. April 2022 heruntergeladen wurden. Er enthält nur Fragedokumente – alle Dokumente mit Antworten wurden entfernt. Jeder Fragentitel wurde mithilfe des Satztransformatormodells <a href="https://huggingface.co/sentence-transformers/multi-qa-mpnet-base-cos-v1">multi-qa-mpnet-base-cos-v1</a> in einen Vektor kodiert. Dieser Datensatz enthält die ersten 2 Millionen Fragen.</p><p>Im Gegensatz zum vorherigen Track enthält jedes Dokument hier neben Vektoren auch andere Felder, um Test-Features wie approximativer kNN mit Filterung und Hybridsuche zu unterstützen. <code>nmslib</code> für OpenSearch fehlt in diesem Test auffallend, da <a href="https://opensearch.org/docs/latest/search-plugins/knn/filter-search-knn/#k-nn-search-with-filters">es keine Filter unterstützt</a>.</p><h3>Aufgaben</h3><p>Jede Aufgabe wird mit 100 Anfragen aufgewärmt und dann werden 100 Anfragen gemessen. Beachten Sie, dass die Aufgaben der Einfachheit halber gruppiert wurden, da der Test 16 Suchtypen mal 2 verschiedene k-Werte mal 3 verschiedene n-Werte enthält.</p><ul><li><p><strong>kNN-10-50</strong>: Suche in 2 Millionen Vektoren ohne Filter, k:10 und n:50</p></li><li><p><strong>knn-10-50-filtered</strong>: Suche in 2 Millionen Vektoren <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">mit Filtern</a>, k:10 und n:50</p></li><li><p><strong>knn-10-50-after-force-merge</strong>: Suche in 2 Millionen Vektoren mit Filtern und nach einer erzwungenen Zusammenführung, k:10 und n:50</p></li><li><p><strong>knn-10-100</strong>: Suche in 2 Millionen Vektoren ohne Filter, k:10 und n:100</p></li><li><p><strong>knn-10-100-filtered</strong>: Suche in 2 Millionen Vektoren <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">mit Filtern</a>, k:10 und n:100</p></li><li><p><strong>knn-10-100-after-force-merge</strong>: Suche in 2 Millionen Vektoren mit Filtern und nach einer erzwungenen Zusammenführung, k:10 und n:100</p></li><li><p><strong>knn-100-1000</strong>: Suche in 2 Millionen Vektoren ohne Filter, k:100 und n:1000</p></li><li><p><strong>knn-100-1000-filtered</strong>: Suche in 2 Millionen Vektoren <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">mit Filtern</a>, k:100 und n:1000</p></li><li><p><strong>knn-100-1000-after-force-merge</strong>: Suche in 2 Millionen Vektoren mit Filtern und nach einer erzwungenen Zusammenführung, k:100 und n:1000</p></li><li><p><strong>exact-knn</strong>: Exakte KNN-Suche <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json#L56">mit und ohne Filter</a>.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8b44e1306af35877/6a17d72f577262aca11bca3e/d4ed2982d55370cad4b2048b23ce97caa55017c0-1440x316.webp" alt="so_vector-Tabelle" /><p>Elasticsearch ist in diesem Test <strong>durchweg schneller</strong> als OpenSearch, nur in zwei Fällen ist OpenSearch schneller, jedoch nicht viel (<em>knn-10-100</em> und <em>knn-100-1000</em>). Aufgaben mit <em>knn-10-50</em>, <em>knn-10-100</em> und <em>knn-100-1000</em> in Kombination mit Filtern zeigen einen Unterschied von bis zu dem <strong>Siebenfachen</strong> (112 ms vs. 803 ms).</p><p>Die Leistung beider Lösungen scheint sich nach einem „force-merge“ verständlicherweise anzugleichen, wie <em>knn-10-50-after-force-merge</em>, <em>knn-10-100-after-force-merge</em> und <em>knn-100-1000-after-force-merge</em> zeigen. Bei diesen Aufgaben ist <code>faiss</code> schneller.</p><p>Die Leistung beim exakten kNN ist erneut sehr unterschiedlich, wobei Elasticsearch diesmal <strong>13 Mal schneller</strong> ist als OpenSearch (~385 ms vs. ~5262 ms).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf80ccc7c2df84559/6a17d7314b055de00d43203f/615cb9228eb05ddd2e9512b3a6a5bc88d4088a1a-1440x1440.webp" alt="so_vector" /><h4>Recall</h4><p></p><p>knn-recall-10-100</p><p>knn-recall-100-1000</p><p>knn-recall-10-50</p><p>Elasticsearch-8.14.0@lucene-hnsw</p><p>1</p><p>1</p><p>1</p><p>Elasticsearch-8.14.0@lucene-int8_hnsw</p><p>1</p><p>0,986667</p><p>1</p><p>OpenSearch-2.14.0@lucene-hnsw</p><p>1</p><p>1</p><p>1</p><p>OpenSearch-2.14.0@faiss</p><p>1</p><p>1</p><p>1</p><p>OpenSearch-2.14.0@faiss-sq_fp16</p><p>1</p><p>1</p><p>1</p><p>OpenSearch-2.14.0@nmslib</p><p>0,9674</p><p>0,910303</p><p>0,976394</p><h2>Elasticsearch und Lucene als klare Sieger</h2><p>Bei Elastic entwickeln wir Apache Lucene und Elasticsearch ständig weiter, um sicherzustellen, dass wir die führende Vektordatenbank für Such- und Abrufanwendungsfälle bereitstellen können, einschließlich RAG (Retrieval-Augmented Generation). Unsere jüngsten Fortschritte haben die Leistung drastisch gesteigert und, aufbauend auf den Vorteilen von Lucene 9.10, die Vektorsuche noch <a href="https://search-labs.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains">schneller und platzsparender</a> gemacht. In diesem Blog wurde eine Studie vorgestellt, die zeigt, dass Elasticsearch beim Vergleich aktueller Versionen bis zu 12 Mal schneller ist als OpenSearch.</p><p>Es ist erwähnenswert, dass beide Produkte dieselbe Version von Lucene verwenden (<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/release-notes-8.14.0.html">Versionshinweise zu Elasticsearch 8.14</a> und <a href="https://github.com/opensearch-project/OpenSearch/blob/2.14/release-notes/opensearch.release-notes-2.14.0.md">zu OpenSearch 2.14</a>).</p><p>Das Innovationstempo bei Elastic wird nicht nur für unsere On-Prem- und Elastic Cloud-Kunden, sondern auch für diejenigen, die unsere <a href="https://www.elastic.co/search-labs/blog/stateless-your-new-state-of-find-with-elasticsearch">zustandslose Plattform</a> nutzen, noch mehr bieten. Features wie die Unterstützung der <a href="https://www.elastic.co/search-labs/blog/int4-scalar-quantization-in-lucene">skalaren Quantisierung auf int4</a> werden mit strengen Tests angeboten, um sicherzustellen, dass Kunden diese Techniken ohne signifikanten Rückgang der Abrufe nutzen können, ähnlich wie <a href="https://www.elastic.co/search-labs/blog/evaluating-scalar-quantization">bei unseren Tests für int8</a>.</p><p>Die Effizienz der Vektorsuche wird aufgrund der Verbreitung von KI- und Machine Learning-Anwendungen allmählich zu einem unverzichtbaren Feature in modernen Suchmaschinen. Für Unternehmen, die nach einer leistungsstarken Suchmaschine suchen, die in der Lage ist, mit den Anforderungen an großvolumige, hochkomplexe Vektordaten Schritt zu halten, ist Elasticsearch die definitive Antwort.</p><p>Egal, ob Sie eine etablierte Plattform erweitern oder neue Projekte initiieren – die Integration von Elasticsearch für Vektorsuchen ist ein strategischer Schritt, der greifbare, langfristige Vorteile bringen wird. Mit seinem nachgewiesenen Leistungsvorteil ist Elasticsearch bereit, die nächste Welle von Innovationen im Bereich des Suchens zu unterstützen.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-opensearch-vector-search-performance-comparison</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-opensearch-vector-search-performance-comparison</guid>
    <category><![CDATA[Vektordatenbank]]></category>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Ugo Sangiorgi]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5d70b25967c2194e/6a17d732b1e11383f879f0ca/13c3c0053e2968fb835ba2f90f34bec3a011b5c0-880x592.webp" length="0" type="image/webp"/>
    <pubDate>Wed, 26 Jun 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Skalarquantisierung in Lucene verstehen]]></title>
    <description><![CDATA[Erfahren Sie, wie Elastic die Skalarquantisierung in Lucene eingeführt hat, einschließlich automatischer Byte-Quantisierung, Quantisierung pro Segment und Einblicken in die Leistung.]]></description>
    <content:encoded><![CDATA[<h2>Automatische Byte-Quantisierung in Lucene</h2><p>HNSW ist zwar eine leistungsstarke und flexible Methode zum Speichern und Durchsuchen von Vektoren, benötigt aber eine beträchtliche Menge an Speicherplatz, um schnell ausgeführt werden zu können. Beispielsweise benötigt die Abfrage von 1 Million float32-Vektoren mit 768 Dimensionen ungefähr  RAM. Sobald man eine beträchtliche Anzahl von Vektoren durchsucht, wird dies teuer. Eine Möglichkeit, den Speicherverbrauch um etwa  zu reduzieren, besteht in der Byte-Quantisierung. Lucene und folglich auch Elasticsearch unterstützen die Indizierung  -Vektoren schon seit einiger Zeit, die Erstellung dieser Vektoren lag jedoch bisher in der Verantwortung des Benutzers. Das wird sich bald ändern, da wir in Lucene die Skalarquantisierung  eingeführt haben.</p><h2>Skalarquantisierung 101</h2><p>Alle Quantisierungstechniken werden als verlustbehaftete Transformationen der Rohdaten betrachtet. Das bedeutet, dass aus Platzgründen einige Informationen verloren gehen. Eine ausführliche Erklärung der Skalarquantisierung finden Sie unter: <a href="https://www.elastic.co/search-labs/scalar-quantization-101">Skalarquantisierung 101</a>. Im Prinzip ist die Skalarquantisierung eine verlustbehaftete Kompressionstechnik. Durch einfache Berechnungen lassen sich erhebliche Speicherplatzeinsparungen erzielen, ohne dass die Speicherkapazität nennenswert beeinträchtigt wird.</p><h2>Die Architektur erkunden</h2><p>Wer bereits Erfahrung mit Elasticsearch hat, kennt diese Konzepte möglicherweise schon, aber hier ist ein kurzer Überblick über die Verteilung der Dokumente für die Suche.</p><p>Jeder Elasticsearch-Index besteht aus <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/size-your-shards.html#size-your-shards">mehreren Shards</a>. Obwohl jeder Shard nur einem einzigen Knoten zugewiesen werden kann, ermöglicht die Verwendung mehrerer Shards pro Index eine parallele Berechnung über mehrere Knoten hinweg.</p><p>Jeder Shard besteht aus einem einzelnen <a href="https://lucene.apache.org/core/9_8_0/core/org/apache/lucene/index/package-summary.html">Lucene-Index</a>. Ein Lucene-Index besteht aus mehreren schreibgeschützten Segmenten. Während der Indizierung werden Dokumente zwischengespeichert und regelmäßig in ein schreibgeschütztes Segment geschrieben. Wenn bestimmte Bedingungen erfüllt sind, können diese Segmente im Hintergrund zu einem größeren Segment zusammengeführt werden. Das alles ist konfigurierbar und birgt seine eigenen Komplexitäten. Wenn wir aber von Segmenten und deren Zusammenführung sprechen, meinen wir schreibgeschützte Lucene-Segmente und die automatische, periodische Zusammenführung dieser Segmente. <a href="https://www.elastic.co/search-labs/vector-search-elasticsearch-rationale">Hier folgt ein detaillierterer Einblick</a> in die Segmentzusammenführung und die damit verbundenen Designentscheidungen.</p><h2>Quantisierung pro Segment in Lucene</h2><p>Jedes Segment in Lucene speichert Folgendes: die einzelnen Vektoren, die HNSW-Graphindizes, die quantisierten Vektoren und die berechneten Quantile. Aus Gründen der Kürze konzentrieren wir uns darauf, wie Lucene quantisierte und rohe Vektoren speichert. Für jedes Segment erfassen wir die Rohvektoren in der  Datei, die quantisierten Vektoren und einen einzelnen Korrekturmultiplikator (Gleitkommazahl) in  sowie die Metadaten rund um die Quantisierung in der  Datei.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1861d0af970fd096/6a17d9887f6f15a45dc099c0/507a4d578f8a5d2225b11d16ac1fc0b7dd39eaa1-1207x228.png" alt="Die .vec-Datei Datei" /><p>Abbildung 1: Vereinfachtes Layout der Rohvektorspeicherdatei. Benötigt Speicherplatz in  da  4 Byte groß sind. Da wir quantisieren, werden diese Daten während der HNSW-Suche nicht geladen. Sie werden nur auf ausdrücklichen Wunsch verwendet (z. B. Brute-Force-Sekundärverfahren mittels <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/filter-search-results.html#query-rescorer">Rescore</a>), oder zur erneuten Quantisierung während der Segmentzusammenführung.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8c52b51939357280/6a17d9897b54f9130f8b3761/ffc6e8bffefc5cd36f14aae315184c89e04a6587-1440x207.png" alt="Die .veq-Datei" /><p>Abbildung 2: Vereinfachtes Layout der  Datei. Benötigt  Speicherplatz und wird während der Suche in den Speicher geladen. Die  Bytes dienen der Berücksichtigung des Korrekturmultiplikators, der zur Anpassung der Bewertung für eine bessere Genauigkeit und Trefferquote verwendet wird.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt28e007988f81e5c0/6a17d98b25daabe23208a0d9/555cc92d7d179d8c9711cefbaaac3e0b815d2e42-1440x182.png" alt="Die .vemq-Datei" /><p>Abbildung 3: Vereinfachter Aufbau der Metadatendatei. Hier erfassen wir die Quantisierung und Vektorkonfiguration sowie die berechneten Quantile für dieses Segment.</p><p>Für jedes Segment speichern wir also nicht nur die quantisierten Vektoren, sondern auch die Quantile, die zur Erstellung dieser quantisierten Vektoren verwendet wurden, sowie die ursprünglichen Rohvektoren. Aber warum bewahren wir die Rohvektoren überhaupt auf?</p><h2>Quantisierung, die mit Ihnen wächst</h2><p>Da Lucene periodisch in schreibgeschützte Segmente schreibt, hat jedes Segment nur einen Teil Ihrer Daten zur Verfügung. Das bedeutet, dass die berechneten Quantile nur für diese Stichprobe Ihrer gesamten Daten direkt gelten. Das ist kein großes Problem, wenn Ihre Stichprobe Ihr gesamtes Korpus angemessen repräsentiert. Lucene ermöglicht es Ihnen jedoch, Ihren Index auf verschiedene Arten zu sortieren. Es könnte also vorkommen, dass die Daten so indiziert werden, dass sie so sortiert sind, dass dies zu Verzerrungen bei der Berechnung der Quantile pro Segment führt. Außerdem können Sie die Daten jederzeit löschen! Ihr Stichprobensatz kann winzig sein, sogar nur ein einziger Vektor. Ein weiterer Haken ist, dass Sie die Kontrolle darüber haben, wann Zusammenführungen erfolgen. Elasticsearch verfügt zwar über voreingestellte Standardwerte und eine regelmäßige Zusammenführung, Sie können aber jederzeit über <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.10/indices-forcemerge.html">die _force_merge</a> -API eine Zusammenführung anfordern. Wie können wir also all diese Flexibilität ermöglichen und gleichzeitig eine gute Quantisierung mit guter Recall-Qualität gewährleisten?</p><p>Die Vektorquantisierung von Lucene passt sich im Laufe der Zeit automatisch an. Da Lucene mit einer Architektur für schreibgeschützte Segmente konzipiert ist, haben wir die Garantie, dass sich die Daten in jedem Segment nicht verändert haben, und klare Abgrenzungen im Code, wann Aktualisierungen möglich sind. Das bedeutet, dass wir während der Segmentzusammenführung die Quantile nach Bedarf anpassen und gegebenenfalls Vektoren neu quantisieren können.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0d9a2390958c3e82/6a17d98cbe6086e4fd0045d5/646baa8279719d06c05c1a4fdb80a0c060e0dd94-1440x446.png" alt="Mehrere Segmentquantile" /><p>Abbildung 4: Drei Beispielsegmente mit unterschiedlichen Quantilen.</p><p>Aber ist eine erneute Quantifizierung nicht teuer? Es verursacht zwar einen gewissen Mehraufwand, aber Lucene geht intelligent mit Quantilen um und führt nur dann eine vollständige Neuquantisierung durch, wenn dies erforderlich ist. Nehmen wir die Segmente in Abbildung 4 als Beispiel. Wir geben den Segmenten  und  jeweils  Dokumente und Segment  nur  Dokumente. Lucene berechnet einen gewichteten Durchschnitt der Quantile. Wenn das resultierende zusammengeführte Quantil nahe genug an den ursprünglichen Quantilen des Segments liegt, muss das Segment nicht erneut quantisiert werden, sondern es werden die neu zusammengeführten Quantile verwendet.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte2596c10ee616cf3/6a17d98eec0f8903b85a6497/58fa20d60dc337ca7108eb01e84a07c330e87ee6-1440x447.png" alt="Zusammengeführte Quantile" /><p>Abbildung 5: Beispiel für zusammengeführte Quantile, wobei die Segmente  und   Dokumente enthalten und  nur  enthält.</p><p>In der in Abbildung 5 dargestellten Situation ist zu erkennen, dass die resultierenden zusammengeführten Quantile den ursprünglichen Quantilen in  und  sehr ähnlich sind. Daher rechtfertigen sie keine Quantisierung der Vektoren. Segment  weicht anscheinend zu stark ab. Folglich würden die Vektoren in  mit den neu zusammengeführten Quantilwerten erneut quantisiert.</p><p>Es gibt tatsächlich extreme Fälle, in denen die zusammengeführten Quantile dramatisch von allen ursprünglichen Quantilen abweichen. In diesem Fall werden wir aus jedem Segment eine Stichprobe entnehmen und die Quantile vollständig neu berechnen.</p><h2>Quantisierungsleistung und Zahlen</h2><p>Ist es also schnell und bietet es trotzdem noch eine gute Speicherkapazität? Die folgenden Zahlen wurden bei der Durchführung des Experiments auf einer <code>c3-standard-8</code> GCP-Instanz ermittelt. Um einen fairen Vergleich mit  zu gewährleisten, verwendeten wir eine ausreichend große Instanz, um Rohvektoren im Speicher zu halten. Wir haben  <a href="https://huggingface.co/datasets/Cohere/wikipedia-22-12-simple-embeddings">Cohere Wiki-</a> Vektoren mithilfe des Maximum-Skalarprodukts indiziert.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfa48e3724ad97fed/6a17d98f445de96aca4cff88/81958f99b8a1397f669db48de00f264cacc6b49f-576x455.png" alt="Quantisierungsrückruf" /><p>Abbildung 6: Recall@10 für quantisierte Vektoren im Vergleich zu Rohvektoren. Die Suchleistung von quantisierten Vektoren ist deutlich schneller als die von Rohvektoren, und der Recall kann schnell wiederhergestellt werden, indem nur 5 weitere Vektoren gesammelt werden; sichtbar durch .</p><p>Abbildung 6 veranschaulicht die Geschichte. Es gibt zwar einen Unterschied in der Erinnerungsleistung, wie zu erwarten war, dieser ist jedoch nicht signifikant. Und der Unterschied in der Trefferquote verschwindet bereits durch das Sammeln von nur 5 weiteren Vektoren. All dies bei  schnellen Segmentzusammenführungen und nur einem Viertel des Speicherbedarfs von  Vektoren.</p><h2>Fazit</h2><p>Lucene bietet eine einzigartige Lösung für ein schwieriges Problem. Für die Quantisierung ist kein „Trainings“- oder „Optimierungs“-Schritt erforderlich. In Lucene funktioniert es einfach. Sie müssen sich keine Sorgen machen, Ihren Vektorindex „neu trainieren“ zu müssen, falls sich Ihre Daten ändern. Lucene erkennt signifikante Änderungen und kümmert sich während der gesamten Lebensdauer Ihrer Daten automatisch darum. Wir freuen uns darauf, wenn wir diese Funktion in Elasticsearch integrieren!</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[ML-Forschung]]></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>
  <item>
    <title><![CDATA[Umsetzung wissenschaftlicher Arbeiten: Erkenntnisse aus Elasticsearch und Lucene]]></title>
    <description><![CDATA[Entdecken Sie Strategien zur Integration von Forschungsarbeiten in eine Softwareanwendung und nutzen Sie dabei unsere Erfahrungen mit Elasticsearch und Lucene.]]></description>
    <content:encoded><![CDATA[<p>Dieser Beitrag stellt Strategien zur Implementierung wissenschaftlicher Arbeiten in einer Softwareanwendung vor. Es greift auf Beispiele von Elasticsearch und Lucene zurück, um anderen Ingenieuren zu helfen, aus unseren Erfahrungen zu lernen. Sie lesen diese Strategien vielleicht und denken: „Aber das ist doch nur Softwareentwicklung!“ Und das wäre in der Tat richtig: Als Ingenieure verfügen wir bereits über die richtigen Vorgehensweisen und Werkzeuge, diese müssen lediglich an eine neue Herausforderung angepasst werden.</p><h2>Hintergrund</h2><p>Bei der Entwicklung von Elasticsearch stoßen wir gelegentlich auf wichtige Probleme, für die es keine einfache oder etablierte Lösungsmethode gibt. Es liegt nahe zu fragen: „Hmm, gibt es dazu eine wissenschaftliche Arbeit?“ Manchmal ist die akademische Arbeit auch eine Quelle der Inspiration. Wir stoßen auf eine wissenschaftliche Arbeit, in der ein neuer Algorithmus oder eine neue Datenstruktur vorgeschlagen wird, und denken: „Das wäre so nützlich!“ Hier sind nur einige Beispiele dafür, wie Elasticsearch und Apache Lucene akademische Arbeiten integrieren:</p><ul><li><p><a href="https://research.google/pubs/pub40671/">HyperLogLog++</a> für <a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.15/search-aggregations-metrics-cardinality-aggregation.html">Kardinalitätsaggregationen</a></p></li><li><p><a href="https://www.usenix.org/system/files/conference/nsdi15/nsdi15-paper-suresh.pdf">C3-Algorithmus</a> zur <a href="https://www.elastic.co/blog/improving-response-latency-in-elasticsearch-with-adaptive-replica-selection">adaptiven Replikaauswahl</a></p></li><li><p><a href="https://arxiv.org/abs/1603.09320">Hierarchische navigierbare Small-World-Graphen (HNSW)</a> für die Suche nach dem nächsten Vektor in Lucene</p></li><li><p><a href="https://jmlr.csail.mit.edu/papers/volume17/15-308/15-308.pdf">MIC-Statistik</a> zur <a href="https://github.com/elastic/ml-cpp/pull/488">Verbesserung der Klassifizierung durch maschinelles Lernen</a></p></li><li><p><a href="http://engineering.nyu.edu/~suel/papers/bmw.pdf">Block-max WAND</a> für <a href="https://www.elastic.co/blog/faster-retrieval-of-top-hits-in-elasticsearch-with-block-max-wand">schnelleres Abrufen von Top-Hits in Lucene</a></p></li><li><p>... und <a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.15/query-dsl-combined-fields-query.html">viele</a> <a href="https://github.com/elastic/elasticsearch/blob/b2a9328890b23e7ccf6c66a3b13d6d65e453a3dd/server/src/main/java/org/elasticsearch/search/sort/BucketedSort.java#L302-L323">mehr</a></p></li></ul><p>Wissenschaftliche Artikel sind eine unschätzbare Ressource für Ingenieure, die datenintensive Systeme entwickeln. Doch ihre Umsetzung kann einschüchternd und fehleranfällig sein – Algorithmenbeschreibungen sind oft komplex, und wichtige praktische Details werden ausgelassen. Und das Testen stellt eine echte Herausforderung dar: Wie können wir beispielsweise einen Algorithmus für maschinelles Lernen gründlich testen, dessen Ausgabe stark vom Datensatz abhängt?</p><h2>Bewerten Sie das Dokument wie eine Softwareabhängigkeit.</h2><p>Das Hinzufügen einer neuen Softwareabhängigkeit erfordert eine sorgfältige Prüfung: Wenn das andere Paket fehlerhaft, langsam oder unsicher ist, könnte unser Projekt es auch sein. Bevor Entwickler eine Abhängigkeit einbinden, stellen sie sicher, dass sie deren Qualität bewerten.</p><p>Das Gleiche gilt für wissenschaftliche Arbeiten, deren Umsetzung Sie erwägen. Man könnte meinen, dass ein Algorithmus, nur weil er in einer wissenschaftlichen Arbeit veröffentlicht wurde, korrekt sein und gut funktionieren muss. Doch selbst wenn eine wissenschaftliche Arbeit den Begutachtungsprozess durchlaufen hat, kann sie Mängel aufweisen. Vielleicht beruht der Korrektheitsbeweis auf Annahmen, die nicht realistisch sind. Oder vielleicht zeigt der Abschnitt „Experimente“ eine deutlich bessere Performance als die Basislinie, aber das gilt nur für einen bestimmten Datensatz. Auch wenn die Arbeit von hoher Qualität ist, passt ihr Ansatz möglicherweise nicht zu Ihrem Projekt.</p><p>Wenn man darüber nachdenkt, ob man eine Abhängigkeit von einer wissenschaftlichen Arbeit eingehen sollte, ist es hilfreich, dieselben Fragen zu stellen, die man auch bei einem Softwarepaket stellen würde:</p><ul><li><p>Ist die Bibliothek weit verbreitet und „praxiserprobt“? → Haben andere Softwarepakete diese Arbeit bereits umgesetzt und hat sie sich dabei bewährt?</p></li><li><p>Sind Leistungsvergleichswerte verfügbar? Erscheinen diese Angaben zutreffend und fair? → Enthält die Arbeit realistische Experimente? Sind sie gut gestaltet?</p></li><li><p>Ist die Leistungsverbesserung groß genug, um die Komplexität zu rechtfertigen? → Lässt sich die Arbeit mit einem soliden Basisansatz vergleichen? Um wie viel übertrifft es diesen Vergleichswert?</p></li><li><p>Lässt sich dieser Ansatz gut in unser System integrieren? → Passen die Annahmen und Abwägungen des Algorithmus zu unserem Anwendungsfall?</p></li></ul><p>Seltsamerweise schneidet die Software, die in einem Leistungsvergleich mit der Konkurrenz auftritt, immer am schnellsten ab! Wenn die Benchmarks von einem Dritten entworfen wurden, sind sie möglicherweise ausgewogener. Das gleiche Phänomen trifft auch auf wissenschaftliche Arbeiten zu. Wenn ein Algorithmus nicht nur in der Originalveröffentlichung gut abschneidet, sondern auch in anderen Veröffentlichungen als starke Basislinie auftaucht, dann ist er mit hoher Wahrscheinlichkeit solide.</p><h2>Werden Sie kreativ beim Testen</h2><p>Algorithmen aus wissenschaftlichen Artikeln weisen oft ein komplexeres Verhalten auf als die Algorithmen, denen wir im Alltag begegnen. Vielleicht handelt es sich um einen Näherungsalgorithmus, der Genauigkeit gegen höhere Geschwindigkeit eintauscht. Oder vielleicht handelt es sich um eine Methode des maschinellen Lernens, die einen großen Datensatz verarbeitet und (manchmal unerwartete) Ergebnisse liefert. Wie können wir Tests für diese Algorithmen schreiben, wenn wir ihr Verhalten nicht auf einfache Weise charakterisieren können?</p><h3>Fokus auf Invarianten</h3><p>Beim Entwerfen von Unit-Tests ist es üblich, in Beispielen zu denken: Wenn wir dem Algorithmus diese Beispieleingabe geben, sollte er diese Ausgabe haben. Leider deckt das Testen anhand von Beispielen das Verhalten der meisten mathematischen Algorithmen nicht ausreichend ab.</p><p>Betrachten wir den C3-Algorithmus, den Elasticsearch verwendet, um herauszufinden, welcher Knoten eine Suchanfrage bearbeiten soll. Es bewertet jeden Knoten anhand einer differenzierten Formel, die die bisherigen Service- und Antwortzeiten des Knotens sowie seine Warteschlangengröße berücksichtigt. Das Testen einiger Beispiele bestätigt nicht wirklich, dass wir die Formel richtig verstanden haben. Es hilft, einen Schritt zurückzutreten und über Testinvarianten nachzudenken: Verringert sich der Rang des Knotens, wenn sich die Bedienzeit erhöht? Wird der Rang, wie in der Studie behauptet, durch die Antwortzeit bestimmt, wenn die Warteschlangenlänge 0 beträgt?</p><p>Die Konzentration auf Invarianten kann in einer Reihe häufiger Fälle hilfreich sein:</p><ul><li><p>Soll die Methode unabhängig von der Reihenfolge sein? In diesem Fall sollte die Übergabe der Eingabedaten in einer anderen Reihenfolge zum gleichen Ergebnis führen.</p></li><li><p>Erzeugt ein Schritt im Algorithmus Klassenwahrscheinlichkeiten? In diesem Fall müssten sich diese Wahrscheinlichkeiten zu 1 addieren.</p></li><li><p>Ist die Funktion symmetrisch um den Ursprung? In diesem Fall sollte das Umkehren des Vorzeichens des Eingangssignals einfach das Vorzeichen des Ausgangssignals umkehren.</p></li></ul><p>Bei der ersten Implementierung von C3 hatten wir einen Fehler in der Formel, bei dem wir versehentlich den Kehrwert der Antwortzeit anstelle der Antwortzeit verwendet haben. Das bedeutete, dass langsamere Knoten höher eingestuft werden konnten! Bei der Behebung des Problems <a href="https://github.com/elastic/elasticsearch/pull/70283">haben wir darauf geachtet, Invariantenprüfungen einzubauen,</a> um zukünftigen Fehlern vorzubeugen.</p><h3>Vergleich mit einer Referenzimplementierung</h3><p>Zusammen mit der Veröffentlichung des Artikels haben die Autoren hoffentlich auch eine Implementierung des Algorithmus veröffentlicht. (Dies ist besonders wahrscheinlich, wenn die Arbeit Experimente enthält, da viele Fachzeitschriften von den Autoren die Veröffentlichung des Codes zur Reproduktion der Ergebnisse verlangen.) Sie können Ihren Ansatz anhand dieser Referenzimplementierung testen, um sicherzustellen, dass Sie keine wichtigen Details des Algorithmus übersehen haben.</p><p>Während der Entwicklung der HNSW-Implementierung von Lucene für die Nächste-Nachbarn-Suche haben wir <a href="https://issues.apache.org/jira/browse/LUCENE-9937">diese anhand einer Referenzbibliothek der Autoren des Artikels getestet</a> . Wir haben sowohl Lucene als auch die Bibliothek mit demselben Datensatz ausgeführt und die Genauigkeit ihrer Ergebnisse sowie die Anzahl der durchgeführten Berechnungen verglichen. Wenn diese Zahlen annähernd übereinstimmen, wissen wir, dass Lucene den Algorithmus korrekt implementiert.</p><p>Bei der Integration eines Algorithmus in ein System müssen oft Modifikationen oder Erweiterungen vorgenommen werden, wie z. B. die Skalierung auf mehrere Kerne oder das Hinzufügen von Heuristiken zur Leistungsverbesserung. Am besten implementiert man zunächst eine „Standardversion“, testet sie anhand der Referenz und nimmt dann schrittweise Änderungen vor. So können Sie sicher sein, dass Sie alle wichtigen Aspekte erfasst haben, bevor Sie Anpassungen vornehmen.</p><h3>Duell gegen einen bestehenden Algorithmus</h3><p>Im letzten Abschnitt wird eine weitere Idee für eine Testinvariante vorgestellt: der Vergleich der Ausgabe des Algorithmus mit der Ausgabe eines einfacheren und besser verstandenen Algorithmus. Nehmen wir beispielsweise den Block-Max-WAND-Algorithmus in Lucene, der die Dokumentensuche beschleunigt, indem er Dokumente überspringt, die nicht in den Top-Ergebnissen erscheinen können. Es ist schwierig, genau zu beschreiben, wie sich block-max WAND in jedem Fall verhalten sollte, aber wir wissen, dass die Anwendung die Top-Ergebnisse nicht verändern sollte! Unsere Tests können also mehrere zufällige Suchanfragen generieren, <a href="https://github.com/apache/lucene/blob/main/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java#L669">diese dann sowohl mit als auch ohne WAND-Optimierung ausführen</a> und überprüfen, ob ihre Ergebnisse immer übereinstimmen.</p><p>Ein wichtiger Aspekt dieser Tests ist, dass sie <a href="https://www.elastic.co/blog/elasticsearch-testing-qa-increasing-coverage-randomizing-test-runs">zufällige Eingaben generieren,</a> mit denen der Vergleich durchgeführt wird. Dies kann dabei helfen, Fälle durchzuspielen, an die man sonst nicht gedacht hätte, und unerwartete Probleme aufzudecken. Beispielsweise hat der randomisierte Vergleichstest von Lucene für die BM25F-Bewertung dazu beigetragen <a href="https://issues.apache.org/jira/browse/LUCENE-10039">, Fehler in subtilen Grenzfällen aufzudecken</a>. Die Idee, einen Algorithmus mit zufälligen Eingaben zu füttern, ist eng mit dem Konzept des <a href="https://en.wikipedia.org/wiki/Fuzzing">Fuzzing</a> verwandt, einer gängigen Testmethode in der Computersicherheit.</p><p>Elasticsearch und Lucene verwenden diesen Testansatz häufig. Wenn Sie einen Test sehen, der ein "Duell" zwischen zwei Algorithmen erwähnt (TestDuelingAnalyzers, testDuelTermsQuery...), dann wissen Sie, dass diese Strategie zum Einsatz kommt.</p><h2>Verwenden Sie die im Artikel verwendete Terminologie.</h2><p>Wenn ein anderer Entwickler mit Ihrem Code arbeitet, muss er die Dokumentation konsultieren, um die Details zu verstehen. Der <a href="https://github.com/elastic/elasticsearch/blob/4f22f437ee50cacb94b37b457be1da0b8ba0e8ce/server/src/main/java/org/elasticsearch/search/aggregations/metrics/HyperLogLogPlusPlus.java#L24-L39">Kommentar zur HyperLogLog++-Implementierung von Elasticsearch</a> bringt es gut auf den Punkt: „Der Versuch, die Funktionsweise dieser Klasse zu verstehen, ohne das zugehörige Paper gelesen zu haben, gilt als abenteuerlich.“ Dieser Methodenkommentar ist ebenfalls ein gutes Beispiel. Es enthält einen Link zur wissenschaftlichen Arbeit und hebt hervor, welche Änderungen am Algorithmus im Vergleich zur ursprünglichen Beschreibung vorgenommen wurden.</p><p>Da die Entwickler ihr Verständnis des Codes auf dem Papier aufbauen, ist es hilfreich, genau dieselbe Terminologie zu verwenden. Da die mathematische Notation kurz und bündig ist, kann dies zu Namen führen, die im Allgemeinen nicht als „guter Stil“ gelten würden, im Kontext der Arbeit aber sehr verständlich sind. Formeln aus wissenschaftlichen Arbeiten gehören zu den wenigen Fällen, in denen man in Elasticsearch auf kryptische Variablennamen wie <a href="https://github.com/elastic/elasticsearch/blob/4f22f437ee50cacb94b37b457be1da0b8ba0e8ce/server/src/main/java/org/elasticsearch/node/ResponseCollectorService.java#L151">rS und muBarSInverse</a> stößt.</p><p>
<em>Die vom Autor empfohlene Art, einen wissenschaftlichen Artikel zu lesen: mit einer großen Tasse Kaffee.</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt12d81453c706f7cb/6a17d80e0b0bede6badd3424/d03a8e2a50e173b15a4ec3732810c63ff53321f6-1440x1081.jpg" alt="elastic-blog-academicpaper.jpg" /><h2>Sie können dem Autor eine E-Mail schreiben.</h2><p>Bei der Bearbeitung einer schwierigen Hausarbeit kann es vorkommen, dass man stundenlang über einer Formel grübelt und sich nicht sicher ist, ob man sie falsch verstanden hat oder ob es sich nur um einen Tippfehler handelt. Wenn es sich um ein Open-Source-Projekt handeln würde, könnten Sie eine Frage auf GitHub oder StackOverflow stellen. Aber wo kann man eine wissenschaftliche Arbeit finden? Die Autoren scheinen beschäftigt zu sein und könnten von Ihren E-Mails genervt sein.</p><p>Im Gegenteil, viele Akademiker freuen sich, wenn sie hören, dass ihre Ideen in die Praxis umgesetzt werden, und beantworten gerne Fragen per E-Mail. Wenn Sie an einem Produkt arbeiten, mit dem sie vertraut sind, listen sie die Anwendung möglicherweise sogar auf ihrer Website auf!</p><p>Es gibt auch einen wachsenden Trend unter Akademikern, wissenschaftliche Arbeiten öffentlich zu diskutieren und dabei viele der gleichen Werkzeuge wie in der Softwareentwicklung zu verwenden. Wenn zu einer Veröffentlichung ein Softwarepaket gehört, finden Sie möglicherweise Antworten auf <a href="https://github.com/facebookresearch/faiss/issues/1928">häufig gestellte Fragen auf GitHub</a>. In den Stack Exchange-Communities „Theoretical Computer Science“ und „Cross Validated“ finden sich auch <a href="https://cstheory.stackexchange.com/questions/49296/problem-in-the-paper-stable-minimum-space-partitioning-in-linear-time">detaillierte Diskussionen über populärwissenschaftliche Artikel</a>. Einige Konferenzen haben damit begonnen, alle Paper-Reviews online zu veröffentlichen. Diese Rezensionen beinhalten <a href="https://openreview.net/forum?id=H1eA7AEtvS">einen Dialog</a> mit den Autoren, der hilfreiche Einblicke in den Ansatz ermöglichen kann.</p><h2>Fortgesetzt werden</h2><p>Dieser Beitrag konzentriert sich auf die Grundlagen der Auswahl einer wissenschaftlichen Arbeit und </p><p>Die Implementierung erfolgt zwar korrekt, deckt aber nicht alle Aspekte der tatsächlichen Bereitstellung des Algorithmus ab. Wenn der Algorithmus beispielsweise nur eine Komponente in einem komplexen System ist, wie können wir sicherstellen, dass Änderungen an dieser Komponente zu Verbesserungen des gesamten Systems führen? Und was ist, wenn die Integration des Algorithmus wesentliche Änderungen oder Erweiterungen erfordert, die in der Originalveröffentlichung nicht behandelt werden? Dies sind wichtige Themen, über die wir in zukünftigen Beiträgen gerne mehr berichten werden.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/implementing-academic-papers-lessons-learned-from-elasticsearch-and-lucene</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/implementing-academic-papers-lessons-learned-from-elasticsearch-and-lucene</guid>
    <category><![CDATA[ML-Forschung]]></category>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Julie Tibshirani]]></dc:creator>
    <pubDate>Wed, 29 Sep 2021 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>