= MariaDBとMroongaで作る\n全言語対応\n超高速全文検索システム : author 須藤功平 : institution クリアコード : content-source 第一回 JPMUG DB勉強会 : date 2018-01-30 : start-time 2018-01-30T19:25:00+09:00 : end-time 2018-01-30T20:10:00+09:00 : theme . = 全文検索システム\n対象 (('tag:center')) (('tag:large')) (('tag:margin-bottom * 2')) 大量のテキスト * 例:Wikiのデータ * 例:オフィス文書のテキスト * 例:商品説明・口コミ = 全文検索システム\n目的 * 必要な情報を * 必要なときに * 活用 = 必要な情報を活用 * × * 探している情報が見つからない * ○ * 探している情報が見つかる * ◎ * 意識していなかったけど\n ((*実は欲しかった*))情報も見つかる! = 必要なときに活用 * × * なかなか見つからない * ○ * すぐに見つかる * ◎ * すでに見つかっていた * 例:レコメンデーション = 実装方法\n選択肢 * 全文検索サーバーを使う * MariaDBでLIKEを使う = 全文検索サーバー案\nメリット * 必要な機能が揃っている * +αの機能もある * 速い = 全文検索サーバー案\nデメリット * 実装コスト大 * それぞれ独自の使い方だから * マスターデータの同期はどうする? * メンテナンスコスト大 * それぞれ独自の仕組みだから = MariaDBでLIKE案\nメリット * 実装コスト小 * 新しく覚えることが少ない * データの一元管理 * メンテナンスコスト小 * 既存の運用ノウハウを使える * データ少なら実用的な速度 = MariaDBでLIKE案\nデメリット * 機能不足 * それっぽい順のソート不可 * 全文検索ではソート順が重要 * ユーザーは先頭n件しか見ない * SQLの表現力不足 * nクエリーで実現すると性能に影響 = 実現方法\n第3の選択肢 * MariaDB経由(SQL)で\n 全文検索エンジンを使う = メリット * 高速で豊富な機能 * それっぽい順のソート可 * 実装コスト小 * メンテナンスコスト小 = デメリット * MariaDBに拡張機能が必要 * RDS・Azure databaseで使えない\n (('note:(Azure database for MariaDBはまだリリース前)')) = オススメの選択肢\n全文検索の知識ナシ * まだ単純な機能で十分 * データ少:MariaDB単独でLIKE\n (('note:(数十万件とか)')) * データ中以上:\n MariaDB経由で全文検索エンジン * いまどきの全文検索機能が必要 * MariaDB経由で全文検索エンジン = オススメの選択肢\n全文検索の知識アリ * カリカリにチューニングしたい * MariaDBと全文検索サーバーを併用 * それ以外 * MariaDB経由で全文検索エンジン = 説明する選択肢 MariaDB経由で\n 全文検索\n エンジン = 全文検索エンジン\nGroonga(ぐるんが) * 組込可能な全文検索エンジン * MariaDB・MySQLに組込→Mroonga * PostgreSQLに組込→PGroonga * 全文検索サーバーとして\n 単独でも使用可能 * MariaDBと全文検索サーバーを併用\n もできる = Groongaの得意なこと * データの追加・更新 * 新鮮な情報をすぐに検索可能に! * 更新中も検索性能を落とさない! * 日本語 * 開発者が日本人 * 便利機能が組み込み = GroongaとUnicode * NFKCベースの正規化機能を組込 * Unicode 5.1ベースで古い * 2008年の仕様 * Unicode 10.0(最新)対応中\n (('note:正規化方式を変えるとインデックスの互換性がなくなる(=要インデックス再構築)のでデフォルトは変えない')) = Mroonga(むるんが) * MariaDBのストレージエンジン * InnoDB・MyISAMなどと同じレイヤー * MariaDB 10.0.15から標準バンドル * 使用方法 * (({CREATE TABLE (...) ENGINE=Mroonga})) = 照合順序:COLLATION * 文字の並び順の規則 * 文字が同一かどうかの判定にも利用 * 適切な日本語規則なし * いわゆる🍣=🍺問題 (('note:MySQL 8では適切な日本語規則が追加される'))\n (('note:utf8mb4_ja_0900_as_csなど')) = Mroongaの照合順序 * MariaDB互換のもの * (({utf8mb4_ja_0900_*}))互換は対応予定 * MariaDB互換を微調整したもの * 日本語でもいい感じ * Groonga提供のもの * NFKCベースのもの * 日本語でもいい感じ = Mroongaで照合順序 * MariaDB互換がよい! * 互換正規化処理を使用:デフォルト * MariaDB互換は気にしないから\n いい感じに! * Groonga提供のものを使用 = 全文検索性能\n計測データ * 対象:Wikipedia日本語版 * レコード数:約185万件 * データサイズ:約7GB * メモリー4GB・SSD250GB(('note:(ConoHa)')) = 検索性能1 (('tag:center')) キーワード:テレビアニメ\n (('note:(ヒット数:約2万3千件)')) # RT delimiter = [|] InnoDB ngram | 3m2s InnoDB MeCab | 6m20s Mroonga:((*1*)) | 0.11s = 検索性能2 (('tag:center')) キーワード:データベース\n (('note:(ヒット数:約1万7千件)')) # RT delimiter = [|] InnoDB ngram | 36s InnoDB MeCab:((*1*)) | 0.03s Mroonga:((*2*)) | 0.09s = 検索性能3 (('tag:center')) キーワード:PostgreSQL OR MySQL\n (('note:(ヒット数:約400件)')) # RT delimiter = [|] InnoDB ngram | N/A(Error) InnoDB MeCab:((*1*)) | 0.005s Mroonga:((*2*)) | 0.028s = 検索性能4 (('tag:center')) キーワード:日本\n (('note:(ヒット数:約63万件)')) # RT delimiter = [|] InnoDB ngram | 1.3s InnoDB MeCab | 1.3s Mroonga:((*1*)) | 0.21s = 全文検索性能まとめ * Mroonga:安定して速い * ((*SQLで使えて機能豊富で速い!*)) * InnoDB FTS MeCab * ハマれば速い * InnoDB FTS ngram * 安定して遅い = 普通の検索も速い * カラムストアを活かした最適化 * ポイント1:余計なI/Oを減らす * ポイント2:I/Oを局所化 = カラムストア # image # src = images/column-store.svg # relative_width = 100 = 必要なカラムのみアクセス # coderay sql -- aのみにアクセス SELECT a FROM table -- cのみにアクセス WHERE c = XXX; -- bにはアクセスしない = 減ったI/O # image # src = images/not-access-to-needless-columns.svg # relative_width = 100 = 行カウント # coderay sql -- カラムの値は必要ない SELECT COUNT(*) FROM table -- cの全文検索インデックスにだけアクセス WHERE MATCH(c) AGAINST('+keyword' IN BOOLEAN MODE); -- a, b, cはアクセスしない = 減ったI/O # image # src = images/count-star.svg # relative_width = 100 = (({ORDER BY LIMIT})) # coderay sql SELECT a FROM table WHERE MATCH(c) AGAINST('+keyword' IN BOOLEAN MODE) -- MariaDBではなくMroongaがORDER BY LIMITを処理 -- →Mroongaは10レコードだけMariaDBに返す -- マッチしたレコードすべては返さない ORDER BY a LIMIT 10; = (({ORDER BY LIMIT}))の最適化 * Mroongaが検索 * カラム毎の処理でI/Oを局所化\n (('note:(索引非使用時)')) * Mroongaがソート * カラム毎の処理でI/Oを局所化 * Mroongaが(({OFFSET}))/(({LIMIT}))を処理 = カラム毎の処理は速い # image # src = images/per-column-processing.svg # relative_width = 100 = condition push downの最適化 * 従来はMariaDBが処理していた検索条件をストレージエンジンが処理する仕組み * ストレージエンジンでの処理の方が高速なら全体として高速になる * Mroonga 7.10から実験的に対応 * デフォルトオフ = condition push downの効果 * 全文検索インデックスのみ * 等価条件:シーケンシャルスキャン * 全文検索:インデックススキャン * データ * シカゴの犯罪データ(('note:(651万レコード)')) (('tag:xx-small')) 詳細:((<"https://github.com/kou/rabbit-slide-kou-jpmug-db-study-1/blob/master/memo.md"|URL:https://github.com/kou/rabbit-slide-kou-jpmug-db-study-1/blob/master/memo.md>)) = 等価条件:数値1つ (('tag:center')) 26万件ヒットするケース # RT delimiter = [|] InnoDB | 1.3s Mroonga\n(デフォルト) | 1.3s Mroonga\n(最適化ON) | 0.4s = 等価条件:数値1つ+真偽値2つ (('tag:center')) 7000件ヒットするケース # RT delimiter = [|] InnoDB | 1.6s Mroonga\n(デフォルト) | 2.3s Mroonga\n(最適化ON) | 0.4s = 全文検索+等価条件 (('tag:center')) 4000件ヒットするケース # RT delimiter = [|] InnoDB | 18s Mroonga\n(デフォルト) | 0.4s Mroonga\n(最適化ON) | 0.4s (('note:このパターンはデフォルトで最適化が効く')) = Mroongaの検索性能まとめ * 最適化が効くと桁違いに速い * 全文検索のときはデフォルトで効く * 7.10からさらなる最適化が!\n (('note:まだ実験的扱いなのでデフォルトオフ')) * OLAP用途にも使える * MariaDB ColumnStoreを補完する\n 立ち位置も可 (('tag:xx-small')) 参考:((<"http://mroonga.org/ja/docs/reference/server_variables.html#mroonga-condition-push-down-type"|URL:http://mroonga.org/ja/docs/reference/server_variables.html#mroonga-condition-push-down-type>)) = 全文検索システムの実装 * 全文検索 * キーワードハイライト * 周辺テキスト表示 * オートコンプリート * 同義語展開 * 関連文書の表示 = 全文検索 # image # src = images/php-document-search-search.png # relative_height = 100 = テーブル定義 # coderay sql CREATE TABLE entries ( title text, content text, -- 全文検索用インデックス -- よくわからないならデフォルトのまま使うこと! FULLTEXT INDEX (title, content) ) ENGINE=Mroonga DEFAULT CHARSET=utf8mb4; = データ挿入 # coderay sql -- 普通に挿入するだけでよい INSERT INTO entries VALUES ('タイトル', '高速に全文検索したいですね!'); = 全文検索 # coderay sql SELECT title FROM entries WHERE -- MATCH AGAINSTで全文検索 MATCH (title, content) -- デフォルトORがMariaDBの仕様 -- 「検索」または「高速」を含むとマッチ AGAINST ('検索 高速' IN BOOLEAN MODE); = AND全文検索 # coderay sql MATCH (title, content) -- 各キーワードの前に「+」をつけるとAND -- 「検索」かつ「高速」を含むとマッチ AGAINST ('+検索 +高速' IN BOOLEAN MODE); = 使いやすいAND全文検索 # coderay sql MATCH (title, content) -- 最初に「*D+」をつけるとデフォルトAND -- Mroonga独自機能 -- 「検索」かつ「高速」を含むとマッチ AGAINST ('*D+ 検索 高速' IN BOOLEAN MODE); = それっぽい順のソート # coderay sql SELECT title, -- ここのMATCH AGAINSTはスコアーを返す MATCH (title, content) AGAINST ('*D+ 検索 高速' IN BOOLEAN MODE) AS score FROM entries WHERE -- ... -- それっぽさでソート ORDER BY score DESC LIMIT 10; = ハイライト # image # src = images/php-document-search-search.png # relative_height = 100 = ハイライト # coderay sql SELECT mroonga_highlight_html( title, '*D+ 検索 高速' AS query) -- クエリーからハイライト対象のキーワードを抽出 FROM entries WHERE MATCH (title, content) AGAINST ('*D+ 検索 高速' IN BOOLEAN MODE); = ハイライト結果例 # coderay html で高速全文検索! ↓ <Groonga>で ← タグをエスケープ 高速 全文 ↑↓キーワードはclass付け 検索! = 周辺テキスト # image # src = images/php-document-search-search.png # relative_height = 100 = 周辺テキスト # coderay sql SELECT mroonga_snippet_html( content, '*D+ 検索 高速' AS query) -- クエリーから対象のキーワードを抽出 FROM entries WHERE MATCH (title, content) AGAINST ('*D+ 検索 高速' IN BOOLEAN MODE); = 周辺テキスト結果例 # coderay html ...で高速全文検索!... ↓
←1つ目 ga>で ←タグをエスケープ 高速 全文 ↑↓キーワードはclass付け 検索/span>!
...
←2つ目 = オートコンプリート # image # src = images/php-document-search.png # relative_height = 100 = オートコンプリート:必要なもの * マスターテーブル * 候補(例:牛乳) * 候補のヨミ(カタカナ・複数可) * 例1:ギュウニュウ * 例2:ミルク = オートコンプリート:実装方法 * 以下の検索のOR * ヨミでの前方一致検索 * 候補を緩い全文検索 * 候補でソートして提示 = オートコンプリート:テーブル定義 # coderay sql CREATE TABLE terms ( term varchar(256), -- 補完候補 reading varchar(256), -- ヨミガナ PRIMARY KEY (term, reading), FULLTEXT INDEX (term) -- 候補全文検索用 -- 緩い全文検索用トークナイザー COMMENT 'tokenizer "TokenBigramSplitSymbolAlpha"', FULLTEXT INDEX (reading) -- ヨミガナ前方一致用 COMMENT 'normalizer "NormalizerAuto", tokenizer "off"' -- トークナイザー不要 ) ENGINE=Mroonga DEFAULT CHARSET=utf8mb4; = オートコンプリート:データ例 # coderay sql INSERT INTO terms VALUES ( '牛乳', -- 補完候補 'ギュウニュウ' --ヨミガナはカタカナで指定 ); INSERT INTO terms VALUES ( '牛乳', 'ミルク' -- 「ミルク」でも補完できるように ); = オートコンプリート\nデータ管理のポイント * 普通のテーブルなので管理が楽 * 追加・削除・更新が楽 * ダンプ・リストアもいつも通り * レプリケーションもいつも通り = オートコンプリート\n検索方法 # coderay sql SELECT DISTINCT(term) FROM terms WHERE MATCH (reading) -- ヨミガナ前方一致検索 AGAINST (CONCAT('*SS prefix_rk_search(reading, ', mroonga_escape(${入力} AS script), ')') IN BOOLEAN MODE) OR MATCH (term) -- 候補を緩く全文検索 AGAINST (CONCAT('*D+ ', mroonga_escape(${入力})) IN BOOLEAN MODE) ORDER BY term LIMIT 10; -- ソート = オートコンプリート\n検索例:漢字1 # coderay sql -- ユーザーが「牛」を入力した場合 SELECT DISTINCT(term) FROM terms WHERE MATCH (reading) -- ヨミガナ前方一致検索 AGAINST (CONCAT('*SS prefix_rk_search(reading, ', mroonga_escape('牛' AS script), ')') IN BOOLEAN MODE) OR MATCH (term) -- 候補を緩く全文検索(ヒット) AGAINST (CONCAT('*D+ ', mroonga_escape('牛')) IN BOOLEAN MODE) ORDER BY term LIMIT 10; -- ソート = オートコンプリート\n検索例:漢字2 # coderay sql -- ユーザーが「乳」を入力した場合 SELECT DISTINCT(term) FROM terms WHERE MATCH (reading) -- ヨミガナ前方一致検索 AGAINST (CONCAT('*SS prefix_rk_search(reading, ', mroonga_escape('乳' AS script), ')') IN BOOLEAN MODE) OR MATCH (term) -- 候補を緩く全文検索(ヒット) AGAINST (CONCAT('*D+ ', mroonga_escape('乳')) IN BOOLEAN MODE) ORDER BY term LIMIT 10; -- ソート = オートコンプリート\n検索例:カタカナ # coderay sql -- ユーザーが「ギュウ」を入力した場合 SELECT DISTINCT(term) FROM terms WHERE MATCH (reading) -- ヨミガナ前方一致検索(ヒット) AGAINST (CONCAT('*SS prefix_rk_search(reading, ', mroonga_escape('ギュウ' AS script), ')') IN BOOLEAN MODE) OR MATCH (term) -- 候補を緩く全文検索 AGAINST (CONCAT('*D+ ', mroonga_escape('ギュウ')) IN BOOLEAN MODE) ORDER BY term LIMIT 10; -- ソート = オートコンプリート\n検索例:ひらがな # coderay sql -- ユーザーが「ぎゅう」を入力した場合 SELECT DISTINCT(term) FROM terms WHERE MATCH (reading) -- ヨミガナ前方一致検索(ヒット) AGAINST (CONCAT('*SS prefix_rk_search(reading, ', mroonga_escape('ぎゅう' AS script), ')') IN BOOLEAN MODE) OR MATCH (term) -- 候補を緩く全文検索 AGAINST (CONCAT('*D+ ', mroonga_escape('ぎゅう')) IN BOOLEAN MODE) ORDER BY term LIMIT 10; -- ソート = オートコンプリート\n検索例:ローマ字 # coderay sql -- ユーザーが「gyu」を入力した場合 SELECT DISTINCT(term) FROM terms WHERE MATCH (reading) -- ヨミガナ前方一致検索(ヒット) AGAINST (CONCAT('*SS prefix_rk_search(reading, ', mroonga_escape('gyu' AS script), ')') IN BOOLEAN MODE) OR MATCH (term) -- 候補を緩く全文検索 AGAINST (CONCAT('*D+ ', mroonga_escape('gyu')) IN BOOLEAN MODE) ORDER BY term LIMIT 10; -- ソート = 同義語展開 * 同義語 * 同じ意味だが表記が異なる語 * 例:「刺身」と「お造り」 * どの表記でもヒットして欲しい * 同義語展開→同義語すべてでOR検索 = 同義語展開\n実装方法 * 同義語管理テーブルを作成 * クエリー内の同義語を展開 * 展開後のクエリーで検索 = 同義語展開:Mroonga\nテーブル定義 # coderay sql CREATE TABLE synonyms ( term varchar(255), -- 展開対象の語 synonym varchar(255), -- 同義語 INDEX (term) -- 高速化と精度向上 COMMENT 'normalizer "NormalizerAuto"' ) ENGINE=Mroonga DEFAULT CHARSET=utf8mb4; = 同義語展開\nデータ例 # coderay sql INSERT INTO synonyms -- 「刺身」を「刺身 OR お造り」に展開 VALUES ('刺身', '刺身'), ('刺身', 'お造り'), -- 「お造り」を「お造り OR 刺身」に展開 ('お造り', 'お造り'), ('お造り', '刺身'); = 同義語展開\nデータ管理のポイント * 普通のテーブルなので管理が楽 * 追加・削除・更新が楽 * ダンプ・リストアもいつも通り * レプリケーションもいつも通り = 同義語展開:Mroonga\n確認方法 # coderay sql SELECT mroonga_query_expand( 'synonyms', -- テーブル名 'term', -- 展開対象のカラム名 'synonym', -- 対応する同義語のカラム名 '居酒屋 刺身' -- クエリー ); -- '居酒屋 ((刺身) OR (お造り))' = 同義語展開:Mroonga\n検索方法 # coderay sql SELECT title FROM entries WHERE MATCH (title) -- '*D+ 居酒屋 OR ((刺身) OR (お造り))'になる AGAINST (mroonga_query_expand('synonyms', 'term', 'synonym', '*D+ 居酒屋 刺身') IN BOOLEAN MODE); = 類似文書検索 * 検索クエリーは文書そのもの * キーワードではない * 関連エントリーの提示に使える * メタデータがあるなら組み合わせる\n →精度向上 * メタデータ:タグ・行動履歴など = 類似文書検索:Mroonga\nインデックス定義 # coderay sql CREATE TABLE entries ( -- ... FULLTEXT INDEX (content) -- TokenMecabを使わないと精度がでない -- 必要なときだけカスタマイズ! COMMENT 'tokenizer "TokenMecab"' ) -- ... = 類似文書検索:Mroonga\n検索方法 # coderay sql SELECT title FROM entries WHERE MATCH (content) -- ↓ 既存文書の内容をそのまま指定 AGAINST ('...Groongaで高速全文検索!...' IN NATURAL LANGUAGE MODE); = 類似文書検索:Mroonga\n結果例 クエリー: ...Groongaで高速全文検索!... ヒット例: ...Mroongaで高速全文検索!... = 全文検索システムの実装\nまとめ * 全文検索 * キーワードハイライト * 周辺テキスト表示 * オートコンプリート * 同義語展開 * 関連文書の表示 = 全文検索システムの実装\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 * (('tag:xx-small')) 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:活用例 * 抽出したテキスト * Mroongaへ挿入 * 抽出したメタデータ * Mroongaへ挿入 * 絞り込みに活用 * 作成したスクリーンショット * 検索結果表示時に掲載 = まとめ * MariaDBの全文検索まわり * 全文検索システム実装例を紹介 * 構造化データの対応方法を紹介 * ChupaText = 扱わなかった話題 * 運用について * 障害対策・レプリケーション * チューニング * Groongaの機能を直接使う方法 = サポートサービス紹介 * 導入支援(('note:(設計支援・性能検証・移行支援・…)')) * 開発支援\n (('note:(サンプルコード提供・問い合わせ対応・…)')) * 運用支援(('note:(障害対応・チューニング支援・…)')) 問い合わせ先: (('tag:x-small')) ((<"https://www.clear-code.com/contact/?type=groonga"|URL:https://www.clear-code.com/contact/?type=groonga>))