概要

bin/mahout lucene.vectorを使って、LuceneのインデックスからVectorを作ってみる。

Mahoutのバージョンは0.7。Luceneは3.6.0。

@CretedDate 2012/09/23
@Env Java7, Mahout0.7, Lucene3.6

lucene-kuromojiを落としてくる

今回は形態素解析しつつインデックスを作成してくれるJapaneseAnalyzerを使う。

こことかからlucene-kuromojiを落としてclasspathに入れるなり、Mavenのdependencyに入れるなりする。

Luceneのインデックスを生成する

Luceneのインデックス作成はSolrでやるという手もあるけど、今回は普通にJavaのコードを書いて生成してみる。

こんな感じのコード。

public void createFromDir(File inputDir, File outputDir) throws IOException {

    // JapaneseAnalyzerを利用
    IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_36,
            new JapaneseAnalyzer(Version.LUCENE_36));

    // IndexWriterを生成(Java7のtry-with-resources利用)
    try (IndexWriter writer = new IndexWriter(FSDirectory.open(outputDir), conf)) {
        // 指定ディレクトリ配下のファイルをインデックスに追加
        for (File file : inputDir.listFiles()) {
            Document doc = new Document();
            // 本文をDocumentに追加(文書自体はストアせずにインデックスのみ追加)
            doc.add(new Field("content", FileUtils.readFileToString(file), Field.Store.NO,
                    Field.Index.ANALYZED, Field.TermVector.YES));
            // ファイル名をDocumentに追加(後で判別しやすいように)
            doc.add(new Field("title", file.getName(), Field.Store.YES, Field.Index.ANALYZED));
            // IDを付けておく(なくても動く)
            doc.add(new Field("id", String.valueOf(id++), Field.Store.YES,
                    Field.Index.NOT_ANALYZED));
            // DocumentをWriterに追加
            writer.addDocument(doc);
        }
    }
}

これでテキストファイルがわらわら入ったディレクトリをinputDirに指定して実行すると、outputDirにLuceneのインデックスが出力される。

本文はcontentというフィールドに、ファイル名はtitleというフィールドに登録している。

本文を登録するフィールドはTermVectorの情報がいるので、contentをdocにaddする際はField.TermVector.YESを登録しておく。

lucene.vectorを叩いてみる

試しに上記コードを使って青空文庫から落とした「銀河鉄道の夜」「オツベルと象」「セロ弾きのゴーシュ」「注文の多い料理店」「風の又三郎」をインデックスに投入。

出力されたインデックスに対して、lucene.vectorを実行してみる。

$ bin/mahout lucene.vector \
    --dir /tmp/lucene_index \
    --output /tmp/lucene_vector \
    --dictOut /tmp/lucene_dic \
    --field content \
    --idField id

--fieldで本文が入ってるフィールド名を指定。

--dirがluceneのインデックスが入ったディレクトリ(ローカル)。結果は--outputで指定した先(HDFS)に出力される。

seqdumperで出力されたVectorファイルを見てみる。

$ bin/mahout seqdumper -i /tmp/lucene_vector -b 80

Input Path: /tmp/lucene_vector
Key class: class org.apache.hadoop.io.LongWritable Value Class: class org.apache.mahout.math.VectorWritable
Key: 0: Value: 0:{3293:1.9162907600402832,3195:7.901069164276123,3191:8.352917671203613,3190:1.
Key: 1: Value: 1:{3285:1.2231435775756836,2228:2.7100443840026855,2215:1.5108256340026855,34:2.
Key: 2: Value: 2:{3630:1.9162907600402832,3939:1.4142135381698608,1891:1.9162907600402832,17:1.
Key: 3: Value: 3:{3949:2.8284270763397217,3947:1.2231435775756836,3946:3.700752019882202,3944:3
Key: 4: Value: 4:{3949:1.4142135381698608,3948:2.6168267726898193,3947:1.7297861576080322,3946:
Count: 5

なにやらそれっぽいVectorファイルが出ている。

--dictOutで指定した先に辞書ができてる(これはローカルに出力される)。これと付き合わせると、どのTermがどのIDになってるのかが分かる。

Javeのコードで同じことをやってみる

lucene.vectorコマンド内で使ってるLuceneIteratorというクラスが便利そうだったので、試しにJavaのコード上から使ってみる。

try (IndexReader reader = IndexReader.open(FSDirectory.open(new File("data/lucene_index")))) {
    VectorMapper mapper = new TFDFMapper(reader, new TFIDF(), new CachedTermInfo(reader,
            "content", 1, 99));
    LuceneIterable ite = new LuceneIterable(reader, "id", "content", mapper,
            LuceneIterable.NO_NORMALIZING);

    for (Vector vec : ite) {
        System.out.println(vec.toString().substring(0, 80));
    }
}

実行結果。

0:{1925:1.9162907600402832,1904:3.021651268005371,3828:1.2231435775756836,1900:1
1:{2741:1.2231435775756836,1904:1.5108256340026855,3848:1.2231435775756836,1900:
2:{1948:1.0,3867:1.2231435775756836,1891:1.9162907600402832,1888:1.5108256340026
3:{3526:1.5108256340026855,3525:3.3783087730407715,3524:1.5108256340026855,3523:
4:{3949:1.4142135381698608,3948:2.6168267726898193,3947:1.7297861576080322,3946:

なんかVectorになって返ってきた。この辺のコードで引数を変えながら結果と戯れると感覚が養えそう。