LINQ to ES|QL : écrire du C#, interroger Elasticsearch

Exploration du nouveau fournisseur LINQ to ES|QL dans le client Elasticsearch .NET, qui vous permet d'écrire du code C# automatiquement traduit en requêtes ES|QL.

À partir des versions 9.3.4 et 8.19.18, le client .NET Elasticsearch inclut un fournisseur LINQ (Language Integrated Query) qui traduit les expressions LINQ C# en requêtes ES|QL (Elasticsearch Query Language) à l'exécution. Au lieu d'écrire manuellement des chaînes ES|QL, vous composez vos requêtes à l'aide des fonctions Where, Select, OrderBy, GroupBy et d'autres opérateurs standard. Le fournisseur se charge de la traduction, du paramétrage et de la désérialisation des résultats, y compris le flux par ligne qui maintient l'utilisation de la mémoire constante, quelle que soit la taille de l'ensemble des résultats.

Votre première requête

Commencez par définir un objet CLR (POCO) classique qui correspond à votre index Elasticsearch. Les noms de propriétés sont résolus en noms de colonnes ES|QL via des attributs standard System.Text.Json, comme [JsonPropertyName], ou via un JsonNamingPolicy configuré. Les mêmes règles de sérialisation des sources que celles qui s'appliquent au reste du client s'appliquent également ici.

Une fois le type défini, une requête ressemble à ceci :

Le fournisseur la traduit en ES|QL comme ceci :

Quelques détails à noter :

  • Résolution des noms de propriété : p.Price devient price_usd en raison de l'attribut [JsonPropertyName], et p.Brand devient brand conformément à la politique de dénomination camelCase par défaut.
  • Capture des paramètres : les variables C# minPrice et brand sont capturées comme paramètres nommés (?minPrice, ?brand). Elles sont envoyées séparément de la chaîne de requête dans la charge utile JSON, ce qui empêche l'injection et permet la mise en cache du plan de requête côté serveur.
  • Flux en continu : QueryAsync<T> renvoie IAsyncEnumerable<T>. Les lignes se matérialisent une à une à mesure de leur arrivée depuis Elasticsearch.

Vous pouvez également inspecter la requête générée et ses paramètres sans l'exécuter :

Comment ça marche ? Petit rappel sur LINQ

Le mécanisme qui rend possibles les fournisseurs LINQ est la distinction entre IEnumerable<T> et IQueryable<T>.

Lorsque vous appelez .Where(p => p.Price > 100) sur un IEnumerable<T>, la lambda est compilée en un Func<Product, bool>, un délégué standard que le runtime exécute en interne. C'est le principe du LINQ-to-Objects.

Lorsque vous appelez la même méthode sur un IQueryable<T>, le compilateur C# enveloppe la lambda dans un Expression<Func<Product, bool>> à la place. Il s'agit d'une structure de données qui représente la structure du code plutôt que sa forme exécutable. L'arbre d'expression peut être inspecté, analysé et traduit dans un autre langage au moment de l'exécution.

L’interface IQueryProvider est le point d’extension. Tout fournisseur peut implémenter CreateQuery<T> et Execute<T> pour traduire ces arbres d’expressions dans une langue cible. Entity Framework utilise ceci pour émettre du SQL. Le fournisseur LINQ to ES|QL l'utilise pour émettre ES|QL.

L'arbre d'expression de la requête ci-dessus ressemble à ceci :

Arbre d'expression de l'exemple de requête.

L'arbre est imbriqué de l'intérieur vers l'extérieur : Take englobe OrderByDescending, qui englobe Where, qui englobe From, qui englobe la racine constante EsqlQueryable<Product>. Le prédicat Where est lui-même un sous-arbre des nœuds BinaryExpression pour les opérateurs &&, >=, et les opérateurs ==, avec des feuilles MemberExpression pour les accès aux propriétés et des captures de fermeture pour les variables minPrice et brand. C'est cette structure de données que le fournisseur parcourt pour produire le code ES|QL final.

Sous le capot : le pipeline de traduction

Le chemin d'une expression LINQ vers les résultats de la requête suit un pipeline en six étapes :

Aperçu du pipeline de traduction.

1. Capture de l'arbre d'expressions

Lorsque vous chaînez .Where(), .OrderBy(), .Take() et d’autres opérateurs sur un IQueryable<T>, l’infrastructure standard de LINQ construit un arbre d’expressions. EsqlQueryable<T> met en œuvre IQueryable<T> et délègue à EsqlQueryProvider.

2. Traduction

Lors de l'exécution de la requête (par énumération, appel de ToList() ou utilisation de await foreach)), le EsqlExpressionVisitor parcourt l'arbre d'expressions de l'intérieur vers l'extérieur. Il envoie chaque appel de méthode LINQ à un visiteur spécialisé :

VisiteurEst traduitEn
WhereClauseVisitor.Where(predicate)Condition WHERE
SelectProjectionVisitor.Select(selector)EVAL + KEEP + RENAME
GroupByVisitor.GroupBy().Select()STATS ... BY
OrderByVisitor.OrderBy() / .ThenBy()Champ SORT [ASC\|DESC]
EsqlFunctionTranslatorEsqlFunctions.*, Math.*, méthodes stringPlus de 80 fonctions ES|QL

Lors de la traduction, les variables C# référencées dans les expressions sont capturées comme des paramètres nommés.

3. Modèle de requête

Les visiteurs ne produisent pas directement des chaînes de caractères. À la place, ils produisent des objets QueryCommand , une représentation intermédiaire immuable. Un objet FromCommand, un objet WhereCommand, un objet SortCommand et un objet LimitCommand, chacun représentant une commande de traitement ES|QL. Ces objets sont ensuite regroupés dans un modèle EsqlQuery.

Modèle de requête et schéma de commande.

Ce modèle intermédiaire est découplé de l'arbre d'expression et du format de sortie. Il peut être inspecté, intercepté (via IEsqlQueryInterceptor) ou modifié avant d'être formaté.

4. Formatage

EsqlFormatter parcourt chaque QueryCommand dans l'ordre et produit la chaîne ES|QL finale. Chaque commande devient une ligne, séparée par l'opérateur pipe (|) utilisé par ES|QL pour chaîner les commandes de traitement. Les identificateurs contenant des caractères spéciaux sont automatiquement échappés par des guillemets inversés.

5. Exécution

La chaîne ES|QL formatée et les paramètres capturés sont envoyés au point de terminaison /_query d'Elasticsearch sous forme de charge utile JSON. L'interface IEsqlQueryExecutor masque la couche transport, où l'architecture de packages en couches prend tout son sens.

6. Matérialisation

EsqlResponseReader transmet la réponse JSON sans mettre en mémoire tampon l'ensemble des résultats. Un arbre ColumnLayout, précalculé une fois par requête, mappe les noms de colonnes ES|QL plats (comme address.street, address.city) aux propriétés POCO imbriquées. Chaque ligne est assemblée dans une instance T et renvoyée une par une via IEnumerable<T> ou IAsyncEnumerable<T>.

L'architecture en couches

La fonctionnalité LINQ to ES|QL est répartie sur trois packages :

Architecture des packages.
Elastic.Esql est le moteur de traduction pur. Il ne dépend d'aucun HTTP et intègre les visiteurs d'expressions, le modèle de requêtes, le formateur et le lecteur de réponses. Vous pouvez l'utiliser de manière autonome pour créer et analyser des requêtes ES|QL sans connexion à Elasticsearch, ce qui est utile pour les tests, la journalisation des requêtes ou la création de votre propre couche d'exécution.

Elastic.Clients.Esql est un client ES|QL léger et autonome. Il ajoute l'exécution HTTP en plus de Elastic.Esql via Elastic.Transport. Si votre application n'a besoin que d'ES|QL et d'aucune autre API Elasticsearch, il s'agit de l'option de dépendance minimale.

Elastic.Clients.Elasticsearch est le client complet Elasticsearch .NET. Il s'appuie également sur Elastic.Esql et expose le fournisseur LINQ via l'espace de noms client.Esql. C'est le point d'entrée recommandé pour la plupart des applications.

Les deux packages de la couche d'exécution fournissent leur propre implémentation de IEsqlQueryExecutor, l'interface de stratégie qui fait le lien entre la traduction et le transport.

Les trois packages sont compatibles avec Native AOT lorsqu'ils sont utilisés avec un JsonSerializerContext généré par la source. Pour le client complet, consultez la documentation Native AOT.

Au-delà des bases

L'exemple ci-dessus traitait du filtrage, du tri et de la pagination. Le fournisseur prend en charge un ensemble d'opérations plus étendu.

Agrégations

GroupBy, associé aux fonctions d'agrégation dans Select, se traduit en ES|QL STATS ... BYpar :

Projections

Select, avec des types anonymes, génère les commandes EVAL, KEEP et RENAME :

Bibliothèque riche en fonctions

Plus de 80 fonctions ES|QL sont disponibles via la classe EsqlFunctions, couvrant la gestion des dates et heures, des chaînes de caractères, des opérations mathématiques, des adresses IP, la correspondance de modèles et le calcul de scores. Les méthodes standard Math.* et string.* se traduisent également par :

LOOKUP JOIN

Les recherches par index croisé se traduisent en ES|QL LOOKUP JOINpar :

Séquence d'échappement pour ES|QL brut

Pour les fonctionnalités ES|QL non encore prises en charge par le fournisseur LINQ, vous pouvez ajouter des fragments bruts :

Requêtes asynchrones côté serveur

Pour les requêtes de longue durée, soumettez-les pour un traitement en arrière-plan sur le serveur :

Les requêtes asynchrones côté serveur sont particulièrement utiles pour les requêtes analytiques de longue durée/le traitement de grands ensembles de données, qui peuvent dépasser les seuils de délai d'expiration habituels, ou dans les environnements sensibles aux délais d'expiration avec équilibreurs de charge, passerelles API ou proxys qui imposent des délais d'expiration HTTP stricts. Les requêtes asynchrones évitent les interruptions de connexion en découplant la soumission et la récupération des résultats.

Premiers pas

LINQ to ES|QL est disponible à partir de :

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

Installation depuis NuGet :

dotnet add package Elastic.Clients.Elasticsearch

Les points d’entrée sont sur client.Esql:

MéthodeRetoursCas d'utilisation
Query<T>(...)IEnumerable<T>Exécution synchrone
QueryAsync<T>(...)IAsyncEnumerable<T>Streaming asynchrone
CreateQuery<T>()IEsqlQueryable<T>Composition et inspection avancées
SubmitAsyncQueryAsync<T>(...)EsqlAsyncQuery<T>Requêtes de longue durée côté serveur

Pour une description complète des fonctionnalités, notamment les options de requête, l'accès à plusieurs champs, les objets imbriqués et la gestion des champs à valeurs multiples, consultez la documentation LINQ to ES|QL.

Conclusion

LINQ to ES|QL apporte toute la puissance d'expression de LINQ to C# au langage de requêtes ES|QL d'Elasticsearch, vous permettant d'écrire des requêtes fortement typées et composables sans avoir à les concevoir manuellement. Grâce à la capture automatique des paramètres, la matérialisation en flux continu et une architecture de packages modulaire scalable, allant d'une simple traduction à un client Elasticsearch complet, il s'intègre naturellement aux applications .NET de toute taille. applications .NET de toute taille. Installez le client le plus récent, configurez vos expressions LINQ pour qu'elles pointent vers un index, et laissez le fournisseur gérer le reste.

Ce contenu vous a-t-il été utile ?

Pas utile

Plutôt utile

Très utile

Pour aller plus loin

Prêt à créer des expériences de recherche d'exception ?

Une recherche suffisamment avancée ne se fait pas avec les efforts d'une seule personne. Elasticsearch est alimenté par des data scientists, des ML ops, des ingénieurs et bien d'autres qui sont tout aussi passionnés par la recherche que vous. Mettons-nous en relation et travaillons ensemble pour construire l'expérience de recherche magique qui vous permettra d'obtenir les résultats que vous souhaitez.

Jugez-en par vous-même