次のデータベース構造があります
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');
残念ながら、私たちはパフォーマンスについて心配しています。データセットは、おそらく次のようになります(さらに増加すると予想されます)。
私が考えたこと:
次のような非正規化テーブルを作成します。
CREATE TABLE task_documents (
id int PRIMARY KEY,
doc tsvector
)
フィールド「doc」には、タスクの説明、関連するオブジェクトの名前とアドレス、俳優の名前の連結が含まれます。このフィールドにインデックスを作成する必要があり、全文検索クエリで使用されます。このテーブルは、タスク、アクター、オブジェクトの更新/挿入トリガーで更新されます。
欠点:大量の重複データ(これは私があまり気にしていません)、、およびマスターテーブルへの更新は、更新された行数に関して予測不可能になります(たとえば、一部のオブジェクトの名前であり、突然、task_documentsの数千行を更新する必要があります。
正直なところ、これ以上(良い)アイデアはありません。元のクエリのWHERE
句で使用されるように、3つのテーブルにまたがるインデックスを作成することは明らかに不可能です。
[〜#〜]更新[〜#〜]
以下は、DBスキーマといくつかのデータを含む sqlfiddle です。現時点では実際のデータがないので、それを補わなければなりませんでした。
あなたは正しい道を進んでいます。
あなたはどちらかが必要
あなたがおそらく望んでいるのは 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はルールを満たしています。
タグ付けのために[text]
のような構文を導入し、Googleと正規化されたインデックスを再構築するよりも回答だけを検索するためにis:answer
を導入すると、クエリを生成する方がはるかに簡単になります。
これは、リレーショナルデータベースでのかなり一般的な全文検索の問題のように見えます。
アクターまたはオブジェクトの更新が非正規化された構造で厄介であるというあなたの予測は、実物に見えます。特にテーブルのサイズが控えめであるため、非正規化を考える前に、正規化されたスキーマの可能性をよりよく使い切ります。
すべてのテキストフィールドを個別にFTインデックスを作成し、それらすべてをクエリし、結果をOR論理結合を介してUNIONを介して結合するという考えに基づいて設計されたクエリを使用することをお勧めします。
Indexing(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));
検索中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
に対して再度結合する必要があります。