用于在 PostgreSQL 中搜索嵌套 JSONB 数组元素的索引

Index for searching nested JSONB array elements in PostgreSQL

我对 postgres 还很陌生,目前使用的是 9.6。 当尝试使用它的 jsonb 文档在 postgres 中实现全文搜索时,我注意到嵌套数组的搜索结果很慢。我使用了 'explain' 命令,但它没有使用任何索引。 为了简单起见,我创建了一个 table 来进行调查:

CREATE TABLE book (
  id   BIGSERIAL NOT NULL,
  data JSONB     NOT NULL
);

我的可用指数:

CREATE INDEX book_author_idx
  ON book USING GIN (to_tsvector('english', book.data ->> 'author'));
CREATE INDEX book_author_name_idx
  ON book USING GIN (to_tsvector('english', book.data -> 'author' ->> 'name'));

和一些数据来填充文件:

INSERT INTO book (data)
VALUES (CAST('{"author": [{"id": 0, "name": "Cats"}, ' ||
             '           {"id": 1, "name": "Dogs"}]}' AS JSONB));

我可以使用以下查询来搜索书籍元素,但它不使用任何索引。我的 120k 产品的实际数据大约需要 1200 毫秒,而其他带有索引的搜索需要 0.2 毫秒。

EXPLAIN ANALYZE
SELECT
  id,
  data ->> 'author' AS author
FROM book, jsonb_array_elements(data #> '{author}') author_array
WHERE to_tsvector('english', author_array ->> 'name') @@ to_tsquery('cat');

相比之下,下一个查询使用 book_author_name_idx 但由于数组结构没有找到任何东西。

EXPLAIN ANALYZE
SELECT
  id,
  data ->> 'author' AS author
FROM book
WHERE to_tsvector('english', data -> 'author' ->> 'name') @@ to_tsquery('cat');

如何调整查询以使用语言索引? 我知道,我可以为作者创建一个新的 table 并且只引用 ID,但我宁愿将所有数据保存在一个 table 中以提高性能。

根据 posz 的提示,我找到了解决方案。 因为'||'函数不能按我需要的方式工作,我为 tsvectors 使用了自定义 concat 函数。我在 github 上使用了 glittershark 中的代码,并将 to_tsvector 从 'default' 更改为 'english' 以满足我的需要。

CREATE OR REPLACE FUNCTION concat_tsvectors(tsv1 TSVECTOR, tsv2 TSVECTOR)
  RETURNS TSVECTOR AS $$
BEGIN
  RETURN coalesce(tsv1, to_tsvector('english', ''))
         || coalesce(tsv2, to_tsvector('english', ''));
END;
$$ LANGUAGE plpgsql;

CREATE AGGREGATE tsvector_agg (
BASETYPE = TSVECTOR,
SFUNC = concat_tsvectors,
STYPE = TSVECTOR,
INITCOND = ''
);

这是我写的自定义函数。输入是 JSONB 形式的数据,输出是具有聚合作者姓名的 tsvector。

CREATE OR REPLACE FUNCTION author_function(
  IN  data        JSONB,
  OUT resultNames TSVECTOR
)
  RETURNS TSVECTOR AS $$
DECLARE
  authorRecords   RECORD;
  combinedAuthors JSONB [];
  singleAuthor    JSONB;
BEGIN
  FOR authorRecords IN (SELECT value
                        FROM jsonb_array_elements(data #> '{author}'))
  LOOP
    combinedAuthors := combinedAuthors || authorRecords.value;
  END LOOP;
  FOREACH singleAuthor IN ARRAY coalesce(combinedAuthors, '{}')
  LOOP
    resultNames := concat_tsvectors(resultNames, to_tsvector('english', singleAuthor ->> 'name'));
  END LOOP;
END; $$
LANGUAGE plpgsql
IMMUTABLE;

然后我为我的图书对象设置了一个索引。

CREATE INDEX book_author_function_idx
  ON book USING GIN (author_function(book.data));

作者姓名已经通过 to_tsvector('english', singleAuthor) 函数,所以我可以这样查询它们:

EXPLAIN ANALYSE
SELECT
  id,
  data ->> 'author' AS author
FROM book
WHERE author_function(book.data) @@ to_tsquery('cat');

因此,对我的实际数据的查询从 1100-1200 毫秒减少到 ~0.5 毫秒。 我不确定这是否是最好的解决方案,所以如果您有更好的建议,请告诉我。