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で検索結果の改善

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)

オプションの指定方法

指定可能オプション一覧

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日目 天橋立

トークナイザーの指定方法

指定可能トークナイザー一覧

ステミング(語幹処理)

語形変化n意味は同じだがn語の形が変わる

ステミング(語幹処理)

例えば

意味は同じだが語形は異なる

ステミング(語幹処理)

語幹:単語の変化しない部分

ステミング(語幹処理)

((‘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のスコアリング

PGroongaのスコアリングnTF(デフォルト)

単語の((*出現数*))nが大事

PGroongaのスコアリングnTF(デフォルト)

PGroongaのスコアリングnTF-IDF

単語の((*レア度*))nが大事

PGroongaのスコアリングnTF-IDF

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

参考資料