PDF をページ単位でインデックスする Solr の RequestHandlerを作成する
はじめに
Solr 6以降では PDF やワードなどのバイナリファイルをインデックスする機能(ExtractingRequstHandler)がサポートされています。
ファイル内に含まれるテキストをまとめて1つの文書として、メタデータ(作成日時、作成者等)と共にインデックスを作成してくれるのでこれはとても便利な機能ではありますが、用途によってはキーワードが何ページ目にヒットするのかを知りたいこともあります。そこで、自前の RequestHandler 作成の練習として PDF をページ単位でインデックスする ReqestHandler を作成してみました。
Apache Tika
Solr では PDF 等の各種フォーマットを扱うために Apache Tika を利用しています。
Tika では PDF 等を XHTML に変換した上で SAX パーサーにコンテンツハンドラを渡して XHML の要素毎の処理を実行させられます。それと同時にファイルに含まれるメタデータが Metadata クラスのオブジェクトに格納されます。
従って、Tika の呼び出し側は
- ファイルに含まれる構造化コンテンツが XHTML に変換されたもの
- ファイルのメタデータ
を扱うことができます。
Tika によって PDF から変換された XHTML がどんなフォーマットになっているかは、extractOnly=true オプションを指定することで見ることができます。
bin/post -c test -params "extractOnly=true&wt=json&indent=true" -out yes /tmp/test.pdf
XHTML の body 部分だけを抜粋すると以下のようになっています。
<body> <div class="page"> <p/> <p>テストドキュメント1ページ目です。</p> <p/> </div> <div class="page"> <p/> <p>2ページ目の文章です。</p> <p/> </div> <div class="page"> <p/> <p>これは3ページ目です。</p> <p/> </div> </body> </html>
ページ毎に
<div class="page">
に囲まれた構造になっていることが分かります。
ExtractingRequestHandler には capture という実行時パラメータがあり、これを指定することで特定の XHTML 要素(この場合div)を個別にインデックスすることができるのですが
- 文書IDはファイル全体で共通
- 同じ名前の要素が複数存在する場合は multivalued のフィールドに入れられる
という仕様のためページ番号との紐付けができそうもなかったのが、今回自前で実装してみようと思ったきっかけでした。
SolrがPDFを扱う仕組み
ExtractingRequestHander では以下のクラス構成で PDF 等のインデックス処理を実行しています。
- ExtractingRequestHander
リクエストのエントリポイント(/solr/update/extract)。- 設定の読み込み
- Tika のパーサーが生成する SAX イベントを処理するコンテンツハンドラ(SolrContentHandler)用のファクトリ(SolrContentHandlerFactory)を生成
- ExtractingDocumentLoader を生成して load メソッド(ファイル読み込みとインデックス処理のメイン)を実行
- ExtractingDocumentLoader
実際にファイルを読み込んでインデックス処理を実行。- メタデータ読み込み
- コンテンツの種類に対応したパーサーを生成
- ExtractingRequestHander から与えられたファクトリ(SolrContentHandlerFactory)を使ってコンテンツハンドラ(SolrContentHandler)オブジェクトを生成
- パーサーにコンテンツハンドラ(SolrContentHandler)を与えてパース処理を実行
- 生成されたメタデータオブジェクトとコンテンツ文字列をインデックスに投入
- SolrContentHandler
Tika のパーサーが生成する SAX イベントを処理する。
基本的に XHTML に含まれるコンテンツ部分の文字列を結合して1つの大きな文字列を作っている。 - SolrContentHandlerFactory
SolrContentHandler のファクトリメソッドを提供する。
ページ単位のインデックス処理を実装
上記の4クラスをそれぞれ継承したクラスを実装しました。(コードはこの記事の最後に)
実装の内容は以下の通りです。
- 設定の読み込みは親クラスに任せる
- 具象クラスの生成処理は上書き
- SolrContentHandler のサブクラスで
<div class="page">
を認識してページ番号とページ毎のコンテンツとが対応付けされたテーブルを作成
- ExtractingDocumentLoader のサブクラスでページ毎に文書IDを振ってインデックス投入
利用方法
- 4クラスをコンパイルして Jar ファイルを生成
- contrib/extraction/lib に Jar ファイルをコピー
- solrconfig.xml の
<requestHandler name="/update/extract" startup="lazy" class="solr.extraction.ExtractingRequestHandler" >
の箇所を jp.co.splout.solr.plugin.MyExtractingRequestHandler に変更
- Solr 再起動
実行例
投入
$ bin/post -c test -params "extractOnly=false&wt=json&indent=true&literal.id=testpdf1" -out yes test.pdf
検索結果
最後に
PDF をページ単位でインデックスする Solr のプラグインを作成しました。
PoC ということで最小限の実装しかしていませんが、ちゃんとするなら
- せっかくなのでメタデータも有効に使いたい
- ExtractingRequestHandler の実行時パラメータで共用できるものは共用したい
- もっというなら ExtractingRequestHandler の1機能として統合?
ということも考えたいと思います。