PGroongaを使ってn全文検索結果をnより良くする方法¶ ↑
: author 堀本 泰弘 : institution 株式会社クリアコード : content-source PostgreSQL Conference Japan 2021 : date 2021-11-12 : allotted-time 45m : start-time 2021-11-12T16:10:00+09:00 : end-time 2021-11-12T16:55:00+09:00 : theme .
本日の資料¶ ↑
自己紹介¶ ↑
# image # src = images/self-introduction.png # relative_height = 107
目次¶ ↑
(1) 検索の評価指標 (2) PGroongaで検索結果の改善 (3) 参考資料
検索の評価指標¶ ↑
よく検索結果がn ((*いまいち*))だ…n という話をn聞きます
検索の評価指標¶ ↑
# image # src = images/search-result-01.png # relative_height = 100
検索の評価指標¶ ↑
-
😞検索漏れ
-
😞ノイズが多い
-
😞有用な情報を探し出せない
検索の評価指標¶ ↑
(1) 効率性n低コストで検索できるかどうか (2) ((*有効性*))n検索結果の全体 or 一部がn((*欲しい情報*))だったかどうか
検索の評価指標¶ ↑
今日は有効性にnついてのお話です
有効性の指標¶ ↑
(1) 適合率 (2) 再現率 (3) ランキング
適合率と再現率¶ ↑
# image # src = images/precision-recall-1.png # relative_height = 82
適合率と再現率¶ ↑
# image # src = images/precision-hight-recall-low.png # relative_height = 90
適合率と再現率¶ ↑
# image # src = images/precision-low-recall-hight.png # relative_height = 90
ランキング¶ ↑
欲しい情報がnランキング((*上位*))にあるか
ランキング¶ ↑
ユーザーはn((*上位数件*))nしか見ない
PGroongaで検索結果の改善¶ ↑
-
PGroongaで適合率/再現率改善
-
ノーマライザーを使う
-
トークナイザーを使う
-
ステミングを使う
-
fuzzy検索を使う
-
同義語展開を使う
-
PGroongaで検索結果の改善¶ ↑
-
PGroongaでランキング改善
-
スコアラーを使う
-
PGroongaのノーマライザーn(デフォルト)¶ ↑
# coderay sql CREATE DATABASE pgroonga_test; CREATE EXTENSION pgroonga; CREATE TABLE normalizer_test ( id integer, content text ); CREATE INDEX pgroonga_content_index ON normalizer_test USING pgroonga (content); INSERT INTO normalizer_test VALUES (1, 'キログラム'); INSERT INTO normalizer_test VALUES (2, 'きろぐらむ'); INSERT INTO normalizer_test VALUES (3, '㌕'); INSERT INTO normalizer_test VALUES (4, 'キログラム'); INSERT INTO normalizer_test VALUES (5, 'kiroguramu'); INSERT INTO normalizer_test VALUES (6, 'kiroguramu'); SELECT * FROM normalizer_test WHERE content &@ 'キログラム';
PGroongaのノーマライザーn(デフォルト)¶ ↑
# RT delimiter = [|] id | content 1 | キログラム 3 | ㌕ 4 | キログラム
PGroongaのノーマライザーn(デフォルト)¶ ↑
-
半角/全角を同一視
-
㌕とキログラムを同一視
PGroongaのノーマライザーn(デフォルト)¶ ↑
PGroongaのデフォルトはNFKCを使った正規化n ※対象のテキストのエンコードがUTF-8の時
ノーマライザーの変更¶ ↑
再現率を上げたい
PGroongaのノーマライザーn(NormalizerNFKC130)¶ ↑
# coderay sql DROP INDEX pgroonga_content_index; CREATE INDEX pgroonga_content_index ON normalizer_test USING pgroonga (content) WITH (normalizers='NormalizerNFKC130("unify_to_romaji", true)'); SELECT * FROM normalizer_test WHERE content &@ 'キログラム';
PGroongaのノーマライザーn(NormalizerNFKC130)¶ ↑
# RT delimiter = [|] id | content 1 | キログラム 2 | きろぐらむ 3 | ㌕ 4 | キログラム
PGroongaのノーマライザーn(NormalizerNFKC130)¶ ↑
# RT delimiter = [|] id | content 5 | kiroguramu 6 | kiroguramu
PGroongaのノーマライザーn(NormalizerNFKC130)¶ ↑
-
Unify_to_romaji
-
ローマ字に正規化nローマ字で読んだときに同じ語は同一視する
-
(e.g. 「kiroguramu」と「きろぐらむ」を同一視。ローマ字読みが同じだから)
-
オプションの指定方法¶ ↑
-
‘NormalizerNFKC130n(“オプション名”, true)’);
指定可能オプション一覧¶ ↑
-
NormalizerNFKC130のnオプション一覧
PGroongaのトークナイザーn(デフォルト)¶ ↑
# coderay sql CREATE TABLE tokenizer_test ( title text ); CREATE INDEX pgroonga_content_index ON tokenizer_test USING pgroonga (title); INSERT INTO tokenizer_test VALUES ('京都府 1日目 金閣寺'); INSERT INTO tokenizer_test VALUES ('京都府 2日目 嵐山'); INSERT INTO tokenizer_test VALUES ('京都府 3日目 天橋立'); INSERT INTO tokenizer_test VALUES ('東京都 1日目 スカイツリー'); INSERT INTO tokenizer_test VALUES ('東京都 2日目 浅草寺'); INSERT INTO tokenizer_test VALUES ('北海道 1日目 函館'); INSERT INTO tokenizer_test VALUES ('北海道 2日目 トマム'); INSERT INTO tokenizer_test VALUES ('北海道 3日目 富良野'); INSERT INTO tokenizer_test VALUES ('北海道 4日目 美瑛'); INSERT INTO tokenizer_test VALUES ('北海道 5日目 旭川'); SELECT * FROM tokenizer_test WHERE title &@ '京都';
PGroongaのトークナイザーn(デフォルト)¶ ↑
# RT delimiter = [|] title 京都府 1日目 金閣寺 京都府 2日目 嵐山 京都府 3日目 天橋立 東京都 1日目 スカイツリー 東京都 2日目 浅草寺
トークナイザーの変更¶ ↑
適合率を上げたい
PGroongaのトークナイザーn(TokenMecab)¶ ↑
# coderay sql DROP INDEX pgroonga_content_index; CREATE INDEX pgroonga_content_index ON tokenizer_test USING pgroonga (title) WITH (tokenizer='TokenMecab'); SELECT * FROM tokenizer_test WHERE title &@ '京都';
PGroongaのトークナイザーn(TokenMecab)¶ ↑
# RT delimiter = [|] title 京都府 1日目 金閣寺 京都府 2日目 嵐山 京都府 3日目 天橋立
トークナイザーの指定方法¶ ↑
-
tokenizer=‘トークナイザー名’
指定可能トークナイザー一覧¶ ↑
-
使用可能なトークナイザー
ステミング(語幹処理)¶ ↑
語形変化n意味は同じだがn語の形が変わる
ステミング(語幹処理)¶ ↑
例えば
-
develop(原形)
-
developped(過去形)
-
developing(進行形)
意味は同じだが語形は異なる
ステミング(語幹処理)¶ ↑
語幹:単語の変化しない部分
ステミング(語幹処理)¶ ↑
((‘tag:left’)) ((develop))n ((develop))pedn ((develop))ing
ステミング(語幹処理)¶ ↑
語幹で検索n ->語形変化後の語も検索できる
PGroongaのステミングn(未使用)¶ ↑
# coderay sql CREATE TABLE steming_test ( title text ); CREATE INDEX pgroonga_content_index ON steming_test USING pgroonga (title); INSERT INTO tokenizer_test VALUES ('I develop Groonga'); INSERT INTO tokenizer_test VALUES ('I am developing Groonga'); INSERT INTO tokenizer_test VALUES ('I developed Groonga'); SELECT * FROM tokenizer_test WHERE title &@ 'develop';
PGroongaのステミングn(未使用)¶ ↑
# RT delimiter = [|] title I develop Groonga
PGroongaのステミング¶ ↑
# coderay sql CREATE INDEX pgroonga_content_index ON steming_test USING pgroonga (title) WITH (plugins='token_filters/stem', token_filters='TokenFilterStem');
PGroongaのステミング¶ ↑
# RT delimiter = [|] title I develop Groonga I am developing Groonga I developed Groonga
同義語¶ ↑
同義語:同じ意味を持つ別の語
同義語¶ ↑
例えばn 「ミルク」とn「牛乳」
同義語¶ ↑
意味が同じものはヒットしてほしい
同義語展開¶ ↑
ミルク -> n ミルク OR 牛乳
PGroongaの同義語展開¶ ↑
# coderay sql CREATE TABLE synonyms ( term text PRIMARY KEY, synonyms text[] ); CREATE INDEX synonyms_search ON synonyms USING pgroonga (term pgroonga.text_term_search_ops_v2); INSERT INTO synonyms (term, synonyms) VALUES ('ミルク', ARRAY['ミルク', '牛乳']); INSERT INTO synonyms (term, synonyms) VALUES ('牛乳', ARRAY['牛乳', 'ミルク']); CREATE TABLE memos ( id integer, content text ); INSERT INTO memos VALUES (1, '牛乳石鹸'); INSERT INTO memos VALUES (2, 'ミルクジャム'); INSERT INTO memos VALUES (3, 'ストロベリー'); CREATE INDEX pgroonga_content_index ON memos USING pgroonga (content); SELECT * FROM memos WHERE content &@~ pgroonga_query_expand('synonyms', 'term', 'synonyms', '牛乳');
同義語展開¶ ↑
# RT delimiter = [|] id | content 1 | 牛乳石鹸 2 | ミルクジャム
曖昧検索¶ ↑
typo対策
曖昧検索¶ ↑
-
似たような語ならヒットする
(完全一致じゃなくてもヒットする)
曖昧検索¶ ↑
「テノクロジー」でn 「テクノロジー」がヒット
PGroongaのfuzzy検索¶ ↑
# coderay sql CREATE TABLE tags ( name text ); CREATE INDEX tags_search ON tags USING pgroonga(name) WITH (tokenizer=''); INSERT INTO tags VALUES ('テクノロジー'); INSERT INTO tags VALUES ('テクニカル'); SELECT name FROM tags WHERE name &` ('fuzzy_search(name, ' || pgroonga_escape('テノクロジー') || ', {"with_transposition": true, "max_distance": 1})');
曖昧検索¶ ↑
# RT delimiter = [|] name テクノロジー
PGroongaでランキング改善¶ ↑
何を基準にnランキングをn決めるのか
PGroongaのスコアリング¶ ↑
-
TF(PGroongaのデフォルト)
-
TF-IDF
PGroongaのスコアリングnTF(デフォルト)¶ ↑
単語の((*出現数*))nが大事
PGroongaのスコアリングnTF(デフォルト)¶ ↑
-
検索キーワードが文書内に多く含まれる文書のスコアーが高くなる
PGroongaのスコアリングnTF-IDF¶ ↑
単語の((*レア度*))nが大事
PGroongaのスコアリングnTF-IDF¶ ↑
-
文書に出てくる頻度が高いn(レア度低い)
-
文書に出てくる頻度が低いn(レア度高い)
PGroongaのスコアリングnTF-IDF¶ ↑
# coderay sql CREATE TABLE memos ( title text, content text ); CREATE INDEX pgroonga_memos_index ON memos USING pgroonga (content); INSERT INTO memos VALUES ('PostgreSQL', 'PostgreSQLはリレーショナル・データベース管理システムです。'); INSERT INTO memos VALUES ('Groonga', 'Groongaは日本語対応の高速な全文検索エンジンです。'); INSERT INTO memos VALUES ('PGroonga', 'PGroongaはインデックスとしてGroongaを使うためのPostgreSQLの拡張機能です。'); INSERT INTO memos VALUES ('コマンドライン', 'groongaコマンドがあります。'); SELECT *, pgroonga_score(tableoid, ctid) AS score FROM memos WHERE content &@~ ('PostgreSQL OR 検索', ARRAY[1], ARRAY['scorer_tf_idf($index)'], 'pgroonga_memos_index')::pgroonga_full_text_search_condition_with_scorers ORDER BY score DESC;
PGroongaのスコアリングnTF-IDF¶ ↑
# RT delimiter = [|] title | score Groonga | 1.3862943649291992 PostgreSQL | 1 PGroonga | 1
参考資料¶ ↑
-
PGroonga自体の解説