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.
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 :
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
.
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 :
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
.
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.