使用 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
一旦您拥有了一些数据,就可以使用 tsvector
和 tsquery
搜索书籍
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 数据库客户端实现分面搜索。