还在使用 Jaeger/Sentry?Uptrace 是一个 开源 APM,用于 OpenTelemetry,您可以使用它来监控应用程序并设置警报,以通过电子邮件、Slack、Telegram 等方式接收通知。

使用 PostgreSQL 全文搜索进行分面搜索

分面搜索或分面导航允许用户通过应用从某些属性或标签生成的多个过滤器来缩小搜索结果范围。在本文中,我们将使用 PostgreSQL 全文搜索在新窗口中打开ts_stat 函数来实现分面搜索。

GitHub 搜索是分面导航的一个很好的例子(参见右侧的图像)。


创建表

让我们从创建一个包含名称、标签(属性)和文本搜索向量的 books 表开始

CREATE TABLE books (
  id bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
  name varchar(1000),
  tags jsonb,
  tsv tsvector
);

tsvector在新窗口中打开 是一个排序的、用于搜索的、不同的规范化词语(词素)列表。您可以使用 to_tsvector 函数创建 tsvector

SELECT to_tsvector('english', 'The Fat Rats');

   to_tsvector
-----------------
 'fat':2 'rat':3

您可以在插入行到表时使用 to_tsvector

INSERT INTO books (name, tsv)
VALUES
  ('hello word', to_tsvector('english', 'hello world')),
  ('foo bar', to_tsvector('english', 'foo bar'))
RETURNING *;

 id |    name    | attrs |         tsv
----+------------+-------+---------------------
  1 | hello word |       | 'hello':1 'world':2
  2 | foo bar    |       | 'bar':2 'foo':1

一旦您拥有了一些数据,就可以使用 tsvectortsquery 搜索书籍

SELECT * FROM books
WHERE tsv @@ websearch_to_tsquery('english', 'hello');

 id |    name    | tags |         tsv
----+------------+------+---------------------
  1 | hello word |      | 'hello':1 'world':2

如果您的数据集很大,该查询可能会很慢,但您可以通过在 tsv 列上添加反向索引来使其更快

CREATE INDEX books_tsv_idx ON books USING GIN (tsv);

并检查 PostgreSQL 是否使用了索引

EXPLAIN ANALYZE
SELECT * FROM books
WHERE tsv @@ websearch_to_tsquery('english', 'hello');

从标签创建分面

我们将使用以下数据集来测试我们的查询

- model: Book
  rows:
    - name: The Gods Themselves by Isaac Asimov
      tags:
        - moods:adventurous
        - moods:challenging
        - pace:medium
    - name: Legend by David Gemmell
      tags:
        - moods:adventurous
        - moods:emotional
        - pace:fast
    - name: Lord of Light by Roger Zelazny
      tags:
        - moods:adventurous
        - moods:challenging
        - pace:medium
    - name: The Name of the Wind by Patrick Rothfuss
      tags:
        - moods:adventurous
        - moods:mysterious
        - pace:medium
    - name: Hyperion by Dan Simmons
      tags:
        - moods:mysterious
        - moods:adventurous
        - pace:medium

您可以使用以下查询插入这些书籍

INSERT INTO "books" ("name", "tags", "tsv")
VALUES
  ('The Gods Themselves by Isaac Asimov', '["moods:adventurous","moods:challenging","pace:medium"]', array_to_tsvector('{"moods:adventurous","moods:challenging","pace:medium"}')),
  ('Legend by David Gemmell', '["moods:adventurous","moods:emotional","pace:fast"]', array_to_tsvector('{"moods:adventurous","moods:emotional","pace:fast"}')),
  ('Lord of Light by Roger Zelazny', '["moods:adventurous","moods:challenging","pace:medium"]', array_to_tsvector('{"moods:adventurous","moods:challenging","pace:medium"}')),
  ('The Name of the Wind by Patrick Rothfuss', '["moods:adventurous","moods:mysterious","pace:medium"]', array_to_tsvector('{"moods:adventurous","moods:mysterious","pace:medium"}')),
  ('Hyperion by Dan Simmons', '["moods:mysterious","moods:adventurous","pace:medium"]', array_to_tsvector('{"moods:mysterious","moods:adventurous","pace:medium"}'));

然后按标签过滤书籍

SELECT name, tags FROM books WHERE tsv @@ 'moods\:mysterious'::tsquery;

                   name                   |                           tags
------------------------------------------+----------------------------------------------------------
 The Name of the Wind by Patrick Rothfuss | ["moods:adventurous", "moods:mysterious", "pace:medium"]
 Hyperion by Dan Simmons                  | ["moods:mysterious", "moods:adventurous", "pace:medium"]

构建分面

让我们从定义一个我们期望最终获得的分面开始

属性书籍数量
情绪冒险5
情绪挑战2
情绪情感1
情绪神秘2
节奏1
节奏中等4

我们可以使用以下查询轻松实现该结果

WITH tags AS (
  SELECT jsonb_array_elements_text(tags) AS tag
  FROM books
)
SELECT
  split_part(tag, ':', 1) AS attr,
  split_part(tag, ':', 2) AS value,
  count(*) AS count
FROM tags
GROUP by attr, value
ORDER BY attr, value, count DESC;

 attr  |    value    | count
-------+-------------+-------
 moods | adventurous |     5
 moods | challenging |     2
 moods | emotional   |     1
 moods | mysterious  |     2
 pace  | fast        |     1
 pace  | medium      |     4

但这相当缓慢且效率低下,因为我们需要选择所有 tags 来构建分面。我们可以做得更好吗?是的,使用 ts_stat 函数直接从 tsv 列获取所需数据。

检索文档统计信息

函数 ts_stat 允许检索由 PostgreSQL 全文搜索引擎在 tsvector 列中维护的文档统计信息。

SELECT word, ndoc FROM ts_stat($$ SELECT tsv FROM books $$) ORDER BY word;

       word        | ndoc
-------------------+------
 moods:adventurous |    5
 moods:challenging |    2
 moods:emotional   |    1
 moods:mysterious  |    2
 pace:fast         |    1
 pace:medium       |    4

如您所见,PostgreSQL 已经维护了我们仅使用 tsv 列构建分面所需的统计信息

SELECT
  split_part(word, ':', 1) AS attr,
  split_part(word, ':', 2) AS value,
  ndoc AS count
FROM ts_stat($$ SELECT tsv FROM books $$)
ORDER BY word;

 attr  |    value    | count
-------+-------------+-------
 moods | adventurous |     5
 moods | challenging |     2
 moods | emotional   |     1
 moods | mysterious  |     2
 pace  | fast        |     1
 pace  | medium      |     4

要构建一个精炼的分面,您可以对同一个 tsv 列使用快速过滤器,该列由我们之前创建的索引覆盖

SELECT
  split_part(word, ':', 1) AS attr,
  split_part(word, ':', 2) AS value,
  ndoc AS count
FROM ts_stat($$
  SELECT tsv FROM books
  WHERE tsv @@ 'pace\:fast'::tsquery
$$)
ORDER BY word;

 attr  |    value    | count
-------+-------------+-------
 moods | adventurous |     1
 moods | emotional   |     1
 pace  | fast        |     1

结论

PostgreSQL 提供了构建高达 100 万行的快速分面搜索所需的一切。对于更大的数据集,处理时间成为一个问题,您可能需要对数据库进行分片。

您还可以查看 pg-faceted-search在新窗口中打开 示例,该示例演示了如何使用 Go 和 Bun 数据库客户端实现分面搜索。

另请参阅