多个相关表的全文搜索:索引和性能

shy*_*ent 6 postgresql performance full-text-search postgresql-performance

我们有以下数据库结构

CREATE TABLE objects (
    id       int   PRIMARY KEY,
    name     text,
    address  text
);

CREATE TABLE tasks (
    id int           PRIMARY KEY,
    object_id int    NOT NULL,
    actor_id int     NOT NULL,
    description text
);

CREATE TABLE actors (
    id   int  PRIMARY KEY,
    name text
);
Run Code Online (Sandbox Code Playgroud)

用户输入以空格分隔的单词列表(基本上是搜索词),我们必须搜索满足以下条件的任务:如果每个搜索词在任务描述的串联中至少出现一次,则该任务是“匹配”,其关联对象的名称和地址以及关联参与者的名称。

现在,如果我们不关心性能,我们可以这样做(给定查询“foo bar”):

SELECT t.id, t.description
FROM tasks AS t
INNER JOIN actors AS a ON t.actor_id = a.id
INNER JOIN objects AS o ON t.object_id = o.id
WHERE to_tsvector(concat_ws(' ', t.description, o.name, o.address, a.name)) @@
    plainto_tsquery('foo bar');
Run Code Online (Sandbox Code Playgroud)

不幸的是,我们正在关注的性能。数据集可能如下(并且预计会增长):

  • 大约 10000 个对象
  • 约1000名演员
  • 大约 100000 个任务均匀分布在对象之间

我考虑过的:

制作一个像这样的非规范化表:

CREATE TABLE task_documents (
    id int PRIMARY KEY,
    doc tsvector
)
Run Code Online (Sandbox Code Playgroud)

字段“doc”将包含任务描述、关联对象的名称和地址以及参与者名称的串联。我们必须在这个字段上创建一个索引,它将用于全文搜索查询。此表将在任务、参与者、对象的更新/插入触发器中更新。

缺点:大量重复数据(我不太关心这个),并且主表的更新在更新的行数方面将变得不可预测(例如,您更新某个对象的名称,现在突然您必须更新数千task_documents 中的行数)。

老实说,我没有更多(好)点子了。显然不可能创建一个跨越 3 个表的索引,以便将其用于WHERE原始查询中的子句。

UPD

这是一个带有数据库架构和一些数据的sqlfiddle。我不得不弥补,因为我们目前没有真正的数据。

Eva*_*oll 8

优化

你走在正确的轨道上。

你要么需要

  1. 非规范化
  2. 缓存

缓存结果

您可能想要的是一个MATERIALIZED VIEW. 这很容易,而且效果很好。

CREATE MATERIALIZED VIEW foo
AS
SELECT t.id, to_tsvector(concat_ws(' ',a.name, o.address, t.description, a.name)) AS tsv
FROM tasks AS t
INNER JOIN actors AS a ON t.actor_id = a.id
INNER JOIN objects AS o ON t.object_id = o.id 
;
Run Code Online (Sandbox Code Playgroud)

那么就

SELECT * FROM foo WHERE tsv @@ plainto_tsquery('foo bar');
Run Code Online (Sandbox Code Playgroud)

表的非规范化

这可以有很多种形式,不过你做对了。

重新设计

像这样以模糊的方式搜索所有内容是一场失败的游戏。即使是《龙与地下城》的这种翻版符合雅虎问答也有规则。

在此处输入图片说明

当您引入[text]用于标记的语法时,生成查询变得容易is:answer得多,并且只搜索答案,而不是重建 Google 和规范化索引。

  • 我想,物化视图与非规范化表(如我的问题所示)没有太大区别,因为它本质上是一个表。而且您甚至无法重新生成其中的一部分,只能完整地重新生成(“刷新物化视图”)。至于特殊语法,- 恐怕在我们的情况下行不通。 (2认同)

Dan*_*ité 8

这看起来像是关系数据库中相当通用的全文搜索问题。

您关于参与者或对象的更新在非规范化结构中会很麻烦的预测看起来是正确的。在考虑非规范化之前,最好先穷尽规范化模式的可能性,特别是因为您的表大小适中。

我建议分别对所有文本字段进行 FT 索引,并使用基于查询所有文本字段并通过 UNION 将结果与 OR 逻辑连接组合的想法而设计的查询。

索引(使用simple文本配置进行精确和与语言无关的匹配,但使用最适合您情况的内容,前提是它与查询中的相同):

create index idx1 on objects using gin(to_tsvector('simple', name||' '||address));
create index idx2 on tasks using gin(to_tsvector('simple', description));
create index idx3 on actors using gin(to_tsvector('simple', name));
Run Code Online (Sandbox Code Playgroud)

搜寻中在索引表达式中搜索word1word2

WITH
 words(w) AS (VALUES ('word1'), ('word2')),
 matching_objects(id) as (select o.* from objects as o, words where to_tsquery('simple',w) @@ to_tsvector('simple', o.name||' '||o.address)),
 matching_tasks as (select t.* from tasks as t, words where to_tsquery('simple',w) @@ to_tsvector('simple', t.description)),
 matching_actors as (select a.* from actors as a, words where to_tsquery('simple',w) @@ to_tsvector('simple', a.name))
SELECT * FROM (
 SELECT t.id, t.description, a.name as actor_name, o.name as object_name
   FROM matching_tasks AS t JOIN actors AS a ON t.actor_id = a.id JOIN objects AS o ON t.object_id = o.id
UNION
  SELECT t.id, t.description, a.name as actor_name, o.name as object_name
    FROM tasks AS t JOIN matching_actors AS a ON t.actor_id = a.id JOIN objects AS o ON t.object_id = o.id
UNION
  SELECT t.id, t.description, a.name as actor_name, o.name as object_name
    FROM tasks AS t JOIN actors AS a ON t.actor_id = a.id JOIN matching_objects AS o ON t.object_id = o.id
) AS result;
Run Code Online (Sandbox Code Playgroud)

正在寻找word1word2在同一字段中AND可以通过替换

 words(w) AS (VALUES ('word1'), ('word2'))
Run Code Online (Sandbox Code Playgroud)

 words(w) AS (VALUES ('word1 & word2'))
Run Code Online (Sandbox Code Playgroud)

如果word1ANDword2必须同时出现在同一个“任务”(包括连接表)中,但不一定出现在同一字段中,则可以通过在上面添加 GROUP BY 步骤来过滤掉没有的结果。当搜索 N 个单词时,恰好有 N 个命中。

查询变为:

WITH
 words(w) AS (VALUES ('word1'), ('word2')),
 matching_objects as (select w, o.* from objects as o, words where to_tsquery('simple',w) @@ to_tsvector('simple', o.name||' '||o.address)),
 matching_tasks as (select w,t .* from tasks as t, words where to_tsquery('simple',w) @@ to_tsvector('simple', t.description)),
 matching_actors as (select w, a.* from actors as a, words where to_tsquery('simple',w) @@ to_tsvector('simple', a.name))
SELECT id FROM (
 SELECT w, t.id
   FROM matching_tasks AS t JOIN actors AS a ON t.actor_id = a.id JOIN objects AS o ON t.object_id = o.id
UNION
  SELECT w, t.id
    FROM tasks AS t JOIN matching_actors AS a ON t.actor_id = a.id JOIN objects AS o ON t.object_id = o.id
UNION
  SELECT w, t.id
    FROM tasks AS t JOIN actors AS a ON t.actor_id = a.id JOIN matching_objects AS o ON t.object_id = o.id
) AS r GROUP BY id HAVING count(*)=(select count(*) FROM words);
Run Code Online (Sandbox Code Playgroud)

当在 UNION 构造的不同子查询中找到相同的单词时,UNION 对元组进行重复数据删除,这一事实可以处理过滤情况。

此查询仅生成任务 ID。它们需要反复连接actors才能objects取回需要显示或返回的列。