アペフチ

取り敢えずPDFを全文検索するシステムのための最少ステップ

Groongaで学ぶ全文検索 2016-03-25に行って来た。

今日は、仕事でPDFを全文検索できるようにしたいから話を聞きに来たという参加者がいたので、PDFを全文検索できるよう、Groongaのデータベースを作るまでをその場でやった。

まず、PDFを全文検索するために必要なことの概要を説明した。

全文検索できるようにするまでの概要

PDFを全文検索するには

  1. 全文検索できるようにするための準備(データベースの構築)
  2. データベースを使って全文検索をする

という二段階が必要になる。

準備は、

  1. PDFからテキストを抜き出す
  2. テキストをGroongaに突っ込む
  3. Groongaが(勝手に)インデックスを作る

という手順に分解できる。ここのところ僕が説明したのだけど、「テキストをGroongaに突っ込む」のところ、「どのような形で」というのが抜けていて、そこがぴんとこなかったようだ。あとで@ktouさんとの質疑応答で「PDFのパスとかのメタデータと一緒に(JSONとかのフォーマットにして)突っ込む」と言ったところでぴんときたようだった。

手順に戻って、全文検索の実行のところ。

  1. 検索語をGroongaに与える
  2. Groongaがその語を含むPDFのパス(とか、ページ番号とかの付随的な物)を返す

ということになる。

PDFを全文検索できるようにする

ここまで説明した所で、手元のPDFファイルを使って実際にデータベースの作成から検索してみるまでを@ktouさんがやって見せてくれた。時間も限られているので、「本文に関する検索をして、そのPDFのパスが得られるようにする」というのを目標にした。

1. テーブルを作る

コマンドラインでテーブルを作る(暗黙に、Groongaはテーブルの形でデータを表現しているということになる)。

table_create pdfs \
  TABLE_HASH_KEY \
  ShortText

ShortTextのところは、ハッシュテーブルのキーの型を決めている。

こんなファイルを作って、groongaコマンドに入力すると、pdfsテーブルが出来る。

まずデータベース(ファイル)を作る。

% groonga -n /tmp/grn

次にファイルに書いたコマンドを読み込ませる。

% groonga /tmp/grn < /tmp/table-create.grn

これでテーブルが出来る。ただ、今はハッシュテーブルのキーのところにしかデータが保存できないので、PDF本文を保存する場所をこのテーブルのカラムとして作る。

column_create pdfs body \
  COLUMN_SCALAR \
  Text

COLUMN_SCALARの所はCOLUMN_SCALARCOLUMN_VECTORかが選べて、このカラムに入るのが分割できない単位なのか、配列のようにその単位を複合させた物なのかで選ぶ。今回は、一つのキー(PDFファイルのパス)に複数の本文が入ることはないので、スカラーにしている。

データを入れる

で、実際にデータを入れる。

その前に、PDFからテキストの形式にしないといけない。更に、Groongaは(コマンドでは)JSONフォーマットでデータを入力するので、データを加工しなければならない。

ここでは、Poppler付属のpdftotextコマンドで抽出したテキストをファイルに保存して、それを使ってRubyで、ファイルのパス情報と一緒にJSONにしていた。本題ではないので割愛。以下のようなファイルをRubyで作って、groongaコマンドに読み込ませる。

load --table pdfs
[
  {
    "_key": "path/to/pdf"
    "body": "でかい本文"
  }
]

インデックス無しでの検索

Groongaはインデックスを作らないでも検索できる。

select --table pdfs \
  --query body:@API

@APIの所が、(その前にあるbodyカラムに対して)APIで検索する、という指定になっている。インデックスが無い時は、ただの線形検索になる。@ktouさんの手元のPDF、手元のGroongaで試したところ、この検索には60ミリ秒くらい掛かった。インデックスを作って使うとこれより速いはずなので後で試してみる。

インデックスを作る

RDBMSでは、インデックスを張るには単に張りたいカラムを指定すればよかったが、Groongaではより検索方法に合わせた柔軟なインデックスが作れるように、色々パラーターを指定しながら自分でインデックスを作る。そのインデックスは、上でやったようなテーブルとして作る。

が、初めての時などはおすすめの設定があるので取り敢えずテンプレとしてそれを使えばよい。

table_create terms \
  TABLE_PAT_KEY \
  ShortText \
  --default_tokenizer TokenBigram \
  --normalzer NormalizarAuto

全文検索する時は、(pdfsテーブルでハッシュテーブルにしていたところに相当するところが)パトリシアトライというのがおすすめなので、TABLE_PAT_KEYと書く。これはこのまま使っておけばよい。「terms」というのがテーブル名なので、ここだけ気分によって変えればよい。

次はカラム。

column_create terms body_index \
  COLUMN_INDEX|WITH_POSITION \
  pdfs \
  body

これもテンプレで、「body_index」は変えてよい。名前から想像付くように、pdfsテーブルのbodyカラムのためのインデックスだからこの名前になっている。分かりやすいのがいいだろう。「pdfs」は、pdfsテーブルに対するインデックスということを意味し、「body」はその中のbodyカラムへのインデックスだということをGroongaに教えている。

これでさっきのように

select --table pdfs \
  --query body:@API

すると、今度は0.2ミリ秒程度で検索結果が返って来た。二桁の差が付いたことになる(PDF結構でかくて、1MiBくらいあった)。

あとは、アプリケーションの要件に合わせてウェブUIを付けたりすると出来上がりだ。

参考までに既にGroongaを使ったPDF検索アプリケーションとしてhonyomiがあって、検索結果からそのまま読み始めたりできるので便利。