Skip to content

Geo Location / Filter Feature #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import java.lang.annotation.*;

/**
* Query
* AbstractQuery
*
* @author Rizwan Idrees
* @author Mohsin Husen
Expand All @@ -37,7 +37,7 @@
String value() default "";

/**
* Named Query Named looked up by repository.
* Named AbstractQuery Named looked up by repository.
*
* @return
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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<FilterBuilder> fbList = new LinkedList<FilterBuilder>();
FilterBuilder filter = null;

ListIterator<Criteria> 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<FilterBuilder> 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<FilterBuilder> createFilterFragmentForCriteria(Criteria chainedCriteria) {
Iterator<Criteria.CriteriaEntry> it = chainedCriteria.getFilterCriteriaEntries().iterator();
List<FilterBuilder> filterList = new LinkedList<FilterBuilder>();

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<FilterBuilder> buildNegationFilter(String fieldName, Iterator<Criteria.CriteriaEntry> it){
List<FilterBuilder> notFilterList = new LinkedList<FilterBuilder>();

while (it.hasNext()){
Criteria.CriteriaEntry criteriaEntry = it.next();
FilterBuilder notFilter = notFilter(processCriteriaEntry(criteriaEntry.getKey(), criteriaEntry.getValue(), fieldName));
notFilterList.add(notFilter);
}

return notFilterList;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand All @@ -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<QueryBuilder> shouldQueryBuilderList = new LinkedList<QueryBuilder>();
List<QueryBuilder> mustNotQueryBuilderList = new LinkedList<QueryBuilder>();
List<QueryBuilder> mustQueryBuilderList = new LinkedList<QueryBuilder>();


ListIterator<Criteria> 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<Criteria.CriteriaEntry> it = chainedCriteria.getCriteriaEntries().iterator();
boolean singeEntryCriteria = (chainedCriteria.getCriteriaEntries().size() == 1);
if(chainedCriteria.getQueryCriteriaEntries().isEmpty())
return null;

Iterator<Criteria.CriteriaEntry> it = chainedCriteria.getQueryCriteriaEntries().iterator();
boolean singeEntryCriteria = (chainedCriteria.getQueryCriteriaEntries().size() == 1);

String fieldName = chainedCriteria.getField().getName();
Assert.notNull(fieldName,"Unknown field");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ public interface ElasticsearchOperations {
*/
<T> Page<T> queryForPage(SearchQuery query, Class<T> clazz);


/**
* Execute the query against elasticsearch and return result as {@link Page}
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -177,9 +178,20 @@ public <T> List<String> queryForIds(SearchQuery query) {

@Override
public <T> Page<T> queryForPage(CriteriaQuery criteriaQuery, Class<T> 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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading