PGroonga 2

: subtitle

PostgreSQLでの全文検索の決定版

: author

須藤功平

: institution

クリアコード

: content-source

PostgreSQL Conference Japan 2017

: date

2017-11-03

: start-time

2017-11-03T16:10:00+0900

: end-time

2017-11-03T17:00:00+0900

: theme

.

対象者

* PostgreSQLで全文検索したい
* 全文検索はよく知らない
* PGroongaは使ったことがない

全文検索システム:対象

(('tag:center')) (('tag:large')) (('tag:margin-bottom * 2')) 大量のテキスト

* 例:Wikiのデータ
* 例:オフィス文書のテキスト
* 例:商品説明・口コミ
* 例:チャットログ

全文検索システム:目的

* 必要な情報を
* 必要なときに
* 活用

必要な情報を活用

* ×
  * 探している情報が見つからない
* ○
  * 探している情報が見つかる
* ◎
  * 意識していなかったけど\n
    ((*実は欲しかった*))情報も見つかる!

必要なときに活用

* ×
  * なかなか見つからない
* ○
  * すぐに見つかる
* ◎
  * すでに見つかっていた
  * 例:レコメンデーション

実装方法n選択肢

* 全文検索サーバーを使う
* PostgreSQLを使う

全文検索サーバー案nメリット

* 必要な機能が揃っている
* +αの機能もある
* 速い

全文検索サーバー案nデメリット

* 実装コスト大
  * それぞれ独自の使い方だから
  * マスターデータの同期はどうする?
* メンテナンスコスト大
  * それぞれ独自の仕組みだから

PostgreSQL案nメリット

* 実装コスト小
  * 新しく覚えることが少ない
  * データの一元管理
* メンテナンスコスト小
  * 既存の運用ノウハウを使える

PostgreSQL案nデメリット

* 組込機能では機能不足
* SQLの表現力不足
  * 1クエリーで実現できない機能アリ
  * ↑は性能を出しにくい

実現方法n第3の選択肢

* PostgreSQL経由(SQL)で\n
  全文検索エンジンを使う

メリット

* 高速で豊富な機能
* 実装コスト小
* メンテナンスコスト小

デメリット

* PostgreSQLに拡張機能が必要
  * DBaaSで使えない

オススメの選択肢n全文検索の知識ナシ

* まだ単純な機能で十分
  * データ少:PostgreSQLでLIKE\n
    (('note:(数十万件とか)'))
* いまどきの全文検索機能が必要
  * PostgreSQL経由で全文検索エンジン

オススメの選択肢n全文検索の知識アリ

* カリカリにチューニングしたい
  * PostgreSQL+全文検索サーバー
* それ以外
  * PostgreSQL経由で全文検索エンジン

説明する選択肢

PostgreSQL経由でn 全文検索n エンジン

全文検索エンジンnGroonga(ぐるんが)

* 組込可能な全文検索エンジン
  * PostgreSQLに組込→PGoonga
* 全文検索サーバーとして\n
  単独でも使用可能
  * PostgreSQL+全文検索サーバー構成\n
    もできる

Groongaの得意なことnデータの追加・更新

* 新鮮な情報がすぐ検索可能!
  * バッチで更新しなくてもよい
  * チャットくらいの頻度でもOK\n
    例:ZulipはPGroongaを採用
* 更新中も検索性能が落ちない!
  * 利用ユーザーが多い時でも更新可能

Groongaの得意なことn日本語まわり

* 開発者が日本人
* 便利機能が組み込み

(('tag:center')) (('tag:margin-top * 3')) (('note:もちろん、日本語以外もOK!'))

PGroongan(ぴーじーるんが)

* PostgreSQLのインデックス
  * B-tree・GINなどと同じレイヤー
* 使用方法
  * (({CREATE INDEX ...}))\n
    (({USING PGroonga ...}))

PostgreSQLと全文検索

* LIKE:組込機能
* textsearch:組込機能
* pg_trgm:標準添付
  * アーカイブには含まれている
  * 別途インストールすれば使える

LIKEと速度

* 少ないデータ
  * 十分実用的
  * 400文字×20万件くらいなら1秒とか
* 少なくないデータ
  * 性能問題アリ

LIKEと全文検索システム

* 👍速度が実用的なことも多い
  * 少ないデータなら
* 👎それっぽい順のソート不可
  * 全文検索ではソート順が重要
  * ユーザーは先頭n件しか見ない

textsearch

* インデックスを作るので速い
* 言語毎にモジュールが必要
  * 英語やフランス語などは組込
  * 日本語は別途必要
* 日本語用モジュールはあるが…
  * 公式にはメンテナンスされていない\n
    (('note:forkして動くようにしている人はいる'))

pg_trgm

* インデックスを作るので速い
  * 注:ヒット件数が増えると遅い
  * 注:テキスト量が多いと遅い
  * 注:1,2文字の検索は遅い(('note:(米・日本)'))

* 日本語を使うにはひと工夫必要
  * C.UTF-8を使う
  * ソースを変更してビルド

プラグイン

* pg_bigm
  * pg_trgmの日本語対応強化版
  * それっぽい順のソート不可
* PGroonga
  * 本気の全文検索エンジンを利用
  * 速いし日本語もバッチリ!
  * それっぽい順のソート可

ベンチマーク:pg_bigm

# image
# src = images/search-pg-bigm.pdf
# relative_height = 100

ベンチマーク:PGroonga

# image
# src = images/search-pgroonga-pg-bigm.pdf
# relative_height = 100

PostgreSQLで全文検索システム

* PostgreSQLで全文検索
  * PGroongaがベスト!💯
* PGroonga
  * 高速
  * 日本語対応
  * それっぽい順でソート可

基本機能

* 高速全文検索+ソート
* 検索キーワードハイライト
* キーワード周辺テキスト表示

高度な機能

* オートコンプリート
  * ローマ字対応(zen→全文検索)
* 類似文書検索
* 同義語展開
  * 「牛乳」→\n
    「牛乳 OR ミルク」

高速全文検索+ソート

# image
# src = images/php-document-search-search.png
# relative_height = 100

テーブル定義

# coderay sql

CREATE TABLE entries (
  -- 主キーを用意する
  -- それっぽい順でソートするために必要
  id integer PRIMARY KEY,
  title text,
  content text
);

インデックス定義

# coderay sql

-- 全文検索用インデックス
-- よくわからないなら
-- デフォルトのまま使うこと!
CREATE INDEX entries_full_text_search
  ON entries
  --「USING PGroonga」=「PGroongaを使う」
  -- 主キーはそれっぽい順ソートのため!
  USING PGroonga (id, title, content);

データ挿入

# coderay sql

-- 普通に挿入するだけでよい
INSERT INTO entries
  VALUES (1,
          'Groongaで高速全文検索!',
          '高速に全文検索したいですね!');

全文検索

# coderay sql

SELECT title FROM entries
  WHERE
-- &@~で全文検索
-- 「検索」と「高速」をAND検索
    title &@~ '検索 高速' OR
    content &@~ '検索 高速';

全文検索:LIKE

# coderay sql

SELECT title FROM entries
  WHERE
-- LIKEでもインデックスが効く
--=アプリを書き換えずに高速化可能
-- ただし&@~より性能が落ちる
    title LIKE '%検索%' OR
    content LIKE '%検索%';

それっぽい順のソート

# coderay sql

SELECT
    title,
    -- pgroonga_score(テーブル名)で
    -- それっぽさを数値で取得
    pgroonga_score(entries) AS score
  FROM entries
  WHERE -- ...
  -- それっぽさでソート
  ORDER BY score DESC LIMIT 10;

キーワードハイライト

# image
# src = images/php-document-search-search.png
# relative_height = 100

HTML用にハイライト

# coderay sql

SELECT
  pgroonga_highlight_html(
    title,
    -- クエリーから対象キーワードを抽出
    pgroonga_query_extract_keywords('検索 高速'))
  FROM entries
  WHERE title &@~ '検索 高速' OR
        content &@~ '検索 高速';

HTML用ハイライト結果例

# coderay html

<Groonga>で高速全文検索!
↓
&lt;Groonga&gt;で ← タグをエスケープ
<span class="keyword">高速</span>
全文 ↑↓キーワードはclass付け
<span class="keyword">検索</span>!

周辺テキスト

# image
# src = images/php-document-search-search.png
# relative_height = 100

HTML用に周辺テキスト取得

# coderay sql

SELECT
  pgroonga_snippet_html(
    content,
    -- クエリーから対象キーワードを抽出
    pgroonga_query_extract_keywords('検索 高速'))
  FROM entries
  WHERE title &@~ '検索 高速' OR
        content &@~ '検索 高速';

HTML用周辺テキスト結果例

# coderay html

...<Groonga>で高速全文検索!...
↓
ARRAY[
  ↓ 1つ目
  'ga&gt;で ←タグをエスケープ
   <span class="keyword">高速</span>
   全文 ↑↓キーワードはclass付け
   <span class="keyword">検索/span>!',
  '...' ← 2つ目
]

オートコンプリート

# image
# src = images/php-document-search.png
# relative_height = 100

オートコンプリート:必要なもの

* マスターテーブル
  * 候補(例:牛乳)
  * 候補のヨミ(カタカナ・複数可)
    * 例1:ギュウニュウ
    * 例2:ミルク

オートコンプリート:実装方法

* 以下の検索のOR
  * ヨミでの前方一致検索
  * 候補を緩い全文検索
* 候補でソートして提示

(('tag:xx-small')) ((<“pgroonga.github.io/ja/how-to/auto-complete.html”|URL:https://pgroonga.github.io/ja/how-to/auto-complete.html>))

オートコンプリートnテーブル定義

# coderay sql

CREATE TABLE terms (
  -- 補完候補
  term text,
  -- この候補のヨミ(N個可)
  readings text[],
);

オートコンプリート:データ例

# coderay sql

INSERT INTO terms VALUES (
  '牛乳', -- 補完候補
  ARRAY[
   -- ヨミはカタカナで指定
   'ギュウニュウ',
   -- 「ミルク」でも補完可
   'ミルク'
  ]
);

オートコンプリートnデータ管理のポイント

* 普通のテーブルなので管理が楽
  * 追加・削除・更新が楽
  * ダンプ・リストアもいつも通り
  * レプリケーションもいつも通り

オートコンプリートn前方一致用インデックス

# coderay sql

CREATE INDEX prefix_search ON terms
  USING PGroonga
  -- ...text_array_term_search...
  (readings pgroonga_text_array_term_search_ops_v2);

オートコンプリートn緩い全文検索用

# coderay sql

CREATE INDEX loose_search ON terms
  USING PGroonga (term)
  -- 緩い全文検索用トークナイザー
  WITH (tokenizer='TokenBigramSplitSymbolAlphaDigit');

オートコンプリートn検索方法

# coderay sql

SELECT term FROM terms
        -- ヨミで前方一致検索
  WHERE readings &^~ '${入力}' OR
        -- 緩い全文検索
        term &@ '${入力}'
  ORDER BY term LIMIT 10; -- ソート

オートコンプリートn検索例:漢字1

# coderay sql

-- ユーザーが「牛」を入力した場合
SELECT term FROM terms
        -- ヨミで前方一致検索
  WHERE readings &^~ '牛' OR
        -- 緩い全文検索(ヒット)
        term &@ '牛'
  ORDER BY term LIMIT 10; -- ソート

オートコンプリートn検索例:漢字2

# coderay sql

-- ユーザーが「乳」を入力した場合
SELECT term FROM terms
        -- ヨミで前方一致検索
  WHERE readings &^~ '乳' OR
        -- 緩い全文検索(ヒット)
        term &@ '乳'
  ORDER BY term LIMIT 10; -- ソート

オートコンプリートn検索例:カタカナ

# coderay sql

-- ユーザーが「ギュウ」を入力した場合
SELECT term FROM terms
     -- ヨミで前方一致検索(ヒット)
  WHERE readings &^~ 'ギュウ' OR
        -- 緩い全文検索
        term &@ 'ギュウ'
  ORDER BY term LIMIT 10; -- ソート

オートコンプリートn検索例:ひらがな

# coderay sql

-- ユーザーが「ぎゅう」を入力した場合
SELECT term FROM terms
     -- ヨミで前方一致検索(ヒット)
  WHERE readings &^~ 'ぎゅう' OR
        -- 緩い全文検索
        term &@ 'ぎゅう'
  ORDER BY term LIMIT 10; -- ソート

オートコンプリートn検索例:ローマ字

# coderay sql

-- ユーザーが「gyu」を入力した場合
SELECT term FROM terms
     -- ヨミで前方一致検索(ヒット)
  WHERE readings &^~ 'gyu' OR
        -- 緩い全文検索
        term &@ 'gyu'
  ORDER BY term LIMIT 10; -- ソート

同義語展開

* 同義語
  * 同じ意味だが表記が異なる語
  * 例:「牛乳」と「ミルク」
* どの表記でもヒットして欲しい
  * 同義語展開→同義語すべてでOR検索

同義語展開n実装方法

* 同義語管理テーブルを作成
* クエリー内の同義語を展開
* 展開後のクエリーで検索

(('tag:xx-small')) ((<“pgroonga.github.io/ja/reference/functions/pgroonga-query-expand.html”|URL:https://pgroonga.github.io/ja/reference/functions/pgroonga-query-expand.html>))

同義語展開:テーブル定義

# coderay sql
CREATE TABLE synonyms (
  -- 展開対象の語
  term text,
  -- 同義語のリスト
  -- term自身も含める
  -- 含めない場合はtermが検索禁止語になる
  terms text[]
);

同義語展開:データ例

# coderay sql
INSERT INTO synonyms
  VALUES ('牛乳', -- 「牛乳」を展開
          ARRAY['牛乳', 'ミルク']),
         ('ミルク', -- 「ミルク」を展開
          ARRAY['ミルク', '牛乳']);

同義語展開:データ管理のポイント

* 普通のテーブルなので管理が楽
  * 追加・削除・更新が楽
  * ダンプ・リストアもいつも通り
  * レプリケーションもいつも通り

同義語展開:インデックス定義

# coderay sql
CREATE INDEX synonym_search ON synonyms
  USING PGroonga
  -- ...text_term_search...
  -- termで完全一致検索をするため
  (term pgroonga_text_term_search_ops_v2);

同義語展開:確認方法

# coderay sql

SELECT pgroonga_query_expand(
  'synonyms', -- テーブル名
  'term', -- 展開対象のカラム名
  'terms', -- 対応する同義語配列のカラム名
  '牛乳' -- クエリー
);
-- '((牛乳) OR (ミルク))'

同義語展開:検索方法

# coderay sql
SELECT title FROM entries
  WHERE
 -- title &@~ 'アイス ((牛乳) OR (ミルク))'になる
    title &@~
      pgroonga_query_expand('synonyms',
                            'term',
                            'terms',
                            'アイス 牛乳');

類似文書検索

# image
# src = images/php-document-search-similar-search.png
# relative_height = 100

類似文書検索

* 検索クエリーは文書そのもの
  * キーワードではない
* 関連エントリーの提示に使える
  * メタデータがあるなら組み合わせる\n
    →精度向上
  * メタデータ:タグ・行動履歴など

類似文書検索:実現方法

* 類似検索用インデックスが必要
  * 自然言語に合わせた処理で精度向上
  * 日本語ならMeCabを活用
* 類似検索用の演算子を使う

類似文書検索:インデックス定義

# coderay sql

CREATE INDEX entries_similar_search
  ON entries
  -- タイトルと内容を合わせたテキストをインデックス
  -- 理由1:タイトルも重要→対象に加えて精度向上
  -- 理由2:PostgreSQLが全文検索インデックスと
  --        区別できるように
  USING PGroonga (id, (title || ' ' || content))
  -- TokenMecabを使うと精度向上
  WITH (tokenizer='TokenMecab');

類似文書検索:検索方法

# coderay sql

SELECT title,
    pgroonga_score(entries) AS score
  FROM entries
  WHERE
    -- &@*で類似文書検索
    -- 既存文書の内容をそのまま指定
    (title || ' ' || content) &@*
      '...Groongaで高速全文検索!...'
  ORDER BY score DESC LIMIT 3;

類似文書検索:結果例

クエリー:
   ...Groongaで高速全文検索!...

ヒット例:
   ...PGroongaで高速全文検索!...

おさらい:基本機能

* 高速全文検索+ソート
* 検索キーワードハイライト
* キーワード周辺テキスト表示

おさらい:高度な機能

* オートコンプリート
  * ローマ字対応(zen→全文検索)
* 類似文書検索
* 同義語展開
  * 「牛乳」→\n
    「牛乳 OR ミルク」

全文検索システムの実装n次の一歩

* 構造化データ対応
  * オフィス文書・HTMLなど
* 対応に必要な処理
  * テキスト抽出
  * メタデータ抽出(('note:(例:タイトル・更新日時)'))
  * スクリーンショット作成(('note:(なおよい)'))

抽出ツール

* Apache Tika
  * Apache Luceneのサブプロジェクト
  * 対応フォーマット数が多い
* ChupaText
  * Groongaのサブプロジェクト
  * スクリーンショット作成対応

ChupaText

* 対応フォーマット
  * Word/Excel/PowerPoint
  * ODT/ODS/ODP(('note:(OpenDocument)'))
  * PDF/HTML/XML/CSV/...
* インターフェイス
  * HTTPとコマンドライン

ChupaText:インストール

* DockerかVagrantを使うのが楽
  * (('tag:xx-small'))
    ((<"https://github.com/ranguba/chupa-text-docker"|URL:https://github.com/ranguba/chupa-text-docker>))
  * (('tag:xx-small'))
    ((<"https://github.com/ranguba/chupa-text-vagrant"|URL:https://github.com/ranguba/chupa-text-vagrant>))

ChupaText:Docker

# coderay console
% GITHUB=https://github.com
% git clone \
   ${GITHUB}/ranguba/chupa-text-docker.git
% cd chupa-text-docker
% docker-compose up --build

ChupaText:使い方

# coderay console
% curl \
    --form data=@XXX.pdf \
    http://localhost:20080/extraction.json

ChupaText:結果例

# coderay json

{
  "mime-type": "application/pdf", # 元データのMIMEタイプ
  "size": 147159, # メタデータ
  ...,
  "texts": [ # 抽出されたテキスト(N個)
    {
      "mime-type": "text/plain", # 抽出後のMIMEタイプ
      ...,
      "creator": "Adobe Illustrator CS3", # メタデータ
      "body": "This is sample PDF. ...", # 抽出したテキスト
      "screenshot": {
        "mime-type": "image/png", # スクリーンショットのMIMEタイプ
        "data": "iVBORw...", # Base64にした画像データ
        "encoding": "base64" # Base64であることを明記
      }
    }
  ]
}

ChupaText:Web UI

# image
# src = images/chupa-text-web-ui-form.png
# relative_height = 100

ChupaText:Web UI抽出例

# image
# src = images/chupa-text-web-ui-extract-metadata.png
# relative_height = 100

ChupaText:Web UI抽出例

# image
# src = images/chupa-text-web-ui-extract-text-and-screenshot.png
# relative_height = 100

ChupaText:Vagrant

# coderay console
% GITHUB=https://github.com
% git clone \
   ${GITHUB}/ranguba/chupa-text-vagrant.git
% cd chupa-text-vagrant
% vagrant up

(('tag:center')) 使い方はDocker版と同じ

ChupaText:活用例

* 抽出したテキスト
  * PGroongaへ挿入
* 抽出したメタデータ
  * PGroongaへ挿入
  * 絞り込みに活用
* 作成したスクリーンショット
  * 検索結果表示時に掲載

まとめ

* PostgreSQL経由で\n
  全文検索エンジン
  * 採用の判断材料を提供
* 全文検索システム実装例を紹介
  * PGroonga
* 構造化データの対応方法を紹介
  * ChupaText

扱わなかった話題

* 運用について
  * 障害対策・レプリケーション
  * ロジカルレプリケーションは\n
    対応済み!(('note:(少しだけ設定方法を紹介)'))
* チューニング
* Groongaの機能を直接使う方法

ロジカルレプリケーションnpostgresql.conf

# coderay conf
# マスター
wal_level = logical
max_wal_senders = 10
max_replication_slots = 10

ロジカルレプリケーションnDB作成・PGroongaインストール

# coderay sql
-- マスター
CREATE DATABASE test_master;
\c test_master
CREATE EXTENSION pgroonga;

ロジカルレプリケーションnテーブル作成

# coderay sql
-- マスター
CREATE TABLE memos (
  id integer PRIMARY KEY,
  content text
);
CREATE INDEX
  full_text_search_index ON memos
  USING pgroonga (id, content);

ロジカルレプリケーションnレプリケーション用ユーザー作成

# coderay sql
-- マスター
-- pg_hba.confも編集すること
CREATE ROLE replica
  WITH REPLICATION
    LOGIN PASSWORD 'pass';
GRANT SELECT
  ON ALL TABLES IN SCHEMA public
  TO replica;

ロジカルレプリケーションnpublication作成

# coderay sql
-- マスター
CREATE PUBLICATION pub
  FOR TABLE memos;

ロジカルレプリケーションnDB作成・PGroongaインストール

# coderay sql
-- スレーブ
CREATE DATABASE test_slave;
\c test_slave
CREATE EXTENSION pgroonga;

ロジカルレプリケーションnテーブル作成

# coderay sql
-- スレーブ
CREATE TABLE memos (
  id integer PRIMARY KEY,
  content text
);
CREATE INDEX
  full_text_search_index ON memos
  USING pgroonga (id, content);

ロジカルレプリケーションnsubscription作成

# coderay sql
-- スレーブ
CREATE SUBSCRIPTION sub
  CONNECTION
    'host=192.168.0.18 port=5432
     user=replica password=pass
     dbname=test_master'
  PUBLICATION pub;

ロジカルレプリケーションn確認

# coderay sql
-- マスター
INSERT INTO memos VALUES (1, '全文検索');
-- スレーブ
SELECT * FROM memos
  WHERE content &@~ '全文検索';
--  id | content  
-- ----+----------
--   1 | 全文検索
-- (1 row)

サポートサービス紹介

* 導入支援(('note:(設計支援・性能検証・移行支援・…)'))
* 開発支援\n
  (('note:(サンプルコード提供・問い合わせ対応・…)'))
* 運用支援(('note:(障害対応・チューニング支援・…)'))

問い合わせ先:

(('tag:x-small')) ((<“www.clear-code.com/contact/?type=groonga”|URL:https://www.clear-code.com/contact/?type=groonga>))