org.hibernate.search.elasticsearch.query.impl.QueryHitConverter 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.query.impl;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.hibernate.search.bridge.FieldBridge;
import org.hibernate.search.bridge.TwoWayFieldBridge;
import org.hibernate.search.bridge.spi.ConversionContext;
import org.hibernate.search.bridge.util.impl.ContextualExceptionBridgeHelper;
import org.hibernate.search.elasticsearch.ElasticsearchProjectionConstants;
import org.hibernate.search.elasticsearch.impl.JsonBuilder;
import org.hibernate.search.elasticsearch.logging.impl.Log;
import org.hibernate.search.elasticsearch.util.impl.FieldHelper;
import org.hibernate.search.elasticsearch.util.impl.FieldHelper.ExtendedFieldType;
import org.hibernate.search.elasticsearch.work.impl.SearchResult;
import org.hibernate.search.engine.metadata.impl.BridgeDefinedField;
import org.hibernate.search.engine.metadata.impl.DocumentFieldMetadata;
import org.hibernate.search.engine.metadata.impl.TypeMetadata;
import org.hibernate.search.engine.spi.DocumentBuilderIndexedEntity;
import org.hibernate.search.engine.spi.EntityIndexBinding;
import org.hibernate.search.exception.AssertionFailure;
import org.hibernate.search.query.engine.impl.EntityInfoImpl;
import org.hibernate.search.query.engine.spi.EntityInfo;
import org.hibernate.search.spatial.Coordinates;
import org.hibernate.search.spi.IndexedTypeIdentifier;
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.JsonPrimitive;
/**
* @author Yoann Rodiere
*/
class QueryHitConverter {
private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() );
private static final String SPATIAL_DISTANCE_FIELD = "_distance";
public static Builder builder(ElasticsearchQueryFactory queryFactory,
Map targetedEntityBindingsByName) {
return new Builder( queryFactory, targetedEntityBindingsByName );
}
private final Map targetedEntityBindingsByName;
private final Map idProjectionByEntityBinding;
private final Map fieldProjectionsByEntityBinding;
private final JsonElement sourceFilter;
private final JsonElement scriptFields;
private final boolean trackScore;
private final String[] projectedFields;
private final Integer sortByDistanceIndex;
// Private constructor; use builder() instead
private QueryHitConverter(Map targetedEntityBindingsByName,
Map idProjectionByEntityBinding,
Map fieldProjectionsByEntityBinding,
JsonElement sourceFilter, JsonElement scriptFields, boolean trackScore,
String[] projectedFields, Integer sortByDistanceIndex) {
this.targetedEntityBindingsByName = targetedEntityBindingsByName;
this.idProjectionByEntityBinding = idProjectionByEntityBinding;
this.fieldProjectionsByEntityBinding = fieldProjectionsByEntityBinding;
this.sourceFilter = sourceFilter;
this.trackScore = trackScore;
this.projectedFields = projectedFields;
this.scriptFields = scriptFields;
this.sortByDistanceIndex = sortByDistanceIndex;
}
public void contributeToPayload(JsonBuilder.Object payloadBuilder) {
if ( trackScore ) {
payloadBuilder.addProperty( "track_scores", true );
}
payloadBuilder.add( "_source", sourceFilter );
if ( scriptFields != null ) {
payloadBuilder.add( "script_fields", scriptFields );
}
}
public EntityInfo convert(SearchResult searchResult, JsonObject hit) {
String type = hit.get( "_type" ).getAsString();
EntityIndexBinding binding = targetedEntityBindingsByName.get( type );
if ( binding == null ) {
LOG.warnf( "Found unknown type in Elasticsearch index: " + type );
return null;
}
DocumentBuilderIndexedEntity documentBuilder = binding.getDocumentBuilder();
IndexedTypeIdentifier typeId = documentBuilder.getTypeIdentifier();
ConversionContext conversionContext = new ContextualExceptionBridgeHelper();
conversionContext.setConvertedTypeId( typeId );
FieldProjection idProjection = idProjectionByEntityBinding.get( binding );
Object id = idProjection.convertHit( hit, conversionContext );
Object[] projections = null;
if ( projectedFields != null ) {
projections = new Object[projectedFields.length];
for ( int i = 0; i < projections.length; i++ ) {
String field = projectedFields[i];
if ( field == null ) {
continue;
}
switch ( field ) {
case ElasticsearchProjectionConstants.SOURCE:
projections[i] = hit.getAsJsonObject().get( "_source" ).toString();
break;
case ElasticsearchProjectionConstants.ID:
projections[i] = id;
break;
case ElasticsearchProjectionConstants.OBJECT_CLASS:
projections[i] = typeId.getPojoType();
break;
case ElasticsearchProjectionConstants.SCORE:
projections[i] = hit.getAsJsonObject().get( "_score" ).getAsFloat();
break;
case ElasticsearchProjectionConstants.SPATIAL_DISTANCE:
JsonElement distance = null;
// if we sort by distance, we need to find the index of the DistanceSortField and use it
// to extract the values from the sort array
// if we don't sort by distance, we use the field generated by the script_field added earlier
if ( sortByDistanceIndex != null ) {
distance = hit.getAsJsonObject().get( "sort" ).getAsJsonArray().get( sortByDistanceIndex );
}
else {
JsonElement fields = hit.getAsJsonObject().get( "fields" );
if ( fields != null ) { // "fields" seems to be missing if there are only null results in script fields
distance = hit.getAsJsonObject().get( "fields" ).getAsJsonObject()
.get( SPATIAL_DISTANCE_FIELD );
}
}
if ( distance != null && distance.isJsonArray() ) {
JsonArray array = distance.getAsJsonArray();
distance = array.size() >= 1 ? array.get( 0 ) : null;
}
if ( distance == null || distance.isJsonNull() ) {
projections[i] = null;
}
else {
Double distanceAsDouble = distance.getAsDouble();
if ( distanceAsDouble == Double.MAX_VALUE || distanceAsDouble.isInfinite() ) {
/*
* When we extract the distance from the sort, its default value is:
* - Double.MAX_VALUE on older ES versions (5.0 and lower)
* - Double.POSITIVE_INFINITY on newer ES versions (from somewhere around 5.2 onwards)
*/
projections[i] = null;
}
else {
projections[i] = distance.getAsDouble();
}
}
break;
case ElasticsearchProjectionConstants.TOOK:
projections[i] = searchResult.getTook();
break;
case ElasticsearchProjectionConstants.TIMED_OUT:
projections[i] = searchResult.getTimedOut();
break;
case ElasticsearchProjectionConstants.THIS:
// Use EntityInfo.ENTITY_PLACEHOLDER as placeholder.
// It will be replaced when we populate
// the EntityInfo with the real entity.
projections[i] = EntityInfo.ENTITY_PLACEHOLDER;
break;
default:
FieldProjection projection = fieldProjectionsByEntityBinding.get( binding )[i];
projections[i] = projection.convertHit( hit, conversionContext );
}
}
}
return new EntityInfoImpl( typeId, documentBuilder.getIdPropertyName(), (Serializable) id, projections );
}
public static class Builder {
private final ElasticsearchQueryFactory queryFactory;
private final Map targetedEntityBindingsByName;
private final Map idProjectionByEntityBinding = new HashMap<>();
private final Map fieldProjectionsByEntityBinding = new HashMap<>();
private boolean trackScore = false;
private boolean includeAllSource = false;
private boolean hasSpatialDistanceProjection = false;
private Integer sortByDistanceIndex = null;
private Coordinates spatialSearchCenter;
private String spatialFieldName;
private final JsonBuilder.Array sourceFilterCollector = JsonBuilder.array();
private String[] projectedFields;
private Builder(ElasticsearchQueryFactory queryFactory, Map targetedEntityBindingsByName) {
this.queryFactory = queryFactory;
this.targetedEntityBindingsByName = targetedEntityBindingsByName;
/*
* IDs are always projected: always initialize their projections regardless of the
* "projectedFields" attribute.
*/
for ( EntityIndexBinding binding : targetedEntityBindingsByName.values() ) {
DocumentBuilderIndexedEntity documentBuilder = binding.getDocumentBuilder();
String idFieldName = documentBuilder.getIdFieldName();
TypeMetadata typeMetadata = documentBuilder.getTypeMetadata();
FieldProjection projection = createProjection( typeMetadata, idFieldName );
idProjectionByEntityBinding.put( binding, projection );
}
}
public Builder setSortByDistance(Integer sortIndex, Coordinates spatialSearchCenter, String spatialFieldName) {
this.sortByDistanceIndex = sortIndex;
this.spatialSearchCenter = spatialSearchCenter;
this.spatialFieldName = spatialFieldName;
return this;
}
public Builder setProjectedFields(String[] projectedFields) {
if ( this.projectedFields != null ) {
throw new AssertionFailure( "Projected fields set twice for a single query hit extractor" );
}
this.projectedFields = projectedFields;
if ( projectedFields == null ) {
return this;
}
for ( int i = 0 ; i < projectedFields.length ; ++i ) {
String projectedField = projectedFields[i];
if ( projectedField == null ) {
continue;
}
switch ( projectedField ) {
case ElasticsearchProjectionConstants.SOURCE:
includeAllSource = true;
break;
case ElasticsearchProjectionConstants.SCORE:
// Make sure to compute scores even if we don't sort by relevance
trackScore = true;
break;
case ElasticsearchProjectionConstants.ID:
case ElasticsearchProjectionConstants.THIS:
case ElasticsearchProjectionConstants.OBJECT_CLASS:
case ElasticsearchProjectionConstants.TOOK:
case ElasticsearchProjectionConstants.TIMED_OUT:
// Ignore: no impact on source filtering
break;
case ElasticsearchProjectionConstants.SPATIAL_DISTANCE:
hasSpatialDistanceProjection = true;
break;
default:
for ( EntityIndexBinding binding : targetedEntityBindingsByName.values() ) {
TypeMetadata typeMetadata = binding.getDocumentBuilder().getTypeMetadata();
FieldProjection projection = createProjection( typeMetadata, projectedField );
FieldProjection[] projectionsForType = fieldProjectionsByEntityBinding.get( binding );
if ( projectionsForType == null ) {
projectionsForType = new FieldProjection[projectedFields.length];
fieldProjectionsByEntityBinding.put( binding, projectionsForType );
}
projectionsForType[i] = projection;
}
break;
}
}
return this;
}
public QueryHitConverter build() {
JsonElement sourceFilter;
if ( includeAllSource ) {
sourceFilter = new JsonPrimitive( "*" );
}
else {
JsonArray array = sourceFilterCollector.build();
if ( array.size() > 0 ) {
sourceFilter = array;
}
else {
// Projecting only on score or other document-independent values
sourceFilter = new JsonPrimitive( false );
}
}
JsonElement scriptFields = null;
if ( hasSpatialDistanceProjection && sortByDistanceIndex == null ) {
// when the results are sorted by distance, Elasticsearch returns the distance in a "sort" field in
// the results. If we don't sort by distance, we need to request for the distance using a script_field.
scriptFields = JsonBuilder.object().add( SPATIAL_DISTANCE_FIELD, JsonBuilder.object()
.add(
"script",
queryFactory.createSpatialDistanceScript( spatialSearchCenter, spatialFieldName )
)
)
.build();
}
return new QueryHitConverter( targetedEntityBindingsByName,
idProjectionByEntityBinding, fieldProjectionsByEntityBinding,
sourceFilter, scriptFields, trackScore, projectedFields, sortByDistanceIndex );
}
private FieldProjection createProjection(TypeMetadata rootTypeMetadata, String projectedField) {
DocumentFieldMetadata fieldMetadata = rootTypeMetadata.getDocumentFieldMetadataFor( projectedField );
if ( fieldMetadata != null ) {
return createProjection( rootTypeMetadata, fieldMetadata );
}
else {
// We check if it is a field created by a field bridge
BridgeDefinedField bridgeDefinedField = rootTypeMetadata.getBridgeDefinedFieldMetadataFor( projectedField );
if ( bridgeDefinedField != null ) {
String absoluteName = bridgeDefinedField.getAbsoluteName();
ExtendedFieldType type = FieldHelper.getType( bridgeDefinedField );
sourceFilterCollector.add( new JsonPrimitive( absoluteName ) );
return new PrimitiveProjection( rootTypeMetadata, absoluteName, type );
}
else {
/*
* No metadata: fall back to dynamically converting the resulting
* JSON to the most appropriate Java type.
*/
sourceFilterCollector.add( new JsonPrimitive( projectedField ) );
return new JsonDrivenProjection( projectedField );
}
}
}
private FieldProjection createProjection(TypeMetadata rootTypeMetadata,
DocumentFieldMetadata fieldMetadata) {
String absoluteName = fieldMetadata.getAbsoluteName();
FieldBridge fieldBridge = fieldMetadata.getFieldBridge();
ExtendedFieldType type = FieldHelper.getType( fieldMetadata );
if ( ExtendedFieldType.BOOLEAN.equals( type ) ) {
sourceFilterCollector.add( new JsonPrimitive( absoluteName ) );
return new PrimitiveProjection( rootTypeMetadata, absoluteName, type );
}
else if ( fieldBridge instanceof TwoWayFieldBridge ) {
Collection bridgeDefinedFields = fieldMetadata.getBridgeDefinedFields().values();
Set objectFieldNames = new HashSet<>();
Map primitiveProjections = new HashMap<>();
for ( BridgeDefinedField bridgeDefinedField : bridgeDefinedFields ) {
String nestedAbsoluteName = bridgeDefinedField.getAbsoluteName();
ExtendedFieldType nestedType = FieldHelper.getType( bridgeDefinedField );
if ( ExtendedFieldType.OBJECT.equals( nestedType ) ) {
objectFieldNames.add( nestedAbsoluteName );
}
else {
PrimitiveProjection projection =
new PrimitiveProjection( rootTypeMetadata, nestedAbsoluteName, type );
primitiveProjections.put( nestedAbsoluteName, projection );
}
sourceFilterCollector.add( new JsonPrimitive( nestedAbsoluteName ) );
}
if ( !objectFieldNames.contains( absoluteName )
&& !primitiveProjections.containsKey( absoluteName ) ) {
/*
* The default field was not overridden: add it to the projection
* just in case we're not dealing with a MetadataProvidingFieldBridge.
*/
PrimitiveProjection defaultFieldProjection = new PrimitiveProjection( rootTypeMetadata, absoluteName, type );
primitiveProjections.put( absoluteName, defaultFieldProjection );
sourceFilterCollector.add( new JsonPrimitive( absoluteName ) );
}
return new TwoWayFieldBridgeProjection(
absoluteName, (TwoWayFieldBridge) fieldBridge, objectFieldNames, primitiveProjections
);
}
else {
/*
* Don't fail immediately: this entity type may not be present in the results, in which case
* we don't need to be able to project on this field for this exact entity type.
* Just make sure we *will* ultimately fail if we encounter this entity type.
*/
return new FailingOneWayFieldBridgeProjection( absoluteName, fieldBridge.getClass() );
}
}
}
}