Hibernate Search是一个库,它通过自动索引实体,将Hibernate ORM与Apache Lucene或Elasticsearch集成,实现高级搜索功能:全文、地理空间、聚合等。更多信息,请参阅hibernate.org上的Hibernate Search。

使用复杂类型编写查询在Hibernate Search中可能有些令人惊讶。对于这些多字段类型,关键是针对查询中的每个单独字段进行定位。让我们讨论一下这是如何工作的。

什么是复杂类型?

Hibernate Search允许你编写自定义类型,这些类型接受一个Java属性并在文档中创建Lucene字段。只要有一个属性对应一个字段的关系,你就没问题。如果你的自定义桥接器将属性存储在多个Lucene字段中,就会变得微妙。例如,一个具有数字部分和货币部分的Amount类型。

让我们从一个使用Infinispan的搜索引擎的用户那里举一个真实例子 - 由Hibernate Search自豪地提供。

字段桥接器
public class JodaTimeSplitBridge implements TwoWayFieldBridge {

    /**
     * Set year, month and day in separate fields
     */
    @Override
    public void set(String name, Object value, Document document, LuceneOptions luceneoptions) {
        DateTime datetime = (DateTime) value;
        luceneoptions.addFieldToDocument(
            name+".year", String.valueOf(datetime.getYear()), document
        );
        luceneoptions.addFieldToDocument(
            name+".month", String.format("%02d", datetime.getMonthOfYear()), document
        );
        luceneoptions.addFieldToDocument(
            name+".day", String.format("%02d", datetime.getDayOfMonth()), document
        );
    }

    @Override
    public Object get(String name, Document document) {
        IndexableField fieldyear = document.getField(name+".year");
        IndexableField fieldmonth = document.getField(name+".month");
        IndexableField fieldday = document.getField(name+".day");
        String strdate = fieldday.stringValue()+"/"+fieldmonth.stringValue()+"/"+fieldyear.stringValue();
        DateTime value = DateTime.parse(strdate, DateTimeFormat.forPattern("dd/MM/yyyy"));
        return String.valueOf(value);
    }

    @Override
    public String objectToString(Object date) {
        DateTime datetime = (DateTime) date;
        int year = datetime.getYear();
        int month = datetime.getMonthOfYear();
        int day = datetime.getDayOfMonth();
        String value = String.format("%02d",day)+"/"+String.format("%02d",month)+"/"+String.valueOf(year);
        return String.valueOf(value);
    }
}
使用桥接器的实体
[...]
@Indexed
class BlogEntry {
    [...]

    @Field(store=Store.YES, index=Index.YES)
    @FieldBridge(impl=JodaTimeSplitBridge.class)
    DateTime creationdate;
}

让我们查询这个字段

一个天真但直观的查询看起来像这样。

错误的查询
QueryBuilder qb = sm.buildQueryBuilderForClass(BlogEntry.class).get();
Query q = qb.keyword().onField("creationdate").matching(new DateTime()).createQuery();
CacheQuery cq = sm.getQuery(q, BlogEntry.class);
System.out.println(cq.getResultSize());

遗憾的是,这个查询总是返回0个结果。你能发现问题吗?

结果是Hibernate Search并不知道这些子字段creationdate.yearcreationdate.monthcreationdate.day。字段桥接器对于Hibernate Search查询DSL来说有点像黑盒,因此它假设你将数据索引在由name参数提供的字段名中(在这个例子中是creationdate)。

在Hibernate Search的下一个不太远的版本中,我们有计划解决该问题。它只要求你在编写这样的高级自定义字段桥接时提供一些元数据。但那是未来,现在我们该怎么办呢?

使用单个字段

我在这里有些作弊,但尽可能地保持一个属性对应一个字段的映射。这样会让你的生活简单得多。在这个具体的JodaTime类型示例中,这非常简单。使用自定义桥接,但不要创建三个字段(年、月、日),而是保持为单个字段,形式为yyyymmdd

让我们再次使用我们的用户真实解决方案。

使用单个字段的桥接
public class JodaTimeSingleFieldBridge implements TwoWayFieldBridge {

    /**
     * Store the data in a single field in yyymmdd format
     */
    @Override
    public void set(String name, Object value, Document document, LuceneOptions luceneoptions) {
        DateTime datetime = (DateTime) value;
        luceneoptions.addFieldToDocument(
            name, datetime.toString(DateTimeFormat.forPattern("yyyyMMdd")), document
        );
    }


    @Override
    public Object get(String name, Document document) {
        IndexableField strdate = document.getField(name);
        return DateTime.parse(strdate.stringValue(), DateTimeFormat.forPattern("yyyyMMdd"));
    }

    @Override
    public String objectToString(Object date) {
        DateTime datetime = (DateTime) date;
        return datetime.toString(DateTimeFormat.forPattern("yyyyMMdd"));
    }
}

在这种情况下,甚至最好使用Lucene数值格式字段。它们更紧凑,在范围查询中更高效。使用luceneOptions.addNumericFieldToDocument( name, numericDate, document );

上面的查询现在将按预期工作。

但我的类型必须具有多个字段!

好吧,好吧。我不会回避这个问题。解决方案是禁用Hibernate Query DSL魔法并直接针对字段。

让我们看看如何根据第一个FieldBridge实现来做到这一点。

针对多个字段的查询
int year = datetime.getYear();
int month = datetime.getMonthOfYear();
int day = datetime.getDayOfMonth();

QueryBuilder qb = sm.buildQueryBuilderForClass(BlogEntry.class).get();
Query q = qb.bool()
    .must( qb.keyword().onField("creationdate.year").ignoreFieldBridge().ignoreAnalyzer()
                .matching(year).createQuery() )
    .must( qb.keyword().onField("creationdate.month").ignoreFieldBridge().ignoreAnalyzer()
                .matching(month).createQuery() )
    .must( qb.keyword().onField("creationdate.day").ignoreFieldBridge().ignoreAnalyzer()
                .matching(day).createQuery() )
   .createQuery();

CacheQuery cq = sm.getQuery(q, BlogEntry.class);
System.out.println(cq.getResultSize());

关键是

  • 直接针对每个字段,

  • 禁用查询的字段桥接转换,

  • 并且禁用分析器可能是个好主意。

这是一个相当高级的话题,大多数时候查询DSL会做正确的事。现在不必恐慌。

但如果你遇到复杂的类型需求,了解其背后的情况是有趣的。


返回顶部