Wie wir Elasticsearch simdvec entwickelten, um die Vektorsuche zu einer der schnellsten weltweit zu machen

Wie wir Elasticsearch simdvec entwickelten, die von Hand optimierte SIMD-Kernel-Bibliothek hinter jeder Vektorsuchanfrage in Elasticsearch.

Elasticsearch simdvec ist die Engine hinter jeder Vektordistanzberechnung in Elasticsearch. Sie bietet von Hand optimierte AVX-512- und NEON-Kernel für jeden von Elasticsearch unterstützten Vektortyp. Ihre Bulk-Scoring-Architektur verbirgt Speicherlatenz durch explizites Prefetching auf x86 und verschachteltes Laden auf ARM und übertrifft Bibliotheken wie FAISS und jvector um das bis zu Vierfache, wenn Daten den CPU-Cache überschreiten. In diesem Beitrag geht es darum, warum wir sie entwickelt haben, was darin steckt und wie sie die Vektorsuche in Elasticsearch zu einer der schnellsten weltweit macht.

Wie wir Elasticsearch simdvec entwickelten

Jede Vektorsuchanfrage in Elasticsearch, sei es ein Hierarchical Navigable Small World (HNSW) Traversal, ein Inverted File (IVF) Scan oder ein Reranking-Pass, reduziert sich auf dasselbe Problem: die Berechnung der Distanzen zwischen Vektoren, und zwar millionenfach pro Anfrage. Elasticsearch unterstützt eine breite Palette von Datentypen und Quantisierungsstrategien, von float32 über int8, bfloat16, binär und Better Binary Quantization (BBQ). Jede Option bringt unterschiedliche Kompromisse zwischen Speicher, Durchsatz und Abruf mit sich. Hinter all dem steht eine einzige Engine: simdvec.

Wir haben simdvec so entwickelt, dass jede Entfernungsberechnung so schnell erfolgt, wie es die Hardware erlaubt. In diesem Beitrag geht es darum, warum wir sie entwickelt haben, was in ihr steckt und wo sie die größte Wirkung erzielt.

Gebaut wie ein Rennwagen

Als Formel-1-Enthusiasten, mit einer Person, die bereits mit dem Formel-1-Team von Ferrari gearbeitet hat, sehen wir eine klare Parallele. Ein Formel-1-Wagen wird mit einem einzigen Ziel entwickelt: die beste Rundenzeit zu erzielen. Motorleistung, Aerodynamik und Fahrwerkskonstruktion spielen nur insofern eine Rolle, als dass sie zu diesem Ergebnis beitragen. Das Gleiche gilt für eine Vektordatenbank, in der Indexierungsdurchsatz, Abfragelatenz und Rückruf den Erfolg definieren.

Das Endergebnis ist zwar wichtig, aber um ein Höchstmaß an Leistung zu erreichen, muss jede Komponente ihr Bestes geben. Es reicht nicht, wenn sie einfach nur gut sind, sie müssen die Besten in ihrer Kategorie sein. Simdvec wurde mit dieser Denkweise entwickelt und konzentriert sich auf einen entscheidenden Teil des Systems: die Engine. Sie ist eine zweckgebundene, Single Instruction Multiple Data (SIMD) optimierte Kernel-Bibliothek, die von Hand optimierte, native C++ Distanzfunktionen bereitstellt, die von Java über die Panama Foreign Function Interface (FFI) aufgerufen werden. Sie unterstützt Bulk-Scoring, Prefetching von Cache-Linien sowie alle in Elasticsearch verwendeten Vektortypen und Layouts.

Das ist der Motor hinter jeder Abfrage.

Warum wir unser eigenes System entwickelt haben

Wir haben 2023 mit der Panama Vector API in Apache Lucene begonnen. Für float32-Punktprodukte war das zunächst ideal, doch die Anforderungen von Elasticsearch überstiegen schnell die Möglichkeiten des Systems. Elasticsearch unterstützt eine breite Palette quantisierter Vektortypen: int8, int4, bfloat16, Single-Bit und asymmetrisches BBQ. Jedes System verfügt über unterschiedliche SIMD-Strategien, Packungs- und Akkumulatoranforderungen. Über die Typabdeckung hinaus verlangen die Scoring-Pfade von Elasticsearch mehr als einen Durchsatz für einzelne Paare: HNSW muss mehrere Graphnachbarn in einem Durchgang bewerten, IVF benötigt eine Massenbewertung von Tausenden von Kandidaten mit Vorabruf und die auf Festplatten basierte Bewertung muss direkt auf dem mmap-Speicher arbeiten, ohne zu kopieren. Kein Angebot auf dem Markt deckte das gesamte Spektrum ab.

Also haben wir simdvec entwickelt: Von Hand optimierte, native C++-Kernel, die über FFI aus Java aufgerufen werden, mit Bulk-Scoring, Prefetching und Unterstützung für jeden von Elasticsearch verwendeten Vektortyp. Als Inhaber der Bibliothek kontrollieren wir den gesamten Stack. Wenn wir einen neuen Quantisierungstyp wie BBQ hinzufügen, wird ein optimierter SIMD-Kernel im gesamten System integriert. Wir warten nicht auf die Unterstützung durch eine Upstream-Bibliothek und gehen absolut keine Kompromisse bei der Leistung ein. Jede Vektorabfrage in Elasticsearch, ob HNSW, IVF, Reranking oder Hybrid, läuft auf dieser Engine, die auf den von uns tatsächlich verwendeten Operationen und Typen aufbaut.

Simdvec verfügt über separate native Bibliotheken für x86 und ARM, wobei jeweils mehrere ISA-Ebenen (Instruction Set Architecture) beim Starten ausgewählt werden. Der Call-Overhead von Java über FFI ist mit einstelligen Nanosekunden sehr gering.

Die Landschaft

Wir sind nicht die Einzigen, die SIMD-optimierte Vektordistanz-Kernel erstellen. Das Ökosystem ist vielfältig und wir wollten verstehen, wie simdvec funktioniert. Nicht um Projekte zu bewerten, sondern um Kontext zu liefern und zu erklären, wo die Elasticsearch-Engine angesiedelt ist. Wir haben drei Projekte als Referenzpunkte ausgewählt, die jeweils einen anderen Ansatz repräsentieren:

  • jvector: Eine Java-Bibliothek für Approximate Nearest Neighbor (ANN), die die Panama Vector API für vektorisierte Entfernungsberechnungen verwendet, mit optionaler nativer C-Beschleunigung auf x86.
  • FAISS: Ein weit verbreitetes Open-Source-Framework zur Vektorsuche mit von Hand optimierten AVX2/AVX-512-Kernel.
  • NumKong (ehemals SimSIMD): Eine umfassende Suite von über 2.000 von Hand optimierten SIMD-Kernel über Distanzfunktionen, Matrixoperationen und georäumliche Berechnungen.

Jedes Projekt verfolgt einen anderen Zweck und geht unterschiedliche Kompromisse ein. Wir fügen daraus Referenznummern hinzu, um Kontext für die Leistung von simdvec bei den spezifischen Abläufen zu erhalten, die Elasticsearch benötigt.

Wie wir messen

Die simdvec- und jvector-Benchmarks wurden in Java mit JMH, dem Standard JVM-Microbenchmark-Harness, geschrieben, einschließlich FFI-Overhead. Für die NumKong-Benchmarks und die FAISS-Benchmarks haben wir mit Google Benchmark, dem Standard-C++-Mikrobenchmark-Framework, kleine C/C++-Harnesses geschrieben. Beide Frameworks melden Nanosekunden pro Operation mit Aufwärmung und Iterationskalibrierung. Wir haben anhand von Hardware-Leistungszählern überprüft, dass alle Bibliotheken auf beiden Plattformen SIMD verwenden. Der gesamte Benchmark-Code ist in den verlinkten GitHub-Repositories (und im Fall von simdvec im Elasticsearch-Repository) öffentlich verfügbar.

Software: JDK 25.0.2, JMH 1.37, GCC 14, Google Benchmark (neueste Version).

Ein Vektor nach dem anderen

Die grundlegendste Funktion der Vektorsuche ist die Berechnung des Abstands zwischen zwei Vektoren. Jede HNSW-Nachbarbewertung, jede IVF-Kandidatenbewertung und jeder Reranking-Vergleich dreht sich um diese interne Schleife.

Wir haben den Durchsatz einzelner Datenpaare in 1024 Dimensionen auf beiden Plattformen gemessen, beginnend mit float32, dem Basistyp, bei dem das Ökosystem am wettbewerbsfähigsten ist. Wir vergleichen simdvec mit FAISS und jvector, wobei wir NumKong ausgeschlossen haben, da es float64-Akkumulatoren für float32 verwendet, was es je nach Plattform 3,2- bis 5,3-mal langsamer macht und numerische Präzision über Durchsatz gestellt wird. Um den Vergleich nicht zu verfälschen, testen wir NumKong stattdessen auf int8, wo es die gleiche Akkumulatorstrategie wie simdvec verwendet.

Auf x86 ist FAISS AVX-512 der schnellste Einzelpaar-Kernel mit 23 ns. Simdvec AVX-512 folgt mit 28 ns, eine Lücke, die den Overhead des FFI-Aufrufs widerspiegelt. Beide verwenden 512-Bit-FMA mit Multi-Akkumulator-Unrolling. Auf der AVX2-Ebene liegen die beiden sehr viel näher beieinander, nämlich 36 ns bzw. 39 ns, die beide durch die 256-Bit-Register- und Speicherladebreiten eingeschränkt sind. jvector landet bei 44 ns mit der Java Panama Vector API. Panama generiert guten SIMD-Code, aber von Hand optimierte C++-Intrinsics sind hier im Vorteil

Auf ARM liegt simdvec mit 70 ns deutlich vor jvector mit 110 ns und FAISS mit 156 ns. Simdvec verfügt über von Hand optimierte NEON-Kernel für aarch64. Jvector hat keinen nativen ARM-Code und basiert auf Panama. FAISS verlässt sich auf die automatische Vektorisierung des Compilers und nicht auf explizite NEON-Intrinsics, was den größeren Abstand erklärt. Dies spiegelt einen praktischen Vorteil des Besitzes der Kernel-Bibliothek wider: Als Elasticsearch zu Graviton erweitert wurde, fügten wir eigens entwickelte NEON-Kernel hinzu. Weder jvector noch FAISS haben nativem ARM-Code den gleichen Stellenwert eingeräumt.

Elasticsearch bewertet aber nicht nur float32. Int8-Quantisierung reduziert den Speicher um das Vierfache, bfloat16 um das Doppelte und BBQ um das 32-Fache. Jeder Typ benötigt seine eigene SIMD-Strategie und simdvec bietet von Hand optimierte, native Kernel für alle diese Typen.

Von den Bibliotheken, die wir verglichen haben, hat nur NumKong vergleichbare Kernel für int8. Wir haben das int8-Skalarprodukt, die quadrierte euklidische Distanz und den Kosinus bei 1024 Dimensionen gemessen.

Int8 Einzelpaar-Wertung (1024 Dimensionen, ns/vec op – je niedriger, desto besser)

Auf beiden Architekturen ist NumKong bei kleinen bis mittleren Dimensionen gleich schnell oder sogar schneller, wobei der Unterschied hauptsächlich auf den geringeren Call-Overhead zurückzuführen ist (direkter C-Call vs. Java FFI). Bei größeren Dimensionen holt simdvec auf, wobei die effizientere Kernel-Implementierung (die Cascade Unrolling verwendet) die Aufrufkosten amortisiert: Mit zunehmender Dimension schließt sich diese Lücke und kehrt sich schließlich um. Die Übergangsdimensionen liegen je nach Funktion und Architektur zwischen 768 und 1536.

Trotz des etwas höheren Aufwands von Java FFI befindet sich simdvec mit hochoptimierten C/C++-Bibliotheken auf Augenhöhe. Sie ist nicht nur die einzige Bibliothek mit optimierten Kernel für float32 und int8, sondern führt auch beim ARM und liegt auf x86 (für float32) nur leicht hinter FAISS zurück und auf beiden Architekturen sehr nahe an NumKong (für int8). Für bfloat16, int4, binär und BBQ gibt es zwar Alternativen, aber simdvec zeichnet sich durch von Hand optimierte SIMD aus, die auf das Datenlayout jedes Typs zugeschnitten ist.

Eine Produktionssuchmaschine bewertet jedoch nicht einen Vektor nach dem anderen, sondern Tausende pro Abfrage. Die nächste Frage ist, was in diesem Maßstab passiert.

Tausende auf einmal

Die Leistung eines einzelnen Paares stellt nur einen Teil des Gesamtbildes dar. In der Praxis ist entscheidend, wie sich Systeme unter Last verhalten. Eine einzelne HNSW-Abfrage kann Hunderte von Nachbarn im Graphen bewerten. Ein IVF-Scan kann Tausende von Einträgen in Posting-Listen bewerten. Ein Reranking-Durchlauf kann Zehntausende von Kandidaten bewerten. Der Durchsatz pro Paar ist wichtig, aber noch wichtiger ist, wie schnell Sie viele Vektoren bewerten können und wie gut die Leistung nachlässt, wenn das Working Set nicht mehr in die CPU-Caches passt.

Simdvec bietet Bulk-Scoring für jeden Datentyp. Dabei handelt es sich nicht nur um Schleifen über Single-Pair-Kerne, sondern auch um innere Schleifen mit mehreren Akkumulatoren, die den Abfragevektor einmal pro Dimensionsschritt laden und ihn auf mehrere Dokumentvektoren verteilen, mit explizitem Cache-Line-Prefetching für den nächsten Batch. Weder jvector noch FAISS bieten ein Äquivalent (zum jetzigen Zeitpunkt). Jvector hat keine Bulk-API, sodass der Aufrufer ein Paar nach dem anderen in einer Schleife bewertet. FAISS stellt fvec_inner_products_ny zur Verfügung, das zum Zeitpunkt der Erstellung dieses Artikels als Schleife über die Single-Pair-Distanzfunktion ohne Amortisierung der Abfrage oder Vorabruf implementiert ist.

Float32. Um die Wirkung auf Kernel-Ebene zu messen, bewerteten wir eine einzelne Abfrage gegen zunehmende Zahlen von 1024-Dimension-float32-Dokumentvektoren mit Random-Access-Mustern, die HNSW-ähnliche verstreute Graph-Nachbarschaftsabfragen simulieren. Die drei Datensatzgrößen von 32, 625 und 32.500 Vektoren werden so gewählt, dass der Arbeitssatz den L1-, L2- und L3-Cache übersteigt.

Wenn die Daten in den Cache passen, ist simdvec auf beiden Plattformen am schnellsten, aber die Margen sind moderat, da die Kernel-Arithmetik dominiert. Die tatsächliche Trennung zeigt sich, wenn der Arbeitssatz über L3 hinauswächst. Auf x86 erzielt simdvec 95 ns pro Vektor, während FAISS 165 ns und jvector 412 ns benötigt. Auf ARM ist das Muster dasselbe: simdvec bleibt bei 162 ns, während FAISS auf 347 ns und jvector auf 476 ns ansteigt. Das Prefetching und die Abfrage-Amortisierung in simdvec verbergen die Speicherlatenz auf eine Weise, die eine einfache Schleife über Einzelpaar-Kernel nicht erreichen kann, und der Vorteil erweitert sich genau dort, wo reale Suchworkloads tief im Hauptspeicher arbeiten.

Int8. Das gleiche Muster gilt für quantisierte Datentypen. Wir haben die Bulk-Bewertung von int8-Punktprodukten in 1024 Dimensionen mit Datensatzgrößen gemessen, die so gewählt wurden, dass sie die gleichen L1-, L2- und L3-Cache-Grenzen überschritten, und die Bulk-Bewertung von simdvec mit der Single-Pair-Bewertung von NumKong in einer Schleife verglichen.

Auf x86 ist simdvec zwischen 1,2-mal und 1,9-mal schneller, was auf die Kombination aus explizitem Prefetching und Batch-Verarbeitung zurückzuführen ist. Auf ARM ist simdvec bei allen Datensätzen erneut im Vorteil (1,7- bis 1,9-mal schneller). Der Vorteil liegt in der Batch-Verarbeitung von vier Vektoren auf einmal, die über ein verschachteltes Zugriffsmuster Parallelisierung auf Speicherebene bietet. In beiden Fällen ist das auffälligste Ergebnis, was bei der größten Datensatzgröße passiert, wo es am wichtigsten ist.

Die Ergebnisse für quadrierten Abstand und Kosinus zeigen ein ähnliches Muster, mit Beschleunigungen von 1,4x bis 1,8x für ARM und von 1,3x bis 3,0x für x86 (mehr dazu hier).

Wo Speicher eine Rolle spielt

Vektorindizes in der Produktion passen normalerweise nicht in den CPU-Cache. Ein 10M-Vektor-Int8-Index bei 1024 Dimensionen entspricht 10 GB. Die Bewertung von Kandidaten bedeutet das Streamen von Daten aus dem DRAM, und genau hier macht die Architektur für die Massenbewertung den Unterschied.

Wir verwendeten Hardware-Leistungszähler, um zu messen, was während der Massenbewertung innerhalb der CPU passiert, und stellten fest, dass das Verbergen der Speicherlatenz zwei grundlegend verschiedene Strategien erfordert: eine pro Architektur.

Auf x86 eliminiert explizites Prefetching Cache-Fehler. Der Bulk-Kernel verarbeitet Vektoren sequenziell – einen nach dem anderen vollständig berechnet – während er gleichzeitig Prefetch-Anweisungen für den nächsten Batch ausgibt. Künftige Daten werden in den L1-Cache geladen, bevor die CPU sie benötigt.

Auf ARM funktionierte derselbe sequentielle Ansatz nicht so gut, selbst mit Prefetching. Stattdessen verschachtelt der Bulk-Kernel Ladungen von vier Vektoren an jeder Schrittposition, wodurch die Out-of-Order-Engine vier unabhängige Speicherstreams erhält. Die CPU holt Daten nicht schneller, sondern wartet vielmehr weniger, weil sie während der Bearbeitung von Speicheranfragen immer etwas anderes zu berechnen hat. Eine detaillierte Analyse finden Sie in diesem GitHub-Issue.

Die Zahlen erzählen zwei unterschiedliche Geschichten:

  1. Auf x86 verwandelt Prefetching 139K Cache-Fehler in 19K und die Anweisungen pro Zyklus (IPC) werden mehr als verdoppelt. Der Massenvorteil wächst mit der Größe der Datensätze, von 1,2-mal in L2 bis 2,8-mal jenseits von L3, weil das Prefetching zunehmend teurere DRAM-Roundtrips verbirgt.
  2. Bei ARM ändern sich Cache-Fehler kaum. Was sich ändert, ist die Auslastung: Backend-Verzögerungen sinken um 40 %, da das verschachtelte Zugriffsmuster die Pipeline füttert. Dieser Vorteil bleibt konstant bei 1,8-mal, unabhängig von der Größe der Datensätze, da die Parallelität auf Speicherebene gilt, unabhängig davon, ob Daten aus dem Cache oder dem DRAM stammen.

Zwei Architekturen, zwei Strategien, ein Ergebnis: Im Produktionsmaßstab beschäftigt simdvec die CPU-Pipeline durchgehend, selbst wenn die Vektoren über den Hauptspeicher verteilt sind.

Was das für Elasticsearch-Nutzer bedeutet

Diese Fähigkeiten auf Kernel-Ebene verstärken sich gegenseitig. Eine einzelne Vektorsuchabfrage kann Millionen von Distanzoperationen berechnen: HNSW-Graph-Durchläufe, Kandidatenbewertung, Neusortierung. Bei Tausenden gleichzeitiger Abfragen lassen sich Nanosekunden pro Operation direkt in Abfragelatenz und Cluster-Durchsatz umrechnen. Egal ob Sie float32, int8, bfloat16 oder BBQ verwenden, egal ob sich Ihr Index im Arbeitsspeicher oder auf der Festplatte befindet, simdvec ist die zugrunde liegende Engine, wobei jede dieser Operationen über dieselbe Engine läuft, die bis auf die letzte Nanosekunde genau abgestimmt ist.

Die wichtigste Erkenntnis ist, dass die Leistung der Vektorsuche im Produktionsmaßstab nicht primär durch den reinen SIMD-Durchsatz bestimmt wird. Entscheidend ist, wie effizient das System die Speicherlatenz verbirgt und gleichzeitig die Rechenleistung über Millionen kleiner Operationen hinweg aufrechterhält.

Die simdvec-Kernel werden mit fast jeder Elasticsearch-Version verbessert. Wenn neue Quantisierungstypen und Hardwareplattformen entstehen, erhalten sie von Anfang an optimierte Kernel. Und die bestehenden Typen werden immer schneller, während wir die bereits ausgelieferten Implementierungen optimieren.

Wie hilfreich war dieser Inhalt?

Nicht hilfreich

Einigermaßen hilfreich

Sehr hilfreich

Zugehörige Inhalte

Sind Sie bereit, hochmoderne Sucherlebnisse zu schaffen?

Eine ausreichend fortgeschrittene Suche kann nicht durch die Bemühungen einer einzelnen Person erreicht werden. Elasticsearch wird von Datenwissenschaftlern, ML-Ops-Experten, Ingenieuren und vielen anderen unterstützt, die genauso leidenschaftlich an der Suche interessiert sind wie Sie. Lasst uns in Kontakt treten und zusammenarbeiten, um das magische Sucherlebnis zu schaffen, das Ihnen die gewünschten Ergebnisse liefert.

Probieren Sie es selbst aus