Eclipseやantを使わないLucene入門

後輩に煽られたのでLuceneを使えるようにしてみた.ようやく積ん読になっていた "Lucene in Action" がついに火を噴くときがきた模様.

長らくJavaは触っていなかったけれど,JavaライブラリってEclipseのようなIDE使わないとimport地獄にはまったり,ant使わないとそもそもコンパイルできなかったりと,なかなかゆとりには厳しい印象がある.実際,前回のチャレンジではそれで挫折してLuceneが嫌いになった.

今回はantやIDEに頼らずにメモ帳プログラムでLuceneを動かしてみた.

やってみるとハマるところはあるものの,とても簡単だったので備忘録程度にメモ.コードはLucene in Actionの(pp.20-25)あたりのサンプルコードを参考にした.

インストールと準備

まず準備から.最新のlucene-3.1.0の例で説明.

インデクス構築

基本的には難しいことを考えず,

  • 各文書はdocid,bodyの情報を持つ
  • トークン化は特に考えない (今回はスペース区切り)

という検索インデクス構築と,そのインデクスに対する検索するサンプルコードを書いた.文書追加部分を適当なハードコーディングしているけれど,この部分を好きなファイルから読み込めば,自分の用途に合ったインデクスを構築することができる.

import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.analysis.WhitespaceAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;

import java.io.File;

class HogeIndex {
    public static void main (String [] args) throws Exception {

        // Directory directory = new RAMDirectory();
        Directory directory = FSDirectory.open(new File("./index"));

        IndexWriter writer = new IndexWriter(directory,
                                            new WhitespaceAnalyzer(),
                                            IndexWriter.MaxFieldLength.UNLIMITED);
        Document doc1 = new Document();
        doc1.add(new Field("id", "1",
                           Field.Store.YES,
                           Field.Index.NOT_ANALYZED));
        doc1.add(new Field("body", "hoge fuga bar piyo",
                           Field.Store.YES,
                           Field.Index.ANALYZED));
        writer.addDocument(doc1);

        Document doc2 = new Document();
        doc2.add(new Field("id", "2",
                           Field.Store.YES,
                           Field.Index.NOT_ANALYZED));
        doc2.add(new Field("body", "hoge あひゃひゃひゃ ほげ",
                           Field.Store.YES,
                           Field.Index.ANALYZED));
        writer.addDocument(doc2);


        writer.close();
    }

}
  • 何をimportすればよいのかということは,勉強も兼ねてLucene JavaDocを眺めて手打ちした.Eclipseの自動補完などを使うのがよいと思われる.
  • FSDirectoryはファイルシステム上にインデクスを作成するDirectoryクラス
  • IndexWriterクラスがインデクス構築を行う
    • WhitespaceAnalyzerは空白区切りで単語をトークナイズする
    • IndexWriter.MaxFieldLength.UNLIMITED (deprecated) は,各フィールドに含まれるトークン数を Integer.MAX_VALUE にする.
  • Documentは1個以上のFieldを持つ
  • Fieldは,Field名と値をStringで持つ.それ以降の引数の意味は以下のとおり
    • Field.Store.YES => インデクスにフィールドの値を保持する
    • Field.Index.NOT_ANALYZED => トークナイズなどを行わない
    • Field.Index.ANALYZED => IndexWriteにセットされたAnalyzerによって処理
  • 超適当なイメージ
    • Field < Document < Directory

検索

検索はこんな感じ.

import org.apache.lucene.util.Version;

import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.store.FSDirectory;

import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.ScoreDoc;

import org.apache.lucene.analysis.WhitespaceAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;

import java.io.File;

class HogeSearch {
    public static void main (String [] args) throws Exception {

        Directory dir    = FSDirectory.open(new File("./index"));
        IndexSearcher is = new IndexSearcher(dir);

        QueryParser parser = new QueryParser(Version.LUCENE_30,
                                             "body",
                                             new WhitespaceAnalyzer());

        Query query1 = parser.parse("hoge");
        //Query query = parser.parse("hoge piyo");
        //Query query = parser.parse("hoge fuga");

        TopDocs docs1 = is.search(query1, 10);
        System.out.println("Found " + docs1.totalHits + " docs.");

        for (ScoreDoc scoreDoc : docs1.scoreDocs) {
            Document doc = is.doc(scoreDoc.doc);
            System.out.println(doc.get("id"));
            System.out.println(doc.get("body"));
        }


        Query query2 = parser.parse("ほげ");

        TopDocs docs2 = is.search(query2, 10);
        System.out.println("Found " + docs2.totalHits + " docs.");

        for (ScoreDoc scoreDoc : docs2.scoreDocs) {
            Document doc = is.doc(scoreDoc.doc);
            System.out.println(doc.get("id"));
            System.out.println(doc.get("body"));
        }

        is.close();
    }
}
  • Directoryのオープンはインデクス構築と一緒.
  • 検索するのはIndexSearcherクラス
  • クエリをパースするためにQueryParserクラスを用いる
    • いきなりQueryTermクラスでクエリを作成してもいいけれど,インデクス構築に用いられたAnalyzerと検索に用いるAnalyzerが一致しないと,不整合を起こして検索結果を適切に取得できなくなることがある
  • 用意したQueryParserインスタンスを通してQueryインスタンスを取得
    • 予備検証で"hoge piyo"がきちんとパースされて検索されることを確認
  • IndexSearcher.search()で検索.クエリと取得検索結果数を入力.
  • 検索結果は TopDocs で返される (名称が行けてないと思うのは僕だけ?)
    • TopDocs.totalHits => 検索結果件数
    • TopDocs.scoreDocs => 検索結果の配列 ScoreDoc[]
  • 個々の検索結果はScoreDocに格納されている
  • IndexSearcher.doc(scoreDoc.doc) でDocumentを取得
    • doc.get("xx") でフィールドxxの値を取得

実行

% javac HogeIndex.java
% java HogeIndex

% javac HogeSearch.java
% java HogeSearch
Found 2 docs.
1
hoge fuga bar piyo
2
hoge あひゃひゃひゃ ほげ

Found 1 docs.
2
hoge あひゃひゃひゃ ほげ

ちゃんと動いていることを確認.Documentの重複は自動的にチェックしてくれないので,HogeIndexを2回実行すると,結果が2倍出力される


まとめ

2年くらい前にLuceneを触ってみたときは,イメージがつかなかったのですぐに挫折したものの,今回はLucene in Actionを読んでイメージが大分ついたので,簡単におもちゃコードを書くことができた.

イメージをつけることとサンプルコードがあるのは本当に大事だなぁと実感.

参考にしたのは,下記のLucen in Action 2nd edition.本書を購入すると電子版をダウンロードする権利もついてくるので (少なくとも僕の手元にあるものはそうなっている),紙の本と電子書籍両方を同時に手に入るのでおすすめ.

Lucene in Action: Covers Apache Lucene v.3.0

Lucene in Action: Covers Apache Lucene v.3.0

  • 作者: Michael McCandless,Erik Hatcher,Otis Gospodnetic
  • 出版社/メーカー: Manning Publications
  • 発売日: 2010/06/30
  • メディア: ペーパーバック
  • 購入: 1人 クリック: 10回
  • この商品を含むブログ (3件) を見る

TODO

  • deprecated methodを除外する
  • ややこしいimportをEmacs上でもなんとかできないか
  • いろんなAnalyzerを試す
    • CJKTokenizerでbigramができるっぽい
  • 簡単な検索プログラムを書く