Effectuer une recherche sur du texte indexé

Cette page fournit des exemples de recherche dans BigQuery. Lorsque vous indexez vos données, BigQuery peut optimiser certaines requêtes qui utilisent la fonction SEARCH ou d'autres fonctions et opérateurs, par exemple =, IN, LIKE et STARTS_WITH.

Les requêtes SQL renvoient des résultats corrects à partir de toutes les données ingérées, même si certaines des données ne sont pas encore indexées. Toutefois, les performances des requêtes peuvent être considérablement améliorées avec un index. Les économies en octets traités et en millisecondes d'emplacement sont maximisées lorsque le nombre de résultats de recherche représente une fraction relativement faible du total de lignes dans votre table, car le volume de données analysées est moindre. Pour déterminer si un index a été utilisé pour une requête, consultez la section Utilisation des index de recherche.

créer un index de recherche ;

La table suivante, appelée Logs, permet de présenter différentes manières d'utiliser la fonction SEARCH. Cet exemple de table est assez petit, mais en pratique, les gains de performances obtenus avec SEARCH s'améliorent en fonction de la taille de la table.

CREATE TABLE my_dataset.Logs (Level STRING, Source STRING, Message STRING)
AS (
  SELECT 'INFO' as Level, '65.177.8.234' as Source, 'Entry Foo-Bar created' as Message
  UNION ALL
  SELECT 'WARNING', '132.249.240.10', 'Entry Foo-Bar already exists, created by 65.177.8.234'
  UNION ALL
  SELECT 'INFO', '94.60.64.181', 'Entry Foo-Bar deleted'
  UNION ALL
  SELECT 'SEVERE', '4.113.82.10', 'Entry Foo-Bar does not exist, deleted by 94.60.64.181'
  UNION ALL
  SELECT 'INFO', '181.94.60.64', 'Entry Foo-Baz created'
);

La table ressemble à ceci :

+---------+----------------+-------------------------------------------------------+
| Level   | Source         | Message                                               |
+---------+----------------+-------------------------------------------------------+
| INFO    | 65.177.8.234   | Entry Foo-Bar created                                 |
| WARNING | 132.249.240.10 | Entry Foo-Bar already exists, created by 65.177.8.234 |
| INFO    | 94.60.64.181   | Entry Foo-Bar deleted                                 |
| SEVERE  | 4.113.82.10    | Entry Foo-Bar does not exist, deleted by 94.60.64.181 |
| INFO    | 181.94.60.64   | Entry Foo-Baz created                                 |
+---------+----------------+-------------------------------------------------------+

Créez un index de recherche sur la table Logs à l'aide de l'analyseur de texte par défaut :

CREATE SEARCH INDEX my_index ON my_dataset.Logs(ALL COLUMNS);

Pour en savoir plus sur les index de recherche, consultez la page Gérer les index de recherche.

Utiliser la fonction SEARCH

La fonction SEARCH fournit une recherche tokenisée sur les données. SEARCH est conçu pour être utilisé avec un index, afin d'optimiser les recherches. Vous pouvez utiliser la fonction SEARCH pour effectuer une recherche dans une table entière, ou limiter votre recherche à des colonnes spécifiques.

Effectuer des recherches dans une table entière

La requête suivante recherche la valeur bar dans toutes les colonnes de la table Logs et renvoie les lignes contenant cette valeur, quelle que soit l'utilisation des majuscules. Étant donné que l'index de recherche utilise l'analyseur de texte par défaut, vous n'avez pas besoin de le spécifier dans la fonction SEARCH.

SELECT * FROM my_dataset.Logs WHERE SEARCH(Logs, 'bar');
+---------+----------------+-------------------------------------------------------+
| Level   | Source         | Message                                               |
+---------+----------------+-------------------------------------------------------+
| INFO    | 65.177.8.234   | Entry Foo-Bar created                                 |
| WARNING | 132.249.240.10 | Entry Foo-Bar already exists, created by 65.177.8.234 |
| INFO    | 94.60.64.181   | Entry Foo-Bar deleted                                 |
| SEVERE  | 4.113.82.10    | Entry Foo-Bar does not exist, deleted by 94.60.64.181 |
+---------+----------------+-------------------------------------------------------+

La requête suivante recherche la valeur `94.60.64.181` dans toutes les colonnes de la table Logs et renvoie les lignes contenant cette valeur. Les accents graves permettent d'effectuer une recherche exacte. C'est pourquoi la dernière ligne de la table Logs contenant 181.94.60.64 est omise.

SELECT * FROM my_dataset.Logs WHERE SEARCH(Logs, '`94.60.64.181`');
+---------+----------------+-------------------------------------------------------+
| Level   | Source         | Message                                               |
+---------+----------------+-------------------------------------------------------+
| INFO    | 94.60.64.181   | Entry Foo-Bar deleted                                 |
| SEVERE  | 4.113.82.10    | Entry Foo-Bar does not exist, deleted by 94.60.64.181 |
+---------+----------------+-------------------------------------------------------+

Effectuer des recherches dans un sous-ensemble de colonnes

SEARCH facilite la spécification d'un sous-ensemble de colonnes dans lequel rechercher des données. La requête suivante recherche la valeur 94.60.64.181 dans la colonne Message de la table Logs et renvoie les lignes contenant cette valeur.

SELECT * FROM my_dataset.Logs WHERE SEARCH(Message, '`94.60.64.181`');
+---------+----------------+-------------------------------------------------------+
| Level   | Source         | Message                                               |
+---------+----------------+-------------------------------------------------------+
| SEVERE  | 4.113.82.10    | Entry Foo-Bar does not exist, deleted by 94.60.64.181 |
+---------+----------------+-------------------------------------------------------+

La requête suivante effectue une recherche à la fois sur les colonnes Source et Message de la table Logs. Elle renvoie les lignes contenant la valeur 94.60.64.181 de l'une des colonnes.

SELECT * FROM my_dataset.Logs WHERE SEARCH((Source, Message), '`94.60.64.181`');
+---------+----------------+-------------------------------------------------------+
| Level   | Source         | Message                                               |
+---------+----------------+-------------------------------------------------------+
| INFO    | 94.60.64.181   | Entry Foo-Bar deleted                                 |
| SEVERE  | 4.113.82.10    | Entry Foo-Bar does not exist, deleted by 94.60.64.181 |
+---------+----------------+-------------------------------------------------------+

Si une table comporte plusieurs colonnes et que vous souhaitez effectuer une recherche sur la plupart d'entre elles, il peut être plus facile de ne spécifier que les colonnes à exclure de la recherche. La requête ci-dessous recherche dans toutes les colonnes de la table Logs, à l'exception de la colonne Message. Elle renvoie les lignes de toutes les colonnes autres que Message contenant la valeur 94.60.64.181.

SELECT *
FROM my_dataset.Logs
WHERE SEARCH(
  (SELECT AS STRUCT Logs.* EXCEPT (Message)), '`94.60.64.181`');
+---------+----------------+---------------------------------------------------+
| Level   | Source         | Message                                           |
+---------+----------------+---------------------------------------------------+
| INFO    | 94.60.64.181   | Entry Foo-Bar deleted                             |
+---------+----------------+---------------------------------------------------+

Utiliser un autre analyseur de texte

L'exemple suivant crée une table appelée contact_info avec un index qui utilise l'analyseur de texte NO_OP_ANALYZER :

CREATE TABLE my_dataset.contact_info (name STRING, email STRING)
AS (
  SELECT 'Kim Lee' AS name, 'kim.lee@example.com' AS email
  UNION ALL
  SELECT 'Kim' AS name, 'kim@example.com' AS email
  UNION ALL
  SELECT 'Sasha' AS name, 'sasha@example.com' AS email
);
CREATE SEARCH INDEX noop_index ON my_dataset.contact_info(ALL COLUMNS)
OPTIONS (analyzer = 'NO_OP_ANALYZER');
+---------+---------------------+
| name    | email               |
+---------+---------------------+
| Kim Lee | kim.lee@example.com |
| Kim     | kim@example.com     |
| Sasha   | sasha@example.com   |
+---------+---------------------+

La requête suivante recherche Kim dans la colonne name et kim dans la colonne email. Étant donné que l'index de recherche n'utilise pas l'analyseur de texte par défaut, vous devez transmettre le nom de l'analyseur à la fonction SEARCH.

SELECT
  name,
  SEARCH(name, 'Kim', analyzer=>'NO_OP_ANALYZER') AS name_Kim,
  email,
  SEARCH(email, 'kim', analyzer=>'NO_OP_ANALYZER') AS email_kim
FROM
  my_dataset.contact_info;

NO_OP_ANALYZER ne modifie pas le texte. Par conséquent, la fonction SEARCH ne renvoie TRUE que pour les correspondances exactes :

+---------+----------+---------------------+-----------+
| name    | name_Kim | email               | email_kim |
+---------+----------+---------------------+-----------+
| Kim Lee | FALSE    | kim.lee@example.com | FALSE     |
| Kim     | TRUE     | kim@example.com     | FALSE     |
| Sasha   | FALSE    | sasha@example.com   | FALSE     |
+---------+----------+---------------------+-----------+

Configurer les options des analyseurs de texte

Les analyseurs de texte LOG_ANALYZER et PATTERN_ANALYZER peuvent être personnalisés en ajoutant une chaîne au format JSON aux options de configuration. Vous pouvez configurer des analyseurs de texte dans la fonction SEARCH, dans l'instruction LDD CREATE SEARCH INDEX, et dans la fonction TEXT_ANALYZE.

L'exemple suivant crée une table appelée complex_table avec un index qui utilise l'analyseur de texte LOG_ANALYZER. Il utilise une chaîne au format JSON pour configurer les options de l'analyseur :

CREATE TABLE dataset.complex_table(
  a STRING,
  my_struct STRUCT<string_field STRING, int_field INT64>,
  b ARRAY<STRING>
);

CREATE SEARCH INDEX my_index
ON dataset.complex_table(a, my_struct, b)
OPTIONS (analyzer = 'LOG_ANALYZER', analyzer_options = '''{
  "token_filters": [
    {
      "normalization": {"mode": "NONE"}
    }
  ]
}''');

Les tableaux suivants présentent des exemples d'appels à la fonction SEARCH avec différents analyseurs de texte et leurs résultats. La première table appelle la fonction SEARCH à l'aide de l'analyseur de texte par défaut, LOG_ANALYZER :

Appel de fonction Renvoie Motif
SEARCH('foobarexample', NULL) ERREUR La valeur du champ search_terms est "NULL".
SEARCH('foobarexample', '') ERREUR La variable search_terms ne contient aucun jeton.
SEARCH('foobar-example', 'foobar example') VRAI '-' et ' ' sont des délimiteurs.
SEARCH('foobar-example', 'foobarexample') FAUX La variable search_terms n'est pas séparée.
SEARCH('foobar-example', 'foobar\\&example') VRAI La double barre oblique inverse échappe l'esperluette, qui est un délimiteur.
SEARCH('foobar-example', R'foobar\&example') VRAI La barre oblique inverse unique échappe l'esperluette dans une chaîne brute.
SEARCH('foobar-example', '`foobar&example`') FAUX Les accents graves nécessitent une correspondance exacte pour foobar&example.
SEARCH('foobar&example', '`foobar&example`') VRAI Une correspondance exacte est trouvée.
SEARCH('foobar-example', 'example foobar') VRAI L'ordre des termes n'a pas d'importance.
SEARCH('foobar-example', 'foobar example') VRAI Les jetons sont mis en minuscules.
SEARCH('foobar-example', '`foobar-example`') VRAI Une correspondance exacte est trouvée.
SEARCH('foobar-example', '`foobar`') FAUX Les accents graves conservent la casse.
SEARCH('`foobar-example`', '`foobar-example`') FAUX Les accents graves n'ont pas de signification particulière pour data_to_search et
SEARCH('foobar@example.com', '`example.com`') VRAI Une correspondance exacte est trouvée après le délimiteur dans data_to_search.
SEARCH('a foobar-example b', '`foobar-example`') VRAI Une correspondance exacte est trouvée entre les délimiteurs d'espace.
SEARCH(['foobar', 'example'], 'foobar example') FAUX Aucune entrée de tableau ne correspond à tous les termes de recherche.
SEARCH('foobar=', '`foobar\\=`') FAUX La valeur de search_terms est équivalente à foobar\=.
SEARCH('foobar=', R'`foobar\=`') FAUX Cela équivaut à l'exemple précédent.
SEARCH('foobar=', 'foobar\\=') VRAI Le signe égal est un délimiteur dans les données et la requête.
SEARCH('foobar=', R'foobar\=') VRAI Cela équivaut à l'exemple précédent.
SEARCH('foobar.example', '`foobar`') VRAI Une correspondance exacte est trouvée.
SEARCH('foobar.example', '`foobar.`') FAUX "foobar." n'est pas analysé en raison d'accents graves, il n'est pas
SEARCH('foobar..example', '`foobar.`') VRAI "foobar." n'est pas analysé en raison d'accents graves, il est suivi

Le tableau suivant présente des exemples d'appels à la fonction SEARCH à l'aide de l'analyseur de texte NO_OP_ANALYZER et les raisons de diverses valeurs de retour :

Appel de fonction Renvoie Motif
SEARCH('foobar', 'foobar', analyzer=>'NO_OP_ANALYZER') VRAI Une correspondance exacte est trouvée.
SEARCH('foobar', '`foobar`', analyzer=>'NO_OP_ANALYZER') FAUX Les accents graves ne sont pas des caractères spéciaux pour NO_OP_ANALYZER.
SEARCH('foobar', 'foobar', analyzer=>'NO_OP_ANALYZER') FAUX L'utilisation de la casse ne correspond pas.
SEARCH('foobar example', 'foobar', analyzer=>'NO_OP_ANALYZER') FAUX Il n'existe aucun délimiteur pour NO_OP_ANALYZER.
SEARCH('', '', analyzer=>'NO_OP_ANALYZER') VRAI Il n'existe aucun délimiteur pour NO_OP_ANALYZER.

Autres opérateurs et fonctions

Vous pouvez effectuer des optimisations de l'index de recherche avec plusieurs opérateurs, fonctions et prédicats.

Optimiser avec des opérateurs et des fonctions de comparaison

BigQuery peut optimiser certaines requêtes utilisant l'opérateur égal (=), l'opérateur IN, l'opérateur LIKE ou la fonction STARTS_WITH pour comparer des littéraux de chaîne à des données indexées.

Optimiser avec des prédicats de chaîne

Les prédicats suivants sont éligibles à l'optimisation de l'index de recherche :

  • column_name = 'string_literal'
  • 'string_literal' = column_name
  • struct_column.nested_field = 'string_literal'
  • string_array_column[OFFSET(0)] = 'string_literal'
  • string_array_column[ORDINAL(1)] = 'string_literal'
  • column_name IN ('string_literal1', 'string_literal2', ...)
  • STARTS_WITH(column_name, 'prefix')
  • column_name LIKE 'prefix%'

Optimiser avec des prédicats numériques

Pour obtenir de l'aide pendant la version preview, envoyez un e-mail à bq-search-team@google.com.

Si l'index de recherche a été créé avec des types de données numériques, BigQuery peut optimiser certaines requêtes qui utilisent l'opérateur égal (=) ou l'opérateur IN avec des données indexées. Les prédicats suivants sont éligibles à l'optimisation de l'index de recherche :

  • INT64(json_column.int64_field) = 1
  • int64_column = 1
  • int64_array_column[OFFSET(0)] = 1
  • int64_column IN (1, 2)
  • struct_column.nested_int64_field = 1
  • struct_column.nested_timestamp_field = TIMESTAMP "2024-02-15 21:31:40"
  • timestamp_column = "2024-02-15 21:31:40"
  • timestamp_column IN ("2024-02-15 21:31:40", "2024-02-16 21:31:40")

Optimiser les fonctions qui produisent des données indexées

BigQuery permet l'optimisation de l'index de recherche lorsque certaines fonctions sont appliquées aux données indexées. Si l'index de recherche utilise l'analyseur de texte LOG_ANALYZER par défaut, vous pouvez appliquer les fonctions UPPER ou LOWER à la colonne, par exemple UPPER(column_name) = 'STRING_LITERAL'.

Pour les données de type chaîne scalaire JSON, extraites d'une colonne JSON indexée, vous pouvez appliquer la fonction STRING, ou bien sa version sécurisée, SAFE.STRING. Si la valeur JSON extraite n'est pas une chaîne, la fonction STRING génère une erreur et la fonction SAFE.STRING renvoie NULL.

Pour les données STRING indexées et au format JSON (et non pas les données JSON à proprement parler), vous pouvez appliquer les fonctions suivantes :

Par exemple, supposons que vous disposiez de la table indexée suivante, appelée dataset.person_data, avec une colonne JSON et une colonne STRING :

+----------------------------------------------------------------+-----------------------------------------+
| json_column                                                    | string_column                           |
+----------------------------------------------------------------+-----------------------------------------+
| { "name" : "Ariel", "email" : "cloudysanfrancisco@gmail.com" } | { "name" : "Ariel", "job" : "doctor" }  |
+----------------------------------------------------------------+-----------------------------------------+

Les requêtes suivantes sont éligibles à l'optimisation :

SELECT * FROM dataset.person_data
WHERE SAFE.STRING(json_column.email) = 'cloudysanfrancisco@gmail.com';
SELECT * FROM dataset.person_data
WHERE JSON_VALUE(string_column, '$.job') IN ('doctor', 'lawyer', 'teacher');

Les combinaisons de ces fonctions, telles que UPPER(JSON_VALUE(json_string_expression)) = 'FOO', vont également être optimisées.

Utiliser l'index de recherche

Pour déterminer si un index de recherche a été utilisé pour une requête, consultez les informations sur la tâche de la requête dans les résultats de requête. Les champs Index Usage Mode (Mode d'utilisation de l'index) et Index Unused Reasons (Motifs de non utilisation de l'indexation) fournissent des informations détaillées sur l'utilisation de l'index de recherche.

Informations sur la tâche montrant pourquoi un index de recherche n&#39;était pas utilisé.

Des informations sur l'utilisation de l'index de recherche sont également disponibles via le champ searchStatistics dans la méthode API Jobs.Get. Le champ indexUsageMode dans searchStatistics indique si un index de recherche a été utilisé avec les valeurs suivantes :

  • UNUSED : aucun index de recherche n'a été utilisé.
  • PARTIALLY_USED : une partie de la requête a utilisé les index de recherche.
  • FULLY_USED : chaque fonction SEARCH de la requête a utilisé un index de recherche.

Lorsque la valeur de indexUsageMode est définie sur UNUSED ou PARTIALLY_USED, le champ indexUnusuedReasons permet de comprendre pourquoi les index de recherche n'ont pas été utilisés dans la requête.

Pour afficher searchStatistics pour une requête, exécutez la commande bq show.

bq show --format=prettyjson -j JOB_ID

Exemple

Supposons que vous exécutiez une requête qui appelle la fonction SEARCH sur les données d'une table. Vous pouvez afficher les détails de la tâche de la requête pour trouver l'ID de tâche, puis exécuter la commande bq show pour afficher plus d'informations :

bq show --format=prettyjson --j my_project:US.bquijob_123x456_789y123z456c

Le résultat contient de nombreux champs, y compris searchStatistics, qui ressemble à ceci : Dans cet exemple, indexUsageMode indique que l'index n'a pas été utilisé. En effet, la table n'a pas d'index de recherche. Pour résoudre ce problème, créez un index de recherche sur la table. Consultez le champ code indexUnusedReason pour obtenir la liste de toutes les raisons pour lesquelles un index de recherche peut ne pas être utilisé dans une requête.

"searchStatistics": {
  "indexUnusedReasons": [
    {
      "baseTable": {
        "datasetId": "my_dataset",
        "projectId": "my_project",
        "tableId": "my_table"
      },
      "code": "INDEX_CONFIG_NOT_AVAILABLE",
      "message": "There is no search index configuration for the base table `my_project:my_dataset.my_table`."
    }
  ],
  "indexUsageMode": "UNUSED"
},

Bonnes pratiques

Les sections suivantes décrivent les bonnes pratiques concernant l'utilisation de la fonction SEARCH.

Rechercher de manière sélective

La recherche fonctionne mieux lorsque votre recherche renvoie peu de résultats. Effectuez des recherches aussi spécifiques que possible.

Optimisation ORDER BY LIMIT

Les requêtes qui utilisent SEARCH, =, IN, LIKE ou STARTS_WITH sur une très grande table partitionnée peuvent être optimisées lorsque vous utilisez une clause ORDER BY sur le champ partitionné et une clause LIMIT. Pour les requêtes qui ne contiennent pas la fonction SEARCH, vous pouvez utiliser les autres opérateurs et fonctions pour tirer parti de l'optimisation. L'optimisation est appliquée, que la table soit indexée ou non. Cette méthode est adaptée si vous recherchez un terme commun. Par exemple, supposons que la table Logs créée précédemment soit partitionnée sur une colonne de type DATE supplémentaire appelée day. La requête suivante est optimisée :

SELECT
  Level, Source, Message
FROM
  my_dataset.Logs
WHERE
  SEARCH(Message, "foo")
ORDER BY
  day
LIMIT 10;

Lorsque vous utilisez la fonction SEARCH, faites porter la recherche sur les seules colonnes de la table qui sont susceptibles selon vous de contenir vos termes de recherche. Cela améliore les performances et réduit le nombre d'octets à analyser.

Utiliser des accents graves

Lorsque vous utilisez la fonction SEARCH avec l'analyseur de texte LOG_ANALYZER, le fait de placer votre requête de recherche entre accents graves force la recherche d'une correspondance exacte. Cela est utile si votre recherche est sensible à la casse ou contient des caractères qui ne doivent pas être interprétés comme des délimiteurs. Par exemple, pour rechercher l'adresse IP 192.0.2.1, utilisez `192.0.2.1`. Sans accents graves, la recherche va renvoyer toutes les lignes contenant les jetons individuels 192, 0, 2 et 1, dans n'importe quel ordre.