Lucene2.9で自前でComparatorを作って、複数フィールドの値を協調させてソートさせる

複数フィールドの値をみてソートをするという要件がでてきて、ページングとかの面倒さを考えるとソートはすべてLucene内部で完結させたいので自前でComparatorを作ろうと思ったけど、けっこう難易度が高かったのでその記録。

マニングのLucene In Actionのサンプルソースコードがけっこう参考になったけど、この本だとLucene1.5がターゲットになってるので、2.9とはやっぱりだいぶ違う。とはいえ、Luceneを触るなら一通りコードを眺めるだけでも意味はある。
http://www.manning.com/hatcher2/

やりたいこと

  • ふたつのフィールドの値をみながらソートさせる

まあこれだけなんですけどね。

FieldComparatorSourceの実装

SortFieldクラスはFieldComparatorSourceをコンストラクタにとるので、FieldComparatorSourceを継承したクラスが必要です。
FieldComparatorを返すnewComparatorメソッドでこのあとで作る自前のFieldComparatorを返してやる以外、とくにやることないけど、FieldComparatorのコンストラクタに渡したい値があればFieldComparatorSourceのコンストラクタで渡しておく必要があります。

import java.io.IOException;

import org.apache.lucene.search.FieldComparator;
import org.apache.lucene.search.FieldComparatorSource;

public class MyFieldComparatorSource extends FieldComparatorSource {

    private final String field1;
    private final String field2;

    // ここではフィールド名を渡してる
    public MyFieldComparatorSource(final String field1, final String field2) {
        this.field1 = field1;
        this.field2 = field2;
    }

    @Override
    public FieldComparator newComparator(final String fieldname, final int numHits, final int sortPos,
            final boolean reversed) throws IOException {
        return new MultiFieldComparator(numHits, field1, field2);
    }
}

FieldComparatorの実装

FieldCacheから値をフィールド変数にコピーしてるのがポイントなのか?
はじめはIndexReaderをFieldComparatorSourceから渡してもらおうかと思ってたけど、Indexが複数ファイルの分かれてるのに対応できないのに気づいて、素直に、org.apache.lucene.search.FieldComparator.StringValComparatorを参考にした。

import java.io.IOException;

import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.FieldCache;
import org.apache.lucene.search.FieldComparator;

public class MultiFieldComparator extends FieldComparator {

    private final String[] valuesStartDate;
    private final String[] valuesEndDate;
    private String[] currentReaderValuesStartDate;
    private String[] currentReaderValuesEndDate;
    private int bottom;

    private final String field1;
    private final String field2;

    MultiFieldComparator(final int numHits, final Calendar calendar, final String field1, final String field2) {
        valuesStartDate = new String[numHits];
        valuesEndDate = new String[numHits];
        this.field1 = field1;
        this.field2 = field2;
    }

    @Override
    public int compare(final int slot1, final int slot2) {
        final String val1Start = valuesStartDate[slot1];
        final String val1End = valuesEndDate[slot1];
        final String val2Start = valuesStartDate[slot2];
        final String val2End = valuesEndDate[slot2];

        // あとは好きなように比較
    }

    @Override
    public int compareBottom(final int doc) {
        final String val1Start = valuesStartDate[bottom];
        final String val1End = valuesEndDate[bottom];
        final String val2Start = currentReaderValuesStartDate[doc];
        final String val2End = currentReaderValuesEndDate[doc];

        // あとは好きなように比較
    }

    @Override
    public void copy(final int slot, final int doc) {
        valuesStartDate[slot] = currentReaderValuesStartDate[doc];
        valuesEndDate[slot] = currentReaderValuesEndDate[doc];
    }

    /**
     * {@inheritDoc}
     * <p>
     * 
     */
    @Override
    public void setNextReader(final IndexReader reader, final int docBase) throws IOException {
        currentReaderValuesStartDate = FieldCache.DEFAULT.getStrings(reader, field1);
        currentReaderValuesEndDate = FieldCache.DEFAULT.getStrings(reader, field2);
    }

    @Override
    public void setBottom(final int bottom) {
        this.bottom = bottom;
    }

    @Override
    public Comparable value(final int slot) {
        return valuesStartDate[slot];
    }
}

重要なのはcompare,copyくらいなのかなー。あとはよくわからん。

compareBottom(int)

比較対象の最後に来たら呼ばれるのかと思ったけどそうでもないっぽい。よくわかんない。

copy(int, int)

よくわかんないです。
キャッシュに使われる配列に値を入れてる?

setBottom(int)

比較対象の最後に来たら呼ばれるのかと思ったけどそうでもないっぽい。よくわかんない。

setNextReader(IndexReader, int)

LukeでIndexファイルを編集したりするとファイルが増えるので、そういうIndexを読んでいると、このメソッドが呼ばれる。

value(int)

これを実装するとなにが嬉しいのか不明。
ソースを追うとFieldDocを作るのに使われているのはわかるけど、なにが嬉しいのかよくわからん。

  FieldDoc fillFields(final Entry entry) {
    final int n = comparators.length;
    final Comparable[] fields = new Comparable[n];
    for (int i = 0; i < n; ++i) {
      fields[i] = comparators[i].value(entry.slot);
    }
    //if (maxscore > 1.0f) doc.score /= maxscore;   // normalize scores
    return new FieldDoc(entry.docID, entry.score, fields);
  }

SortFieldのコンストラクタに渡してやる

使うときはこんな感じ。

Sort sort = new Sort(new SortField("", new MyFieldComparatorSource("startDate", "endDate")));

第一引数のフィールドは使わないのでなんでもいい。