Tagbangers Blog

Hibernate SearchのFieldBridgeとignoreFieldBridgeについて

前にだいぶはまってしまい色々な方のアドバイスにより解決し(てもらっ)たところをまとめます。

結論

Hibernate Searchで複数のカラムを条件にした検索がしたい場合など、お決まりのフィールドをカスタムしたい場合はFieldBridgeを使って表現することができます。
またFieldBridgeを使って登録された項目を検索するときはignoreFieldBridgeを使います。

私の間違いとやりたかったこと

人・商品・ブランドというEntityがあり、Entity同士の関連は
Person 1-n Item n-1 Brand
となっていたとします。
そのときに「あるブランドの商品を持っている人」の検索がしたいとします。

/** 間違えたコード */

FullTextEntityManager ftem = Search.getFullTextEntityManager(em);
QueryBuilder qb = fullTextEntityManager.getSearchFactory()
            .buildQueryBuilder()
            .forEntity(Person.class)
            .get();

BooleanJunction<BooleanJunction> junction = qb.bool();
junction.must(qb.all().createQuery());

//他の検索条件...

BooleanJunction<BooleanJunction> subJunction = qb.bool();
subJunction.must(queryBuilder.keyword().onField("items.brand.name").matching(target.getCode()).createQuery());
subJunction.must(queryBuilder.keyword().onField("items.code").matching(target.getCode()).createQuery());
junction.must(subJunction.createQuery());
/** できた生成クエリ */

//たとえばブランド名がNike, 商品コードが 1234 という条件をセットした場合
searchQuery = .. +(+items.brand.name:Nike +items.code:1234)

これでNikeのコード番号1234の商品を持った人を絞れてると思っていました。
しかし実際に結果は、Nike以外のブランドの、商品コード1234を持ってる人も取得できてしまいました。
初歩的なミスですがここで各々の条件を紐付けるon句のようなものがないので、
これでは「(商品の)ブランド名はNike」「商品のコードは1234」が別々に評価されてしまいます。

発想を変える

じゃあどうしたらよいのか
1つのクエリにまとめてしまう。「商品のNikeは商品コード1234」という風になっていれば検索が楽だ。
つまり

/** 期待する生成クエリ */
searchQuery = .. +(+items.Nike:1234)

みたいにかけるといいなと。

FieldBridgeを使ってJavaのプロパティとLuceneDocumentの橋渡しをする

上記のような形になるようにFieldBridgeクラスを継承したItemsBridgeクラスを作成します。

Person.class

@Table(name = "person")
@Inheritance(strategy = InheritanceType.JOINED)
@DynamicInsert
@DynamicUpdate
@Indexed
public class Person extends DomainObject<Long> {
    @OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
    @SortNatural
    @Field(bridge = @FieldBridge(impl = ItemsBridge.class))
    private SortedSet<Item> items = new TreeSet<>();
}

ItemsBridge.class

public class ItemsBridge implements FieldBridge {

    @Override
    public void set(String name, Object value, Document document, LuceneOptions luceneOptions) {
        Collection<Item> items = (Collection<Item>) value;
        if (items != null) {
            for (Item item : items) {
                if (item.getType() != null) {
                    luceneOptions.addFieldToDocument(name + "." + item.getBrand().getName(), item.getCode().toString(), document); //nameはitems が入る
                }
            }
        }
    }
}

検索はignoreFieldBridge

この状態でインデクシングをし直し、検索をしてみます。

FullTextEntityManager ftem = Search.getFullTextEntityManager(em);
QueryBuilder qb = fullTextEntityManager.getSearchFactory()
            .buildQueryBuilder()
            .forEntity(Person.class)
            .get();

BooleanJunction<BooleanJunction> junction = qb.bool();
junction.must(qb.all().createQuery());

//他の検索条件...
String brandName = request.getBrandCode(); //request.getBrandName()=Nike
BooleanJunction<BooleanJunction> subJunction = qb.bool();
subJunction.must(queryBuilder.keyword().onField("items." + brandName).ignoreFieldBridge().matching(request.getItemCode()).createQuery()); //request.getItemCode()=1234
junction.must(subJunction.createQuery());
/** できた生成クエリ */
searchQuery = .. +(+items.Nike:1234)

これで期待したクエリを生成することができ、期待通りの検索をすることができました。
ここで、ignoreFieldBridge()というメソッドを使っています。ただ、なぜこれでうまいこと検索できるのかよく理解できませんでした。
字面だけ見ると先ほど作ったFieldBridge使っているのならignoreはおかしいんじゃないかと思ってしまうのですが…
色々調べてみると、これはFiedlBridgeを使うのはインデクシング時のとき、辞書登録時だけ"items.brandName:itemCode"の形で辞書登録するためにこの変換が必要なのに対し
検索の際は既に登録されているインデックスファイルから選べばよいだけ = fieldBridgeを使う必要がない(むしろ余計な変換しちゃだめ) なので
逆にignoreしないといけない、ということになるようでした。

備忘録として。