LINQ zu ES|QL: C# schreiben, Elasticsearch abfragen

Erkundung des neuen LINQ to ES|QL-Providers im Elasticsearch .NET Client, mit dem Sie C#-Code schreiben können, der automatisch in ES|QL-Abfragen übersetzt wird.

Ab v9.3.4 und v8.19.18 enthält der Elasticsearch .NET-Client einen Language Integrated Query (LINQ)-Provider, der C# LINQ-Ausdrücke in die Elasticsearch Query Language (ES|QL)-Abfragen zur Laufzeit übersetzt. Anstatt ES|QL-Zeichenfolgen von Hand zu schreiben, stellen Sie Abfragen mit Where, Select, OrderBy, GroupBy und anderen Standardoperatoren zusammen. Der Anbieter kümmert sich um die Übersetzung, Parametrisierung und Deserialisierung der Ergebnisse, einschließlich des zeilenweisen Streamings, das die Speichernutzung unabhängig von der Größe der Ergebnismenge konstant hält.

Ihre erste Abfrage

Definieren Sie zunächst ein einfaches altes CLR-Objekt (POCO), das auf Ihren Elasticsearch-Index abgebildet wird. Eigenschaftsnamen werden über Standardattribute System.Text.Json , wie z. B. [JsonPropertyName], oder über ein konfiguriertes JsonNamingPolicy in ES|QL-Spaltennamen aufgelöst. Die gleichen Regeln für die Quellenserialisierung, die für den Rest des Clients gelten, gelten auch hier.

Nachdem der Typ festgelegt wurde, sieht eine Abfrage folgendermaßen aus:

Der Anbieter übersetzt dies in folgende ES|QL:

Ein paar Details, die zu beachten sind:

  • Namensauflösung der Eigenschaften: p.Price wird aufgrund des Attributs [JsonPropertyName] zu price_usd , und p.Brand wird gemäß der Standard-CamelCase-Namenskonvention zu brand .
  • Parametererfassung: Die C#-Variablen minPrice und brand werden als benannte Parameter erfasst (?minPrice, ?brand). Sie werden separat von der Abfragezeichenfolge im JSON-Payload gesendet, was Injektionen verhindert und serverseitiges Abfrageplan-Caching ermöglicht.
  • Streaming: QueryAsync<T> gibtIAsyncEnumerable<T> zurück. Zeilen werden einzeln materialisiert, sobald sie von Elasticsearch eintreffen.

Sie können auch die generierte Abfrage und ihre Parameter inspizieren, ohne sie auszuführen:

Wie funktioniert das? Eine kurze LINQ-Auffrischung

Der Mechanismus, der LINQ-Anbieter ermöglicht, ist die Unterscheidung zwischen IEnumerable<T> und IQueryable<T>.

Wenn Sie.Where(p => p.Price > 100) auf einem IEnumerable<T>aufrufen, kompiliert die Lambda zu einem Func<Product, bool>, einem regulären Delegierten, den die Laufzeit während des Prozesses ausführt. Dies ist LINQ-to-Objects.

Wenn Sie dieselbe Methode auf einem IQueryable<T>aufrufen, umwickelt der C#-Compiler stattdessen das Lambda in einem Expression<Func<Product, bool>> . Dies ist eine Datenstruktur, die die Struktur des Codes repräsentiert und nicht seine ausführbare Form. Der Ausdrucksbaum kann zur Laufzeit inspiziert, analysiert und in eine andere Sprache übersetzt werden.

Die IQueryProvider Schnittstelle ist der Erweiterungspunkt. Jeder Anbieter kann CreateQuery<T> und Execute<T> implementieren, um diese Ausdrucksbäume in eine Zielsprache zu übersetzen. Entity Framework verwendet dies, um SQL auszugeben. Der LINQ to ES|QL-Anbieter verwendet es, um ES|QL zu emittieren.

Der Ausdrucksbaum für die obige Abfrage sieht wie folgt aus:

Ausdrucksbaum für die Beispielabfrage.

Der Baum ist von innen nach außen verschachtelt: Take umschließt OrderByDescending, welches Where umschließt, welches From umschließt, welches die Wurzel EsqlQueryable<Product> Konstante umschließt. Das Where -Prädikat ist selbst ein Teilbaum von BinaryExpression Knoten für die Operatoren &&, >=, und == , mit MemberExpression Blättern für Eigenschaften-Zugriffe und Abschluss-Erfassungen für die Variablen minPrice und brand . Dies ist die Datenstruktur, die der Provider durchläuft, um das endgültige ES|QL zu erzeugen.

Unter der Haube: Die Übersetzungspipeline

Der Weg von einem LINQ-Ausdruck zu den Abfrageergebnissen folgt einer sechsstufigen Pipeline:

Überblick über die Übersetzungspipeline.

1. Erfassung des Ausdrucksbaums

Wenn man .Where(), .OrderBy(), .Take() und andere Operatoren auf einem IQueryable<T> verkettet, erstellt die Standard-LINQ-Infrastruktur einen Ausdrucksbaum. EsqlQueryable<T> implementiert IQueryable<T> und delegiert an EsqlQueryProvider.

2. Übersetzung

Wenn die Abfrage ausgeführt wird (durch Enumerieren, Aufrufen von ToList() oder Verwenden von await foreach)), durchläuft EsqlExpressionVisitor den Ausdrucksbaum von innen nach außen. Es sendet jeden LINQ-Methodenaufruf an einen spezialisierten Besucher:

BesucherÜbersetztIn
WhereClauseVisitor.Where(predicate)WHERE-Bedingung
SelectProjectionVisitor.Select(selector)EVAL + KEEP + RENAME
GroupByVisitor.GroupBy().Select()STATISTIKEN ... NACH
OrderByVisitor.OrderBy() / .ThenBy()SORT field [ASC\|DESC]
EsqlFunctionTranslatorEsqlFunctions.*, Math.*, string methods80+ ES|QL functions

Bei der Übersetzung werden in Ausdrücken referenzierte C#-Variablen als benannte Parameter erfasst.

3. Abfragemodell

Die Besucher produzieren nicht direkt Zeichenfolgen. Stattdessen produzieren sie QueryCommand Objekte, eine unveränderliche Zwischenrepräsentation. Ein FromCommand, ein WhereCommand, ein SortCommand und ein LimitCommand, jeweils einen ES|QL-Verarbeitungsbefehl repräsentierend. Diese werden in einem EsqlQuery Modell gesammelt.

Abfragemodell und Befehlsmuster.

Dieses Zwischenmodell ist vom Ausdrucksbaum und vom Ausgangsformat entkoppelt. Es kann inspiziert, abgefangen (über IEsqlQueryInterceptor) oder vor der Formatierung modifiziert werden.

4. Formatierung

EsqlFormatter besucht jede QueryCommand in der richtigen Reihenfolge und erstellt die finale ES|QL-Zeichenfolge. Jeder Befehl wird zu einer Zeile, getrennt durch den Pipe-Operator (|), den ES|QL zur Verkettung von Verarbeitungsbefehlen verwendet. Bezeichner, die Sonderzeichen enthalten, werden automatisch mit Backticks versehen.

5. Ausführung

Die formatierte ES|QL-Zeichenfolge und erfasste Parameter werden als JSON-Payload an den /_query -Endpunkt von Elasticsearch gesendet. Die Schnittstelle IEsqlQueryExecutor abstrahiert die Transportschicht, an der die geschichtete Paketarchitektur zum Tragen kommt.

6. Materialisierung

EsqlResponseReader streamt die JSON-Reaktion, ohne das gesamte Ergebnis-Set in den Speicher zu puffern. Ein ColumnLayout -Baum, der einmal pro Abfrage vorab berechnet wird, ordnet flache ES|QL-Spaltennamen (wie address.street, address.city) verschachtelten POCO-Eigenschaften zu. Jede Zeile wird zu einer T -Instanz zusammengestellt und einzeln über IEnumerable<T> oder IAsyncEnumerable<T> zurückgegeben.

Die mehrschichtige Architektur

Die LINQ-to-ES|QL-Funktionalität ist auf drei Pakete aufgeteilt:

Paketarchitektur.
Elastic.Esql ist die reine Übersetzungsmaschine. Es hat keine HTTP-Abhängigkeiten und enthält die Expression Visitors, das Abfragemodell, den Formatter und den Response Reader. Sie können es eigenständig verwenden, um ES|QL-Abfragen ohne Elasticsearch-Verbindung zu erstellen und zu inspizieren, was für Tests, Abfrageprotokollierung oder den Aufbau einer eigenen Ausführungsschicht nützlich ist.

Elastic.Clients.Esql ist ein leichter, eigenständiger ES|QL-Client. Es fügt die HTTP-Ausführung über Elastic.Esql mittels Elastic.Transport hinzu. Wenn Ihre Anwendung nur ES|QL und keine der anderen Elasticsearch-APIs benötigt, ist dies die Option mit den geringsten Abhängigkeiten.

Elastic.Clients.Elasticsearch ist der vollständige Elasticsearch.NET-Client. Es baut außerdem auf Elastic.Esql auf und stellt den LINQ-Provider über den Namespace client.Esql bereit. Dies ist der empfohlene Einstieg für die meisten Anwendungen.

Beide Ausführungsschicht-Pakete bieten ihre eigene Implementierung von IEsqlQueryExecutor, der Strategieschnittstelle, die Übersetzung und Transport miteinander verbindet.

Alle drei Pakete sind mit Native AOT kompatibel, wenn sie mit einem quellgenerierten JsonSerializerContext verwendet werden. Für den vollständigen Client siehe die Native AOT-Dokumentation.

Über die Grundlagen hinaus

Das obige Beispiel umfasste das Filtern, Sortieren und Paginieren. Der Anbieter unterstützt ein breiteres Spektrum an Operationen.

Aggregationen

GroupBy, kombiniert mit Aggregatfunktionen in Select, übersetzt in ES|QL STATS ... BY:

Projektionen

Select, mit anonymen Typen erzeugt die Befehle EVAL, KEEP und RENAME:

Umfassende Funktionsbibliothek

Über 80 ES|QL-Funktionen sind über die EsqlFunctions -Klasse verfügbar und decken Datum/Uhrzeit, Zeichenfolgen, Mathematik, IP, Musterabstimmung und Wertung ab. Die Standardmethoden Math.* und string.* werden ebenfalls übersetzt:

LOOKUP JOIN

Indexübergreifende Suchvorgänge werden in ES|QL LOOKUP JOIN übersetzt:

Raw ES|QL Umgehungsmöglichkeit

Für ES|QL-Features, die vom LINQ-Anbieter noch nicht unterstützt werden, können Sie Rohfragmente anfügen:

Serverseitige asynchrone Abfragen

Bei langlaufenden Abfragen sollten diese zur Hintergrundverarbeitung auf dem Server eingereicht werden:

Serverseitige asynchrone Abfragen sind besonders nützlich für langlaufende analytische Abfragen / die Verarbeitung großer Datensätze, die die typischen Timeout-Schwellenwerte überschreiten könnten, oder in Timeout-sensiblen Umgebungen mit Load-Balancern, API-Gateways oder Proxys, die strikte HTTP-Timeouts durchsetzen. Asynchrone Abfragen vermeiden Verbindungsabbrüche, indem die Übermittlung von dem Abruf der Ergebnisse entkoppelt wird.

Erste Schritte

LINQ to ES|QL ist verfügbar ab:

  • Elastic.Clients.Elasticsearch v9.3.4 (9.x branch)
  • Elastic.Clients.Elasticsearch v8.19.18 (8.x Branch)

Installation über NuGet:

dotnet add package Elastic.Clients.Elasticsearch

Die Einstiegspunkte befinden sich auf client.Esql:

MethodeRückgabenAnwendungsfall
Query<T>(...)IEnumerable<T>Synchrone Ausführung
QueryAsync<T>(...)IAsyncEnumerable<T>Asynchrones Streaming
CreateQuery<T>()IEsqlQueryable<T>Erweiterte Zusammensetzung und Inspektion
SubmitAsyncQueryAsync<T>(...)EsqlAsyncQuery<T>Langlaufende serverseitige Abfragen

Eine vollständige Feature-Referenz – einschließlich Abfrageoptionen, Zugriff auf mehrere Felder, verschachtelter Objekte und der Verarbeitung mehrwertiger Felder – finden Sie in der LINQ to ES|QL-Dokumentation.

Fazit

LINQ to ES|QL bringt die volle Ausdruckskraft von C# LINQ in die ES|QL-Abfragesprache von Elasticsearch, sodass Sie stark typisierte, kombinierbare Abfragen schreiben können, ohne Abfragezeichenfolgen von Hand erstellen zu müssen. Mit automatischer Parametererfassung, Streaming-Materialisierung und einer geschichteten Paketarchitektur, die von eigenständiger Übersetzung zum vollständigen Elasticsearch-Client skaliert, fügt es sich natürlich in .NET-Anwendungen jeder Größe ein. Installieren Sie den neuesten Client, verweisen Sie Ihre LINQ-Ausdrücke auf einen Index und überlassen Sie den Rest dem Provider.

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