PGroongaの実装

: subtitle

(('note:ぴーじーるんがのじっそう'))

: author

須藤功平

: institution

株式会社クリアコード

: content-source

PostgreSQLカンファレンス2015

: date

2015-11-27

: allotted-time

40m

: theme

.

(建前の)目的

PostgreSQLのamindexを説明

方法

PGroongaの実装を紹介n ((‘note:PGroongaはamindexとして実装されているため’))n ((‘note:ただし、ヒントだけなので詳細はPGroongaのソースを参照’))n ((‘note:github.com/pgroonga/pgroonga’))

本当の目的

PGroongaの自慢

PGroongaとは

amindexの一種

amindex

索引の実装をn PostgreSQLにn 追加する仕組み

amindexの使い方

# coderay sql
CREATE INDEX name ON table
  USING pgroonga (column);

((‘tag:center’)) 組み込みの索引と同じn ((({USING}))を指定するだけ)

PGroongaの提供機能

索引を使ったn ((*超高速な*))n 全文検索機能

PostgreSQLと全文検索

課題あり

PostgreSQLと全文検索1

# coderay sql
CREATE INDEX name ON table
  USING gin (to_tsvector('english', column));
SELECT * FROM table
  WHERE to_tsvector('english', column) @@ '...';

((‘tag:center’)) 組み込みの全文検索機能n 日本語非対応n ((‘note:www.postgresql.org/docs/current/static/textsearch.html’))

PostgreSQLと全文検索2

# coderay sql
CREATE INDEX name ON table
  USING gin (column gin_trgm_ops);
SELECT * FROM table
  WHERE column LIKE '%...%';

((‘tag:center’)) contrib/pg_trgmn 日本語非対応n ((‘note:www.postgresql.org/docs/current/static/pgtrgm.html’))

PostgreSQLと全文検索

日本語非対応

PGroongaとPostgreSQL

日本語対応!

PGroongaを使う

# coderay sql
CREATE INDEX name ON table
  USING pgroonga (column);
SELECT * FROM table
  WHERE column @@ '全文検索';

((‘tag:center’)) 日本語対応!

しかも速い!

ヒット数と検索時間

# RT
delimiter = [|]

ヒット数 | 検索時間

368 | 0.030s(('   '))
17,172 | 0.121s(('   '))
22,885 | 0.179s(('   '))
625,792 | 0.646s(*)

((‘note:(*) work_memを10MBに増やしている’))

((‘tag:center’)) データ:Wikipedia日本語版n ((‘note:約184万レコード・平均サイズ約3.8KB’))n ((‘note:詳細:www.clear-code.com/blog/2015/5/25.html’))

検索時間:比較

# RT
delimiter = [|]

ヒット数 | PGroonga | pg_bigm

368 | ((*0.030s*)) | (('0.107s'))
17,172 | ((*0.121s*)) | (('1.224s'))
22,885 | ((*0.179s*)) | (('2.472s'))
625,792\n(('note:(*)')) | (('0.646s')) | ((*0.556s*))

((‘note:(*) 他は検索語が3文字以上でこれだけ2文字’))

((‘tag:center’)) PGroongaは安定して速い!

なぜ速いのか

バックエンドがn 外部の本格的なn 全文検索n ライブラリー

amindexでのポイント

* (({_PG_init()}))
  * ライブラリーを初期化
* (({on_proc_exit()}))
  * 後始末

全文検索ライブラリー

Groongan (ぐるんが)

Groonga

((*本格的な*))n 全文検索n エンジンn ((‘note:サーバーとしてもライブラリーとしても使える’))

本格的な例1

((‘tag:center’)) 長い文書でも検索性能が落ちない

((‘tag:margin-top * 2’)) この特徴が有用なサービス例:

* Wiki
  * イメージ:Wikipedia
* ドキュメント検索
  * イメージ:ファイルサーバー検索

長い文書でも速い理由

((*完全*))n 転置索引

転置索引

* 完全:位置情報あり
  * Groonga
* 無印:位置情報なし
  * GIN
  * Groonga(('note:(必要なければ入れないことができる)'))

転置索引の違い

# image
# src = images/inverted-index.svg
# relative_height = 100

索引での検索の違い

# image
# src = images/search-with-inverted-index.svg
# relative_height = 100

検索速度の違い

* 完全転置索引:安定して速い
  * 索引だけで検索完了
* 転置索引:速さが安定しない
  * 索引での検索+全件スキャン
  * 候補文書が多い・長い→遅くなる

amindexでのポイント

* 全件スキャンを無効にする
  * (({scan->xs_recheck = false}))
  * (({tbm_add_tuples(..., false)}))

本格的な例2

((‘tag:center’)) 常時更新・常時検索に強い

((‘tag:margin-top * 2’)) この特徴が有用なサービス例:

* SNS
  * イメージ:Twitter
* ナレッジ共有サービス
  * イメージ:Qiita・teratail

常時更新・検索に強い?

更新中もn 参照性能がn 落ちない

性能傾向

# image
# src = images/performance-charcteristic-for-constant-read-and-write.svg
# relative_height = 95

落ちない理由

更新時にn 参照ロックなし

参照ロック

獲得したらn 他の処理はn 参照不可になるn ロック

GINと更新と参照

# image
# src = images/read-while-write-gin.svg
# relative_height = 95

Groongaと更新と参照

# image
# src = images/read-while-write-groonga.svg
# relative_height = 95

PGroongaと更新と参照

# image
# src = images/read-while-write-pgroonga.svg
# relative_height = 95

参照ロックフリーの実現

# image
# src = images/reference-lock-free-idea.svg
# relative_height = 100

amindexでのポイント

* (({Lock*()}))(('note:(*)'))を呼ばない\n
  (('note:(*) PostgreSQL提供のロックAPI'))
  * 呼ぶとGroongaの参照ロックフリーの有意性を殺してしまう

本格的な例3

((‘tag:center’)) 継続的な更新に強い

((‘tag:margin-top * 2’)) この特徴が有用なサービス例:

* SNS
  * イメージ:Twitter・Facebook
* チャット
  * イメージ:Slack

継続的な更新に強い?

* 間欠的な性能劣化がない
  * 更新も検索も
* GINは両方ある
  * (({FASTUPDATE}))を無効にしていない場合

間欠的性能劣化:Groonga

((‘tag:center’)) Groongaは間欠的性能劣化なし

* 常に最新ポスティングリストを\n
  維持しているから
  * 更新負荷が高くならない対策入り\n
    (('note:https://github.com/groonga/groonga/wiki/Memo'))

Groongaの索引更新

# image
# src = images/update-groonga.svg
# relative_height = 100

間欠的性能劣化:GIN

((‘tag:center’)) GINは間欠的性能劣化あり

* 最新ポスティングリスト維持を\n
  サボって高速化しているから
  * サボったつけを払うときに性能劣化
  * 例:検索時・更新が溜まりすぎた時

GINの索引更新

# image
# src = images/update-gin.svg
# relative_height = 100

GINの索引検索

# image
# src = images/search-gin.svg
# relative_height = 100

本格的な例4

((‘tag:center’)) 索引の作り直しが速い

((‘tag:margin-top * 2’)) この特徴が有用なケース:

* ダンプのリストア
* サービス復旧

索引作成

# RT
delimiter = [|]

元データの\nロード時間 | 索引\n作成時間

16分31秒 | 25分37秒

((‘tag:center’)) データ:Wikipedia日本語版n ((‘note:約184万レコード・平均サイズ約3.8KB’))n ((‘note:詳細:www.clear-code.com/blog/2015/5/25.html’))

索引作成:比較

# RT
delimiter = [|]

PGroonga | pg_bigm

25分37秒 | 5時間56分15秒

((‘tag:center’)) pg_bigmより約14倍速い!

速い理由

((*静的*))n 索引構築をn サポート

索引構築方法

* ((*動的*))索引構築
  * 構築中も完了した分は検索可能
  * 即時反映可・一括登録不向き
* ((*静的*))索引構築
  * 構築完了まで使えないが速い\n
    (('note:(処理時間は入力に比例。指数関数的ではない。)'))
  * 即時反映不可・一括登録向き

SQLの違い

# coderay sql
-- 動的索引構築
CREATE INDEX ...;
INSERT ...;
-- 静的索引構築
INSERT ...;
CREATE INDEX ...;

amindexでのポイント

* (({aminsert()}))
  * 動的索引構築を実装
* (({ambuild()}))
  * 静的索引構築を実装

速い理由のまとめ

* 索引だけで検索可能
* 更新中も検索可能
* 間欠的な性能劣化なし
* 静的索引構築をサポート

速さ以外の利点1

便利な独自機能

独自機能1

見慣れたn クエリー言語

  # coderay sql
  body @@ 'PostgreSQL OR MySQL -Oracle'

* Web検索エンジン互換
  * →ユーザーの入力をそのまま使える
* デフォルトAND
* OR・-(除外)あり

独自機能2

配列+全文検索

# coderay sql
CREATE TABLE logs (hosts text[]);
INSERT INTO logs
  VALUES (Array['web', 'db']);
CREATE INDEX index ON logs
  USING pgroonga (hosts);
SELECT * FROM logs
  WHERE hosts @@ '各ホスト名を全文検索';

独自機能3

JSON+全文検索

# coderay sql
INSERT INTO logs (record)
  VALUES ('{"host": "ダウンホスト"}'),
         ('{"message": "シャットダウン"}');
SELECT * FROM logs
  WHERE record @@ 'string @@ "ダウン"'
--           record
-- ----------------------------
-- {"host":    "ダウンホスト"}
-- {"message": "シャットダウン"}

((‘tag:center’)) JSON内の全テキストから全文検索

独自機能4

ノーマライザー

ノーマライザー

* 文字を正規化するモジュール
  * 表記の違いを吸収できる
* (('wait'))アルファベット:全部小文字
* (('wait'))ひらがな・カタカナ:全部全角
* (('wait'))㍉→ミリ
* (('wait'))UnicodeのNFKCベース

独自機能5

トークナイザー

トークナイザー

* キーワード切り出しモジュール
  * クエリーに指定できる\n
    キーワードを調整
* 例:すもも|も|もも|も

デフォルト:可変長Ngram

* 英語:字種区切り
  * 例:Hello|World|!!!
  * Bigramだとノイズが多い
* 日本語:Bigram
  * 例:ポスグレ→ポス|スグ|グレ|レ
  * 漏れがない

形態素解析器ベース

* MeCab:OSS
  * 新語対応には辞書メンテが必要
  * 参考:mecab-ipadic-neologd
* JMAT:商用製品
  * ジャストシステム社製
  * ATOKでも使っている辞書を提供
  * 新語にも強い\n
    (('note:参考:「JMAT Groonga Tokenizer Talks」で検索'))

amindexでのポイント

* (({amoptions()}))
  * (({CREATE INDEX}))でのオプションを定義

# coderay sql
CREATE INDEX index ON memos
  USING pgroonga (content)
   WITH (normalizer = 'NormalizerMySQLUnicodeCI',
         tokenizer  = 'TokenMecab');

速さ以外の利点2

見慣れた機能n ((‘note:B-tree・GINの代わりに使える’))

タグ検索

# coderay sql
CREATE TABLE memos (
  tags varchar(1023)[]
);
CREATE INDEX index ON memos
  USING pgroonga (tags);
SELECT * FROM memos
  WHERE tags %% 'タグ';

範囲検索

# coderay sql
CREATE TABLE users (age int);
CREATE INDEX index ON users
  USING pgroonga (age);
SELECT * FROM users
  WHERE age < 20;

Index Only Scan

* 索引がデータも返す
  * テーブルにアクセスしないので高速
* PGroonga・B-tree:サポート\n
  (('note:PostgreSQL 9.5からはGiSTもサポート'))
* GIN:未サポート

amindexでのポイント

* (({amcanreturn()}))
  * (({true}))を返す
* (({amgettuple()}))
  * (({scan->xs_want_itup}))なら\n
    (({scan->xs_itup}))にデータを設定

LIKE

# coderay sql
CREATE INDEX index ON memos
  USING pgroonga (content);
SELECT * FROM memos
  WHERE content LIKE '%...%';

((‘tag:center’)) 索引を使って高速検索n アプリケーションの変更不要

マルチカラムインデックス

# coderay sql
CREATE INDEX index ON memos
  USING pgroonga (title, content);
SELECT * FROM memos
  WHERE title @@ '...' AND
      content @@ '...';

((‘tag:center’)) ((‘note:titleでもcontentでもマッチ!’))n ((‘note:と書けないのでそんなにうれしくない’))

設計ミス

(({text @@ text}))

((‘tag:center’)) 組み込みの定義と競合n ((‘note:ts_vector @@ ts_queryにキャストされる’))

((‘tag:margin-top * 2’)) 回避方法:

# coderay sql
ALTER DATABASE name
  SET search_path = '$user',public,pgroonga,pg_catalog;

(({jsonb @@ text}))

((‘tag:center’)) 全文検索にすればよかった

# coderay sql
jsonb @@ 'string @ "キーワード"'
-- ↓
jsonb @@ 'キーワード'

((‘tag:center’)) ((‘note:今の細かい検索条件を指定できる機能は別演算子にする’))

今後

もっとGroongaを活かす

# RT
delimiter = [|]

PGroonga | pg_bigm | Groonga

(('0.646s')) | (('0.556s')) | ((*0.085s*))

((‘note:ヒット数635,792、検索語は2文字’))

((‘tag:center’)) 生Groongaは1桁速い!n ((‘note:詳細:github.com/groonga/wikipedia-search/issues/3’))

同義語展開サポート

# coderay sql
body @@ pgroonga.expand_query('ネジ')
-- ↓
body @@ 'ネジ OR ねじ OR ボルト'

((‘tag:center’)) ((‘note:Groongaでは使える’))

ステミングサポート

found/findsn ↓n findn ((‘note:Groongaでは使える’))

text @@ pgroonga.query

# coderay sql
body @@ 'ポスグレ'::pgroonga.query

((‘tag:center’)) ((‘note:組み込みの(({text @@ text}))との競合回避’))

重みサポート

# coderay sql
-- タイトルのほうが本文より10倍重要
body @@ ('title * 10 || body', 'ポスグレ')

((‘tag:center’)) ((‘note:Groongaでは使える’))

複合主キーサポート

# coderay sql
CREATE TABLE t (
  c1 INT,
  c2 INT,
  PRIMARY KEY (c1, c2)
);
CREATE INDEX index ON t
  USING pgroonga (c1, c2);

((‘ ’))

まとめn ((‘note:時間が余ったらチュートリアルを自慢する’))

まとめ

* PGroongaは速い
* PGroongaは便利
* PGroongaには設計ミスがある
* PGroongaはもっと便利になる

((‘wait’)) ((‘tag:center’)) PGroongaを使おう!

おしらせ

* Groonga Meatup 2015\n
  (('note:https://groonga.doorkeeper.jp/events/31482'))
  * PGroongaの話題もアリ
  * 多少空きアリ(('note:(定員を多少増やせる)'))
* 11月29日(日)13:30開始
* (('note:来年2月9日は「MySQLとPostgreSQLと日本語全文検索」'))\n
  (('note:https://groonga.doorkeeper.jp/events/35295'))

# == スライドプロパティ

# : background-image # private/images/groonga-meatup-2015-design.svg

# : background-image-relative-width # 22

# : background-image-align # right

# : background-image-vertical-align # bottom