データベース●●●○○

全文検索

Full-Text Search ・ ぜんぶんけんさく

文章を語に分解して索引にし、キーワードを含む文書を関連の強い順にすばやく返す仕組み。

✦ 30秒で試す — 航海検索が一瞬で終わるのは、探す前に全部読み終えているから「ツリー」で検索すると、この文書は?予想してみる

概要

全文検索は、大量の文書の「本文そのもの」を対象に、キーワードで文書を探し出すための仕組みです。ブログの記事検索、EC サイトの商品検索、社内ドキュメントの横断検索、ログ基盤での障害調査など、「入力した言葉を含む文書を、関連の強い順に一瞬で返す」場面の裏側には、ほぼ必ず全文検索エンジンがいます。

ポイントは、文書をあらかじめ語(トークン)に分解して索引を作っておくことです。検索のたびに全文書を頭から読むのではなく、「どの語がどの文書に出てくるか」の一覧表を作っておき、検索のときはその表を引くだけにします。この表を転置インデックスと呼び、インデックスの一種です。書籍の巻末索引で「この用語は何ページに載っているか」を引くのと同じ発想を、機械的にすべての語へ当てはめたものといえます。なお、大規模言語モデルが文章を数えるトークンも「文章を単位に切る」点は同じですが、あちらは頻度から学習した部分語で切るのに対し、全文検索の語は辞書や文字数の規則で切ります。

どう切るか(トークナイズ)で、何が見つかり何が漏れるかが決まります。全文検索を使いこなすことは、この切り方と、見つかった文書の並べ方を設計することでもあります。

なぜ生まれたか

素朴に考えると、文字列の検索はRDBMSの LIKE '%キーワード%' でもできます。しかし前後にワイルドカードの付いた部分一致では、並びをたどるインデックスが使えず、文書が増えるほど全行を読む線形の走査になります。数万件ならまだしも、数百万・数億の文書では応答が秒単位・分単位に膨らみ、実用になりません。さらに LIKE は「文字列がそのまま含まれるか」しか見ないので、複数の言葉の関連度で並べ替える、活用形の違いを吸収する、といった検索らしい振る舞いができません。

図書館の索引や文献検索の世界では、1950〜60年代から「語から文書を引く表」を作って検索する情報検索(Information Retrieval)の研究が進んでいました。文書と語を行列として扱い、語の重みで関連度を測る Gerard Salton らのベクトル空間モデル(1970年代)もこの流れから生まれています。Web の登場で文書が爆発的に増えると、検索時間を文書数に依存させないために「検索のときに探す」のをやめて「登録のときに索引を作っておく」方式が決定的になりました。計算の手間を検索時から登録時へ移すこの転換を、1999年に Doug Cutting が公開した Lucene のようなオープンソースの実装が、誰でも使える部品にしました。

詳細

転置インデックス — 「文書→語」を「語→文書」にひっくり返す

文書は本来「文書1には東京、天気、晴れという語が含まれる」という文書→語の向きで存在します。転置インデックスはこれを文字どおり転置し、「東京は文書1と3に出てくる」という語→文書の向きの表を作ります。各語に対応する文書の番号の並びをポスティングリストと呼びます。検索のときは検索語のポスティングリストを引くだけで候補の文書が得られ、2語の AND 検索はリスト同士の共通部分、OR 検索は和集合を取るだけです。リストは文書番号の順に並べておくので、共通部分は2本のリストを先頭から同時にたどる1回の走査で求まります。実際の索引には文書番号に加えて、語の出現位置(フレーズ検索用)や出現回数(関連度の計算用)も入っています。

文書語に分解転置インデックス文書1東京の天気は晴れ文書2京都の紅葉が見頃文書3東京で雨のち晴れトークナイズ形態素解析など東京→ 文書1, 3晴れ→ 文書1, 3京都→ 文書2天気・紅葉・雨 …→ …検索「東京 晴れ」 → 2本のリストの共通部分 → 文書1, 3文書が何億件あっても、読むのは検索語のリストだけ。本文を頭から読み直すことはしない
転置インデックスの構造 — 文書を語に分解し、語から文書を引ける表を作る

トークナイズ — 形態素解析と N-gram

索引の質を決めるのがトークナイズ、つまり文章をどう語に切るかです。英語は空白で区切ればおおよそ語になりますが、日本語には分かち書きがないため、2つの流儀を使い分けます。

1つは形態素解析で、辞書と文法の知識を使って「京都駅から東京駅へ」を「京都/駅/から/東京/駅/へ」のように意味のある単位へ切る方式です(MeCab、Sudachi、Elasticsearch の kuromoji など)。索引が小さく、意味の単位で当たるので精度も高い一方、辞書にない新語や固有名詞は思わぬ単位で切れ、検索漏れの原因になります。もう1つは N-gram で、意味を無視して機械的に N 文字ずつずらしながら切る方式です(2文字なら bigram: 「東京都」→「東京/京都」)。辞書が要らず、新語でも部分一致で拾える代わりに、索引が大きく膨らみ、「京都」で検索すると「東京都」まで当たるようなノイズが混ざります。実務では、取りこぼしを嫌う場面は N-gram、精度を重視する場面は形態素解析、あるいは両方の索引を持って結果を合わせる方法が選ばれます。

文: 東京都に住む形態素解析東京都に住む4語(助詞の「に」は索引に入れないことが多い)2-gram東京京都都にに住住む5語(文字数 − 1)検索語「京都」がここに当たる形態素解析は語の境目を守るので当たらない。そのかわり辞書にない語は切り方を誤り、漏れが出る
同じ文を2通りに切る。形態素解析は意味の単位、2-gram は2文字ずつ機械的に切るので「京都」が「東京都」の中から当たる

切る前後の処理も重要です。全角と半角、大文字と小文字、「サーバ」と「サーバー」の長音、旧字体と新字体のような表記ゆれは、索引を作るときと検索するときの両方で同じ規則にそろえます(文字コードの正規化)。N-gram でも、この正規化をしなければ「サーバー」と「サーバー」は別の文字列なので漏れます。英語では「running」「ran」を「run」にそろえるステミングを行いますが、日本語では形態素解析が活用形を辞書の見出し語(「走った」→「走る」)に戻す見出し語化がこれにあたります。仕組みが違うので、英語のステミングの設定を日本語にそのまま当てはめることはできません。

文書を書き足すと索引がどう伸びるか、そして切り方を変えると何が当たり何が漏れるかを、部品で確かめてください。

⚡ 体験: 文書を登録して転置インデックスを引く
切り方
検索語
文書(小さな四角 = 1ページ。LIKE は全部読む)D1東京都の天気は晴れ東京都の天気は晴れD2京都の紅葉が見頃京都の紅葉が見頃D3東京で雨のち晴れ東京で雨のち晴れD4京都駅から東京駅へ京都駅から東京駅へ書き足せる文書(表へ運ぶ)東京スカイツリーの夜景転置インデックス語 10種類・番号 14件(形態素解析)検索語を選ぶと、その語の一覧だけを引くLIKE '%…%'—索引—一覧1本 ≒ 1ページ。文書が100万件になっても、索引が読むのは検索語の一覧の本数だけ
文書4索引の語10種類もう一方の切り方なら24種類
○ 文書を足して索引が伸びた○ 2語の共通部分を取った○ 2-gram の「京都」で東京都が当たった○ 形態素解析で「ツリー」が漏れた

まず下の検索語を選んでください。検索は本文を読まず、語ごとの一覧(ポスティングリスト)を引いて突き合わせます。文書のチップを表へ運ぶと、索引が伸びます。

スコアリング — 「関連の強い順」を作る

全文検索が LIKE と決定的に違うもう1つの点は、当たった文書を関連度の順に並べることです。古典的な考え方が TF-IDF で、「その文書の中で何度も出てくる語(TF が高い)は重要、ただしどの文書にも出てくるありふれた語(IDF が低い)は割り引く」という重み付けをします。現在の主流はその改良である BM25 で、長い文書が語数の多さだけで有利にならないよう長さで補正し、同じ語が何度出ても効果が頭打ちになるようにしたものです。Elasticsearch や OpenSearch の既定のスコアもこれです。

近年は、キーワードの一致ではなく意味の近さで探すベクトル検索と組み合わせることが増えました。文書を埋め込みのベクトルに変換してベクトルデータベースで近い文書を探すと、言い換えや同義語にも当たります。一方で型番や固有名詞の完全一致は BM25 のほうが得意なので、両方の結果を合わせるハイブリッド検索が、RAG で資料を探す定石になっています。

実装と実務 — 検索は別のシステムに持つことが多い

代表的な実装は、Java 製ライブラリの Apache Lucene と、それを分散したサーバとして使えるようにした Elasticsearch・OpenSearch、同じく Lucene を土台にする Solr です。ほかに軽量な Meilisearch や Typesense、SaaS の Algolia、RDBMSに組み込まれた機能(PostgreSQL の全文検索と pg_trgm、MySQL の FULLTEXT インデックス)もあります。規模が小さく、日本語の切り方にこだわらないなら、まず RDBMS の機能で足りるかを確かめるのが近道です。

実務でよくある構成は、業務データの本体を RDBMS に置き、検索用の写しを Elasticsearch へ同期して検索だけを任せる形です。このとき検索の索引は本体の派生物なので、同期の遅れや失敗で「DB にはあるのに検索に出ない」というずれが起こります。これは結果整合性を受け入れる設計で、索引を最初から作り直す再インデックスの手段を用意しておくのが運用の定石です。また、切り方(アナライザ)の設定は後から変えると既存の索引と食い違うので、辞書やアナライザの変更は再インデックスとセットで計画します。索引が作られるまでの時間(Elasticsearch では既定で約1秒ごとに検索できるようになる)も、書いた直後に読むアプリでは考慮が必要です。

試問

4つの文書と、そこから作った転置インデックス(語ごとに、その語を含む文書の番号を並べた一覧)があります。文書の切り方は「形態素解析(辞書の語で区切る)」と「2-gram(2文字ずつ区切る)」から選べ、下の文書のチップを表へ運ぶと文書を書き足せます。検索は本文を読まず、索引の一覧だけを引きます。

✦ 命題挑戦中

「ツリー」で索引を引いて、「東京スカイツリーの夜景」の文書を当てよ

  • 文書「東京スカイツリーの夜景」が索引に登録されている
  • 「ツリー」で索引を引いた結果に、その文書が入る
⚡ 体験: 「ツリー」で、スカイツリーの文書を当てる
切り方
検索語
文書(小さな四角 = 1ページ。LIKE は全部読む)D1東京都の天気は晴れ東京都の天気は晴れD2京都の紅葉が見頃京都の紅葉が見頃D3東京で雨のち晴れ東京で雨のち晴れD4京都駅から東京駅へ京都駅から東京駅へ書き足せる文書(表へ運ぶ)東京スカイツリーの夜景転置インデックス語 10種類・番号 14件(形態素解析)検索語を選ぶと、その語の一覧だけを引くLIKE '%…%'—索引—一覧1本 ≒ 1ページ。文書が100万件になっても、索引が読むのは検索語の一覧の本数だけ
文書4索引の語10種類もう一方の切り方なら24種類

まず下の検索語を選んでください。検索は本文を読まず、語ごとの一覧(ポスティングリスト)を引いて突き合わせます。文書のチップを表へ運ぶと、索引が伸びます。