web-dev-qa-db-ja.com

複数の関連テーブルの全文検索:インデックスとパフォーマンス

次のデータベース構造があります

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

ユーザーは空白で区切られた単語のリスト(基本的に検索語)を入力し、次の条件を満たすタスクを検索する必要があります。タスクの説明の連結で各検索語が少なくとも1回出現する場合、タスクは「一致」します。関連付けられたオブジェクトの名前とアドレス、および関連付けられたアクターの名前。

ここで、パフォーマンスを気にしない場合は、次のようにすることができます(クエリ "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');

残念ながら、私たちはパフォーマンスについて心配しています。データセットは、おそらく次のようになります(さらに増加すると予想されます)。

  • 約10000個のオブジェクト
  • 約1000人の俳優
  • オブジェクト間で均等に分散された約100000のタスク

私が考えたこと:

次のような非正規化テーブルを作成します。

CREATE TABLE task_documents (
    id int PRIMARY KEY,
    doc tsvector
)

フィールド「doc」には、タスクの説明、関連するオブジェクトの名前とアドレス、俳優の名前の連結が含まれます。このフィールドにインデックスを作成する必要があり、全文検索クエリで使用されます。このテーブルは、タスク、アクター、オブジェクトの更新/挿入トリガーで更新されます。

欠点:大量の重複データ(これは私があまり気にしていません)、、およびマスターテーブルへの更新は、更新された行数に関して予測不可能になります(たとえば、一部のオブジェクトの名前であり、突然、task_documentsの数千行を更新する必要があります。

正直なところ、これ以上(良い)アイデアはありません。元のクエリのWHERE句で使用されるように、3つのテーブルにまたがるインデックスを作成することは明らかに不可能です。

[〜#〜]更新[〜#〜]

以下は、DBスキーマといくつかのデータを含む sqlfiddle です。現時点では実際のデータがないので、それを補わなければなりませんでした。

5
shylent

最適化

あなたは正しい道を進んでいます。

あなたはどちらかが必要

  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 
;

じゃあ

SELECT * FROM foo WHERE tsv @@ plainto_tsquery('foo bar');

テーブルを非正規化する

これはさまざまな形をとることができますが、これは正しいことです。

再設計

このようなあいまいな方法ですべてを検索することは、負けたゲームです。ダンジョンとドラゴンズのこのノックオフでさえ、Yahoo Answersはルールを満たしています。

enter image description here

タグ付けのために[text]のような構文を導入し、Googleと正規化されたインデックスを再構築するよりも回答だけを検索するためにis:answerを導入すると、クエリを生成する方がはるかに簡単になります。

5
Evan Carroll

これは、リレーショナルデータベースでのかなり一般的な全文検索の問題のように見えます。

アクターまたはオブジェクトの更新が非正規化された構造で厄介であるというあなたの予測は、実物に見えます。特にテーブルのサイズが控えめであるため、非正規化を考える前に、正規化されたスキーマの可能性をよりよく使い切ります。

すべてのテキストフィールドを個別にFTインデックスを作成し、それらすべてをクエリし、結果をOR論理結合を介してUNIONを介して結合するという考えに基づいて設計されたクエリを使用することをお勧めします。

Indexingsimpleを使用すると、正確で言語にとらわれないマッチングのためのテキスト設定が使用されますが、クエリと同じである場合は、ケースに最適なものを使用してください):

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

検索中Word1またはWord2インデックス付き式の任意の場所:

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;

探している Word1 AND Word2同じフィールドで置換することで機能します

 words(w) AS (VALUES ('Word1'), ('Word2'))

 words(w) AS (VALUES ('Word1 & Word2'))

Word1 AND Word2は、同じ「タスク」(結合されたテーブルを含む)に同時に存在する必要がありますが、必ずしも同じフィールドに存在する必要はありません。上記の上に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);

UNION構造の異なるサブクエリ間で同じWordが見つかった場合、UNIONがタプルを重複排除するという事実により、ケースのフィルタリングが処理されます。

このクエリは、タスクのIDのみを生成します。表示または返される必要のある列を取得するには、actorsおよびobjectsに対して再度結合する必要があります。

2
Daniel Vérité