En travaillant chez Coralogix, une plateforme d’observabilité complète, j’ai récemment été confronté à un défi intéressant.

Mon équipe développe le langage de requête DataPrime et le moteur de requête, qui permettent d’interroger facilement les journaux et autres données d’observabilité sur la plateforme, généralement sous forme de fichiers Parquet sur AWS S3. Au sein du moteur, nos requêtes DataPrime sont transformées en plans de requête avec des expressions similaires à SQL, par exemple dans les filtres. Pour assurer la compatibilité avec les versions antérieures, le moteur de requête doit également pouvoir interroger OpenSearch au lieu des fichiers Parquet. Nous avons donc dû traduire ces expressions en DSL de requête OpenSearch.

Bien qu’OpenSearch offre un certain support pour SQL, nos expérimentations ont rapidement montré que ce support n’est pas très complet. Dans cet article, je souhaite aborder un aspect spécifique de cette traduction qui s’est révélé particulièrement intéressant : la sémantique des valeurs nulles.

Les Fondamentaux de la Technologie

Dans SQL, une expression de filtre valide, par exemple dans une clause WHERE, peut avoir trois résultats : VRAI, FAUX ou NULL. Les lignes dont le résultat est NULL seront bien sûr également exclues, tout comme celles avec FAUX, mais NULL se comporte de manière très différente par rapport à VRAI et FAUX dans l’expression.

Illustration :

CREATE TABLE test (
  prenom varchar(255),
  nom varchar(255),
  nom_de_famille varchar(255)
 );

INSERT INTO test (prenom, nom_de_famille)
VALUES ('Marc', 'Dupont');


SELECT * FROM test WHERE prenom='Garry';
// 0 Résultat


SELECT * FROM test WHERE !(prenom='Garry');
// 1 Résultat


SELECT * FROM test WHERE nom='Garry';
// 0 Résultat


SELECT * FROM test WHERE !(nom='Garry');
// 0 Résultat !!!

La quatrième requête SELECT ne renverra aucun résultat car pour la seule ligne, nom est NULL, et l’opérateur = renverra NULL si l’un des côtés est NULL. De plus, !NULL est également NULL.

Cela s’appelle la logique à trois valeurs, spécifiquement les logiques de Kleene et Priest. L’idée est assez simple : si vous considérez NULL comme indéterminé, vous obtiendrez toujours NULL si le résultat ne peut pas être déterminé.

Par exemple, FAUX && NULL est FAUX car peu importe ce que NULL représente, le résultat sera toujours FAUX.

En revanche, FAUX || NULL sera NULL, car en substituant FAUX, le résultat sera FAUX, et en substituant VRAI, le résultat sera VRAI.

Par ailleurs, le DSL d’OpenSearch ne comprend pas du tout les expressions. Il ne connaît que les filtres, c’est-à-dire des opérations qui prennent un ensemble de documents et renvoient ceux qui correspondent.

La Théorie de la Technologie

À première vue, réduire VRAI, FAUX et NULL à seulement VRAI et FAUX peut sembler une tâche impossible.

Cependant, la solution réside dans le contexte dans lequel une expression est utilisée. J’ai précédemment mentionné qu’une clause WHERE interprétera une expression renvoyant NULL de la même manière que FAUX, en filtrant la ligne. Ainsi, WHERE est en réalité également binaire, il filtrera ou ne filtrera pas la ligne !

Formellement, en utilisant du pseudocode :

filter(expr)=expr==true
            =!(expr==false || expr==null)
            =!is_false_or_null(expr)

Nous commençons à avancer. Il nous suffit maintenant de déterminer is_false_or_null pour toutes les expressions que nous souhaitons prendre en charge. Commençons par les bases :

is_false_or_null(null) =true
                       ={ "match_all": {}}
is_false_or_null(true) =false
                       ={ "match_none": {}}
is_false_or_null(false)=true
                       ={ "match_all": {}}
is_false_or_null(field)=!matches(field, true) || !exists(field)
                       ={
                           "bool": {
  	                     "should": [
                               {
        	                 "bool": {
           	                   "must_not": {
              	                     "matches": {
                 		       "field": "true"
              	                    }
           	                  }
        	                }
                              },
     	                      {
        	                "bool": {
           	                  "must_not": {
              	                   "exists": "field"
           	                 }
        	               }
     	     

Comprendre les Opérateurs Booléens dans les Requêtes

Les opérateurs booléens, à savoir OU, ET et NON, jouent un rôle crucial dans la formulation des requêtes. Leur utilisation permet de structurer des conditions complexes de manière efficace.

Analyse des Opérateurs Booléens

Pour mieux appréhender ces opérateurs, il est utile de se référer aux tables de vérité, qui fournissent une base logique pour leur fonctionnement.

Table de vérité pour les opérateurs booléens

Par exemple, considérons l’expression est_faux_ou_nul(expr1 && expr2). Les résultats où cette expression est vraie sont mis en évidence ci-dessous :

Résultats de l'expression

Il est intéressant de noter que cela correspond à l’union des cas où A est FAUX ou NUL, et où B est également FAUX ou NUL.

Union des résultats

En conséquence, nous pouvons établir que :

est_faux_ou_nul(expr1 && expr2)=est_faux_ou_nul(expr1) || est_faux_ou_nul(expr2)

Exploration de l’Opérateur OU

Pour l’opérateur OU, la table de vérité présente une configuration différente :

Table de vérité pour l'opérateur OU

Dans ce cas, il s’agit de l’intersection des situations où A est FAUX ou NUL, et où B est également FAUX ou NUL. Ainsi, nous avons :

est_faux_ou_nul(expr1 || expr2)=est_faux_ou_nul(expr1) && est_faux_ou_nul(expr2)

Comprendre l’Opérateur NON

Enfin, examinons l’opérateur NON.

Table de vérité pour l'opérateur NON

Il est clair que NON(A) est FAUX ou NUL lorsque A est VRAI ou NUL :

est_faux_ou_nul(!expr)=est_vrai_ou_nul(expr)

Nous devons également définir est_vrai_ou_nul, qui est assez similaire à mettre en œuvre :

est_vrai_ou_nul(nul)=vrai
est_vrai_ou_nul(vrai)=vrai
est_vrai_ou_nul(faux)=faux
est_vrai_ou_nul(champ)=correspond(champ, vrai) || !existe(champ)

Conclusion sur les Technologies

Avec cette compréhension théorique, la partie la plus complexe du problème est résolue.

Le reste consiste principalement à traduire les concepts de la langue source que l’on souhaite prendre en charge. Il est à noter que les capacités du DSL d’ElasticSearch peuvent rapidement atteindre leurs limites, car même des opérations arithmétiques simples n’ont pas d’équivalent direct, ce qui oblige à recourir à des requêtes scriptées, moins performantes mais capables de couvrir presque tous les cas d’utilisation. Pour éviter de traduire chaque expression deux fois avec des différences minimes pour est_vrai_ou_nul et est_faux_ou_nul, il est préférable d’avoir une fonction unique est_valeur_ou_nul(valeur: bool, ...).

Pour éviter que les requêtes ne deviennent trop volumineuses et surtout trop imbriquées, nous avons également choisi une représentation intermédiaire du DSL d’ElasticSearch avant de la convertir en JSON.

Cela nous permet d’effectuer confortablement une série d’optimisations en amont :

  • Appliquer les lois de De Morgan
  • Aplatir les clauses booléennes imbriquées par associativité
  • Inverser les clauses négatives

Il semble que le texte fourni soit incomplet ou ne contienne pas d’article à réécrire. Veuillez fournir un article complet pour que je puisse le reformuler selon vos instructions.

Show Comments (0)
Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *