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 000000000..381a4273b --- /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 0da61b4d0..d33259183 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.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.QueryBuilders.*; @@ -32,106 +34,141 @@ import static org.springframework.data.elasticsearch.core.query.Criteria.Operati * * @author Rizwan Idrees * @author Mohsin Husen + * @author Franck Marchand */ class CriteriaQueryProcessor { - QueryBuilder createQueryFromCriteria(Criteria criteria) { - BoolQueryBuilder query = boolQuery(); - 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)); - } - } - return query; - } + QueryBuilder createQueryFromCriteria(Criteria criteria) { + if(criteria == null) + return null; - private QueryBuilder createQueryFragmentForCriteria(Criteria chainedCriteria) { - Iterator it = chainedCriteria.getCriteriaEntries().iterator(); - boolean singeEntryCriteria = (chainedCriteria.getCriteriaEntries().size() == 1); + List shouldQueryBuilderList = new LinkedList(); + List mustNotQueryBuilderList = new LinkedList(); + List mustQueryBuilderList = new LinkedList(); - String fieldName = chainedCriteria.getField().getName(); - Assert.notNull(fieldName, "Unknown field"); - QueryBuilder query = null; - if (singeEntryCriteria) { - Criteria.CriteriaEntry entry = it.next(); - query = processCriteriaEntry(entry.getKey(), entry.getValue(), fieldName); - } else { - query = boolQuery(); - while (it.hasNext()) { - Criteria.CriteriaEntry entry = it.next(); - ((BoolQueryBuilder) query).must(processCriteriaEntry(entry.getKey(), entry.getValue(), fieldName)); - } - } + ListIterator chainIterator = criteria.getCriteriaChain().listIterator(); + while (chainIterator.hasNext()) { + Criteria chainedCriteria = chainIterator.next(); + QueryBuilder queryFragmentForCriteria = createQueryFragmentForCriteria(chainedCriteria); - addBoost(query, chainedCriteria.getBoost()); - return query; - } + if(queryFragmentForCriteria!=null) { + if(chainedCriteria.isOr()){ + shouldQueryBuilderList.add(queryFragmentForCriteria); + }else if(chainedCriteria.isNegating()){ + mustNotQueryBuilderList.add(queryFragmentForCriteria); + }else{ + mustQueryBuilderList.add(queryFragmentForCriteria); + } + } + } - private QueryBuilder processCriteriaEntry(OperationKey key, Object value, String fieldName) { - if (value == null) { - return null; - } - QueryBuilder query = null; + BoolQueryBuilder query = null; - switch (key) { - case EQUALS: - query = fieldQuery(fieldName, value); - break; - case CONTAINS: - query = fieldQuery(fieldName, "*" + value + "*").analyzeWildcard(true); - break; - case STARTS_WITH: - query = fieldQuery(fieldName, value + "*").analyzeWildcard(true); - break; - case ENDS_WITH: - query = fieldQuery(fieldName, "*" + value).analyzeWildcard(true); - break; - case EXPRESSION: - query = queryString((String) value).field(fieldName); - break; - case BETWEEN: - Object[] ranges = (Object[]) value; - query = rangeQuery(fieldName).from(ranges[0]).to(ranges[1]); - break; - case FUZZY: - query = fuzzyQuery(fieldName, (String) value); - break; - case IN: - query = boolQuery(); - Iterable collection = (Iterable) value; - for (Object item : collection) { - ((BoolQueryBuilder) query).should(fieldQuery(fieldName, item)); - } - break; - } + if(!shouldQueryBuilderList.isEmpty() || !mustNotQueryBuilderList.isEmpty() || !mustQueryBuilderList.isEmpty()) { - return query; - } + query = boolQuery(); - private QueryBuilder buildNegationQuery(String fieldName, Iterator it) { - BoolQueryBuilder notQuery = boolQuery(); - while (it.hasNext()) { - notQuery.mustNot(fieldQuery(fieldName, it.next().getValue())); - } - return notQuery; - } + for(QueryBuilder qb : shouldQueryBuilderList) { + query.should(qb); + } + for(QueryBuilder qb : mustNotQueryBuilderList) { + query.mustNot(qb); + } + for(QueryBuilder qb : mustQueryBuilderList) { + query.must(qb); + } + } - private void addBoost(QueryBuilder query, float boost) { - if (Float.isNaN(boost)) { - return; - } - if (query instanceof BoostableQueryBuilder) { - ((BoostableQueryBuilder) query).boost(boost); - } + return query; + } - } + + private QueryBuilder createQueryFragmentForCriteria(Criteria chainedCriteria) { + 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"); + QueryBuilder query = null; + + if(singeEntryCriteria){ + Criteria.CriteriaEntry entry = it.next(); + query = processCriteriaEntry(entry.getKey(), entry.getValue(), fieldName); + }else{ + query = boolQuery(); + while (it.hasNext()){ + Criteria.CriteriaEntry entry = it.next(); + ((BoolQueryBuilder)query).must(processCriteriaEntry(entry.getKey(), entry.getValue(), fieldName)); + } + } + + addBoost(query, chainedCriteria.getBoost()); + return query; + } + + + private QueryBuilder processCriteriaEntry(OperationKey key, Object value, String fieldName) { + if (value == null) { + return null; + } + QueryBuilder query = null; + + switch (key) { + case EQUALS: + query = fieldQuery(fieldName, value); + break; + case CONTAINS: + query = fieldQuery(fieldName, "*" + value + "*").analyzeWildcard(true); + break; + case STARTS_WITH: + query = fieldQuery(fieldName, value + "*").analyzeWildcard(true); + break; + case ENDS_WITH: + query = fieldQuery(fieldName, "*" + value).analyzeWildcard(true); + break; + case EXPRESSION: + query = queryString((String) value).field(fieldName); + break; + case BETWEEN: + Object[] ranges = (Object[]) value; + query = rangeQuery(fieldName).from(ranges[0]).to(ranges[1]); + break; + case FUZZY: + query = fuzzyQuery(fieldName, (String) value); + break; + case IN: + query = boolQuery(); + Iterable collection = (Iterable) value; + for (Object item : collection) { + ((BoolQueryBuilder) query).should(fieldQuery(fieldName, item)); + } + break; + } + + return query; + } + + private QueryBuilder buildNegationQuery(String fieldName, Iterator it) { + BoolQueryBuilder notQuery = boolQuery(); + while (it.hasNext()) { + notQuery.mustNot(fieldQuery(fieldName, it.next().getValue())); + } + return notQuery; + } + + private void addBoost(QueryBuilder query, float boost) { + if (Float.isNaN(boost)) { + return; + } + if (query instanceof BoostableQueryBuilder) { + ((BoostableQueryBuilder) query).boost(boost); + } + + } } 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 39e11a106..0f2453b55 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java @@ -34,7 +34,9 @@ import org.elasticsearch.client.Requests; import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.unit.TimeValue; 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.facet.Facet; import org.elasticsearch.search.facet.FacetBuilder; @@ -180,8 +182,21 @@ public class ElasticsearchTemplate implements ElasticsearchOperations { @Override public Page queryForPage(CriteriaQuery criteriaQuery, Class clazz) { - QueryBuilder query = new CriteriaQueryProcessor().createQueryFromCriteria(criteriaQuery.getCriteria()); - SearchResponse response = prepareSearch(criteriaQuery, clazz).setQuery(query).execute().actionGet(); + 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 ee43c9b99..7cb1f6fc5 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 @@ package org.springframework.data.elasticsearch.core; import org.elasticsearch.common.xcontent.XContentBuilder; import org.springframework.data.elasticsearch.annotations.*; import org.springframework.data.elasticsearch.core.facet.FacetRequest; +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; @@ -71,6 +72,11 @@ class MappingBuilder { if (isEntity(field)) { mapEntity(xContentBuilder, field.getType(), false, EMPTY, field.getName()); } + + if(field.getType() == GeoLocation.class) { + applyGeoLocationFieldMapping(xContentBuilder, field); + } + Field singleField = field.getAnnotation(Field.class); MultiField multiField = field.getAnnotation(MultiField.class); if (isRootObject && singleField != null && isIdField(field, idFieldName)) { @@ -88,6 +94,12 @@ class MappingBuilder { } + 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()) 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 000000000..817398aba --- /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 000000000..33a4267f8 --- /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 000000000..46ae5d862 --- /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 f0abaa8f2..2bb79698f 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 f375eb2d8..7acc77b4e 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 java.util.Set; 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 @@ import org.springframework.util.Assert; * * @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() { } @@ -171,7 +176,7 @@ public class 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 +197,7 @@ public class Criteria { * @return */ public Criteria is(Object o) { - criteria.add(new CriteriaEntry(OperationKey.EQUALS, o)); + queryCriteria.add(new CriteriaEntry(OperationKey.EQUALS, o)); return this; } @@ -205,7 +210,7 @@ public class Criteria { */ 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 +222,7 @@ public class Criteria { */ 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 +235,7 @@ public class Criteria { */ 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,8 +256,8 @@ public class Criteria { * @return */ public Criteria fuzzy(String s) { - criteria.add(new CriteriaEntry(OperationKey.FUZZY, s)); - return this; + queryCriteria.add(new CriteriaEntry(OperationKey.FUZZY, s)); + return this; } /** @@ -262,7 +267,7 @@ public class Criteria { * @return */ public Criteria expression(String s) { - criteria.add(new CriteriaEntry(OperationKey.EXPRESSION, s)); + queryCriteria.add(new CriteriaEntry(OperationKey.EXPRESSION, s)); return this; } @@ -292,7 +297,7 @@ public class Criteria { 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; } @@ -341,9 +346,37 @@ public class Criteria { */ 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) { if (StringUtils.contains(searchString, CRITERIA_VALUE_SEPERATOR)) { @@ -361,10 +394,18 @@ public class Criteria { 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) * @@ -424,7 +465,7 @@ public class Criteria { } 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 734e710d2..8dfa99de8 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 @@ -34,48 +34,48 @@ public class CriteriaQuery extends AbstractQuery { public CriteriaQuery(Criteria criteria) { this(criteria, null); } + + public CriteriaQuery(Criteria criteria, Pageable pageable) { + this.criteria = criteria; + this.pageable = pageable; + if (pageable != null) { + this.addSort(pageable.getSort()); + } + } - public CriteriaQuery(Criteria criteria, Pageable pageable) { - this.criteria = criteria; - this.pageable = pageable; - if (pageable != null) { - this.addSort(pageable.getSort()); - } - } + public static final Query fromQuery(CriteriaQuery source) { + return fromQuery(source, new CriteriaQuery()); + } - public static final Query fromQuery(CriteriaQuery source) { - return fromQuery(source, new CriteriaQuery()); - } + public static T fromQuery(CriteriaQuery source, T destination) { + if (source == null || destination == null) { + return null; + } - public static T fromQuery(CriteriaQuery source, T destination) { - if (source == null || destination == null) { - return null; - } + if (source.getCriteria() != null) { + destination.addCriteria(source.getCriteria()); + } - if (source.getCriteria() != null) { - destination.addCriteria(source.getCriteria()); - } + if (source.getSort() != null) { + destination.addSort(source.getSort()); + } - if (source.getSort() != null) { - destination.addSort(source.getSort()); - } + return destination; + } - return destination; - } + @SuppressWarnings("unchecked") + public final T addCriteria(Criteria criteria) { + Assert.notNull(criteria, "Cannot add null criteria."); + if (this.criteria == null) { + this.criteria = criteria; + } else { + this.criteria.and(criteria); + } + return (T) this; + } - @SuppressWarnings("unchecked") - public final T addCriteria(Criteria criteria) { - Assert.notNull(criteria, "Cannot add null criteria."); - if (this.criteria == null) { - this.criteria = criteria; - } else { - this.criteria.and(criteria); - } - return (T) this; - } - - public Criteria getCriteria() { - return this.criteria; - } + public Criteria getCriteria() { + return this.criteria; + } } 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 f95099c13..c125e5e12 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 @@ -21,7 +21,7 @@ import java.util.ArrayList; 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 21a925073..e625c968a 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 @@ -17,7 +17,6 @@ package org.springframework.data.elasticsearch.core.query; import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.search.facet.FacetBuilder; import org.elasticsearch.search.sort.SortBuilder; import org.springframework.data.elasticsearch.core.facet.FacetRequest; 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 000000000..671e7d101 --- /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/ElasticsearchTemplateTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java index fde149a6e..bd20de851 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java @@ -25,11 +25,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; -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; @@ -42,12 +44,14 @@ import static org.elasticsearch.index.query.FilterBuilders.boolFilter; 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.*; /** * @author Rizwan Idrees * @author Mohsin Husen + * @author Franck Marchand */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:elasticsearch-template-test.xml") @@ -56,12 +60,18 @@ public class ElasticsearchTemplateTests { @Autowired private ElasticsearchTemplate elasticsearchTemplate; - @Before - public void before() { + @Before + public void before(){ elasticsearchTemplate.deleteIndex(SampleEntity.class); - elasticsearchTemplate.createIndex(SampleEntity.class); - elasticsearchTemplate.refresh(SampleEntity.class, true); - } + elasticsearchTemplate.createIndex(SampleEntity.class); + elasticsearchTemplate.refresh(SampleEntity.class, true); + + elasticsearchTemplate.deleteIndex(GeoAuthor.class); + elasticsearchTemplate.createIndex(GeoAuthor.class); + elasticsearchTemplate.refresh(GeoAuthor.class, true); + + elasticsearchTemplate.putMapping(GeoAuthor.class); + } @Test public void shouldReturnCountForGivenSearchQuery() { @@ -716,4 +726,85 @@ public class ElasticsearchTemplateTests { assertThat(elasticsearchTemplate.indexExists(clazz), is(false)); } + @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")))); + } } diff --git a/src/test/resources/infrastructure.xml b/src/test/resources/infrastructure.xml index 747841218..c9207190f 100644 --- a/src/test/resources/infrastructure.xml +++ b/src/test/resources/infrastructure.xml @@ -5,7 +5,10 @@ xsi:schemaLocation="http://www.springframework.org/schema/data/elasticsearch http://www.springframework.org/schema/data/elasticsearch/spring-elasticsearch.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> - + + + + + \ No newline at end of file