improved support for numeric fields

This commit is contained in:
Timo Westkämper 2010-05-17 19:47:23 +00:00
parent f05b91330f
commit 23dc585bee
5 changed files with 223 additions and 66 deletions

View File

@ -5,25 +5,19 @@
*/
package com.mysema.query.lucene;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.NumericRangeQuery;
import org.apache.lucene.search.PhraseQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TermRangeQuery;
import org.apache.lucene.search.WildcardQuery;
import org.apache.lucene.search.*;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.util.NumericUtils;
@ -34,6 +28,7 @@ import com.mysema.query.types.Operator;
import com.mysema.query.types.Ops;
import com.mysema.query.types.OrderSpecifier;
import com.mysema.query.types.Path;
import com.mysema.query.types.PathType;
/**
* Serializes Querydsl queries to Lucene queries.
@ -41,8 +36,20 @@ import com.mysema.query.types.Path;
* @author vema
*
*/
// TODO Add support for longs, floats etc.
public class LuceneSerializer {
private static final Map<Class<?>,Integer> sortFields = new HashMap<Class<?>,Integer>();
static{
sortFields.put(Integer.class, SortField.INT);
sortFields.put(Float.class, SortField.FLOAT);
sortFields.put(Long.class, SortField.LONG);
sortFields.put(Double.class, SortField.DOUBLE);
sortFields.put(Short.class, SortField.SHORT);
sortFields.put(Byte.class, SortField.BYTE);
sortFields.put(BigDecimal.class, SortField.DOUBLE);
sortFields.put(BigInteger.class, SortField.LONG);
}
public static final LuceneSerializer DEFAULT = new LuceneSerializer(false, true);
@ -89,6 +96,8 @@ public class LuceneSerializer {
return le(operation);
} else if (op == Ops.GOE || op == Ops.AOE) {
return ge(operation);
} else if (op == PathType.DELEGATE){
return toQuery(operation.getArg(0));
}
throw new UnsupportedOperationException("Illegal operation " + operation);
}
@ -116,18 +125,38 @@ public class LuceneSerializer {
}
return new WildcardQuery(new Term(field, normalize(terms[0])));
}
@SuppressWarnings("unchecked")
private Query eq(Operation<?> operation) {
verifyArguments(operation);
String field = toField(operation.getArg(0));
if (operation.getArg(1).getType().equals(Integer.class)) {
return new TermQuery(new Term(field, NumericUtils
.intToPrefixCoded(((Constant<Integer>) operation.getArg(1)).getConstant())));
} else if (operation.getArg(1).getType().equals(Double.class)) {
return new TermQuery(new Term(field, NumericUtils
.doubleToPrefixCoded(((Constant<Double>) operation.getArg(1)).getConstant())));
if (Number.class.isAssignableFrom(operation.getArg(1).getType())){
return new TermQuery(new Term(field, convertNumber(((Constant<Number>)operation.getArg(1)).getConstant())));
}else{
return eq(field, createTerms(operation.getArg(1)));
}
return eq(field, createTerms(operation.getArg(1)));
}
private String convertNumber(Number number){
if (Integer.class.isInstance(number)){
return NumericUtils.intToPrefixCoded(number.intValue());
}else if (Double.class.isInstance(number)){
return NumericUtils.doubleToPrefixCoded(number.doubleValue());
}else if (Long.class.isInstance(number)){
return NumericUtils.longToPrefixCoded(number.longValue());
}else if (Float.class.isInstance(number)){
return NumericUtils.floatToPrefixCoded(number.floatValue());
}else if (Byte.class.isInstance(number)){
return NumericUtils.intToPrefixCoded(number.intValue());
}else if (Short.class.isInstance(number)){
return NumericUtils.intToPrefixCoded(number.intValue());
}else if (BigDecimal.class.isInstance(number)){
return NumericUtils.doubleToPrefixCoded(number.doubleValue());
}else if (BigInteger.class.isInstance(number)){
return NumericUtils.longToPrefixCoded(number.longValue());
}else{
throw new IllegalArgumentException("Unsupported numeric type " + number.getClass().getName());
}
}
private Query eq(String field, String[] terms) {
@ -228,37 +257,45 @@ public class LuceneSerializer {
return range(toField(operation.getArg(0)), operation.getArg(1), null, true, true);
}
// TODO Simplify(?)
// TODO Timo: Check if the the annotation is necessary, thanks! -vema
@SuppressWarnings("unchecked")
private Query range(String field, Expr<?> min, Expr<?> max, boolean minInc, boolean maxInc) {
if (min != null && min.getType().equals(Integer.class) || max != null && max.getType().equals(Integer.class)) {
return integerRange(field, min == null ? null : ((Constant<Integer>) min).getConstant(), max == null ? null : ((Constant<Integer>) max).getConstant(), minInc, maxInc);
} else if (min != null && min.getType().equals(Double.class) || max != null && max.getType().equals(Double.class)) {
return doubleRange(field, min == null ? null : ((Constant<Double>) min).getConstant(), max == null ? null : ((Constant<Double>) max).getConstant(), minInc, maxInc);
if (min != null && Number.class.isAssignableFrom(min.getType()) || max != null && Number.class.isAssignableFrom(max.getType())) {
Class<? extends Number> numType = (Class)(min != null ? min.getType() : max.getType());
return numericRange((Class)numType, field,
(Number)(min == null ? null : ((Constant) min).getConstant()),
(Number)(max == null ? null : ((Constant) max).getConstant()),
minInc, maxInc);
} else {
return stringRange(field, min, max, minInc, maxInc);
}
}
private Query integerRange(String field, Integer min, Integer max, boolean minInc, boolean maxInc) {
return NumericRangeQuery.newIntRange(field, min, max, minInc, maxInc);
}
private Query doubleRange(String field, Double min, Double max, boolean minInc, boolean maxInc) {
return NumericRangeQuery.newDoubleRange(field, min, max, minInc, maxInc);
private <N extends Number> NumericRangeQuery<?> numericRange(Class<N> clazz, String field, N min, N max, boolean minInc, boolean maxInc){
if (clazz.equals(Integer.class)){
return NumericRangeQuery.newIntRange(field, (Integer)min, (Integer)max, minInc, maxInc);
}else if (clazz.equals(Double.class)){
return NumericRangeQuery.newDoubleRange(field, (Double)min, (Double)max, minInc, minInc);
}else if (clazz.equals(Float.class)){
return NumericRangeQuery.newFloatRange(field, (Float)min, (Float)max, minInc, minInc);
}else if (clazz.equals(Long.class)){
return NumericRangeQuery.newLongRange(field, (Long)min, (Long)max, minInc, minInc);
}else if (clazz.equals(Byte.class) || clazz.equals(Short.class)){
return NumericRangeQuery.newIntRange(field,
min != null ? min.intValue() : null,
max != null ? max.intValue() : null, minInc, maxInc);
}else{
throw new IllegalArgumentException("Unsupported numeric type " + clazz.getName());
}
}
private Query stringRange(String field, Expr<?> min, Expr<?> max, boolean minInc, boolean maxInc) {
if (min == null) {
return new TermRangeQuery(field, null, normalize(createTerms(max)[0]), minInc,
maxInc);
return new TermRangeQuery(field, null, normalize(createTerms(max)[0]), minInc, maxInc);
} else if (max == null) {
return new TermRangeQuery(field, normalize(createTerms(min)[0]), null, minInc,
maxInc);
}
return new TermRangeQuery(field, normalize(createTerms(min)[0]), normalize(createTerms(max)[0]), minInc,
maxInc);
return new TermRangeQuery(field, normalize(createTerms(min)[0]), null, minInc, maxInc);
}else{
return new TermRangeQuery(field, normalize(createTerms(min)[0]), normalize(createTerms(max)[0]), minInc, maxInc);
}
}
@SuppressWarnings("unchecked")
@ -282,26 +319,31 @@ public class LuceneSerializer {
List<Expr<?>> arguments = operation.getArgs();
for (int i = 1; i < arguments.size(); ++i) {
if (!(arguments.get(i) instanceof Constant<?>)
&& !(arguments.get(i) instanceof PhraseElement)) {
throw new IllegalArgumentException(
"operation argument was not of type Constant nor PhraseElement.");
&& !(arguments.get(i) instanceof PhraseElement)
&& !(arguments.get(i) instanceof TermElement)) {
throw new IllegalArgumentException("operand was of unsupported type " + arguments.get(i).getClass().getName());
}
}
}
private String[] createTerms(Expr<?> expr) {
if (splitTerms || expr instanceof PhraseElement) {
return StringUtils.split(expr.toString());
}
return new String[] { expr.toString() };
return split(expr, expr.toString());
}
private String[] createEscapedTerms(Expr<?> expr) {
String escaped = QueryParser.escape(expr.toString());
if (splitTerms || expr instanceof PhraseElement) {
return StringUtils.split(escaped);
}
return new String[] { escaped };
return split(expr, QueryParser.escape(expr.toString()));
}
private String[] split(Expr<?> expr, String str){
if (expr instanceof PhraseElement){
return StringUtils.split(str);
}else if (expr instanceof TermElement){
return new String[] {str};
}else if (splitTerms){
return StringUtils.split(str);
}else{
return new String[] {str};
}
}
private String normalize(String s) {
@ -314,29 +356,25 @@ public class LuceneSerializer {
} else if (expr instanceof QueryElement) {
return ((QueryElement) expr).getQuery();
}
throw new IllegalArgumentException("expr was not of type Operation or QueryElement");
throw new IllegalArgumentException("expr was of unsupported type " + expr.getClass().getName());
}
// TODO Add support for sorting floats, longs etc.
public Sort toSort(List<OrderSpecifier<?>> orderBys) {
List<SortField> sortFields = new ArrayList<SortField>(orderBys.size());
for (OrderSpecifier<?> orderSpecifier : orderBys) {
if (!(orderSpecifier.getTarget() instanceof Path<?>)) {
List<SortField> sorts = new ArrayList<SortField>(orderBys.size());
for (OrderSpecifier<?> order : orderBys) {
if (!(order.getTarget() instanceof Path<?>)) {
throw new IllegalArgumentException("argument was not of type Path.");
}
if (orderSpecifier.getTarget().getType().equals(Integer.class)) {
sortFields.add(new SortField(toField((Path<?>) orderSpecifier.getTarget()),
SortField.INT, !orderSpecifier.isAscending()));
} else if (orderSpecifier.getTarget().getType().equals(Double.class)) {
sortFields.add(new SortField(toField((Path<?>) orderSpecifier.getTarget()),
SortField.DOUBLE, !orderSpecifier.isAscending()));
Class<?> type = order.getTarget().getType();
boolean reverse = !order.isAscending();
if (Number.class.isAssignableFrom(type)) {
sorts.add(new SortField(toField(order.getTarget()),sortFields.get(type),reverse));
} else {
sortFields.add(new SortField(toField((Path<?>) orderSpecifier.getTarget()),
Locale.ENGLISH, !orderSpecifier.isAscending()));
sorts.add(new SortField(toField(order.getTarget()),Locale.ENGLISH, reverse));
}
}
Sort sort = new Sort();
sort.setSort(sortFields.toArray(new SortField[sortFields.size()]));
sort.setSort(sorts.toArray(new SortField[sorts.size()]));
return sort;
}
}

View File

@ -5,6 +5,8 @@ import com.mysema.query.types.expr.EString;
import com.mysema.query.types.expr.EStringConst;
/**
* PhraseElement represents the embedded String as a phrase
*
* @author tiwe
*
*/

View File

@ -0,0 +1,43 @@
package com.mysema.query.lucene;
import com.mysema.query.types.Visitor;
import com.mysema.query.types.expr.EString;
import com.mysema.query.types.expr.EStringConst;
/**
* TermElement represents the embedded String as a term
*
* @author tiwe
*
*/
public class TermElement extends EString{
private static final long serialVersionUID = 2350215644019186076L;
private final EStringConst string;
public TermElement(String str) {
this.string = (EStringConst) EStringConst.create(str);
}
@Override
public void accept(Visitor v) {
string.accept(v);
}
@Override
public boolean equals(Object o) {
if (o == this){
return true;
}else if (o instanceof TermElement){
return ((TermElement)o).string.equals(string);
}else{
return false;
}
}
@Override
public int hashCode(){
return string.hashCode();
}
}

View File

@ -30,6 +30,7 @@ import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import com.mysema.query.BooleanBuilder;
import com.mysema.query.MatchingFilters;
import com.mysema.query.Module;
import com.mysema.query.Target;
@ -55,9 +56,18 @@ public class LuceneSerializerTest {
private PString rating;
private PNumber<Integer> year;
private PNumber<Double> gross;
private PNumber<Long> longField;
private PNumber<Short> shortField;
private PNumber<Byte> byteField;
private PNumber<Float> floatField;
private static final String YEAR_PREFIX_CODED = NumericUtils.intToPrefixCoded(1990);
private static final String GROSS_PREFIX_CODED = NumericUtils.doubleToPrefixCoded(900.00);
private static final String LONG_PREFIX_CODED = NumericUtils.longToPrefixCoded(1);
private static final String SHORT_PREFIX_CODED = NumericUtils.intToPrefixCoded(1);
private static final String BYTE_PREFIX_CODED = NumericUtils.intToPrefixCoded(1);
private static final String FLOAT_PREFIX_CODED = NumericUtils.floatToPrefixCoded((float)1.0);
private RAMDirectory idx;
private IndexWriter writer;
@ -72,6 +82,11 @@ public class LuceneSerializerTest {
doc.add(new Field("rating", new StringReader("Good")));
doc.add(new NumericField("year", Store.YES, true).setIntValue(1990));
doc.add(new NumericField("gross", Store.YES, true).setDoubleValue(900.00));
doc.add(new NumericField("longField", Store.YES, true).setLongValue(1));
doc.add(new NumericField("shortField", Store.YES, true).setIntValue(1));
doc.add(new NumericField("byteField", Store.YES, true).setIntValue(1));
doc.add(new NumericField("floatField", Store.YES, true).setFloatValue(1));
return doc;
}
@ -87,6 +102,11 @@ public class LuceneSerializerTest {
year = entityPath.getNumber("year", Integer.class);
rating = entityPath.getString("rating");
gross = entityPath.getNumber("gross", Double.class);
longField = entityPath.getNumber("longField", Long.class);
shortField = entityPath.getNumber("shortField", Short.class);
byteField = entityPath.getNumber("byteField", Byte.class);
floatField = entityPath.getNumber("floatField", Float.class);
idx = new RAMDirectory();
writer = new IndexWriter(idx, new StandardAnalyzer(Version.LUCENE_CURRENT), true, MaxFieldLength.UNLIMITED);
@ -173,6 +193,14 @@ public class LuceneSerializerTest {
public void eq_Numeric_Double() throws Exception {
testQuery(gross.eq(900.00), "gross:" + GROSS_PREFIX_CODED, 1);
}
@Test
public void eq_Numeric() throws Exception{
testQuery(longField.eq(1l), "longField:" + LONG_PREFIX_CODED, 1);
testQuery(shortField.eq((short)1), "shortField:" + SHORT_PREFIX_CODED, 1);
testQuery(byteField.eq((byte)1), "byteField:" + BYTE_PREFIX_CODED, 1);
testQuery(floatField.eq((float)1.0), "floatField:" + FLOAT_PREFIX_CODED, 1);
}
@Test
public void eq_Should_Not_Find_Results_But_Lucene_Semantics_Differs_From_Querydsls() throws Exception {
@ -314,6 +342,15 @@ public class LuceneSerializerTest {
testQuery(gross.between(10.00, 19030.00), "gross:[10.0 TO 19030.0]", 1);
}
@Test
public void between_Numeric() throws Exception{
testQuery(longField.between(0l,2l), "longField:[0 TO 2]", 1);
testQuery(shortField.between((short)0,(short)2), "shortField:[0 TO 2]", 1);
testQuery(byteField.between((byte)0,(byte)2), "byteField:[0 TO 2]", 1);
testQuery(floatField.between((float)0.0,(float)2.0), "floatField:[0.0 TO 2.0]", 1);
}
@Test
public void between_Phrase() throws Exception {
testQuery(title.between("Jurassic Park", "Kundun"), "title:[jurassic TO kundun]", 1);
@ -497,6 +534,12 @@ public class LuceneSerializerTest {
public void goe_Numeric_Double_Not_Found() throws Exception {
testQuery(gross.goe(900.10), "gross:[900.1 TO *]", 0);
}
@Test
public void booleanBuilder() throws Exception{
testQuery(new BooleanBuilder(gross.goe(900.10)), "gross:[900.1 TO *]", 0);
}
@Test
@Ignore

View File

@ -0,0 +1,31 @@
package com.mysema.query.lucene;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import org.junit.Test;
import com.mysema.query.types.path.PString;
public class TermElementTest {
@Test
public void test(){
PString title = new PString("title");
LuceneSerializer serializer = new LuceneSerializer(false,true);
assertEquals("title:\"Hello World\"", serializer.toQuery(title.eq("Hello World")).toString());
assertEquals("title:Hello World", serializer.toQuery(title.eq(new TermElement("Hello World"))).toString());
}
@Test
public void testEqualsAndHashCode(){
TermElement el1 = new TermElement("x"), el2 = new TermElement("x"), el3 = new TermElement("y");
assertEquals(el1, el2);
assertFalse(el1.equals(el3));
assertEquals(el1.hashCode(), el2.hashCode());
}
}