Filtre

Avec Qdrant, vous pouvez définir des conditions pour rechercher ou récupérer des points, ce qui signifie que vous pouvez filtrer par attribut en plus des recherches de similarité pour les vecteurs, tout comme définir des conditions SQL where. Par exemple, vous pouvez définir des conditions pour la charge utile et l'id du point.

Il est important de définir des conditions supplémentaires lorsque toutes les fonctionnalités d'un objet ne peuvent pas être exprimées dans un embedding. Par exemple, divers besoins commerciaux tels que la disponibilité des stocks, la localisation de l'utilisateur ou la plage de prix attendue.

Conditions de filtre

Qdrant vous permet de combiner des conditions dans des clauses. Les clauses sont différentes opérations logiques, telles que OR, AND, et NOT. Les clauses peuvent être imbriquées de manière récursive, vous permettant ainsi de recréer n'importe quelle expression booléenne.

Jetons un coup d'œil aux clauses implémentées dans Qdrant.

Supposons que nous disposions d'un ensemble de points avec des charges utiles :

[
  { "id": 1, "city": "London", "color": "green" },
  { "id": 2, "city": "London", "color": "red" },
  { "id": 3, "city": "London", "color": "blue" },
  { "id": 4, "city": "Berlin", "color": "red" },
  { "id": 5, "city": "Moscou", "color": "green" },
  { "id": 6, "city": "Moscou", "color": "blue" }
]

Doit

Exemple :

POST /collections/{collection_name}/points/scroll

{
    "filter": {
        "must": [
            { "key": "city", "match": { "value": "London" } },
            { "key": "color", "match": { "value": "red" } }
        ]
    }
  ...
}

Le point filtré sera :

[{ "id": 2, "city": "London", "color": "red" }]

Lors de l'utilisation de must, la clause est true seulement si chaque condition répertoriée dans must est satisfaite. En ce sens, must est équivalent à l'opérateur AND.

Devrait

Should est similaire à l'opérateur OR en SQL.

Exemple :

POST /collections/{collection_name}/points/scroll

{
    "filter": {
        "should": [
            { "key": "city", "match": { "value": "London" } },
            { "key": "color", "match": { "value": "red" } }
        ]
    }
  ...
}

Les points filtrés seront :

[
  { "id": 1, "city": "London", "color": "green" },
  { "id": 2, "city": "London", "color": "red" },
  { "id": 3, "city": "London", "color": "blue" },
  { "id": 4, "city": "Berlin", "color": "red" }
]

Lors de l'utilisation de should, la clause est true tant qu'au moins une condition répertoriée dans should est satisfaite. En ce sens, should est équivalent à l'opérateur OR.

ne doit pas

Exemple :

POST /collections/{collection_name}/points/scroll

{
    "filter": {
        "must_not": [
            { "key": "city", "match": { "value": "London" } },
            { "key": "color", "match": { "value": "red" } }
        ]
    }
  ...
}

Les points filtrés seront :

[
  { "id": 5, "city": "Moscou", "color": "green" },
  { "id": 6, "city": "Moscou", "color": "blue" }
]

Lors de l'utilisation de must_not, la sous-clause est true seulement si aucune des conditions répertoriées dans must_not n'est satisfaite. En ce sens, must_not est équivalent à l'expression (NON A) ET (NON B) ET (NON C).

Combinaison de conditions

Il est possible d'utiliser plusieurs conditions simultanément :

POST /collections/{collection_name}/points/scroll

{
    "filter": {
        "must": [
            { "key": "city", "match": { "value": "London" } }
        ],
        "must_not": [
            { "key": "color", "match": { "value": "red" } }
        ]
    }
  ...
}

Les points filtrés seront :

[
  { "id": 1, "city": "London", "color": "green" },
  { "id": 3, "city": "London", "color": "blue" }
]

Dans ce cas, les conditions sont combinées en utilisant ET (AND).

De plus, les conditions peuvent être imbriquées de manière récursive. Par exemple :

POST /collections/{collection_name}/points/scroll

{
    "filter": {
        "must_not": [
            {
                "must": [
                    { "key": "city", "match": { "value": "London" } },
                    { "key": "color", "match": { "value": "red" } }
                ]
            }
        ]
    }
  ...
}

Les points filtrés seront :

[
  { "id": 1, "city": "London", "color": "green" },
  { "id": 3, "city": "London", "color": "blue" },
  { "id": 4, "city": "Berlin", "color": "red" },
  { "id": 5, "city": "Moscou", "color": "vert" },
  { "id": 6, "city": "Moscou", "color": "bleu" }
]

Filtrage des conditions

Dans la charge utile, différents types de valeurs correspondent à différents types de requêtes qui peuvent leur être appliquées. Examinons les variantes de conditions existantes et les types de données auxquels elles s'appliquent.

Correspondance

{
  "key": "color",
  "match": {
    "value": "rouge"
  }
}

Pour les autres types, les conditions de correspondance se présentent exactement de la même manière, simplement avec des types différents utilisés :

{
  "key": "count",
  "match": {
    "value": 0
  }
}

La condition la plus simple vérifie si la valeur stockée est égale à la valeur donnée. Si plusieurs valeurs sont stockées, au moins l'une d'entre elles doit satisfaire la condition. Cela peut s'appliquer aux charges utiles de type mot-clé, entier et booléen.

Toute correspondance

Disponible depuis v1.1.0

Si vous souhaitez vérifier si la valeur stockée est l'une des plusieurs valeurs, vous pouvez utiliser la condition de toute correspondance. La toute correspondance traite la valeur donnée comme une opération logique OU. Elle peut également être décrite comme l'opérateur IN.

Vous pouvez l'appliquer aux charges utiles de type mot-clé et entier.

Exemple :

{
  "key": "color",
  "match": {
    "any": ["noir", "jaune"]
  }
}

Dans cet exemple, si la valeur stockée est noir ou jaune, alors la condition sera satisfaite.

Si la valeur stockée est un tableau, il doit avoir au moins une valeur qui correspond à l'une des valeurs données. Par exemple, si la valeur stockée est ["noir", "vert"], alors la condition sera satisfaite car "noir" est dans ["noir", "jaune"].

Exclusion de correspondance

Disponible depuis v1.2.0

Si vous souhaitez vérifier si la valeur stockée n'est aucune des valeurs données, vous pouvez utiliser la condition d'exclusion de correspondance. L'exclusion de correspondance traite la valeur donnée comme une opération logique NOR. Elle peut également être décrite comme l'opérateur NOT IN.

Vous pouvez l'appliquer aux charges utiles de type mot-clé et entier.

Exemple :

{
  "key": "color",
  "match": {
    "except": ["noir", "jaune"]
  }
}

Dans cet exemple, si la valeur stockée n'est ni noir ni jaune, alors la condition sera satisfait

Si la valeur stockée est un tableau, il doit avoir au moins une valeur qui ne correspond à aucune des valeurs données. Par exemple, si la valeur stockée est ["noir", "vert"], alors la condition sera satisfaite car "vert" ne correspond ni à "noir" ni à "jaune".

Clés imbriquées

Disponible à partir de la version 1.1.0

Étant donné que la charge utile est un objet JSON arbitraire, vous pourriez avoir besoin de filtrer des champs imbriqués.

Pour votre commodité, nous utilisons une syntaxe similaire au projet Jq.

Supposons que nous ayons un ensemble de points avec la charge utile suivante :

[
  {
    "id": 1,
    "country": {
      "name": "Allemagne",
      "cities": [
        {
          "name": "Berlin",
          "population": 3.7,
          "sightseeing": ["Porte de Brandebourg", "Reichstag"]
        },
        {
          "name": "Munich",
          "population": 1.5,
          "sightseeing": ["Marienplatz", "Parc olympique"]
        }
      ]
    }
  },
  {
    "id": 2,
    "country": {
      "name": "Japon",
      "cities": [
        {
          "name": "Tokyo",
          "population": 9.3,
          "sightseeing": ["Tour de Tokyo", "Tokyo Skytree"]
        },
        {
          "name": "Osaka",
          "population": 2.7,
          "sightseeing": ["Château d'Osaka", "Universal Studios Japan"]
        }
      ]
    }
  }
]

Vous pouvez utiliser la notation point pour rechercher des champs imbriqués.

POST /collections/{nom_collection}/points/défilement

{
    "filter": {
        "should": [
            {
                "key": "country.name",
                "match": {
                    "value": "Allemagne"
                }
            }
        ]
    }
}

Vous pouvez également utiliser la syntaxe [ ] pour rechercher dans le tableau en projetant des valeurs internes.

POST /collections/{nom_collection}/points/défilement

{
    "filter": {
        "should": [
            {
                "key": "country.cities[].population",
                "range": {
                    "gte": 9.0,
                }
            }
        ]
    }
}

Cette requête ne renvoie que le point avec l'identifiant 2, car seul le Japon a une ville avec une population supérieure à 9,0.

Les champs imbriqués peuvent également être un tableau.

POST /collections/{nom_collection}/points/défilement

{
    "filter": {
        "should": [
            {
                "key": "country.cities[].sightseeing",
                "match": {
                    "value": "Château d'Osaka"
                }
            }
        ]
    }
}

Cette requête ne renvoie que le point avec l'identifiant 2, car seul le Japon a une ville avec un lieu touristique qui inclut "Château d'Osaka".

Filtrage d'Objets Nommés

Disponible depuis la version 1.2.0

Par défaut, les conditions prennent en considération l'ensemble des données d'un point.

Par exemple, étant donné les deux points dans les données ci-dessous :

[
  {
    "id": 1,
    "dinosaur": "t-rex",
    "diet": [
      { "food": "feuilles", "aime": false },
      { "food": "viande", "aime": true }
    ]
  },
  {
    "id": 2,
    "dinosaur": "diplodocus",
    "diet": [
      { "food": "feuilles", "aime": true },
      { "food": "viande", "aime": false }
    ]
  }
]

La requête suivante correspondrait à ces deux points :

POST /collections/{collection_name}/points/scroll

{
    "filter": {
        "must": [
            {
                "key": "diet[].food",
                  "match": {
                    "value": "viande"
                }
            },
            {
                "key": "diet[].aime",
                  "match": {
                    "value": true
                }
            }
        ]
    }
}

Les deux points ci-dessus correspondent car ils satisfont tous deux ces deux conditions :

  • "t-rex" satisfait diet[1].food avec food = viande et diet[1].aime avec aime = true
  • "diplodocus" satisfait diet[1].food avec food = viande et diet[0].aime avec aime = true

Pour obtenir uniquement des points qui correspondent aux conditions pour les éléments d'array, par exemple, le point avec l'ID 1 dans cet exemple, vous devez utiliser des filtres d'objets nommés imbriqués.

Les filtres d'objets nommés imbriqués permettent de interroger les tableaux d'objets indépendamment.

Cela peut être réalisé en utilisant le type de condition nested, qui consiste en la clé des données d'intérêt et le filtre à appliquer.

La clé devrait pointer vers un tableau d'objets et peut éventuellement utiliser la notation entre crochets ("data" ou "data[]") en option.

POST /collections/{collection_name}/points/scroll

{
    "filter": {
        "must": [
            "nested": {
                {
                    "key": "diet",
                    "filter": {
                        "must": [
                            {
                                "key": "food",
                                "match": {
                                    "value": "viande"
                                }
                            },
                            {
                                "key": "aime",
                                "match": {
                                    "value": true
                                }
                            }
                        ]
                    }
                }
            }
        ]
    }
}

La logique de correspondance est modifiée pour s'appliquer au niveau de l'élément du tableau dans les données.

Le filtre imbriqué fonctionne de la même manière que lors de l'application d'un filtre imbriqué à un seul élément d'un tableau. Tant qu'au moins un élément du tableau correspond au filtre imbriqué, le document parent est considéré comme correspondant à la condition.

Limitation

Les filtres d'objets nommés imbriqués ne prennent pas en charge la condition has_id. Si vous avez besoin de l'utiliser, placez-la dans une clause must adjacente.

POST /collections/{collection_name}/points/scroll

{
    "filter": {
        "must": [
            "nested": {
                {
                    "key": "diet",
                    "filter": {
                        "must": [
                            {
                                "key": "food",
                                "match": {
                                    "value": "viande"
                                }
                            },
                            {
                                "key": "aime",
                                "match": {
                                    "value": true
                                }
                            }
                        ]
                    }
                }
            },
            { "has_id": [1] }
        ]
    }
}

Correspondance de Texte Exacte

Disponible depuis la version 0.10.0

Un cas particulier de la condition match est la condition de correspondance de texte. Elle vous permet de rechercher des sous-chaînes, des jetons ou des phrases spécifiques dans le champ de texte.

Les textes exacts qui satisfont cette condition dépendent de la configuration de l'indexation de texte intégral. La configuration est définie lors de la création de l'index et est décrite dans l'index de texte intégral.

Si le champ n'a pas d'index de texte intégral, cette condition fonctionnera sur la base d'une correspondance exacte de sous-chaîne.

{
  "key": "description",
  "match": {
    "text": "bon et pas cher"
  }
}

Si la requête comporte plusieurs mots, cette condition ne sera satisfaite que lorsque tous les mots apparaissent dans le texte.

Plage

{
  "key": "price",
  "range": {
    "gt": null,
    "gte": 100.0,
    "lt": null,
    "lte": 450.0
  }
}

La condition range définit la plage possible de valeurs pour la charge utile stockée. Si plusieurs valeurs sont stockées, au moins une valeur doit correspondre à la condition.

Les opérations de comparaison disponibles incluent :

  • gt - strictement supérieur à
  • gte - supérieur ou égal à
  • lt - strictement inférieur à
  • lte - inférieur ou égal à

Elle peut être appliquée à des nombres à virgule flottante et à des charges utiles entières.

Boîte de délimitation géographique

{
  "key": "location",
  "geo_bounding_box": {
    "bottom_right": {
      "lat": 52.495862,
      "lon": 13.455868
    },
    "top_left": {
      "lat": 52.520711,
      "lon": 13.403683
    }
  }
}

Cela correspond à la location dans le rectangle avec des coordonnées en bas à droite comme bottom_right et des coordonnées en haut à gauche comme top_left.

Rayon géographique

{
  "key": "location",
  "geo_radius": {
    "center": {
      "lat": 52.520711,
      "lon": 13.403683
    },
    "radius": 1000.0
  }
}

Cela correspond à la location dans le cercle avec le centre à center et un rayon de radius mètres.

Si plusieurs valeurs sont stockées, au moins une valeur doit correspondre à la condition. Ces conditions ne peuvent être appliquées qu'à des charges utiles correspondant au format de données géographiques.

Nombre de valeurs

En plus de la comparaison de valeur directe, le filtrage peut également être basé sur le nombre de valeurs.

Par exemple, étant donné les données suivantes :

[
  { "id": 1, "name": "Produit A", "comments": ["Très bon!", "Excellent"] },
  { "id": 2, "name": "Produit B", "comments": ["Correct", "Attendant plus", "Bien"] }
]

Nous pouvons rechercher uniquement les éléments avec plus de deux commentaires :

{
  "key": "comments",
  "values_count": {
    "gt": 2
  }
}

Le résultat sera :

[{ "id": 2, "name": "Produit B", "comments": ["Correct", "Attendant plus", "Bien"] }]

Si la valeur stockée n'est pas un tableau, on suppose que le nombre de valeurs est égal à 1.

Est vide

Parfois, il est utile de filtrer les enregistrements qui manquent de certaines valeurs. La condition IsEmpty peut vous aider à y parvenir :

{
  "is_empty": {
    "key": "reports"
  }
}

Cette condition correspondra à tous les enregistrements où le champ reports n'existe pas ou a une valeur de null ou [].

IsEmpty est souvent très utile lorsqu'il est utilisé en conjonction avec la négation logique must_not. Dans ce cas, il sélectionnera toutes les valeurs non vides.

Est nul

La condition match ne peut pas tester les valeurs NULL. Nous devons utiliser la condition IsNull :

{
    "is_null": {
        "key": "reports"
    }
}

Cette condition correspondra à tous les enregistrements où le champ reports existe et a une valeur de NULL.

A un ID

Ce type de requête n'est pas lié aux charges utiles, mais il est très utile dans certaines situations. Par exemple, les utilisateurs peuvent vouloir étiqueter certains résultats de recherche comme non pertinents, ou nous ne voulons peut-être rechercher qu'entre des points spécifiques.

POST /collections/{collection_name}/points/scroll

{
    "filter": {
        "must": [
            { "has_id": [1,3,5,7,9,11] }
        ]
    }
  ...
}

Les points filtrés seront :

[
  { "id": 1, "ville": "Londres", "couleur": "verte" },
  { "id": 3, "ville": "Londres", "couleur": "bleue" },
  { "id": 5, "ville": "Moscou", "couleur": "verte" }
]