/*
* 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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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.backend.spi.DeletionQuery;
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 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 );
private static final int DEFAULT_SLOP = 0;
private static final int DEFAULT_MAX_EDIT_DISTANCE = 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<SortField.Type, Number> SORT_FIELD_SCALAR_MINIMUMS = new EnumMap<>( SortField.Type.class );
private static final Map<SortField.Type, Number> SORT_FIELD_SCALAR_DEFAULTS = new EnumMap<>( SortField.Type.class );
private static final Map<SortField.Type, Number> SORT_FIELD_SCALAR_MAXIMUMS = new EnumMap<>( SortField.Type.class );
private static final Map<SortField.Type, Class<? extends Number>> 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() == -1 ? 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 ) );
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() )
.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() )
.addProperty( "analyzer", query.getAnalyzerReference().getAnalyzer().getName( 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() )
.addProperty( "analyzer", query.getAnalyzerReference().getAnalyzer().getName( query.getField() ) )
.append( fuzzinessAppender( query.getMaxEditDistance() ) )
.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<String> 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().getAnalyzer().getName( field.getName() );
String queryRemoteAnalyzerName = query.getQueryRemoteAnalyzerReference().getAnalyzer().getName( 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<Object> NOOP_APPENDER =
new JsonBuilder.JsonAppender<Object>() {
@Override
public void append(Object appendable) {
// Do nothing
}
};
/**
* Appender that adds a "slop" property if necessary.
*/
private static JsonBuilder.JsonAppender<? super JsonBuilder.Object> slopAppender(final int slop) {
if ( slop != DEFAULT_SLOP ) {
return new JsonBuilder.JsonAppender<JsonBuilder.Object>() {
@Override
public void append(JsonBuilder.Object object) {
object.addProperty( "slop", slop );
}
};
}
else {
return NOOP_APPENDER;
}
}
/**
* Appender that adds a "fuzziness" property if necessary.
*/
private static JsonBuilder.JsonAppender<? super JsonBuilder.Object> fuzzinessAppender(final int maxEditDistance) {
if ( maxEditDistance != DEFAULT_MAX_EDIT_DISTANCE ) {
return new JsonBuilder.JsonAppender<JsonBuilder.Object>() {
@Override
public void append(JsonBuilder.Object object) {
object.addProperty( "fuzziness", maxEditDistance );
}
};
}
else {
return NOOP_APPENDER;
}
}
/**
* Appender that adds a "boost" property if necessary.
*/
private static JsonBuilder.JsonAppender<? super JsonBuilder.Object> boostAppender(Query query) {
final float boost = query.getBoost();
if ( boost != DEFAULT_BOOST ) { // We actually want to use float equality here
return new JsonBuilder.JsonAppender<JsonBuilder.Object>() {
@Override
public void append(JsonBuilder.Object object) {
object.addProperty( "boost", boost );
}
};
}
else {
return NOOP_APPENDER;
}
}
private static JsonObject wrapQueryForNestedIfRequired(String field, JsonObject query) {
if ( !isNested( field ) ) {
return query;
}
String path = FieldHelper.getEmbeddedFieldPath( field );
return JsonBuilder.object().add( "nested",
JsonBuilder.object()
.addProperty( "path", path )
.add( "query", query ) )
.build();
}
private static boolean isNested(String field) {
//TODO Drive through meta-data
// return FieldHelper.isEmbeddedField( field );
return false;
}
/**
* Convert a Lucene {@link Sort} to an Elasticsearch sort, trying to preserve
* the exact same meaning as the Sort would have in Lucene.
*
* <p>For instance, missing values on numeric fields are implicitly 0 in Lucene, so this
* method will add it explicitly on any numeric Elasticsearch sort.
*
* @param sort The Lucene {@link Sort} to convert
* @return The equivalent Elasticsearch sort, as a {@link JsonArray}
*/
public static JsonArray fromLuceneSort(Sort sort) {
JsonBuilder.Array builder = JsonBuilder.array();
for ( SortField field : sort.getSort() ) {
builder.add( fromLuceneSortField( field ) );
}
return builder.build();
}
private static JsonBuilder.Object fromLuceneSortField(SortField sortField) {
if ( sortField instanceof DistanceSortField ) {
DistanceSortField distanceSortField = (DistanceSortField) sortField;
Coordinates center = distanceSortField.getCenter();
return JsonBuilder.object().add( "_geo_distance", JsonBuilder.object()
.add( "order", fromLuceneSortFieldOrder( sortField.getType(), sortField.getReverse() ) )
.add( sortField.getField(), JsonBuilder.object()
.addProperty( "lat", center.getLatitude() )
.addProperty( "lon", center.getLongitude() )
)
.addProperty( "unit", "km" )
.addProperty( "distance_type", "arc" ) );
}
else if ( sortField instanceof NativeSortField ) {
NativeSortField nativeSortField = (NativeSortField) sortField;
String sortFieldName = nativeSortField.getField();
String sortDescriptionAsString = nativeSortField.getNativeSortDescription();
JsonElement sortDescription = JSON_PARSER.parse( sortDescriptionAsString );
return JsonBuilder.object().add( sortFieldName, sortDescription );
}
else {
SortField.Type sortFieldType = sortField.getType();
String sortFieldName;
if ( sortField.getField() == null ) {
switch ( sortFieldType ) {
case DOC:
sortFieldName = "_uid";
break;
case SCORE:
sortFieldName = "_score";
break;
default:
throw LOG.cannotUseThisSortTypeWithNullSortFieldName( sortField.getType() );
}
}
else {
sortFieldName = sortField.getField();
}
boolean reverse = sortField.getReverse();
JsonElement order = fromLuceneSortFieldOrder( sortField.getType(), reverse );
JsonBuilder.Object contentBuilder = JsonBuilder.object()
.add( "order", order );
JsonElement missing = fromLuceneSortFieldMissing( sortFieldType, sortField.missingValue, reverse );
if ( missing != null ) {
contentBuilder.add( "missing", missing );
}
return JsonBuilder.object().add( sortFieldName, contentBuilder );
}
}
private static JsonPrimitive fromLuceneSortFieldOrder(Type sortFieldType, boolean reverse) {
switch ( sortFieldType ) {
case SCORE:
return reverse ? SORT_ORDER_ASC : SORT_ORDER_DESC;
default:
return reverse ? SORT_ORDER_DESC : SORT_ORDER_ASC;
}
}
private static JsonPrimitive fromLuceneSortFieldMissing(Type sortFieldType, Object luceneMissing, boolean reverse) {
if ( luceneMissing == null ) {
/*
* Simulate Lucene's behavior of assigning default missing values when none is explicitly provided.
*/
switch ( sortFieldType ) {
case DOUBLE:
case FLOAT:
case INT:
case LONG:
luceneMissing = SORT_FIELD_SCALAR_DEFAULTS.get( sortFieldType );
break;
case STRING:
case STRING_VAL:
luceneMissing = reverse ? SortField.STRING_LAST : SortField.STRING_FIRST;
break;
default:
break;
}
}
switch ( sortFieldType ) {
case DOUBLE:
case FLOAT:
case INT:
case LONG:
// Use a more natural representation of the missing value, if possible
if ( luceneMissing.equals( SORT_FIELD_SCALAR_MINIMUMS.get( sortFieldType ) ) ) {
return reverse ? SORT_MISSING_LAST : SORT_MISSING_FIRST;
}
else if ( luceneMissing.equals( SORT_FIELD_SCALAR_MAXIMUMS.get( sortFieldType ) ) ) {
return reverse ? SORT_MISSING_FIRST : SORT_MISSING_LAST;
}
else {
// Make sure the correct type is used (and throw a ClassCastException if not, as Lucene does)
return new JsonPrimitive( SORT_FIELD_SCALAR_TYPES.get( sortFieldType ).cast( luceneMissing ) );
}
case STRING:
case STRING_VAL:
if ( SortField.STRING_LAST.equals( luceneMissing ) ) {
return SORT_MISSING_LAST;
}
else if ( SortField.STRING_FIRST.equals( luceneMissing ) ) {
return SORT_MISSING_FIRST;
}
else if ( luceneMissing != null ) {
throw new AssertionFailure( "Unexpected missing value specified on a String SortField: " + luceneMissing );
}
else {
return null;
}
default:
if ( luceneMissing != null ) {
throw new AssertionFailure( "Missing value specified on a SortField which is not supposed to support it: " + sortFieldType );
}
else {
return null;
}
}
}
}