使用复杂类型编写查询在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.year
、creationdate.month
和creationdate.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数值格式字段。它们更紧凑,在范围查询中更高效。使用 |
上面的查询现在将按预期工作。
但我的类型必须具有多个字段!
好吧,好吧。我不会回避这个问题。解决方案是禁用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会做正确的事。现在不必恐慌。
但如果你遇到复杂的类型需求,了解其背后的情况是有趣的。