diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java index 933c06d..a9f1404 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Query.java @@ -18,7 +18,7 @@ import java.lang.annotation.*; /** - * Query + * AbstractQuery * * @author Rizwan Idrees * @author Mohsin Husen @@ -37,7 +37,7 @@ String value() default ""; /** - * Named Query Named looked up by repository. + * Named AbstractQuery Named looked up by repository. * * @return */ diff --git a/src/main/java/org/springframework/data/elasticsearch/core/CriteriaFilterProcessor.java b/src/main/java/org/springframework/data/elasticsearch/core/CriteriaFilterProcessor.java new file mode 100644 index 0000000..381a427 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/CriteriaFilterProcessor.java @@ -0,0 +1,148 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core; + +import org.elasticsearch.index.query.*; +import org.springframework.data.elasticsearch.core.geo.GeoBBox; +import org.springframework.data.elasticsearch.core.geo.GeoLocation; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.util.Assert; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; + +import static org.elasticsearch.index.query.FilterBuilders.*; +import static org.springframework.data.elasticsearch.core.query.Criteria.OperationKey; + +/** + * CriteriaFilterProcessor + * + * @author Franck Marchand + */ +class CriteriaFilterProcessor { + + + FilterBuilder createFilterFromCriteria(Criteria criteria) { + List fbList = new LinkedList(); + FilterBuilder filter = null; + + ListIterator chainIterator = criteria.getCriteriaChain().listIterator(); + + while (chainIterator.hasNext()) { + FilterBuilder fb = null; + Criteria chainedCriteria = chainIterator.next(); + if(chainedCriteria.isOr()){ + fb = orFilter(createFilterFragmentForCriteria(chainedCriteria).toArray(new FilterBuilder[]{ })); + fbList.add(fb); + }else if(chainedCriteria.isNegating()){ + List negationFilters = buildNegationFilter(criteria.getField().getName(), criteria.getFilterCriteriaEntries().iterator()); + + if(!negationFilters.isEmpty()) { + fbList.addAll(negationFilters); + } + }else { + fbList.addAll(createFilterFragmentForCriteria(chainedCriteria)); + } + } + + if(!fbList.isEmpty()) { + if(fbList.size() == 1) { + filter =fbList.get(0); + } else { + filter = andFilter(fbList.toArray(new FilterBuilder[]{ })); + } + } + + return filter; + } + + + private List createFilterFragmentForCriteria(Criteria chainedCriteria) { + Iterator it = chainedCriteria.getFilterCriteriaEntries().iterator(); + List filterList = new LinkedList(); + + String fieldName = chainedCriteria.getField().getName(); + Assert.notNull(fieldName,"Unknown field"); + FilterBuilder filter = null; + + while (it.hasNext()){ + Criteria.CriteriaEntry entry = it.next(); + filter = processCriteriaEntry(entry.getKey(), entry.getValue(), fieldName); + filterList.add(filter); + } + + return filterList; + } + + + private FilterBuilder processCriteriaEntry(OperationKey key, Object value, String fieldName) { + if (value == null) { + return null; + } + FilterBuilder filter = null; + + switch (key){ + case WITHIN: { + filter = geoDistanceFilter(fieldName); + + Assert.isTrue(value instanceof Object[], "Value of a geo distance filter should be an array of two values."); + Object[] valArray = (Object[]) value; + Assert.noNullElements(valArray, "Geo distance filter takes 2 not null elements array as parameter."); + Assert.isTrue(valArray.length == 2, "Geo distance filter takes a 2-elements array as parameter."); + Assert.isTrue(valArray[0] instanceof GeoLocation, "First element of a geo distance filter must be a GeoLocation"); + Assert.isTrue(valArray[1] instanceof String, "Second element of a geo distance filter must be a String"); + + GeoLocation loc = (GeoLocation)valArray[0]; + String dist = (String)valArray[1]; + + ((GeoDistanceFilterBuilder)filter).lat(loc.getLat()).lon(loc.getLon()).distance(dist); + break; + } + + case BBOX: { + filter = geoBoundingBoxFilter(fieldName); + + Assert.isTrue(value instanceof Object[], "Value of a geo distance filter should be an array of two values."); + Object[] valArray = (Object[]) value; + Assert.noNullElements(valArray, "Geo bbox filter takes a not null element array as parameter."); + Assert.isTrue(valArray.length == 1, "Geo distance filter takes a 1-elements array as parameter."); + Assert.isTrue(valArray[0] instanceof GeoBBox, "single-element of a geo bbox filter must be a GeoBBox"); + + GeoBBox geoBBox = (GeoBBox)valArray[0]; + ((GeoBoundingBoxFilterBuilder)filter).topLeft(geoBBox.getTopLeft().getLat(), geoBBox.getTopLeft().getLon()); + ((GeoBoundingBoxFilterBuilder)filter).bottomRight(geoBBox.getBottomRight().getLat(), geoBBox.getBottomRight().getLon()); + break; + } + + } + + return filter; + } + + private List buildNegationFilter(String fieldName, Iterator it){ + List notFilterList = new LinkedList(); + + while (it.hasNext()){ + Criteria.CriteriaEntry criteriaEntry = it.next(); + FilterBuilder notFilter = notFilter(processCriteriaEntry(criteriaEntry.getKey(), criteriaEntry.getValue(), fieldName)); + notFilterList.add(notFilter); + } + + return notFilterList; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java b/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java index 01866fa..818d680 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java @@ -22,6 +22,8 @@ import org.springframework.util.Assert; import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; import java.util.ListIterator; import static org.elasticsearch.index.query.QueryBuilders.*; @@ -32,31 +34,63 @@ * * @author Rizwan Idrees * @author Mohsin Husen + * @author Franck Marchand */ class CriteriaQueryProcessor { QueryBuilder createQueryFromCriteria(Criteria criteria) { - BoolQueryBuilder query = boolQuery(); + if(criteria == null) + return null; + + List shouldQueryBuilderList = new LinkedList(); + List mustNotQueryBuilderList = new LinkedList(); + List mustQueryBuilderList = new LinkedList(); + ListIterator chainIterator = criteria.getCriteriaChain().listIterator(); while (chainIterator.hasNext()) { Criteria chainedCriteria = chainIterator.next(); - if(chainedCriteria.isOr()){ - query.should(createQueryFragmentForCriteria(chainedCriteria)); - }else if(chainedCriteria.isNegating()){ - query.mustNot(createQueryFragmentForCriteria(chainedCriteria)); - }else{ - query.must(createQueryFragmentForCriteria(chainedCriteria)); + QueryBuilder queryFragmentForCriteria = createQueryFragmentForCriteria(chainedCriteria); + + if(queryFragmentForCriteria!=null) { + if(chainedCriteria.isOr()){ + shouldQueryBuilderList.add(queryFragmentForCriteria); + }else if(chainedCriteria.isNegating()){ + mustNotQueryBuilderList.add(queryFragmentForCriteria); + }else{ + mustQueryBuilderList.add(queryFragmentForCriteria); + } + } + } + + BoolQueryBuilder query = null; + + if(!shouldQueryBuilderList.isEmpty() || !mustNotQueryBuilderList.isEmpty() || !mustQueryBuilderList.isEmpty()) { + + query = boolQuery(); + + for(QueryBuilder qb : shouldQueryBuilderList) { + query.should(qb); + } + for(QueryBuilder qb : mustNotQueryBuilderList) { + query.mustNot(qb); + } + for(QueryBuilder qb : mustQueryBuilderList) { + query.must(qb); } } + return query; } private QueryBuilder createQueryFragmentForCriteria(Criteria chainedCriteria) { - Iterator it = chainedCriteria.getCriteriaEntries().iterator(); - boolean singeEntryCriteria = (chainedCriteria.getCriteriaEntries().size() == 1); + if(chainedCriteria.getQueryCriteriaEntries().isEmpty()) + return null; + + Iterator it = chainedCriteria.getQueryCriteriaEntries().iterator(); + boolean singeEntryCriteria = (chainedCriteria.getQueryCriteriaEntries().size() == 1); String fieldName = chainedCriteria.getField().getName(); Assert.notNull(fieldName,"Unknown field"); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java index 480f3d5..68605f0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java @@ -89,7 +89,6 @@ public interface ElasticsearchOperations { */ Page queryForPage(SearchQuery query, Class clazz); - /** * Execute the query against elasticsearch and return result as {@link Page} * diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java index 0b08fe2..ace7ede 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java @@ -36,6 +36,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.sort.SortBuilder; import org.elasticsearch.search.sort.SortOrder; @@ -177,9 +178,20 @@ public List queryForIds(SearchQuery query) { @Override public Page queryForPage(CriteriaQuery criteriaQuery, Class clazz) { - QueryBuilder query = new CriteriaQueryProcessor().createQueryFromCriteria(criteriaQuery.getCriteria()); - SearchResponse response = prepareSearch(criteriaQuery,clazz) - .setQuery(query) + QueryBuilder elasticsearchQuery = new CriteriaQueryProcessor().createQueryFromCriteria(criteriaQuery.getCriteria()); + FilterBuilder elasticsearchFilter = new CriteriaFilterProcessor().createFilterFromCriteria(criteriaQuery.getCriteria()); + SearchRequestBuilder searchRequestBuilder = prepareSearch(criteriaQuery, clazz); + + if(elasticsearchQuery!=null) { + searchRequestBuilder.setQuery(elasticsearchQuery); + } else { + searchRequestBuilder.setQuery(QueryBuilders.matchAllQuery()); + } + + if(elasticsearchFilter!=null) + searchRequestBuilder.setFilter(elasticsearchFilter); + + SearchResponse response = searchRequestBuilder .execute().actionGet(); return mapResults(response, clazz, criteriaQuery.getPageable()); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java index 8abfe9e..dd5dc50 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.core.geo.GeoLocation; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; @@ -65,6 +66,11 @@ private static void mapEntity(XContentBuilder xContentBuilder, if(isEntity(field)){ mapEntity(xContentBuilder, field.getType(), false, EMPTY, field.getName()); } + + if(field.getType() == GeoLocation.class) { + applyGeoLocationFieldMapping(xContentBuilder, field); + } + Field fieldAnnotation = field.getAnnotation(Field.class); if(isRootObject && fieldAnnotation != null && isIdField(field, idFieldName)){ applyDefaultIdFieldMapping(xContentBuilder, field); @@ -79,6 +85,12 @@ private static void mapEntity(XContentBuilder xContentBuilder, } + private static void applyGeoLocationFieldMapping(XContentBuilder xContentBuilder, java.lang.reflect.Field field) throws IOException { + xContentBuilder.startObject(field.getName()); + xContentBuilder.field("type", "geo_point") + .endObject(); + } + private static void applyDefaultIdFieldMapping(XContentBuilder xContentBuilder, java.lang.reflect.Field field) throws IOException { xContentBuilder.startObject(field.getName()) .field("type", "string") diff --git a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoBBox.java b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoBBox.java new file mode 100644 index 0000000..817398a --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoBBox.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core.geo; + +import java.util.List; + +/** + * Geo bbox used for #{@link org.springframework.data.elasticsearch.core.query.Criteria}. + * + * @author Franck Marchand + */ +public class GeoBBox { + private GeoLocation topLeft; + private GeoLocation bottomRight; + + public GeoBBox(GeoLocation topLeft, GeoLocation bottomRight) { + this.topLeft = topLeft; + this.bottomRight = bottomRight; + } + + public GeoLocation getTopLeft() { + return topLeft; + } + + public void setTopLeft(GeoLocation topLeft) { + this.topLeft = topLeft; + } + + public GeoLocation getBottomRight() { + return bottomRight; + } + + public void setBottomRight(GeoLocation bottomRight) { + this.bottomRight = bottomRight; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoLocation.java b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoLocation.java new file mode 100644 index 0000000..33a4267 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoLocation.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core.geo; + +/** + * geo-location used for #{@link org.springframework.data.elasticsearch.core.query.Criteria}. + * + * @author Franck Marchand + */ +public class GeoLocation { + private double lat; + private double lon; + + public GeoLocation lat(double lat) { + setLat(lat); + return this; + } + + public GeoLocation lon(double lon) { + setLon(lon); + return this; + } + + public GeoLocation() { + } + + public GeoLocation(double latitude, double longitude) { + this.lat = latitude; + this.lon = longitude; + } + + public double getLat() { + return lat; + } + + public void setLat(double lat) { + this.lat = lat; + } + + public double getLon() { + return lon; + } + + public void setLon(double lon) { + this.lon = lon; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoPolygon.java b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoPolygon.java new file mode 100644 index 0000000..46ae5d8 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/geo/GeoPolygon.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core.geo; + +import java.util.List; + +/** + * Geo polygon used for #{@link org.springframework.data.elasticsearch.core.query.Criteria}. + * + * @author Franck Marchand + */ +public class GeoPolygon { + private List points; + + public GeoPolygon(List points) { + this.points = points; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java index 280654b..888b3cf 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java @@ -1,102 +1,102 @@ -/* - * Copyright 2013 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.elasticsearch.core.query; - -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.util.Assert; - -import java.util.ArrayList; -import java.util.List; - -import static org.apache.commons.collections.CollectionUtils.addAll; - -/** - * AbstractQuery - * - * @author Rizwan Idrees - * @author Mohsin Husen - */ -abstract class AbstractQuery implements Query{ - - protected Pageable pageable = DEFAULT_PAGE; - protected Sort sort; - protected List indices = new ArrayList(); - protected List types = new ArrayList(); - protected List fields = new ArrayList(); - - @Override - public Sort getSort() { - return this.sort; - } - - @Override - public Pageable getPageable() { - return this.pageable; - } - - @Override - public final T setPageable(Pageable pageable) { - Assert.notNull(pageable); - this.pageable = pageable; - return (T) this.addSort(pageable.getSort()); - } - - @Override - public void addFields(String...fields) { - addAll(this.fields, fields); - } - - @Override - public List getFields() { - return fields; - } - - @Override - public List getIndices() { - return indices; - } - - @Override - public void addIndices(String... indices) { - addAll(this.indices,indices); - } - - @Override - public void addTypes(String... types) { - addAll(this.types,types); - } - - @Override - public List getTypes() { - return types; - } - - @SuppressWarnings("unchecked") - public final T addSort(Sort sort) { - if (sort == null) { - return (T) this; - } - - if (this.sort == null) { - this.sort = sort; - } else { - this.sort = this.sort.and(sort); - } - - return (T) this; - } -} +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core.query; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.List; + +import static org.apache.commons.collections.CollectionUtils.addAll; + +/** + * AbstractQuery + * + * @author Rizwan Idrees + * @author Mohsin Husen + */ +abstract class AbstractQuery implements Query{ + + protected Pageable pageable = DEFAULT_PAGE; + protected Sort sort; + protected List indices = new ArrayList(); + protected List types = new ArrayList(); + protected List fields = new ArrayList(); + + @Override + public Sort getSort() { + return this.sort; + } + + @Override + public Pageable getPageable() { + return this.pageable; + } + + @Override + public final T setPageable(Pageable pageable) { + Assert.notNull(pageable); + this.pageable = pageable; + return (T) this.addSort(pageable.getSort()); + } + + @Override + public void addFields(String...fields) { + addAll(this.fields, fields); + } + + @Override + public List getFields() { + return fields; + } + + @Override + public List getIndices() { + return indices; + } + + @Override + public void addIndices(String... indices) { + addAll(this.indices,indices); + } + + @Override + public void addTypes(String... types) { + addAll(this.types,types); + } + + @Override + public List getTypes() { + return types; + } + + @SuppressWarnings("unchecked") + public final T addSort(Sort sort) { + if (sort == null) { + return (T) this; + } + + if (this.sort == null) { + this.sort = sort; + } else { + this.sort = this.sort.and(sort); + } + + return (T) this; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java index a41e2a4..7b2fcd8 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java @@ -25,6 +25,8 @@ import org.apache.commons.lang.StringUtils; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.elasticsearch.core.geo.GeoBBox; +import org.springframework.data.elasticsearch.core.geo.GeoLocation; import org.springframework.util.Assert; /** @@ -33,6 +35,7 @@ * * @author Rizwan Idrees * @author Mohsin Husen + * @author Franck Marchand */ public class Criteria { @@ -48,7 +51,9 @@ public class Criteria { private List criteriaChain = new ArrayList(1); - private Set criteria = new LinkedHashSet(); + private Set queryCriteria = new LinkedHashSet(); + + private Set filterCriteria = new LinkedHashSet(); public Criteria() { } @@ -89,6 +94,12 @@ protected Criteria(List criteriaChain, Field field) { this.field = field; } +// public void splitCriteria(List queries, List filters) { +// for(Criteria c : this.criteriaChain) { +// switch () +// } +// } + /** * Static factory method to create a new Criteria for field with given name * @@ -171,7 +182,7 @@ public Criteria or(Criteria criteria) { Assert.notNull(criteria, "Cannot chain 'null' criteria."); Criteria orConnectedCritiera = new OrCriteria(this.criteriaChain, criteria.getField()); - orConnectedCritiera.criteria.addAll(criteria.criteria); + orConnectedCritiera.queryCriteria.addAll(criteria.queryCriteria); return orConnectedCritiera; } @@ -192,7 +203,7 @@ public Criteria or(String fieldName) { * @return */ public Criteria is(Object o) { - criteria.add(new CriteriaEntry(OperationKey.EQUALS, o)); + queryCriteria.add(new CriteriaEntry(OperationKey.EQUALS, o)); return this; } @@ -205,7 +216,7 @@ public Criteria is(Object o) { */ public Criteria contains(String s) { assertNoBlankInWildcardedQuery(s, true, true); - criteria.add(new CriteriaEntry(OperationKey.CONTAINS, s)); + queryCriteria.add(new CriteriaEntry(OperationKey.CONTAINS, s)); return this; } @@ -217,7 +228,7 @@ public Criteria contains(String s) { */ public Criteria startsWith(String s) { assertNoBlankInWildcardedQuery(s, true, false); - criteria.add(new CriteriaEntry(OperationKey.STARTS_WITH, s)); + queryCriteria.add(new CriteriaEntry(OperationKey.STARTS_WITH, s)); return this; } @@ -230,7 +241,7 @@ public Criteria startsWith(String s) { */ public Criteria endsWith(String s) { assertNoBlankInWildcardedQuery(s, false, true); - criteria.add(new CriteriaEntry(OperationKey.ENDS_WITH, s)); + queryCriteria.add(new CriteriaEntry(OperationKey.ENDS_WITH, s)); return this; } @@ -251,7 +262,7 @@ public Criteria not() { * @return */ public Criteria fuzzy(String s) { - criteria.add(new CriteriaEntry(OperationKey.FUZZY, s)); + queryCriteria.add(new CriteriaEntry(OperationKey.FUZZY, s)); return this; } @@ -263,7 +274,7 @@ public Criteria fuzzy(String s) { * @return */ public Criteria expression(String s) { - criteria.add(new CriteriaEntry(OperationKey.EXPRESSION, s)); + queryCriteria.add(new CriteriaEntry(OperationKey.EXPRESSION, s)); return this; } @@ -293,7 +304,7 @@ public Criteria between(Object lowerBound, Object upperBound) { throw new InvalidDataAccessApiUsageException("Range [* TO *] is not allowed"); } - criteria.add(new CriteriaEntry(OperationKey.BETWEEN, new Object[] { lowerBound, upperBound })); + queryCriteria.add(new CriteriaEntry(OperationKey.BETWEEN, new Object[]{lowerBound, upperBound})); return this; } @@ -335,16 +346,44 @@ public Criteria in(Object... values) { } /** - * Crates new CriteriaEntry for multiple values {@code (arg0 arg1 arg2 ...)} + * Creates new CriteriaEntry for multiple values {@code (arg0 arg1 arg2 ...)} * * @param values the collection containing the values to match against * @return */ public Criteria in(Iterable values) { Assert.notNull(values, "Collection of 'in' values must not be null"); - criteria.add(new CriteriaEntry(OperationKey.IN, values)); - return this; - } + queryCriteria.add(new CriteriaEntry(OperationKey.IN, values)); + return this; + } + + /** + * Creates new CriteriaEntry for {@code location WITHIN distance} + * @param location {@link GeoLocation} center coordinates + * @param distance {@link String} radius as a string (e.g. : '100km'). + * Distance unit : + * either mi/miles or km can be set + * + * @return Criteria the chaind criteria with the new 'within' criteria included. + */ + public Criteria within(GeoLocation location, String distance) { + Assert.notNull(location, "Location value for near criteria must not be null"); + Assert.notNull(location, "Distance value for near criteria must not be null"); + filterCriteria.add(new CriteriaEntry(OperationKey.WITHIN, new Object[]{location, distance})); + return this; + } + + /** + * Creates new CriteriaEntry for {@code location BBOX bounding box} + * @param bbox {@link org.springframework.data.elasticsearch.core.geo.GeoBBox} center coordinates + * + * @return Criteria the chaind criteria with the new 'bbox' criteria included. + */ + public Criteria bbox(GeoBBox bbox) { + Assert.notNull(bbox, "bbox value for bbox criteria must not be null"); + filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, new Object[]{bbox})); + return this; + } private void assertNoBlankInWildcardedQuery(String searchString, boolean leadingWildcard, boolean trailingWildcard) { @@ -363,10 +402,18 @@ public Field getField() { return this.field; } - public Set getCriteriaEntries() { - return Collections.unmodifiableSet(this.criteria); + public Set getQueryCriteriaEntries() { + return Collections.unmodifiableSet(this.queryCriteria); } + public Set getFilterCriteriaEntries() { + return Collections.unmodifiableSet(this.filterCriteria); + } + + public Set getFilterCriteria() { + return filterCriteria; + } + /** * Conjunction to be used with this criteria (AND | OR) * @@ -426,7 +473,7 @@ public String getConjunctionOperator() { } public enum OperationKey { - EQUALS, CONTAINS, STARTS_WITH, ENDS_WITH, EXPRESSION, BETWEEN, FUZZY, IN; + EQUALS, CONTAINS, STARTS_WITH, ENDS_WITH, EXPRESSION, BETWEEN, FUZZY, IN, WITHIN, BBOX, NEAR; } public static class CriteriaEntry { diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java index bc70672..52833ba 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/CriteriaQuery.java @@ -16,9 +16,15 @@ package org.springframework.data.elasticsearch.core.query; +import org.elasticsearch.index.query.FilterBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.sort.SortBuilder; import org.springframework.data.domain.Pageable; import org.springframework.util.Assert; +import java.util.LinkedList; +import java.util.List; + /** * CriteriaQuery * @@ -28,7 +34,8 @@ public class CriteriaQuery extends AbstractQuery{ private Criteria criteria; - private CriteriaQuery() { + + protected CriteriaQuery() { } public CriteriaQuery(Criteria criteria) { @@ -43,7 +50,7 @@ public CriteriaQuery(Criteria criteria, Pageable pageable) { } } - public static final Query fromQuery(CriteriaQuery source) { + public static final CriteriaQuery fromQuery(CriteriaQuery source) { return fromQuery(source, new CriteriaQuery()); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/MoreLikeThisQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/MoreLikeThisQuery.java index 3133a3a..724e7c9 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/MoreLikeThisQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/MoreLikeThisQuery.java @@ -22,7 +22,7 @@ import java.util.List; import static org.apache.commons.collections.CollectionUtils.addAll; -import static org.springframework.data.elasticsearch.core.query.Query.DEFAULT_PAGE; +import static org.springframework.data.elasticsearch.core.query.AbstractQuery.DEFAULT_PAGE; /** * MoreLikeThisQuery diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/SearchQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SearchQuery.java index 55424c4..7411ea0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/SearchQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SearchQuery.java @@ -18,6 +18,7 @@ import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.sort.SortBuilder; + /** * NativeSearchQuery * diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java index 7334395..be7aec0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/StringQuery.java @@ -25,7 +25,7 @@ * @author Rizwan Idrees * @author Mohsin Husen */ -public class StringQuery extends AbstractQuery{ +public class StringQuery extends AbstractQuery { private String source; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java index 1d833bd..5b48ebf 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchStringQuery.java @@ -54,7 +54,7 @@ public class ElasticsearchStringQuery extends AbstractElasticsearchRepositoryQue public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations, String query) { super(queryMethod, elasticsearchOperations); - Assert.notNull(query, "Query cannot be empty"); + Assert.notNull(query, "AbstractQuery cannot be empty"); this.query = query; } diff --git a/src/test/java/org/springframework/data/elasticsearch/GeoAuthor.java b/src/test/java/org/springframework/data/elasticsearch/GeoAuthor.java new file mode 100644 index 0000000..671e7d1 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/GeoAuthor.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch; + +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.core.geo.GeoLocation; + +/** + * @author Franck Marchand + */ +@Document(indexName = "test-geo-index", type = "test-geo-type") +public class GeoAuthor { + + @Id + private String id; + private String name; + + private GeoLocation location; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public GeoLocation getLocation() { + return location; + } + + public void setLocation(GeoLocation location) { + this.location = location; + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java index 4b22025..a3a08c6 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java @@ -29,8 +29,11 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.GeoAuthor; import org.springframework.data.elasticsearch.SampleEntity; import org.springframework.data.elasticsearch.SampleMappingEntity; +import org.springframework.data.elasticsearch.core.geo.GeoBBox; +import org.springframework.data.elasticsearch.core.geo.GeoLocation; import org.springframework.data.elasticsearch.core.query.*; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -43,11 +46,15 @@ import static org.elasticsearch.index.query.FilterBuilders.termFilter; import static org.elasticsearch.index.query.QueryBuilders.fieldQuery; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + /** * @author Rizwan Idrees * @author Mohsin Husen + * @author Franck Marchand */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:elasticsearch-template-test.xml") @@ -63,6 +70,14 @@ public void before(){ deleteQuery.setQuery(matchAllQuery()); elasticsearchTemplate.delete(deleteQuery,SampleEntity.class); elasticsearchTemplate.refresh(SampleEntity.class, true); + + elasticsearchTemplate.createIndex(GeoAuthor.class); + DeleteQuery deleteGeoAuthorQuery = new DeleteQuery(); + deleteGeoAuthorQuery.setQuery(matchAllQuery()); + elasticsearchTemplate.delete(deleteGeoAuthorQuery,GeoAuthor.class); + elasticsearchTemplate.refresh(GeoAuthor.class, true); + + elasticsearchTemplate.putMapping(GeoAuthor.class); } @Test @@ -450,6 +465,32 @@ public void shouldExecuteGivenCriteriaQuery(){ assertThat(sampleEntity1, is(notNullValue())); } +// @Test +// public void shouldExecuteGivenCriteriaQuery(){ +// //given +// String documentId = randomNumeric(5); +// SampleEntity sampleEntity = new SampleEntity(); +// sampleEntity.setId(documentId); +// sampleEntity.setMessage("some test message"); +// sampleEntity.setVersion(System.currentTimeMillis()); +// +// IndexQuery indexQuery = new IndexQuery(); +// indexQuery.setId(documentId); +// indexQuery.setObject(sampleEntity); +// +// elasticsearchTemplate.index(indexQuery); +// elasticsearchTemplate.refresh(SampleEntity.class, true); +// +// Criteria crit = new Criteria("message").contains("test"); +// +// CriteriaQuery query = new CriteriaQuery(crit); +// +// //when +// SampleEntity sampleEntity1 = elasticsearchTemplate.queryForObject(query,SampleEntity.class); +// //then +// assertThat(sampleEntity1, is(notNullValue())); +// } + @Test public void shouldReturnSpecifiedFields(){ //given @@ -566,7 +607,7 @@ public void shouldReturnResultsWithScanAndScroll(){ indexQueries.add(indexQuery2); //when elasticsearchTemplate.bulkIndex(indexQueries); - elasticsearchTemplate.refresh(SampleEntity.class,true); + elasticsearchTemplate.refresh(SampleEntity.class, true); //then SearchQuery searchQuery = new NativeSearchQueryBuilder() @@ -734,5 +775,85 @@ public void shouldDeleteIndexForGivenEntity(){ } + @Test + public void shouldPutMappingForGivenEntityWithGeoLocation()throws Exception{ + //given + Class entity = GeoAuthor.class; + elasticsearchTemplate.createIndex(entity); + //when + assertThat(elasticsearchTemplate.putMapping(entity) , is(true)) ; + } + + @Test + public void shouldReturnListForGivenCriteriaWithGeoLocation(){ + //given + List indexQueries = new ArrayList(); + //first document + String documentId = randomNumeric(5); + GeoAuthor geoAuthor1 = new GeoAuthor(); + geoAuthor1.setId(documentId); + geoAuthor1.setName("Franck Marchand"); + geoAuthor1.setLocation(new GeoLocation(45.7806d, 3.0875d)); // Clermont-Ferrand + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId); + indexQuery1.setObject(geoAuthor1); + indexQueries.add(indexQuery1); + + //second document + String documentId2 = randomNumeric(5); + GeoAuthor geoAuthor2 = new GeoAuthor(); + geoAuthor2.setId(documentId2); + geoAuthor2.setName("Mohsin Husen"); + geoAuthor2.setLocation(new GeoLocation(51.5171d, 0.1062d)); // London + + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(geoAuthor2); + + indexQueries.add(indexQuery2); + + //third document + String documentId3 = randomNumeric(5); + GeoAuthor geoAuthor3 = new GeoAuthor(); + geoAuthor3.setId(documentId3); + geoAuthor3.setName("Rizwan Idrees"); + geoAuthor3.setLocation(new GeoLocation(51.5171d, 0.1062d)); // London + + IndexQuery indexQuery3 = new IndexQuery(); + indexQuery3.setId(documentId3); + indexQuery3.setObject(geoAuthor3); + + indexQueries.add(indexQuery3); + //when + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(GeoAuthor.class, true); + //when + CriteriaQuery geoLocationCriteriaQuery = new CriteriaQuery( + new Criteria("location").within(new GeoLocation(45.7806d, 3.0875d), "20km")); + + List geoAuthorsForGeoCriteria = elasticsearchTemplate.queryForList(geoLocationCriteriaQuery,GeoAuthor.class); + //then + assertThat(geoAuthorsForGeoCriteria.size(),is(1)); + assertEquals("Franck Marchand", geoAuthorsForGeoCriteria.get(0).getName()); + + // query/filter geo distance mixed query + CriteriaQuery geoLocationCriteriaQuery2 = new CriteriaQuery( + new Criteria("name").is("Mohsin Husen").and("location").within(new GeoLocation(51.5171d, 0.1062d), "20km")); + List geoAuthorsForGeoCriteria2 = elasticsearchTemplate.queryForList(geoLocationCriteriaQuery2,GeoAuthor.class); + + assertThat(geoAuthorsForGeoCriteria2.size(),is(1)); + assertEquals("Mohsin Husen", geoAuthorsForGeoCriteria2.get(0).getName()); + + // bbox query + CriteriaQuery geoLocationCriteriaQuery3 = new CriteriaQuery( + new Criteria("location").bbox( + new GeoBBox(new GeoLocation(53.5171d, 0), + new GeoLocation(49.5171d, 0.2062d)))); + List geoAuthorsForGeoCriteria3 = elasticsearchTemplate.queryForList(geoLocationCriteriaQuery3,GeoAuthor.class); + + assertThat(geoAuthorsForGeoCriteria3.size(),is(2)); + assertThat(geoAuthorsForGeoCriteria3, containsInAnyOrder(hasProperty("name", equalTo("Mohsin Husen")), hasProperty("name",equalTo("Rizwan Idrees")))); + } }