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

検索結果の評価指標

よく検索結果がn ((*いまいち*))だ…n という話をn聞きます

検索結果の評価指標

# image
# src = images/search-result-01.png
# relative_height = 100

いまいちな検索結果

検索結果の評価指標

(1) 適合率 (2) 再現率 (3) ランキング

適合率

# image
# src = images/precision-expression.png
# relative_height = 55

再現率

# image
# src = images/recall-expression.png
# relative_height = 60

適合率と再現率

具体例

適合率と再現率

Wikipediaでn“キログラムのn定義”を調べる

適合率と再現率

# coderay sql

SELECT title FROM wikipedia where text &@~ 'キログラム 定義';
                         title                          
--------------------------------------------------------
 水
 力
 国際単位系
  .
  .
  .
キログラム
  .
  .
  .
(229 rows)

適合率と再現率

適合率n 2/229=0.87%

適合率と再現率

*ヒットしなかったけどn欲しかった記事

* SI基本単位の再定義
* 国際キログラム原器

適合率と再現率

再現率n 2/4=50.0%

適合率と再現率の重要度

Web検索n 適合率 > 再現率

適合率と再現率の重要度

特許検索n 適合率 < 再現率

ランキング

検索結果の順序

ランキング

ユーザーはn((*上位数件*))nしか見ない

ランキングの指標(参考)

PGroongaで検索結果の改善

PGroongaで検索結果の改善

ノーマライズ

ノーマライズn(正規化)とは?

ノーマライズ

# image
# src = images/normalizer.png
# relative_height = 90

ノーマライズ

例) カタカナをひらがなに正規化

PGroongaのノーマライザーn(デフォルト)

# coderay sql

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 &@ 'kiroguramu';

PGroongaのノーマライザーn(デフォルト)

# coderay sql

SELECT * FROM normalizer_test WHERE content &@ 'kiroguramu';
 id |       content        
----+----------------------
  5 | kiroguramu
  6 | kiroguramu
(2 rows)

PGroongaのノーマライザーn(デフォルト)

PGroongaのノーマライザーn(デフォルト)

PostgreSQLのノーマライザーn(デフォルト)

# coderay sql

SELECT * FROM normalizer_test
  WHERE content = (SELECT normalize('kiroguramu', NFKC));
 id |  content   
----+------------
  5 | kiroguramu
(1 row)

ノーマライザーの変更

適合率/再現率を上げたい

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 &@ 'kiroguramu';

PGroongaのノーマライザーn(NormalizerNFKC130)

# coderay sql

SELECT * FROM normalizer_test WHERE content &@ 'kiroguramu';
 id |       content        
----+----------------------
  1 | キログラム
  2 | きろぐらむ
  3 | ㌕
  4 | キログラム
  5 | kiroguramu
  6 | kiroguramu
(6 rows)

PGroongaのノーマライザーn(NormalizerNFKC130)

オプションの指定方法

# coderay sql

CREATE INDEX pgroonga_content_index
          ON normalizer_test
       USING pgroonga (content)
        WITH (normalizers='NormalizerNFKC130("unify_to_romaji", true)');

複数オプションの指定方法

# coderay sql

CREATE INDEX pgroonga_content_index
          ON normalizer_test
       USING pgroonga (content)
        WITH (normalizers='NormalizerNFKC130("unify_to_romaji", true,
                                             "unify_hyphen", true)');

指定可能オプション一覧

トークナイズ

# image
# src = images/tokenizer.png
# relative_height = 100

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(デフォルト)

# coderay sql

SELECT * FROM tokenizer_test WHERE title &@~ '京都';
                title                 
--------------------------------------
 京都府 1日目 金閣寺
 京都府 2日目 嵐山
 京都府 3日目 天橋立
 東京都 1日目 スカイツリー
 東京都 2日目 浅草寺
(5 rows)

トークナイザーの変更

適合率を上げたい

PGroongaのトークナイザーn(TokenMecab)

# coderay sql

CREATE INDEX pgroonga_content_index
          ON tokenizer_test
       USING pgroonga (title)
        WITH (tokenizer='TokenMecab');

SELECT * FROM tokenizer_test WHERE title &@~ '京都';

PGroongaのトークナイザーn(TokenMecab)

# coderay sql

SELECT * FROM tokenizer_test WHERE title &@~ '京都';
                title                 
--------------------------------------
 京都府 1日目 金閣寺
 京都府 2日目 嵐山
 京都府 3日目 天橋立
(3 rows)

PGroongaのトークナイザーn(TokenMecab)

# image
# src = images/tokenizer-tokenmecab.png
# relative_height = 100

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

# coderay sql

CREATE INDEX pgroonga_content_index
          ON tokenizer_test
       USING pgroonga (title)
        WITH (tokenizer='TokenMecab');

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

ステミング(語幹処理)

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

ステミング(語幹処理)

例えば

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

ステミング(語幹処理)

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

ステミング(語幹処理)

((‘tag:left’)) ((develop))n ((develop))edn ((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 steming_test VALUES ('I develop Groonga');
INSERT INTO steming_test VALUES ('I am developing Groonga');
INSERT INTO steming_test VALUES ('I developed Groonga');

SELECT * FROM steming_test WHERE title &@~ 'develop';

PGroongaのステミングn(未使用)

# coderay sql

SELECT * FROM steming_test WHERE title &@~ 'develop';
       title       
-------------------
 I develop Groonga
(1 row)

PGroongaのステミング

# coderay sql

CREATE TABLE steming_test (
  title text
);
CREATE INDEX pgroonga_content_index
          ON steming_test
       USING pgroonga (title)
        WITH (plugins='token_filters/stem',
              token_filters='TokenFilterStem');

INSERT INTO steming_test VALUES ('I develop Groonga');
INSERT INTO steming_test VALUES ('I am developing Groonga');
INSERT INTO steming_test VALUES ('I developed Groonga');

SELECT * FROM steming_test WHERE title &@~ 'develop';

PGroongaのステミング

# coderay sql

SELECT * FROM steming_test WHERE title &@~ 'develop';
          title          
-------------------------
 I develop Groonga
 I am developing Groonga
 I developed Groonga
(3 rows)

高度な話題

処理順序

(1) ノーマライズ (2) トークナイズ (3) トークンフィルター

高度な話題

処理順序が問題になることがある

高度な話題

TokenMecab と unify_kana

高度な話題

高度な話題

高度な話題

処理順序

(1) ノーマライズ (2) トークナイズ (3) ((*トークンフィルター*))

高度な話題

TokenFilterNFKC100を使う

高度な話題

同義語

同義語:同じ意味を持つ別の語

同義語

例えば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', '牛乳');

同義語展開

# coderay sql

SELECT * FROM memos
  WHERE
    content &@~
      pgroonga_query_expand('synonyms', 'term', 'synonyms', '牛乳');

 id |      content       
----+--------------------
  1 | 牛乳石鹸
  2 | ミルクジャム
(2 rows)

fuzzy検索

typo対策

fuzzy検索

(完全一致じゃなくてもヒットする)

fuzzy検索

「テノクロジー」でn 「テクノロジー」がヒット

fuzzy検索n編集距離

fuzzy検索n編集距離

((‘tag:center’)) ((‘tag:x-large’)) 編集距離:1

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

fuzzy検索

# coderay sql

SELECT name FROM tags
  WHERE
    name &`
      ('fuzzy_search(name, ' || pgroonga_escape('テノクロジー') || ',
                     {"with_transposition": true,
                      "max_distance": 1})');
        name        
--------------------
 テクノロジー
(1 row)

PGroongaでランキング改善

何を基準にnランキングをn決めるのか

PGroongaのスコアリング

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

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

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

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

# 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 ('PGroonga1', 'PGroongaは全文検索エンジンGroongaを使っています。');
INSERT INTO memos VALUES ('PGroonga2', 'Groonga、Groonga、Groonga、Groonga、Groonga');

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

# coderay sql

SELECT *, pgroonga_score(tableoid, ctid) AS score
  FROM memos
 WHERE content &@~ 'Groonga'
 ORDER BY score DESC;

   title   |                                  content                                  | score 
-----------+---------------------------------------------------------------------------+-------
 PGroonga2 | Groonga、Groonga、Groonga、Groonga、Groonga                               |     5
 Groonga   | Groongaは日本語対応の高速な全文検索エンジンです。                         |     1
 PGroonga  | PGroongaはインデックスとしてGroongaを使うためのPostgreSQLの拡張機能です。 |     1
 PGroonga1 | PGroongaは全文検索エンジンGroongaを使っています。                         |     1
(4 rows)

PGroongaのスコアリングnTF-IDF

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

PGroongaのスコアリングnTF-IDF

PGroongaのスコアリングnTF-IDF

# coderay sql

SELECT *, pgroonga_score(tableoid, ctid) AS score
  FROM memos
 WHERE content &@~
   ('Groonga OR 全文検索',
    ARRAY[1],
    ARRAY['scorer_tf_idf($index)'],
    'pgroonga_memos_index')::pgroonga_full_text_search_condition_with_scorers
 ORDER BY score DESC;

   title   |                                  content                                  | score 
-----------+---------------------------------------------------------------------------+-------
 Groonga   | Groongaは日本語対応の高速な全文検索エンジンです。                         |     2
 PGroonga1 | PGroongaは全文検索エンジンGroongaを使っています。                         |     2
 PGroonga  | PGroongaはインデックスとしてGroongaを使うためのPostgreSQLの拡張機能です。 |     1
 PGroonga2 | Groonga、Groonga、Groonga、Groonga、Groonga                               |     1
(4 rows)

PGroongaのスコアリングnTF at Most

スコアーのn((*最大値*))を制限

PGroongaのスコアリングnTF at Most

# coderay sql

SELECT *, pgroonga_score(tableoid, ctid) AS score
  FROM memos
WHERE content &@~ 'Groonga OR PostgreSQL'
ORDER BY score DESC;

   title    |                                  content                                  | score 
------------+---------------------------------------------------------------------------+-------
 PGroonga2  | Groonga、Groonga、Groonga、Groonga、Groonga                               |     5
 PGroonga   | PGroongaはインデックスとしてGroongaを使うためのPostgreSQLの拡張機能です。 |     2
 Groonga    | Groongaは日本語対応の高速な全文検索エンジンです。                         |     1
 PGroonga1  | PGroongaは全文検索エンジンGroongaを使っています。                         |     1
 PostgreSQL | PostgreSQLはリレーショナル・データベース管理システムです。                |     1
(5 rows)

PGroongaのスコアリングnTF at Most

# coderay sql

SELECT *, pgroonga_score(tableoid, ctid) AS score
  FROM memos
 WHERE content &@~
   ('Groonga OR 全文検索',
    ARRAY[1],
    ARRAY['scorer_tf_at_most($index, 1)'],
    'pgroonga_memos_index')::pgroonga_full_text_search_condition_with_scorers
 ORDER BY score DESC;

   title   |                                  content                                  | score 
-----------+---------------------------------------------------------------------------+-------
 Groonga   | Groongaは日本語対応の高速な全文検索エンジンです。                         |     2
 PGroonga1 | PGroongaは全文検索エンジンGroongaを使っています。                         |     2
 PGroonga  | PGroongaはインデックスとしてGroongaを使うためのPostgreSQLの拡張機能です。 |     1
 PGroonga2 | Groonga、Groonga、Groonga、Groonga、Groonga                               |     1
(4 rows)

参考資料

参考資料

参考資料

参考資料

参考資料

参考資料

参考資料