Luceneで複数フィールドで検索してるときに、そのうちのひとつのフィールドでのスコアでソートする

ついでにほかのフィールドもソート条件に入れたいとか、わりとベタな要望だと思うけど、Lucene wikiで検索してもみつからなかったので考えてしまった。
はじめは、ソート用の結果を取得するのと、ホントのデータを取得するのとで二回サーチしないといかんかと思ったけど、ページングが死ぬほどめんどいのでいろいろ考えてたら思いついた方法。当たり前の方法なんだろうけどね。
ポイントは

  • クエリーを細かく組み立てる
  • 重みを活用する

ということ。

Luceneの本気で使い倒す方法ってあんまり情報ないなあ。Lucene in Action 2nd買うか。あと、Lucene本の改訂版を出してほしいなあ。かなり需要はあると思うけど。

サンプルケース

例えば、

  • スコアでソート
  • 同一スコアであればfooフィールドでソート

みたいなのをサンプルとしたのが以下。

クエリー組み立て

スコアによってソートしたいフィールドと、それ以外の検索したいだけのフィールドを分けてクエリーにして、前者はboostで重みを大きく、後者はboostで重みを小さくする。
そうやってできたクエリーをBooleanQueryにBooleanClause.Occur.SHOULDで入れて、そのクエリーをほかの条件を入れるBooleanQueryに入れる。

    private Query structQuery(final String keyword, final String foo) {
        // クエリー組み立て
        // 条件が指定されていれば、containerに入れていく
        BooleanQuery container = new BooleanQuery();
        // 検索キーワード
        if (StringUtils.isNotBlank(keyword)) {
            BooleanQuery miniContainer = new BooleanQuery();
            Query query = null;
            // スコアでソートしたいフィールドのクエリーを作成
            QueryParser qp = new QueryParser("scorable_field", new StandardAnalyzer());
            qp.setDefaultOperator(Operator.OR);
            try {
                query = qp.parse(keyword);
            } catch (ParseException e) {
                e.printStackTrace();
                System.out.println("検索クエリーのパースに失敗しました");
            }
            // 重みを付ける
            query.setBoost(10f);
            // ここではOR条件でミニコンテナに入れる
            miniContainer.add(query, BooleanClause.Occur.SHOULD);

            qp = new MultiFieldQueryParser(new String[] {
                    "fld1",
                    "fld2",
                    "fld3",
                    "fld4",
                    "fld5",
                    "fld6", }, new StandardAnalyzer());

            qp.setDefaultOperator(Operator.OR);
            try {
                query = qp.parse(keyword);
            } catch (ParseException e) {
                e.printStackTrace();
                System.out.println("検索クエリーのパースに失敗しました");
            }
            // 重みを付ける。この場合は100倍の差をつけた
            query.setBoost(0.1f);
            // ここでもOR条件でミニコンテナに入れる
            miniContainer.add(query, BooleanClause.Occur.SHOULD);

            // 大元のコンテナにいれる
            // ここではAND条件いれないといけない
            container.add(miniContainer, BooleanClause.Occur.MUST);
        }
        // その他の条件は適当に追加していく
        if (StringUtils.isNotBlank(foo)) {
            container.add(
                    new TermQuery(new Term("foo", foo)),
                    BooleanClause.Occur.MUST);
        }
        return container;
    }

検索実行

こんな感じでソート条件を渡す。

            TopDocs topDocs = searcher.search(container, null, offset * length, new Sort(new SortField[] {
                    new SortField(null, SortField.SCORE),
                    new SortField("foo", SortField.STRING), }));

SortFieldの配列は入れた順番でソート条件に適用される。SQLのorder byといっしょ。

これで目的のソート順で結果が返ってくる。