org.hibernate.search.elasticsearch.impl.ToElasticsearch Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of hibernate-search-elasticsearch Show documentation
Show all versions of hibernate-search-elasticsearch Show documentation
Hibernate Search backend which has indexing operations forwarded to Elasticsearch
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or .
*/
package org.hibernate.search.elasticsearch.impl;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.BoostQuery;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.FilteredQuery;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.NumericRangeQuery;
import org.apache.lucene.search.PhraseQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryWrapperFilter;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.SortField.Type;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TermRangeQuery;
import org.apache.lucene.search.WildcardQuery;
import org.hibernate.search.analyzer.impl.RemoteAnalyzerReference;
import org.hibernate.search.backend.spi.DeletionQuery;
import org.hibernate.search.elasticsearch.impl.JsonBuilder.JsonAppender;
import org.hibernate.search.elasticsearch.logging.impl.Log;
import org.hibernate.search.elasticsearch.util.impl.FieldHelper;
import org.hibernate.search.engine.spi.DocumentBuilderIndexedEntity;
import org.hibernate.search.exception.AssertionFailure;
import org.hibernate.search.filter.impl.CachingWrapperQuery;
import org.hibernate.search.query.dsl.impl.DiscreteFacetRequest;
import org.hibernate.search.query.dsl.impl.FacetRange;
import org.hibernate.search.query.dsl.impl.RangeFacetRequest;
import org.hibernate.search.query.dsl.impl.RemoteMatchQuery;
import org.hibernate.search.query.dsl.impl.RemotePhraseQuery;
import org.hibernate.search.query.dsl.impl.RemoteSimpleQueryStringQuery;
import org.hibernate.search.query.dsl.impl.RemoteSimpleQueryStringQuery.Field;
import org.hibernate.search.query.dsl.sort.impl.NativeSortField;
import org.hibernate.search.query.facet.FacetSortOrder;
import org.hibernate.search.query.facet.FacetingRequest;
import org.hibernate.search.spatial.Coordinates;
import org.hibernate.search.spatial.DistanceSortField;
import org.hibernate.search.spatial.impl.DistanceQuery;
import org.hibernate.search.spatial.impl.SpatialHashQuery;
import org.hibernate.search.util.StringHelper;
import org.hibernate.search.util.logging.impl.LoggerFactory;
import java.lang.invoke.MethodHandles;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
/**
* Various utilities to transform Hibernate Search API into Elasticsearch JSON.
*
* @author Guillaume Smet
* @author Gunnar Morling
*/
public class ToElasticsearch {
/*
* A specific suffix for facet fields that avoids conflicts with existing names.
*/
public static final String FACET_FIELD_SUFFIX = "__HSearch_Facet";
private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() );
private static final int DEFAULT_SLOP = 0;
private static final int DEFAULT_MAX_EDIT_DISTANCE = 0;
private static final int DEFAULT_PREFIX_LENGTH = 0;
private static final float DEFAULT_BOOST = 1.0f;
private static final String BOOST_OPERATOR = "^";
private static final JsonParser JSON_PARSER = new JsonParser();
private static final JsonPrimitive SORT_ORDER_ASC = new JsonPrimitive( "asc" );
private static final JsonPrimitive SORT_ORDER_DESC = new JsonPrimitive( "desc" );
private static final JsonPrimitive SORT_MISSING_LAST = new JsonPrimitive( "_last" );
private static final JsonPrimitive SORT_MISSING_FIRST = new JsonPrimitive( "_first" );
private static final Map SORT_FIELD_SCALAR_MINIMUMS = new EnumMap<>( SortField.Type.class );
private static final Map SORT_FIELD_SCALAR_DEFAULTS = new EnumMap<>( SortField.Type.class );
private static final Map SORT_FIELD_SCALAR_MAXIMUMS = new EnumMap<>( SortField.Type.class );
private static final Map> SORT_FIELD_SCALAR_TYPES = new EnumMap<>( SortField.Type.class );
static {
initSortFieldScalarValues( SortField.Type.DOUBLE, Double.class, Double.MIN_VALUE, 0.0d, Double.MAX_VALUE );
initSortFieldScalarValues( SortField.Type.FLOAT, Float.class, Float.MIN_VALUE, 0.0f, Float.MAX_VALUE );
initSortFieldScalarValues( SortField.Type.LONG, Long.class, Long.MIN_VALUE, 0L, Long.MAX_VALUE );
initSortFieldScalarValues( SortField.Type.INT, Integer.class, Integer.MIN_VALUE, 0, Integer.MAX_VALUE );
}
private static void initSortFieldScalarValues(Type type, Class extends Number> clazz,
Number minValue, Number defaultValue, Number maxValue) {
SORT_FIELD_SCALAR_MINIMUMS.put( type, minValue );
SORT_FIELD_SCALAR_DEFAULTS.put( type, defaultValue );
SORT_FIELD_SCALAR_MAXIMUMS.put( type, maxValue );
SORT_FIELD_SCALAR_TYPES.put( type, clazz );
}
private ToElasticsearch() {
}
public static void addFacetingRequest(JsonBuilder.Object jsonQuery, FacetingRequest facetingRequest,
String sourceFieldAbsoluteName, String facetRelativeName) {
String aggregationFieldName = sourceFieldAbsoluteName + "." + facetRelativeName + FACET_FIELD_SUFFIX;
if ( facetingRequest instanceof DiscreteFacetRequest ) {
JsonObject termsJsonQuery = JsonBuilder.object().add( "terms",
JsonBuilder.object()
.addProperty( "field", aggregationFieldName )
.addProperty( "size", facetingRequest.getMaxNumberOfFacets() < 0 ? Integer.MAX_VALUE : facetingRequest.getMaxNumberOfFacets() )
.add( "order", fromFacetSortOrder( facetingRequest.getSort() ) )
.addProperty( "min_doc_count", facetingRequest.hasZeroCountsIncluded() ? 0 : 1 )
).build();
if ( isNested( sourceFieldAbsoluteName ) ) {
JsonBuilder.Object facetJsonQuery = JsonBuilder.object();
facetJsonQuery.add( "nested", JsonBuilder.object()
.addProperty( "path", FieldHelper.getEmbeddedFieldPath( sourceFieldAbsoluteName ) + "." + facetRelativeName + FACET_FIELD_SUFFIX ) );
facetJsonQuery.add( "aggregations", JsonBuilder.object().add( aggregationFieldName, termsJsonQuery ) );
jsonQuery.add( facetingRequest.getFacetingName(), facetJsonQuery );
}
else {
jsonQuery.add( facetingRequest.getFacetingName(), termsJsonQuery );
}
}
else if ( facetingRequest instanceof RangeFacetRequest> ) {
RangeFacetRequest> rangeFacetingRequest = (RangeFacetRequest>) facetingRequest;
for ( FacetRange> facetRange : rangeFacetingRequest.getFacetRangeList() ) {
JsonBuilder.Object comparisonFragment = JsonBuilder.object();
if ( facetRange.getMin() != null ) {
comparisonFragment.addProperty( facetRange.isMinIncluded() ? "gte" : "gt", facetRange.getMin() );
}
if ( facetRange.getMax() != null ) {
comparisonFragment.addProperty( facetRange.isMaxIncluded() ? "lte" : "lt", facetRange.getMax() );
}
JsonObject rangeQuery = wrapQueryForNestedIfRequired( aggregationFieldName,
JsonBuilder.object().add( "range",
JsonBuilder.object().add( aggregationFieldName, comparisonFragment ) ).build() );
jsonQuery.add( facetingRequest.getFacetingName() + "-" + facetRange.getIdentifier(),
JsonBuilder.object().add( "filter", rangeQuery ) );
}
}
else {
throw LOG.facetingRequestHasUnsupportedType( facetingRequest.getClass().getName() );
}
}
private static JsonObject fromFacetSortOrder(FacetSortOrder sortOrder) {
JsonObject sort = new JsonObject();
switch ( sortOrder ) {
case COUNT_ASC:
sort.addProperty( "_count", "asc" );
break;
case COUNT_DESC:
sort.addProperty( "_count", "desc" );
break;
case FIELD_VALUE:
sort.addProperty( "_term", "asc" );
break;
case RANGE_DEFINITION_ORDER:
throw LOG.cannotSendRangeDefinitionOrderToElasticsearchBackend();
}
return sort;
}
public static JsonObject condition(String operator, JsonArray conditions) {
JsonObject jsonCondition;
if ( conditions.size() == 1 ) {
jsonCondition = conditions.get( 0 ).getAsJsonObject();
}
else {
jsonCondition = JsonBuilder.object().add( "bool",
JsonBuilder.object().add( operator, conditions ) ).build();
}
return jsonCondition;
}
public static JsonObject fromLuceneQuery(Query query) {
if ( query instanceof MatchAllDocsQuery ) {
return convertMatchAllDocsQuery( (MatchAllDocsQuery) query );
}
else if ( query instanceof MatchNoDocsQuery ) {
return convertMatchNoDocsQuery( (MatchNoDocsQuery) query );
}
else if ( query instanceof TermQuery ) {
return convertTermQuery( (TermQuery) query );
}
else if ( query instanceof BooleanQuery ) {
return convertBooleanQuery( (BooleanQuery) query );
}
else if ( query instanceof TermRangeQuery ) {
return convertTermRangeQuery( (TermRangeQuery) query );
}
else if ( query instanceof NumericRangeQuery ) {
return convertNumericRangeQuery( (NumericRangeQuery>) query );
}
else if ( query instanceof WildcardQuery ) {
return convertWildcardQuery( (WildcardQuery) query );
}
else if ( query instanceof PrefixQuery ) {
return convertPrefixQuery( (PrefixQuery) query );
}
else if ( query instanceof FuzzyQuery ) {
return convertFuzzyQuery( (FuzzyQuery) query );
}
else if ( query instanceof RemotePhraseQuery ) {
return convertRemotePhraseQuery( (RemotePhraseQuery) query );
}
else if ( query instanceof RemoteMatchQuery ) {
return convertRemoteMatchQuery( (RemoteMatchQuery) query );
}
else if ( query instanceof RemoteSimpleQueryStringQuery ) {
return convertRemoteSimpleQueryStringQuery( (RemoteSimpleQueryStringQuery) query );
}
else if ( query instanceof ConstantScoreQuery ) {
return convertConstantScoreQuery( (ConstantScoreQuery) query );
}
else if ( query instanceof FilteredQuery ) {
return convertFilteredQuery( (FilteredQuery) query );
}
else if ( query instanceof QueryWrapperFilter ) {
JsonObject result = fromLuceneQuery( ( (QueryWrapperFilter) query ).getQuery() );
return wrapBoostIfNecessary( result, query.getBoost() );
}
else if ( query instanceof DistanceQuery ) {
return convertDistanceQuery( (DistanceQuery) query );
}
else if ( query instanceof SpatialHashQuery ) {
return convertSpatialHashFilter( (SpatialHashQuery) query );
}
else if ( query instanceof PhraseQuery ) {
return convertPhraseQuery( (PhraseQuery) query );
}
else if ( query instanceof BoostQuery ) {
JsonObject result = fromLuceneQuery( ( (BoostQuery) query ).getQuery() );
return wrapBoostIfNecessary( result, query.getBoost() );
}
else if ( query instanceof CachingWrapperQuery ) {
JsonObject result = fromLuceneQuery( ( (CachingWrapperQuery) query ).getQuery() );
return wrapBoostIfNecessary( result, query.getBoost() );
}
else if ( query instanceof org.apache.lucene.search.CachingWrapperQuery ) {
JsonObject result = fromLuceneQuery( ( (org.apache.lucene.search.CachingWrapperQuery) query ).getQuery() );
return wrapBoostIfNecessary( result, query.getBoost() );
}
else if ( query instanceof org.apache.lucene.search.CachingWrapperFilter ) {
JsonObject result = fromLuceneQuery( ( (org.apache.lucene.search.CachingWrapperFilter) query ).getFilter() );
return wrapBoostIfNecessary( result, query.getBoost() );
}
throw LOG.cannotTransformLuceneQueryIntoEsQuery( query );
}
public static JsonObject fromDeletionQuery(DocumentBuilderIndexedEntity documentBuilder, DeletionQuery deletionQuery) {
return fromLuceneQuery( deletionQuery.toLuceneQuery( documentBuilder ) );
}
private static JsonObject convertMatchAllDocsQuery(MatchAllDocsQuery matchAllDocsQuery) {
return JsonBuilder.object().add( "match_all", new JsonObject() ).build();
}
private static JsonObject convertMatchNoDocsQuery(MatchNoDocsQuery matchNoDocsQuery) {
/*
* Elasticsearch 2.x does not provide a match_none query, so we work it around
* by targeting a type that doesn't exist.
* We use a type query, because Elasticsearch 5.x has optimizations that convert
* type queries to MatchNoDocsQueries automatically when an unknown type is
* requested.
*/
return JsonBuilder.object()
.add( "type", JsonBuilder.object()
.addProperty( "value", "__HSearch_Workaround_MatchNoDocsQuery" )
).build();
}
private static JsonObject convertBooleanQuery(BooleanQuery booleanQuery) {
JsonArray musts = new JsonArray();
JsonArray shoulds = new JsonArray();
JsonArray mustNots = new JsonArray();
JsonArray filters = new JsonArray();
for ( BooleanClause clause : booleanQuery.clauses() ) {
switch ( clause.getOccur() ) {
case MUST:
musts.add( fromLuceneQuery( clause.getQuery() ) );
break;
case FILTER:
filters.add( fromLuceneQuery( clause.getQuery() ) );
break;
case MUST_NOT:
mustNots.add( fromLuceneQuery( clause.getQuery() ) );
break;
case SHOULD:
shoulds.add( fromLuceneQuery( clause.getQuery() ) );
break;
}
}
JsonBuilder.Object clauses = JsonBuilder.object();
if ( musts.size() > 1 ) {
clauses.add( "must", musts );
}
else if ( musts.size() == 1 ) {
clauses.add( "must", musts.iterator().next() );
}
if ( shoulds.size() > 1 ) {
clauses.add( "should", shoulds );
}
else if ( shoulds.size() == 1 ) {
clauses.add( "should", shoulds.iterator().next() );
}
if ( mustNots.size() > 1 ) {
clauses.add( "must_not", mustNots );
}
else if ( mustNots.size() == 1 ) {
clauses.add( "must_not", mustNots.iterator().next() );
}
if ( filters.size() > 1 ) {
clauses.add( "filter", filters );
}
else if ( filters.size() == 1 ) {
clauses.add( "filter", filters.iterator().next() );
}
clauses.append( boostAppender( booleanQuery ) );
int minimumShouldMatchNumber = booleanQuery.getMinimumNumberShouldMatch();
if ( minimumShouldMatchNumber != 0 ) {
clauses.addProperty( "minimum_should_match", minimumShouldMatchNumber );
}
JsonObject bool = new JsonObject();
bool.add( "bool", clauses.build() );
return bool;
}
private static JsonObject convertTermQuery(TermQuery query) {
String field = query.getTerm().field();
JsonObject matchQuery = JsonBuilder.object()
.add( "term",
JsonBuilder.object().add( field,
JsonBuilder.object()
.addProperty( "value", query.getTerm().text() )
.append( boostAppender( query ) )
)
).build();
return wrapQueryForNestedIfRequired( field, matchQuery );
}
private static JsonObject convertWildcardQuery(WildcardQuery query) {
String field = query.getTerm().field();
JsonObject wildcardQuery = JsonBuilder.object()
.add( "wildcard",
JsonBuilder.object().add( field,
JsonBuilder.object()
.addProperty( "value", query.getTerm().text() )
.append( boostAppender( query ) )
)
).build();
return wrapQueryForNestedIfRequired( field, wildcardQuery );
}
private static JsonObject convertPrefixQuery(PrefixQuery query) {
String field = query.getField();
JsonObject wildcardQuery = JsonBuilder.object()
.add( "prefix",
JsonBuilder.object().add( field,
JsonBuilder.object()
.addProperty( "value", query.getPrefix().text() )
.append( boostAppender( query ) )
)
).build();
return wrapQueryForNestedIfRequired( field, wildcardQuery );
}
private static JsonObject convertFuzzyQuery(FuzzyQuery query) {
String field = query.getTerm().field();
JsonObject fuzzyQuery = JsonBuilder.object()
.add( "fuzzy",
JsonBuilder.object().add( field,
JsonBuilder.object()
.addProperty( "value", query.getTerm().text() )
.addProperty( "fuzziness", query.getMaxEdits() )
.addProperty( "prefix_length", query.getPrefixLength() )
.addProperty( "transpositions", query.getTranspositions() )
.append( boostAppender( query ) )
)
).build();
return wrapQueryForNestedIfRequired( field, fuzzyQuery );
}
/**
* This is best effort only: the PhraseQuery may contain multiple terms at the same position
* (think synonyms) or gaps (think stopwords) and it's in this case impossible to translate
* it into a correct ElasticsearchQuery.
*/
private static JsonObject convertPhraseQuery(PhraseQuery query) {
Term[] terms = query.getTerms();
if ( terms.length == 0 ) {
throw LOG.cannotQueryOnEmptyPhraseQuery();
}
String field = terms[0].field(); // phrase queries are only supporting one field
StringBuilder phrase = new StringBuilder();
for ( Term term : terms ) {
phrase.append( " " ).append( term.text() );
}
JsonObject phraseQuery = JsonBuilder.object()
.add( "match_phrase",
JsonBuilder.object().add( field,
JsonBuilder.object()
.addProperty( "query", phrase.toString().trim() )
.append( slopAppender( query.getSlop() ) )
.append( boostAppender( query ) )
)
).build();
return wrapQueryForNestedIfRequired( field, phraseQuery );
}
private static JsonObject convertRemotePhraseQuery(RemotePhraseQuery query) {
if ( StringHelper.isEmpty( query.getPhrase() ) ) {
throw LOG.cannotQueryOnEmptyPhraseQuery();
}
JsonObject phraseQuery = JsonBuilder.object()
.add( "match_phrase",
JsonBuilder.object().add( query.getField(),
JsonBuilder.object()
.addProperty( "query", query.getPhrase().trim() )
.append( analyzerAppender(
query.getOriginalAnalyzerReference(), query.getQueryAnalyzerReference(),
query.getField()
) )
.append( slopAppender( query.getSlop() ) )
.append( boostAppender( query ) )
)
).build();
return wrapQueryForNestedIfRequired( query.getField(), phraseQuery );
}
private static JsonObject convertRemoteMatchQuery(RemoteMatchQuery query) {
JsonObject matchQuery = JsonBuilder.object()
.add( "match",
JsonBuilder.object().add( query.getField(),
JsonBuilder.object()
.addProperty( "query", query.getSearchTerms() )
.append( analyzerAppender(
query.getOriginalAnalyzerReference(), query.getQueryAnalyzerReference(),
query.getField()
) )
.append( fuzzinessAppender( query.getMaxEditDistance() ) )
.append( prefixLengthAppender( query.getPrefixLength() ) )
.append( boostAppender( query ) )
)
).build();
return wrapQueryForNestedIfRequired( query.getField(), matchQuery );
}
private static JsonObject convertRemoteSimpleQueryStringQuery(RemoteSimpleQueryStringQuery query) {
JsonBuilder.Object queryBuilder = JsonBuilder.object()
.addProperty( "query", query.getQuery() )
.addProperty( "default_operator", query.isMatchAll() ? "and" : "or" );
Set analyzers = new HashSet<>();
String overridingRemoteAnalyzerName = null;
JsonArray fieldArray = new JsonArray();
for ( Field field : query.getFields() ) {
StringBuilder sb = new StringBuilder( field.getName() );
if ( field.getBoost() != DEFAULT_BOOST ) {
sb.append( BOOST_OPERATOR ).append( field.getBoost() );
}
fieldArray.add( sb.toString() );
String originalRemoteAnalyzerName = query.getOriginalRemoteAnalyzerReference().getAnalyzerName( field.getName() );
String queryRemoteAnalyzerName = query.getQueryRemoteAnalyzerReference().getAnalyzerName( field.getName() );
analyzers.add( queryRemoteAnalyzerName );
if ( !queryRemoteAnalyzerName.equals( originalRemoteAnalyzerName ) ) {
if ( overridingRemoteAnalyzerName == null ) {
overridingRemoteAnalyzerName = queryRemoteAnalyzerName;
}
else if ( !overridingRemoteAnalyzerName.equals( queryRemoteAnalyzerName ) ) {
throw LOG.unableToOverrideQueryAnalyzerWithMoreThanOneAnalyzersForSimpleQueryStringQueries(
Arrays.asList( overridingRemoteAnalyzerName, queryRemoteAnalyzerName ) );
}
}
}
// we always have at least one field defined
queryBuilder.add( "fields", fieldArray );
if ( overridingRemoteAnalyzerName != null ) {
if ( analyzers.size() == 1 ) {
queryBuilder.addProperty( "analyzer", overridingRemoteAnalyzerName );
}
else {
throw LOG.unableToOverrideQueryAnalyzerWithMoreThanOneAnalyzersForSimpleQueryStringQueries( analyzers );
}
}
JsonObject simpleQueryStringQuery = JsonBuilder.object()
.add( "simple_query_string",
queryBuilder.append( boostAppender( query ) ) )
.build();
return simpleQueryStringQuery;
}
private static JsonObject convertTermRangeQuery(TermRangeQuery query) {
JsonBuilder.Object interval = JsonBuilder.object();
if ( query.getLowerTerm() != null ) {
interval.addProperty( query.includesLower() ? "gte" : "gt", query.getLowerTerm().utf8ToString() );
}
if ( query.getUpperTerm() != null ) {
interval.addProperty( query.includesUpper() ? "lte" : "lt", query.getUpperTerm().utf8ToString() );
}
interval.append( boostAppender( query ) );
JsonObject range = JsonBuilder.object().add( "range",
JsonBuilder.object().add( query.getField(), interval ) )
.build();
return wrapQueryForNestedIfRequired( query.getField(), range );
}
private static JsonObject convertNumericRangeQuery(NumericRangeQuery> query) {
JsonBuilder.Object interval = JsonBuilder.object();
if ( query.getMin() != null ) {
interval.addProperty( query.includesMin() ? "gte" : "gt", query.getMin() );
}
if ( query.getMax() != null ) {
interval.addProperty( query.includesMax() ? "lte" : "lt", query.getMax() );
}
interval.append( boostAppender( query ) );
JsonObject range = JsonBuilder.object().add( "range",
JsonBuilder.object().add( query.getField(), interval ) )
.build();
return wrapQueryForNestedIfRequired( query.getField(), range );
}
private static JsonObject convertConstantScoreQuery(ConstantScoreQuery query) {
JsonObject constantScoreQuery = JsonBuilder.object()
.add( "constant_score",
JsonBuilder.object()
.add( "filter", fromLuceneQuery( query.getQuery() ) )
.append( boostAppender( query ) )
).build();
return constantScoreQuery;
}
private static JsonObject wrapBoostIfNecessary(JsonObject convertedQuery, float boost) {
if ( boost != DEFAULT_BOOST ) { // We actually want to use float equality here
return JsonBuilder.object()
.add( "bool",
JsonBuilder.object()
.add( "must", convertedQuery )
.addProperty( "boost", boost )
).build();
}
else {
return convertedQuery;
}
}
private static JsonObject convertFilteredQuery(FilteredQuery query) {
JsonObject filteredQuery = JsonBuilder.object()
.add( "bool",
JsonBuilder.object()
.add( "must", fromLuceneQuery( query.getQuery() ) )
.add( "filter", fromLuceneQuery( query.getFilter() ) )
.append( boostAppender( query ) )
).build();
return filteredQuery;
}
private static JsonObject convertDistanceQuery(DistanceQuery query) {
JsonObject distanceQuery = JsonBuilder.object()
.add( "geo_distance",
JsonBuilder.object()
.addProperty( "distance", query.getRadius() + "km" )
.add( query.getCoordinatesField(),
JsonBuilder.object()
.addProperty( "lat", query.getCenter().getLatitude() )
.addProperty( "lon", query.getCenter().getLongitude() )
)
).build();
distanceQuery = wrapQueryForNestedIfRequired( query.getCoordinatesField(), distanceQuery );
// we only implement the approximation optimization when we use the hash method as Elasticsearch
// automatically optimize the geo_distance query with a bounding box filter so we don't need to do it
// ourselves when we use the range method.
Query approximationQuery = query.getApproximationQuery();
if ( approximationQuery instanceof SpatialHashQuery ) {
distanceQuery = JsonBuilder.object()
.add( "bool", JsonBuilder.object()
.add( "must", distanceQuery )
.add( "filter", convertSpatialHashFilter( (SpatialHashQuery) approximationQuery ) )
).build();
}
return distanceQuery;
}
private static JsonObject convertSpatialHashFilter(SpatialHashQuery filter) {
JsonArray cellsIdsJsonArray = new JsonArray();
for ( String cellId : filter.getSpatialHashCellsIds() ) {
cellsIdsJsonArray.add( cellId );
}
JsonObject spatialHashFilter = JsonBuilder.object()
.add( "terms", JsonBuilder.object()
.add( filter.getFieldName(), cellsIdsJsonArray )
).build();
return wrapQueryForNestedIfRequired( filter.getFieldName(), spatialHashFilter );
}
private static final JsonBuilder.JsonAppender