搜索编入索引的文本

本页面举例说明了如何在 BigQuery 中进行搜索。 在将数据编入索引时,BigQuery 可以优化某些使用 SEARCH 函数或其他函数和运算符(例如 =INLIKESTARTS_WITH)的查询。

即使部分数据尚未编入索引,SQL 查询也会从所有注入的数据返回正确的结果。但是,使用索引可以显著提高查询性能。如果搜索结果数量占表中总行数的相对较小部分,则可以最大限度地减少处理字节数槽毫秒数,因为扫描的数据较少。如需确定某一索引是否用于查询,请参阅搜索索引使用情况

创建搜索索引

以下名为 Logs 表用于显示使用 SEARCH 函数的不同方式。此示例表非常小,但实际上表越大,使用 SEARCH 获得的性能提升幅度就越大。

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'
);

该表如下所示:

+---------+----------------+-------------------------------------------------------+
| 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                                 |
+---------+----------------+-------------------------------------------------------+

使用默认文本分析器在 Logs 表上创建搜索索引:

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

使用 SEARCH 函数

SEARCH 函数提供对数据的标记化搜索。SEARCH 旨在与索引搭配使用以优化查询。您可以使用 SEARCH 函数搜索整个表,或将搜索限制为特定列。

搜索整个表

以下查询会在 Logs 表的所有列中搜索 bar 值,并返回包含此值的行,而无论大小写如何。 由于搜索索引使用默认文本分析器,因此您无需在 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 |
+---------+----------------+-------------------------------------------------------+

以下查询会在 Logs 表的所有列中搜索 `94.60.64.181` 值,并返回包含此值的行。 反引号允许进行精确搜索,因此省略了包含 181.94.60.64Logs 表的最后一行。

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 |
+---------+----------------+-------------------------------------------------------+

搜索列的子集

您可以使用 SEARCH 轻松指定要在其中搜索数据的列子集。以下查询会在 Logs 表的 Message 列中搜索值 94.60.64.181,并返回包含此值的行。

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 |
+---------+----------------+-------------------------------------------------------+

以下查询会搜索 Logs 表的 SourceMessage 列。它会返回任一列中包含值 94.60.64.181 的行。

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 |
+---------+----------------+-------------------------------------------------------+

如果一个表包含许多列,并且您想要搜索其中的大部分列,则可以更轻松地指定要从搜索中排除的列。以下查询会搜索 Logs 表中 Message 列除外的所有列。它会返回任何列(包含值 94.60.64.181Message 列除外)的行。

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                             |
+---------+----------------+---------------------------------------------------+

使用其他文本分析器

以下示例会创建一个名为 contact_info 的表,其索引使用 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   |
+---------+---------------------+

以下查询会在 name 列中搜索 Kim,在 email 列中搜索 kim。 由于搜索索引不使用默认文本分析器,因此您必须将该分析器的名称传递给 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 不会修改文本,因此 SEARCH 函数只会返回 TRUE 以找到完全匹配项:

+---------+----------+---------------------+-----------+
| 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     |
+---------+----------+---------------------+-----------+

配置文本分析器选项

LOG_ANALYZERPATTERN_ANALYZER 文本分析器可以通过向配置选项添加 JSON 格式的字符串来自定义。您可以在 SEARCH 函数CREATE SEARCH INDEX DDL 语句TEXT_ANALYZE 函数

以下示例会创建一个名为 complex_table 的表,其索引使用 LOG_ANALYZER 文本分析器。它使用 JSON 格式的字符串来配置分析器选项:

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"}
    }
  ]
}''');

下表举例说明了使用不同文本分析器及其结果调用 SEARCH 函数的过程。第一个表使用默认文本分析器 LOG_ANALYZER 调用 SEARCH 函数:

函数调用 返回值 原因
SEARCH('foobarexample', NULL) 错误 search_terms 为 `NULL`。
SEARCH('foobarexample', '') 错误 search_terms 不包含令牌。
SEARCH('foobar-example', 'foobar example') TRUE “-”和“ ”是分隔符。
SEARCH('foobar-example', 'foobarexample') FALSE search_terms 并未拆分。
SEARCH('foobar-example', 'foobar\\&example') TRUE 双反斜杠会对“&”符号进行转义,后者是分隔符。
SEARCH('foobar-example', R'foobar\&example') TRUE 单反斜杠会对原始字符串中的“&”符号进行转义。
SEARCH('foobar-example', '`foobar&example`') FALSE 反引号需要与 foobar&example 完全匹配。
SEARCH('foobar&example', '`foobar&example`') TRUE 找到完全匹配的内容。
SEARCH('foobar-example', 'example foobar') TRUE 字词的顺序无关紧要。
SEARCH('foobar-example', 'foobar example') TRUE 词法单元设置为小写。
SEARCH('foobar-example', '`foobar-example`') TRUE 找到完全匹配的内容。
SEARCH('foobar-example', '`foobar`') FALSE 反引号保留大小写。
SEARCH('`foobar-example`', '`foobar-example`') FALSE 反引号对于 data_to_search 和
SEARCH('foobar@example.com', '`example.com`') TRUE 在 data_to_search 中的分隔符后面找到完全匹配项。
SEARCH('a foobar-example b', '`foobar-example`') TRUE 可在空格分隔符之间找到完全匹配的内容。
SEARCH(['foobar', 'example'], 'foobar example') FALSE 没有与所有搜索字词相匹配的单个数组条目。
SEARCH('foobar=', '`foobar\\=`') FALSE search_terms 等同于 foobar\=。
SEARCH('foobar=', R'`foobar\=`') FALSE 这等同于上一个示例。
SEARCH('foobar=', 'foobar\\=') TRUE 等号是数据和查询中的分隔符。
SEARCH('foobar=', R'foobar\=') TRUE 这等同于上一个示例。
SEARCH('foobar.example', '`foobar`') TRUE 找到完全匹配的内容。
SEARCH('foobar.example', '`foobar.`') FALSE 由于存在反引号,因此不会分析 `foobar.`;这不是
SEARCH('foobar..example', '`foobar.`') TRUE 由于存在反引号,因此不会分析 `foobar.`;其后接

下表举例说明了使用 NO_OP_ANALYZER 文本分析器调用 SEARCH 函数以及各种返回值的原因:

函数调用 返回值 原因
SEARCH('foobar', 'foobar', analyzer=>'NO_OP_ANALYZER') TRUE 找到完全匹配的内容。
SEARCH('foobar', '`foobar`', analyzer=>'NO_OP_ANALYZER') FALSE 反引号不是 NO_OP_ANALYZER 的特殊字符。
SEARCH('foobar', 'foobar', analyzer=>'NO_OP_ANALYZER') FALSE 大小写不匹配。
SEARCH('foobar example', 'foobar', analyzer=>'NO_OP_ANALYZER') FALSE NO_OP_ANALYZER 没有分隔符。
SEARCH('', '', analyzer=>'NO_OP_ANALYZER') TRUE NO_OP_ANALYZER 没有分隔符。

其他运算符和函数

BigQuery 可以优化某些使用等号运算符 (=)、IN 运算符LIKE 运算符STARTS_WITH 函数的查询,以将字符串字面量与编入索引的数据进行比较。

以下谓词符合搜索索引优化的条件:

  • 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%'

当某些函数应用于编入索引的数据时,BigQuery 还支持优化。如果搜索索引使用默认的 LOG_ANALYZER 文本分析器,则可以将 UPPERLOWER 函数应用于列,例如 UPPER(column_name) = 'STRING_LITERAL'

对于从编入索引的 JSON 列中提取的 JSON 标量字符串数据,您可以应用 STRING 函数或其安全版本 SAFE.STRING。如果提取的 JSON 值不是字符串,则 STRING 函数会产生错误,并且 SAFE.STRING 函数会返回 NULL

对于编入索引的 JSON 格式的 STRING(非 JSON)数据,您可以应用以下函数:

例如,假设您有以下名为 dataset.person_data 的编入索引表,其中包含 JSONSTRING 列:

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

以下查询符合优化条件:

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');

这些函数的组合也经过了优化,例如 UPPER(JSON_VALUE(json_string_expression)) = 'FOO'

搜索索引使用情况

如需确定查询索引是否用于查询,请在查询结果中查看查询的作业信息索引使用模式索引未使用的原因字段提供有关搜索索引使用情况的详细信息。

说明搜索索引未使用的原因的作业信息。

您还可以通过 Jobs.Get API 方法中的 searchStatistics 字段获取有关搜索索引使用情况的信息。searchStatistics 中的 indexUsageMode 字段通过以下值指示是否已使用某一搜索索引:

  • UNUSED:未使用搜索索引。
  • PARTIALLY_USED:部分查询使用了搜索索引,而部分则未使用搜索索引。
  • FULLY_USED:查询中的每个 SEARCH 函数都使用了一个搜索索引。

indexUsageModeUNUSEDPARTIALLY_USED 时,indexUnusuedReasons 字段包含有关未在查询中使用搜索索引的原因的信息。

如需查看查询的 searchStatistics,请运行 bq show 命令。

bq show --format=prettyjson -j JOB_ID

示例

假设您运行一个查询来对表中的数据调用 SEARCH 函数。那么您可以查看查询的作业详情以查找作业 ID,然后运行 bq show 命令来查看更多信息:

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

输出包含许多字段,包括 searchStatistics,如下所示。在此示例中,indexUsageMode 表示未使用索引。这是因为该表没有搜索索引。如需解决此问题,请在该表上创建搜索索引。请查看 indexUnusedReason code 字段,获取未在查询中使用搜索索引的所有可能原因的列表。

"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"
},

最佳做法

以下部分介绍了使用 SEARCH 函数时的最佳实践。

选择性搜索

如果您的搜索结果很少,则搜索功能效果最佳。搜索应尽可能具体。

ORDER BY LIMIT 优化

在对分区字段使用 ORDER BY 子句以及 LIMIT 子句时,对非常大的分区表使用 SEARCH=INLIKESTARTS_WITH 的查询可以优化。对于不包含 SEARCH 函数的查询,您可以使用其他运算符和函数来利用优化。无论表是否编入索引,都会应用优化。这在您搜索常用字词时效果良好。例如,假设之前创建的 Logs 表基于其他名为 dayDATE 类型列进行分区。以下查询经过了优化:

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

使用 SEARCH 函数时,请仅搜索您希望包含搜索字词的表列。这有助于提高性能并减少需要扫描的字节数。

使用英文反引号

SEARCH 函数与 LOG_ANALYZER 文本分析器结合使用时,用英文反引号将搜索查询括起来会强制执行完全匹配。如果搜索区分大小写,或包含不应解读为分隔符的字符,则此方法非常有用。例如,如需搜索 IP 地址 192.0.2.1,请使用 `192.0.2.1`。如果没有英文反引号,搜索会返回包含各个令牌 192021(任意顺序)的任何行。