diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/impl/IndexManagerBackendContext.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/impl/IndexManagerBackendContext.java index 739ac24c325..bd1c7027964 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/impl/IndexManagerBackendContext.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/index/impl/IndexManagerBackendContext.java @@ -156,7 +156,7 @@ public ElasticsearchSearchQueryBuilder createSearchQueryBuilder( ElasticsearchSearchIndexScope scope, BackendSessionContext sessionContext, SearchLoadingContextBuilder loadingContextBuilder, - ElasticsearchSearchProjection rootProjection) { + ElasticsearchSearchProjection rootProjection) { multiTenancyStrategy.documentIdHelper().checkTenantId( sessionContext.tenantIdentifier(), eventContext ); return new ElasticsearchSearchQueryBuilder<>( link.getWorkBuilderFactory(), link.getSearchResultExtractorFactory(), diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/Log.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/Log.java index d82ea514bca..ce586f563d8 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/Log.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/logging/impl/Log.java @@ -709,4 +709,21 @@ SearchException customIndexMappingErrorOnLoading(String filePath, String causeMe SearchException customIndexMappingJsonSyntaxErrors(String filePath, String causeMessage, @Cause Exception cause, @Param EventContext context); + @Message(id = ID_OFFSET + 154, + value = "Invalid context for projection on field '%1$s': the surrounding projection" + + " is executed for each object in field '%2$s', which is not a parent of field '%1$s'." + + " Check the structure of your projections.") + SearchException invalidContextForProjectionOnField(String absolutePath, + String objectFieldAbsolutePath); + + @Message(id = ID_OFFSET + 155, + value = "Invalid cardinality for projection on field '%1$s': the projection is single-valued," + + " but this field is effectively multi-valued in this context," + + " because parent object field '%2$s' is multi-valued." + + " Either call '.multi()' when you create the projection on field '%1$s'," + + " or wrap that projection in an object projection like this:" + + " 'f.object(\"%2$s\").from().as(...).multi()'.") + SearchException invalidSingleValuedProjectionOnValueFieldInMultiValuedObjectField(String absolutePath, + String objectFieldAbsolutePath); + } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/scope/model/impl/ElasticsearchSearchIndexScopeImpl.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/scope/model/impl/ElasticsearchSearchIndexScopeImpl.java index b6c03380503..8b4e5d781d9 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/scope/model/impl/ElasticsearchSearchIndexScopeImpl.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/scope/model/impl/ElasticsearchSearchIndexScopeImpl.java @@ -192,6 +192,11 @@ public ElasticsearchSearchAggregationFactory aggregationFactory() { return new ElasticsearchSearchAggregationFactoryImpl( SearchAggregationDslContext.root( this, predicateFactory() ) ); } + @Override + public ElasticsearchSearchIndexNodeContext field(String fieldPath) { + return super.field( fieldPath ); + } + @Override public Gson userFacingGson() { return userFacingGson; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchMultiIndexSearchIndexCompositeNodeContext.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchMultiIndexSearchIndexCompositeNodeContext.java index 44d4807442f..5d0d18a5d52 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchMultiIndexSearchIndexCompositeNodeContext.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchMultiIndexSearchIndexCompositeNodeContext.java @@ -19,7 +19,7 @@ public final class ElasticsearchMultiIndexSearchIndexCompositeNodeContext ElasticsearchSearchIndexNodeContext > implements ElasticsearchSearchIndexCompositeNodeContext, - ElasticsearchSearchIndexCompositeNodeTypeContext { + ElasticsearchSearchIndexCompositeNodeTypeContext { public ElasticsearchMultiIndexSearchIndexCompositeNodeContext(ElasticsearchSearchIndexScope scope, String absolutePath, diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchMultiIndexSearchIndexValueFieldContext.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchMultiIndexSearchIndexValueFieldContext.java index 9a928852d5e..1c18725e88b 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchMultiIndexSearchIndexValueFieldContext.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchMultiIndexSearchIndexValueFieldContext.java @@ -49,6 +49,11 @@ public ElasticsearchSearchIndexCompositeNodeContext toComposite() { return SearchIndexSchemaElementContextHelper.throwingToComposite( this ); } + @Override + public ElasticsearchSearchIndexCompositeNodeContext toObjectField() { + return SearchIndexSchemaElementContextHelper.throwingToObjectField( this ); + } + @Override public JsonPrimitive elasticsearchTypeAsJson() { return fromTypeIfCompatible( ElasticsearchSearchIndexValueFieldTypeContext::elasticsearchTypeAsJson, Object::equals, diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchSearchIndexNodeContext.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchSearchIndexNodeContext.java index da609e44db8..cc6729856b0 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchSearchIndexNodeContext.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchSearchIndexNodeContext.java @@ -14,6 +14,9 @@ public interface ElasticsearchSearchIndexNodeContext @Override ElasticsearchSearchIndexCompositeNodeContext toComposite(); + @Override + ElasticsearchSearchIndexCompositeNodeContext toObjectField(); + @Override ElasticsearchSearchIndexValueFieldContext toValueField(); diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchSearchIndexScope.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchSearchIndexScope.java index 0c5682587b9..08c2f081a8b 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchSearchIndexScope.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/common/impl/ElasticsearchSearchIndexScope.java @@ -25,6 +25,8 @@ public interface ElasticsearchSearchIndexScope parent, String name); + ElasticsearchSearchIndexNodeContext field(String fieldPath); + Gson userFacingGson(); ElasticsearchSearchSyntax searchSyntax(); diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/AbstractElasticsearchProjection.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/AbstractElasticsearchProjection.java index cce7594edb5..04ea41fe3dc 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/AbstractElasticsearchProjection.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/AbstractElasticsearchProjection.java @@ -11,7 +11,7 @@ import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexScope; import org.hibernate.search.engine.search.projection.spi.SearchProjectionBuilder; -public abstract class AbstractElasticsearchProjection implements ElasticsearchSearchProjection { +public abstract class AbstractElasticsearchProjection

implements ElasticsearchSearchProjection

{ protected final Set indexNames; diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/AccumulatingSourceExtractor.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/AccumulatingSourceExtractor.java new file mode 100644 index 00000000000..6e893918d4c --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/AccumulatingSourceExtractor.java @@ -0,0 +1,118 @@ +/* + * 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.backend.elasticsearch.search.projection.impl; + +import java.util.Arrays; +import java.util.stream.Collectors; + +import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; +import org.hibernate.search.backend.elasticsearch.gson.impl.JsonArrayAccessor; +import org.hibernate.search.backend.elasticsearch.gson.impl.JsonElementTypes; +import org.hibernate.search.backend.elasticsearch.gson.impl.UnexpectedJsonElementTypeException; +import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; +import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +abstract class AccumulatingSourceExtractor + implements ElasticsearchSearchProjection.Extractor { + + static final JsonArrayAccessor REQUEST_SOURCE_ACCESSOR = + JsonAccessor.root().property( "_source" ).asArray(); + + private final String[] fieldPathComponents; + final ProjectionAccumulator accumulator; + + public AccumulatingSourceExtractor(String[] fieldPathComponents, + ProjectionAccumulator accumulator) { + this.fieldPathComponents = fieldPathComponents; + this.accumulator = accumulator; + } + + @Override + public final A extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, + JsonObject source, ProjectionExtractContext context) { + A accumulated = accumulator.createInitial(); + accumulated = collect( projectionHitMapper, hit, source, context, accumulated, 0 ); + return accumulated; + } + + private A collect(ProjectionHitMapper projectionHitMapper, JsonObject hit, + JsonObject parent, ProjectionExtractContext context, + A accumulated, int currentPathComponentIndex) { + if ( parent == null ) { + return accumulated; + } + + JsonElement child = parent.get( fieldPathComponents[currentPathComponentIndex] ); + + if ( currentPathComponentIndex == ( fieldPathComponents.length - 1) ) { + // We reached the field we want to collect. + return collectTargetField( projectionHitMapper, hit, child, context, accumulated ); + } + else { + // We just reached an intermediary object field leading to the field we want to collect. + if ( child == null || child.isJsonNull() ) { + // Not present + return accumulated; + } + else if ( child.isJsonArray() ) { + for ( JsonElement childElement : child.getAsJsonArray() ) { + JsonObject childElementAsObject = toJsonObject( childElement, currentPathComponentIndex ); + accumulated = collect( projectionHitMapper, hit, childElementAsObject, context, + accumulated, currentPathComponentIndex + 1 ); + } + return accumulated; + } + else { + JsonObject childAsObject = toJsonObject( child, currentPathComponentIndex ); + return collect( projectionHitMapper, hit, childAsObject, context, + accumulated, currentPathComponentIndex + 1 ); + } + } + } + + private A collectTargetField(ProjectionHitMapper projectionHitMapper, JsonObject hit, JsonElement fieldValue, + ProjectionExtractContext context, A accumulated) { + if ( fieldValue == null ) { + // Not present + return accumulated; + } + else if ( fieldValue.isJsonNull() ) { + // Present, but null + return accumulator.accumulate( accumulated, extract( projectionHitMapper, hit, fieldValue, context ) ); + } + else if ( fieldValue.isJsonArray() ) { + for ( JsonElement childElement : fieldValue.getAsJsonArray() ) { + accumulated = accumulator.accumulate( accumulated, + extract( projectionHitMapper, hit, childElement, context ) ); + } + return accumulated; + } + else { + return accumulator.accumulate( accumulated, extract( projectionHitMapper, hit, fieldValue, context ) ); + } + } + + protected abstract E extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, + JsonElement sourceElement, ProjectionExtractContext context); + + private JsonObject toJsonObject(JsonElement childElement, int currentPathComponentIndex) { + if ( childElement == null || childElement.isJsonNull() ) { + return null; + } + if ( !JsonElementTypes.OBJECT.isInstance( childElement ) ) { + throw new UnexpectedJsonElementTypeException( + Arrays.stream( fieldPathComponents, 0, currentPathComponentIndex + 1 ) + .collect( Collectors.joining( "." ) ), + JsonElementTypes.OBJECT, childElement + ); + } + return childElement.getAsJsonObject(); + } +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchCompositeProjection.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchCompositeProjection.java index ff0ab6c45ac..742c5e735dd 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchCompositeProjection.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchCompositeProjection.java @@ -12,21 +12,35 @@ import org.hibernate.search.engine.search.loading.spi.LoadingResult; import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; import org.hibernate.search.engine.search.projection.SearchProjection; +import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; import com.google.gson.JsonObject; -class ElasticsearchCompositeProjection - extends AbstractElasticsearchProjection { +/** + * A projection that composes the result of multiple inner projections into a single value. + *

+ * Not to be confused with {@link ElasticsearchObjectProjection}. + * + * @param The type of the temporary storage for component values. + * @param The type of a single composed value. + * @param The type of the temporary storage for accumulated values, before and after being composed. + * @param

The type of the final projection result representing an accumulation of composed values of type {@code V}. + */ +class ElasticsearchCompositeProjection + extends AbstractElasticsearchProjection

{ + private final ElasticsearchSearchProjection[] inners; private final ProjectionCompositor compositor; - private final ElasticsearchSearchProjection[] inners; + private final ProjectionAccumulator accumulator; - private ElasticsearchCompositeProjection(Builder builder) { + public ElasticsearchCompositeProjection(Builder builder, ElasticsearchSearchProjection[] inners, + ProjectionCompositor compositor, ProjectionAccumulator accumulator) { super( builder.scope ); - this.compositor = builder.compositor; - this.inners = builder.inners; + this.inners = inners; + this.compositor = compositor; + this.accumulator = accumulator; } @Override @@ -34,60 +48,87 @@ public String toString() { return getClass().getSimpleName() + "[" + "inners=" + Arrays.toString( inners ) + ", compositor=" + compositor + + ", accumulator=" + accumulator + "]"; } @Override - public void request(JsonObject requestBody, ProjectionRequestContext context) { - for ( ElasticsearchSearchProjection inner : inners ) { - inner.request( requestBody, context ); + public Extractor request(JsonObject requestBody, ProjectionRequestContext context) { + Extractor[] innerExtractors = new Extractor[inners.length]; + for ( int i = 0; i < inners.length; i++ ) { + innerExtractors[i] = inners[i].request( requestBody, context ); } + return new CompositeExtractor( innerExtractors ); } - @Override - public E extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, - ProjectionExtractContext context) { - E extractedData = compositor.createInitial(); + private class CompositeExtractor implements Extractor { + private final Extractor[] inners; - for ( int i = 0; i < inners.length; i++ ) { - Object extractedDataForInner = inners[i].extract( projectionHitMapper, hit, context ); - extractedData = compositor.set( extractedData, i, extractedDataForInner ); + private CompositeExtractor(Extractor[] inners) { + this.inners = inners; } - return extractedData; - } + @Override + public String toString() { + return getClass().getSimpleName() + "[" + + "inners=" + Arrays.toString( inners ) + + ", compositor=" + compositor + + ", accumulator=" + accumulator + + "]"; + } - @Override - public final V transform(LoadingResult loadingResult, E extractedData, - ProjectionTransformContext context) { - E transformedData = extractedData; - // Transform in-place - for ( int i = 0; i < inners.length; i++ ) { - Object extractedDataForInner = compositor.get( transformedData, i ); - Object transformedDataForInner = ElasticsearchSearchProjection.transformUnsafe( inners[i], loadingResult, - extractedDataForInner, context ); - transformedData = compositor.set( transformedData, i, transformedDataForInner ); + @Override + public A extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, + JsonObject source, ProjectionExtractContext context) { + A accumulated = accumulator.createInitial(); + + E components = compositor.createInitial(); + for ( int i = 0; i < inners.length; i++ ) { + Object extractedDataForInner = inners[i].extract( projectionHitMapper, hit, source, context ); + components = compositor.set( components, i, extractedDataForInner ); + } + accumulated = accumulator.accumulate( accumulated, components ); + + return accumulated; } - return compositor.finish( transformedData ); + @Override + public final P transform(LoadingResult loadingResult, A accumulated, + ProjectionTransformContext context) { + for ( int i = 0; i < accumulator.size( accumulated ); i++ ) { + E transformedData = accumulator.get( accumulated, i ); + // Transform in-place + for ( int j = 0; j < inners.length; j++ ) { + Object extractedDataForInner = compositor.get( transformedData, j ); + Object transformedDataForInner = Extractor.transformUnsafe( inners[j], + loadingResult, extractedDataForInner, context ); + transformedData = compositor.set( transformedData, j, transformedDataForInner ); + } + + accumulated = accumulator.transform( accumulated, i, compositor.finish( transformedData ) ); + } + return accumulator.finish( accumulated ); + } } - static class Builder implements CompositeProjectionBuilder { + static class Builder implements CompositeProjectionBuilder { private final ElasticsearchSearchIndexScope scope; - private final ProjectionCompositor compositor; - private final ElasticsearchSearchProjection[] inners; - Builder(ElasticsearchSearchIndexScope scope, ProjectionCompositor compositor, - ElasticsearchSearchProjection ... inners) { + Builder(ElasticsearchSearchIndexScope scope) { this.scope = scope; - this.compositor = compositor; - this.inners = inners; } @Override - public SearchProjection build() { - return new ElasticsearchCompositeProjection<>( this ); + public SearchProjection

build(SearchProjection[] inners, ProjectionCompositor compositor, + ProjectionAccumulator.Provider accumulatorProvider) { + ElasticsearchSearchProjection[] typedInners = + new ElasticsearchSearchProjection[ inners.length ]; + for ( int i = 0; i < inners.length; i++ ) { + typedInners[i] = ElasticsearchSearchProjection.from( scope, inners[i] ); + } + return new ElasticsearchCompositeProjection<>( this, typedInners, + compositor, accumulatorProvider.get() ); } } } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchDistanceToFieldProjection.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchDistanceToFieldProjection.java index 9a2ca075b02..212dbdc5c79 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchDistanceToFieldProjection.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchDistanceToFieldProjection.java @@ -17,7 +17,6 @@ import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexValueFieldContext; import org.hibernate.search.backend.elasticsearch.search.projection.util.impl.SloppyMath; import org.hibernate.search.backend.elasticsearch.types.codec.impl.ElasticsearchGeoPointFieldCodec; -import org.hibernate.search.engine.backend.types.converter.runtime.FromDocumentValueConvertContext; import org.hibernate.search.engine.backend.types.converter.spi.ProjectionConverter; import org.hibernate.search.engine.search.loading.spi.LoadingResult; import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; @@ -33,20 +32,19 @@ /** * A projection on the distance from a given center to the GeoPoint defined in an index field. * - * @param The type of aggregated values extracted from the backend response (before conversion). - * @param

The type of aggregated values returned by the projection (after conversion). + * @param The type of the temporary storage for accumulated values, before and after being transformed. + * @param

The type of the final projection result representing accumulated distance values. */ -public class ElasticsearchDistanceToFieldProjection extends AbstractElasticsearchProjection { +public class ElasticsearchDistanceToFieldProjection extends AbstractElasticsearchProjection

+ implements ElasticsearchSearchProjection.Extractor { private static final JsonObjectAccessor SCRIPT_FIELDS_ACCESSOR = JsonAccessor.root().property( "script_fields" ).asObject(); private static final JsonObjectAccessor FIELDS_ACCESSOR = JsonAccessor.root().property( "fields" ).asObject(); private static final JsonArrayAccessor SORT_ACCESSOR = JsonAccessor.root().property( "sort" ).asArray(); private static final ElasticsearchGeoPointFieldCodec CODEC = ElasticsearchGeoPointFieldCodec.INSTANCE; - private static final ProjectionConverter NO_OP_DOUBLE_CONVERTER = new ProjectionConverter<>( - Double.class, - (value, context) -> value - ); + private static final ProjectionConverter NO_OP_DOUBLE_CONVERTER = + ProjectionConverter.passThrough( Double.class ); private static final Pattern NON_DIGITS_PATTERN = Pattern.compile( "\\D" ); @@ -61,25 +59,26 @@ public class ElasticsearchDistanceToFieldProjection extends AbstractElasti " }"; private final String absoluteFieldPath; - private final boolean multiValued; + private final boolean singleValuedInRoot; private final GeoPoint center; private final DistanceUnit unit; - private final ProjectionAccumulator accumulator; + private final ProjectionAccumulator accumulator; private final String scriptFieldName; - private final ElasticsearchFieldProjection sourceProjection; + private final ElasticsearchFieldProjection sourceProjection; - private ElasticsearchDistanceToFieldProjection(Builder builder, boolean multiValued, - ProjectionAccumulator accumulator) { + private ElasticsearchDistanceToFieldProjection(Builder builder, + ProjectionAccumulator.Provider accumulatorProvider, + ProjectionAccumulator accumulator) { super( builder ); this.absoluteFieldPath = builder.field.absolutePath(); - this.multiValued = multiValued; + this.singleValuedInRoot = !builder.field.multiValuedInRoot(); this.center = builder.center; this.unit = builder.unit; this.accumulator = accumulator; - if ( !multiValued && builder.field.nestedPathHierarchy().isEmpty() ) { + if ( singleValuedInRoot && builder.field.nestedPathHierarchy().isEmpty() ) { // Rely on docValues when there is no sort to extract the distance from. scriptFieldName = createScriptFieldName( absoluteFieldPath, center ); sourceProjection = null; @@ -88,28 +87,28 @@ private ElasticsearchDistanceToFieldProjection(Builder builder, boolean multiVal // Rely on _source when there is no sort to extract the distance from. scriptFieldName = null; this.sourceProjection = new ElasticsearchFieldProjection<>( - builder.scope, absoluteFieldPath, builder.field.absolutePathComponents(), - this::computeDistanceWithUnit, NO_OP_DOUBLE_CONVERTER, accumulator + builder.scope, builder.field, + this::computeDistanceWithUnit, NO_OP_DOUBLE_CONVERTER, accumulatorProvider ); } } @Override public String toString() { - StringBuilder sb = new StringBuilder( getClass().getSimpleName() ) - .append( "[" ) - .append( "absoluteFieldPath=" ).append( absoluteFieldPath ) - .append( ", center=" ).append( center ) - .append( ", unit=" ).append( unit ) - .append( ", accumulator=" ).append( accumulator ) - .append( "]" ); - return sb.toString(); + return getClass().getSimpleName() + "[" + + "absoluteFieldPath=" + absoluteFieldPath + + ", center=" + center + + ", unit=" + unit + + ", accumulator=" + accumulator + + "]"; } @Override - public void request(JsonObject requestBody, ProjectionRequestContext context) { - if ( !multiValued && context.getDistanceSortIndex( absoluteFieldPath, center ) != null ) { + public Extractor request(JsonObject requestBody, ProjectionRequestContext context) { + context.checkValidField( absoluteFieldPath ); + if ( singleValuedInRoot && context.getDistanceSortIndex( absoluteFieldPath, center ) != null ) { // Nothing to do, we'll rely on the sort key + return this; } else if ( scriptFieldName != null ) { // we rely on a script to compute the distance @@ -117,38 +116,36 @@ else if ( scriptFieldName != null ) { .property( scriptFieldName ).asObject() .property( "script" ).asObject() .set( requestBody, createScript( absoluteFieldPath, center ) ); + return this; } else { // we rely on the _source to compute the distance - sourceProjection.request( requestBody, context ); + return sourceProjection.request( requestBody, context ); } } @Override - public E extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, - ProjectionExtractContext context) { - Integer distanceSortIndex = multiValued ? null : context.getDistanceSortIndex( absoluteFieldPath, center ); + public A extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, + JsonObject source, ProjectionExtractContext context) { + Integer distanceSortIndex = singleValuedInRoot ? context.getDistanceSortIndex( absoluteFieldPath, center ) : null; if ( distanceSortIndex != null ) { - E accumulated = accumulator.createInitial(); + A accumulated = accumulator.createInitial(); accumulated = accumulator.accumulate( accumulated, extractDistanceFromSortKey( hit, distanceSortIndex ) ); return accumulated; } - else if ( scriptFieldName != null ) { - E accumulated = accumulator.createInitial(); + else { + A accumulated = accumulator.createInitial(); accumulated = accumulator.accumulate( accumulated, extractDistanceFromScriptField( hit ) ); return accumulated; } - else { - return sourceProjection.extract( projectionHitMapper, hit, context ); - } } @Override - public P transform(LoadingResult loadingResult, E extractedData, + public P transform(LoadingResult loadingResult, A extractedData, ProjectionTransformContext context) { - FromDocumentValueConvertContext convertContext = context.fromDocumentValueConvertContext(); - return accumulator.finish( extractedData, NO_OP_DOUBLE_CONVERTER, convertContext ); + // Nothing to transform: we take the values as they are. + return accumulator.finish( extractedData ); } private Double extractDistanceFromScriptField(JsonObject hit) { @@ -258,7 +255,7 @@ public void unit(DistanceUnit unit) { @Override public

SearchProjection

build(ProjectionAccumulator.Provider accumulatorProvider) { - return new ElasticsearchDistanceToFieldProjection<>( this, !accumulatorProvider.isSingleValued(), + return new ElasticsearchDistanceToFieldProjection<>( this, accumulatorProvider, accumulatorProvider.get() ); } } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchDocumentReferenceProjection.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchDocumentReferenceProjection.java index a95f45e5fbe..a8a3a9a3c09 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchDocumentReferenceProjection.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchDocumentReferenceProjection.java @@ -16,7 +16,8 @@ import com.google.gson.JsonObject; class ElasticsearchDocumentReferenceProjection - extends AbstractElasticsearchProjection { + extends AbstractElasticsearchProjection + implements ElasticsearchSearchProjection.Extractor { private final DocumentReferenceExtractionHelper helper; @@ -32,13 +33,14 @@ public String toString() { } @Override - public void request(JsonObject requestBody, ProjectionRequestContext context) { + public Extractor request(JsonObject requestBody, ProjectionRequestContext context) { helper.request( requestBody, context ); + return this; } @Override public DocumentReference extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, - ProjectionExtractContext context) { + JsonObject source, ProjectionExtractContext context) { return helper.extract( hit, context ); } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchEntityProjection.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchEntityProjection.java index b601ae92bc4..f8ca4280fd8 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchEntityProjection.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchEntityProjection.java @@ -14,7 +14,8 @@ import com.google.gson.JsonObject; -public class ElasticsearchEntityProjection extends AbstractElasticsearchProjection { +public class ElasticsearchEntityProjection extends AbstractElasticsearchProjection + implements ElasticsearchSearchProjection.Extractor { private final DocumentReferenceExtractionHelper helper; @@ -30,13 +31,14 @@ public String toString() { } @Override - public void request(JsonObject requestBody, ProjectionRequestContext context) { + public Extractor request(JsonObject requestBody, ProjectionRequestContext context) { helper.request( requestBody, context ); + return this; } @Override public Object extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, - ProjectionExtractContext context) { + JsonObject source, ProjectionExtractContext context) { return projectionHitMapper.planLoading( helper.extract( hit, context ) ); } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchEntityReferenceProjection.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchEntityReferenceProjection.java index 39a6a9c4e7a..e01822756f5 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchEntityReferenceProjection.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchEntityReferenceProjection.java @@ -15,7 +15,8 @@ import com.google.gson.JsonObject; -public class ElasticsearchEntityReferenceProjection extends AbstractElasticsearchProjection { +public class ElasticsearchEntityReferenceProjection extends AbstractElasticsearchProjection + implements ElasticsearchSearchProjection.Extractor { private final DocumentReferenceExtractionHelper helper; @@ -30,14 +31,14 @@ public String toString() { } @Override - public void request(JsonObject requestBody, ProjectionRequestContext context) { + public Extractor request(JsonObject requestBody, ProjectionRequestContext context) { helper.request( requestBody, context ); + return this; } - @SuppressWarnings("unchecked") @Override public DocumentReference extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, - ProjectionExtractContext context) { + JsonObject source, ProjectionExtractContext context) { return helper.extract( hit, context ); } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchExplanationProjection.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchExplanationProjection.java index 7c4da9ab0bd..7b84ed4dc94 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchExplanationProjection.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchExplanationProjection.java @@ -15,7 +15,8 @@ import com.google.gson.JsonObject; -class ElasticsearchExplanationProjection extends AbstractElasticsearchProjection { +class ElasticsearchExplanationProjection extends AbstractElasticsearchProjection + implements ElasticsearchSearchProjection.Extractor { private static final JsonAccessor REQUEST_EXPLAIN_ACCESSOR = JsonAccessor.root().property( "explain" ).asBoolean(); private static final JsonObjectAccessor HIT_EXPLANATION_ACCESSOR = JsonAccessor.root().property( "_explanation" ).asObject(); @@ -30,13 +31,14 @@ public String toString() { } @Override - public void request(JsonObject requestBody, ProjectionRequestContext context) { + public Extractor request(JsonObject requestBody, ProjectionRequestContext context) { REQUEST_EXPLAIN_ACCESSOR.set( requestBody, true ); + return this; } @Override public JsonObject extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, - ProjectionExtractContext context) { + JsonObject source, ProjectionExtractContext context) { // We expect the optional to always be non-empty. return HIT_EXPLANATION_ACCESSOR.get( hit ).get(); } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchFieldProjection.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchFieldProjection.java index c3437d5a2a3..c99dca60df4 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchFieldProjection.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchFieldProjection.java @@ -7,15 +7,8 @@ package org.hibernate.search.backend.elasticsearch.search.projection.impl; import java.lang.invoke.MethodHandles; -import java.util.Arrays; import java.util.function.Function; -import java.util.stream.Collectors; -import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; -import org.hibernate.search.backend.elasticsearch.gson.impl.JsonArrayAccessor; -import org.hibernate.search.backend.elasticsearch.gson.impl.JsonElementTypes; -import org.hibernate.search.backend.elasticsearch.gson.impl.JsonObjectAccessor; -import org.hibernate.search.backend.elasticsearch.gson.impl.UnexpectedJsonElementTypeException; import org.hibernate.search.backend.elasticsearch.logging.impl.Log; import org.hibernate.search.backend.elasticsearch.search.common.impl.AbstractElasticsearchCodecAwareSearchQueryElementFactory; import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexScope; @@ -38,123 +31,91 @@ /** * A projection on the values of an index field. * - * @param The type of aggregated values extracted from the backend response (before conversion). - * @param

The type of aggregated values returned by the projection (after conversion). * @param The type of individual field values obtained from the backend (before conversion). * @param The type of individual field values after conversion. + * @param

The type of the final projection result representing accumulated values of type {@code V}. */ -public class ElasticsearchFieldProjection extends AbstractElasticsearchProjection { +public class ElasticsearchFieldProjection extends AbstractElasticsearchProjection

{ - private static final JsonArrayAccessor REQUEST_SOURCE_ACCESSOR = JsonAccessor.root().property( "_source" ).asArray(); - private static final JsonObjectAccessor HIT_SOURCE_ACCESSOR = JsonAccessor.root().property( "_source" ).asObject(); + private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() ); private final String absoluteFieldPath; private final String[] absoluteFieldPathComponents; + private final String requiredContextAbsoluteFieldPath; private final Function decodeFunction; private final ProjectionConverter converter; - private final ProjectionAccumulator accumulator; + private final ProjectionAccumulator.Provider accumulatorProvider; - private ElasticsearchFieldProjection(Builder builder, ProjectionAccumulator accumulator) { - this( builder.scope, builder.field.absolutePath(), builder.field.absolutePathComponents(), - builder.codec::decode, builder.converter, accumulator ); + private ElasticsearchFieldProjection(Builder builder, + ProjectionAccumulator.Provider accumulatorProvider) { + this( builder.scope, builder.field, builder.codec::decode, builder.converter, accumulatorProvider ); } ElasticsearchFieldProjection(ElasticsearchSearchIndexScope scope, - String absoluteFieldPath, String[] absoluteFieldPathComponents, + ElasticsearchSearchIndexValueFieldContext field, Function decodeFunction, ProjectionConverter converter, - ProjectionAccumulator accumulator) { + ProjectionAccumulator.Provider accumulatorProvider) { super( scope ); - this.absoluteFieldPath = absoluteFieldPath; - this.absoluteFieldPathComponents = absoluteFieldPathComponents; + this.absoluteFieldPath = field.absolutePath(); + this.absoluteFieldPathComponents = field.absolutePathComponents(); + this.requiredContextAbsoluteFieldPath = accumulatorProvider.isSingleValued() + ? field.closestMultiValuedParentAbsolutePath() : null; this.decodeFunction = decodeFunction; this.converter = converter; - this.accumulator = accumulator; + this.accumulatorProvider = accumulatorProvider; } @Override public String toString() { return getClass().getSimpleName() + "[" + "absoluteFieldPath=" + absoluteFieldPath - + ", accumulator=" + accumulator + + ", accumulatorProvider=" + accumulatorProvider + "]"; } @Override - public void request(JsonObject requestBody, ProjectionRequestContext context) { + public ValueFieldExtractor request(JsonObject requestBody, ProjectionRequestContext context) { + ProjectionRequestContext innerContext = context.forField( absoluteFieldPath, absoluteFieldPathComponents ); + if ( requiredContextAbsoluteFieldPath != null + && !requiredContextAbsoluteFieldPath.equals( context.absoluteCurrentFieldPath() ) ) { + throw log.invalidSingleValuedProjectionOnValueFieldInMultiValuedObjectField( + absoluteFieldPath, requiredContextAbsoluteFieldPath ); + } JsonPrimitive fieldPathJson = new JsonPrimitive( absoluteFieldPath ); - REQUEST_SOURCE_ACCESSOR.addElementIfAbsent( requestBody, fieldPathJson ); - } - - @Override - public E extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, - ProjectionExtractContext context) { - E extracted = accumulator.createInitial(); - JsonObject source = HIT_SOURCE_ACCESSOR.get( hit ).get(); - extracted = collect( source, extracted, 0 ); - return extracted; - } - - @Override - public P transform(LoadingResult loadingResult, E extractedData, ProjectionTransformContext context) { - FromDocumentValueConvertContext convertContext = context.fromDocumentValueConvertContext(); - return accumulator.finish( extractedData, converter, convertContext ); + AccumulatingSourceExtractor.REQUEST_SOURCE_ACCESSOR.addElementIfAbsent( requestBody, fieldPathJson ); + return new ValueFieldExtractor<>( innerContext.relativeCurrentFieldPathComponents(), accumulatorProvider.get() ); } - private E collect(JsonObject parent, E accumulated, int currentPathComponentIndex) { - JsonElement child = parent.get( absoluteFieldPathComponents[currentPathComponentIndex] ); + /** + * @param The type of the temporary storage for accumulated values, before and after being transformed. + */ + private class ValueFieldExtractor extends AccumulatingSourceExtractor { + public ValueFieldExtractor(String[] fieldPathComponents, ProjectionAccumulator accumulator) { + super( fieldPathComponents, accumulator ); + } - if ( currentPathComponentIndex == (absoluteFieldPathComponents.length - 1) ) { - // We reached the field we want to collect. - if ( child == null ) { - // Not present - return accumulated; - } - else if ( child.isJsonNull() ) { - // Present, but null - return accumulator.accumulate( accumulated, null ); - } - else if ( child.isJsonArray() ) { - for ( JsonElement childElement : child.getAsJsonArray() ) { - F decoded = decodeFunction.apply( childElement ); - accumulated = accumulator.accumulate( accumulated, decoded ); - } - return accumulated; - } - else { - F decoded = decodeFunction.apply( child ); - return accumulator.accumulate( accumulated, decoded ); - } + @Override + public String toString() { + return getClass().getSimpleName() + "[" + + "absoluteFieldPath=" + absoluteFieldPath + + ", accumulator=" + accumulator + + "]"; } - else { - // We just reached an intermediary object field leading to the field we want to collect. - if ( child == null || child.isJsonNull() ) { - // Not present - return accumulated; - } - else if ( child.isJsonArray() ) { - for ( JsonElement childElement : child.getAsJsonArray() ) { - JsonObject childElementAsObject = toJsonObject( childElement, currentPathComponentIndex ); - accumulated = collect( childElementAsObject, accumulated, currentPathComponentIndex + 1 ); - } - return accumulated; - } - else { - JsonObject childAsObject = toJsonObject( child, currentPathComponentIndex ); - return collect( childAsObject, accumulated, currentPathComponentIndex + 1 ); - } + + @Override + protected F extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, JsonElement sourceElement, + ProjectionExtractContext context) { + return decodeFunction.apply( sourceElement ); } - } - private JsonObject toJsonObject(JsonElement childElement, int currentPathComponentIndex) { - if ( !JsonElementTypes.OBJECT.isInstance( childElement ) ) { - throw new UnexpectedJsonElementTypeException( - Arrays.stream( absoluteFieldPathComponents, 0, currentPathComponentIndex + 1 ) - .collect( Collectors.joining( "." ) ), - JsonElementTypes.OBJECT, childElement - ); + @Override + public P transform(LoadingResult loadingResult, A extractedData, + ProjectionTransformContext context) { + FromDocumentValueConvertContext convertContext = context.fromDocumentValueConvertContext(); + A transformedData = accumulator.transformAll( extractedData, converter, convertContext ); + return accumulator.finish( transformedData ); } - return childElement.getAsJsonObject(); } public static class Factory @@ -211,11 +172,11 @@ private Builder(ElasticsearchFieldCodec codec, ElasticsearchSearchIndexScope< } @Override - public SearchProjection build(ProjectionAccumulator.Provider accumulatorProvider) { - if ( accumulatorProvider.isSingleValued() && field.multiValuedInRoot() ) { + public

SearchProjection

build(ProjectionAccumulator.Provider accumulatorProvider) { + if ( accumulatorProvider.isSingleValued() && field.multiValued() ) { throw log.invalidSingleValuedProjectionOnMultiValuedField( field.absolutePath(), field.eventContext() ); } - return new ElasticsearchFieldProjection<>( this, accumulatorProvider.get() ); + return new ElasticsearchFieldProjection<>( this, accumulatorProvider ); } } } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchIdProjection.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchIdProjection.java index dccfce59003..af1fba2b62b 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchIdProjection.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchIdProjection.java @@ -15,7 +15,8 @@ import com.google.gson.JsonObject; -public class ElasticsearchIdProjection extends AbstractElasticsearchProjection { +public class ElasticsearchIdProjection extends AbstractElasticsearchProjection + implements ElasticsearchSearchProjection.Extractor { private final ProjectionExtractionHelper extractionHelper; private final ProjectionConverter converter; @@ -34,13 +35,14 @@ public String toString() { } @Override - public void request(JsonObject requestBody, ProjectionRequestContext context) { + public Extractor request(JsonObject requestBody, ProjectionRequestContext context) { extractionHelper.request( requestBody, context ); + return this; } @Override public String extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, - ProjectionExtractContext context) { + JsonObject source, ProjectionExtractContext context) { return extractionHelper.extract( hit, context ); } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchJsonHitProjection.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchJsonHitProjection.java index b4812011640..f6753af8208 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchJsonHitProjection.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchJsonHitProjection.java @@ -14,7 +14,8 @@ import com.google.gson.JsonObject; -class ElasticsearchJsonHitProjection extends AbstractElasticsearchProjection { +class ElasticsearchJsonHitProjection extends AbstractElasticsearchProjection + implements ElasticsearchSearchProjection.Extractor { private ElasticsearchJsonHitProjection(ElasticsearchSearchIndexScope scope) { super( scope ); @@ -26,13 +27,13 @@ public String toString() { } @Override - public void request(JsonObject requestBody, ProjectionRequestContext context) { - // No-op + public Extractor request(JsonObject requestBody, ProjectionRequestContext context) { + return this; } @Override public JsonObject extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, - ProjectionExtractContext context) { + JsonObject source, ProjectionExtractContext context) { return hit; } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchObjectProjection.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchObjectProjection.java new file mode 100644 index 00000000000..edda778fa1e --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchObjectProjection.java @@ -0,0 +1,176 @@ +/* + * 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.backend.elasticsearch.search.projection.impl; + +import java.util.Arrays; + +import org.hibernate.search.backend.elasticsearch.search.common.impl.AbstractElasticsearchCompositeNodeSearchQueryElementFactory; +import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexCompositeNodeContext; +import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexScope; +import org.hibernate.search.engine.search.loading.spi.LoadingResult; +import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; +import org.hibernate.search.engine.search.projection.SearchProjection; +import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; +import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; +import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +/** + * A projection that yields one composite value per object in a given object field. + *

+ * Not to be confused with {@link ElasticsearchCompositeProjection}. + * + * @param The type of the temporary storage for component values. + * @param The type of a single composed value. + * @param

The type of the final projection result representing an accumulation of composed values of type {@code V}. + */ +public class ElasticsearchObjectProjection + extends AbstractElasticsearchProjection

{ + + private final String absoluteFieldPath; + private final String[] absoluteFieldPathComponents; + private final String requiredContextAbsoluteFieldPath; + private final ElasticsearchSearchProjection[] inners; + private final ProjectionCompositor compositor; + private final ProjectionAccumulator.Provider accumulatorProvider; + + public ElasticsearchObjectProjection(Builder builder, ElasticsearchSearchProjection[] inners, + ProjectionCompositor compositor, ProjectionAccumulator.Provider accumulatorProvider) { + super( builder.scope ); + this.absoluteFieldPath = builder.objectField.absolutePath(); + this.absoluteFieldPathComponents = builder.objectField.absolutePathComponents(); + this.requiredContextAbsoluteFieldPath = accumulatorProvider.isSingleValued() + ? builder.objectField.closestMultiValuedParentAbsolutePath() : null; + this.inners = inners; + this.compositor = compositor; + this.accumulatorProvider = accumulatorProvider; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + + "inners=" + Arrays.toString( inners ) + + ", compositor=" + compositor + + ", accumulatorProvider=" + accumulatorProvider + + "]"; + } + + @Override + public Extractor request(JsonObject requestBody, ProjectionRequestContext context) { + ProjectionRequestContext innerContext = context.forField( absoluteFieldPath, absoluteFieldPathComponents ); + if ( requiredContextAbsoluteFieldPath != null + && !requiredContextAbsoluteFieldPath.equals( context.absoluteCurrentFieldPath() ) ) { + throw log.invalidSingleValuedProjectionOnValueFieldInMultiValuedObjectField( + absoluteFieldPath, requiredContextAbsoluteFieldPath ); + } + String[] extractorFieldPathComponents = innerContext.relativeCurrentFieldPathComponents(); + JsonPrimitive fieldPathJson = new JsonPrimitive( absoluteFieldPath ); + AccumulatingSourceExtractor.REQUEST_SOURCE_ACCESSOR.addElementIfAbsent( requestBody, fieldPathJson ); + Extractor[] innerExtractors = new Extractor[inners.length]; + for ( int i = 0; i < inners.length; i++ ) { + innerExtractors[i] = inners[i].request( requestBody, innerContext ); + } + return new ObjectFieldExtractor<>( extractorFieldPathComponents, accumulatorProvider.get(), innerExtractors ); + } + + /** + * @param The type of the temporary storage for accumulated values, before and after being composed. + */ + private class ObjectFieldExtractor extends AccumulatingSourceExtractor { + private final Extractor[] inners; + + private ObjectFieldExtractor(String[] fieldPathComponents, ProjectionAccumulator accumulator, + Extractor[] inners) { + super( fieldPathComponents, accumulator ); + this.inners = inners; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + + "inners=" + Arrays.toString( inners ) + + ", compositor=" + compositor + + ", accumulator=" + accumulator + + "]"; + } + + @Override + protected E extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, JsonElement sourceElement, + ProjectionExtractContext context) { + if ( sourceElement == null || sourceElement.isJsonNull() ) { + return null; + } + JsonObject sourceObject = sourceElement.getAsJsonObject(); + E components = compositor.createInitial(); + for ( int i = 0; i < inners.length; i++ ) { + Object extractedDataForInner = inners[i].extract( projectionHitMapper, hit, sourceObject, context ); + components = compositor.set( components, i, extractedDataForInner ); + } + return components; + } + + @Override + public final P transform(LoadingResult loadingResult, A accumulated, + ProjectionTransformContext context) { + for ( int i = 0; i < accumulator.size( accumulated ); i++ ) { + E transformedData = accumulator.get( accumulated, i ); + if ( transformedData == null ) { + continue; + } + // Transform in-place + for ( int j = 0; j < inners.length; j++ ) { + Object extractedDataForInner = compositor.get( transformedData, j ); + Object transformedDataForInner = Extractor.transformUnsafe( inners[j], + loadingResult, extractedDataForInner, context ); + transformedData = compositor.set( transformedData, j, transformedDataForInner ); + } + + accumulated = accumulator.transform( accumulated, i, compositor.finish( transformedData ) ); + } + return accumulator.finish( accumulated ); + } + } + + public static class Factory + extends AbstractElasticsearchCompositeNodeSearchQueryElementFactory { + @Override + public Builder create(ElasticsearchSearchIndexScope scope, + ElasticsearchSearchIndexCompositeNodeContext objectField) { + return new Builder( scope, objectField ); + } + } + + static class Builder implements CompositeProjectionBuilder { + + private final ElasticsearchSearchIndexScope scope; + private final ElasticsearchSearchIndexCompositeNodeContext objectField; + + Builder(ElasticsearchSearchIndexScope scope, ElasticsearchSearchIndexCompositeNodeContext objectField) { + this.scope = scope; + this.objectField = objectField; + } + + @Override + public SearchProjection

build(SearchProjection[] inners, ProjectionCompositor compositor, + ProjectionAccumulator.Provider accumulatorProvider) { + if ( accumulatorProvider.isSingleValued() && objectField.multiValued() ) { + throw log.invalidSingleValuedProjectionOnMultiValuedField( objectField.absolutePath(), + objectField.eventContext() ); + } + ElasticsearchSearchProjection[] typedInners = + new ElasticsearchSearchProjection[ inners.length ]; + for ( int i = 0; i < inners.length; i++ ) { + typedInners[i] = ElasticsearchSearchProjection.from( scope, inners[i] ); + } + return new ElasticsearchObjectProjection<>( this, typedInners, + compositor, accumulatorProvider ); + } + } +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchScoreProjection.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchScoreProjection.java index 952f0d82e5d..4d0c104724e 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchScoreProjection.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchScoreProjection.java @@ -15,7 +15,8 @@ import com.google.gson.JsonObject; -class ElasticsearchScoreProjection extends AbstractElasticsearchProjection { +class ElasticsearchScoreProjection extends AbstractElasticsearchProjection + implements ElasticsearchSearchProjection.Extractor { private static final JsonAccessor TRACK_SCORES_ACCESSOR = JsonAccessor.root().property( "track_scores" ) .asBoolean(); @@ -30,13 +31,14 @@ public String toString() { } @Override - public void request(JsonObject requestBody, ProjectionRequestContext context) { + public Extractor request(JsonObject requestBody, ProjectionRequestContext context) { TRACK_SCORES_ACCESSOR.set( requestBody, true ); + return this; } @Override public Float extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, - ProjectionExtractContext context) { + JsonObject source, ProjectionExtractContext context) { return hit.get( "_score" ).getAsFloat(); } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchSearchProjection.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchSearchProjection.java index f46638d2803..7eb4230f7bd 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchSearchProjection.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchSearchProjection.java @@ -18,55 +18,82 @@ import com.google.gson.JsonObject; -public interface ElasticsearchSearchProjection extends SearchProjection

{ +public interface ElasticsearchSearchProjection

extends SearchProjection

{ Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() ); Set indexNames(); /** - * Contribute to the request, making sure that the requirements for this projection are met. + * Contributes to the request, making sure that the requirements for this projection are met, + * and creating an {@link Extractor} that will be able to extract the result of the projection + * from the Elasticsearch response. + * * @param requestBody The request body. - * @param context An execution context that will share state with the context passed to - * {@link #extract(ProjectionHitMapper, JsonObject, ProjectionExtractContext)}. + * @param context An execution context for the request. + * @return An {@link Extractor}, to extract the result of the projection from the Elasticsearch response. */ - void request(JsonObject requestBody, ProjectionRequestContext context); + Extractor request(JsonObject requestBody, ProjectionRequestContext context); /** - * Perform hit extraction. - *

- * Implementations should only perform operations relative to extracting content from the index, - * delaying operations that rely on the mapper until - * {@link #transform(LoadingResult, Object, ProjectionTransformContext)} is called, - * so that blocking mapper operations (if any) do not pollute backend threads. + * An object responsible for extracting data from the Elasticsearch response, + * to implement a projection. * - * @param projectionHitMapper The projection hit mapper used to transform hits to entities. - * @param hit The part of the response body relevant to the hit to extract. - * @param context An execution context for the extraction. - * @return The element extracted from the hit. Might be a key referring to an object that will be loaded by the - * {@link ProjectionHitMapper}. This returned object will be passed to {@link #transform(LoadingResult, Object, ProjectionTransformContext)}. + * @param The type of temporary values extracted from the response. May be the same as {@link P}, or not, + * depending on implementation. + * @param

The type of projected values. */ - E extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, - ProjectionExtractContext context); + interface Extractor { - /** - * Transform the extracted data to the actual projection result. - * - * @param loadingResult Container containing all the entities that have been loaded by the - * {@link ProjectionHitMapper}. - * @param extractedData The extracted data to transform, coming from the - * {@link #extract(ProjectionHitMapper, JsonObject, ProjectionExtractContext)} method. - * @param context An execution context for the transforming. - * @return The final result considered as a hit. - */ - P transform(LoadingResult loadingResult, E extractedData, ProjectionTransformContext context); + /** + * Performs hit extraction. + *

+ * Implementations should only perform operations relative to extracting content from the index, + * delaying operations that rely on the mapper until + * {@link #transform(LoadingResult, Object, ProjectionTransformContext)} is called, + * so that blocking mapper operations (if any) do not pollute backend threads. + * + * @param projectionHitMapper The projection hit mapper used to transform hits to entities. + * @param hit The part of the response body relevant to the hit to extract. + * @param source The part of the source that this extractor should extract from (if relevant). + * @param context An execution context for the extraction. + * @return The element extracted from the hit. Might be a key referring to an object that will be loaded by the + * {@link ProjectionHitMapper}. + * This returned object will be passed to {@link #transform(LoadingResult, Object, ProjectionTransformContext)}. + */ + E extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, + JsonObject source, ProjectionExtractContext context); + + /** + * Transforms the extracted data to the actual projection result. + * + * @param loadingResult Container containing all the entities that have been loaded by the + * {@link ProjectionHitMapper}. + * @param extractedData The extracted data to transform, coming from the + * {@link #extract(ProjectionHitMapper, JsonObject, JsonObject, ProjectionExtractContext)} method. + * @param context An execution context for the transforming. + * @return The final result considered as a hit. + */ + P transform(LoadingResult loadingResult, E extractedData, ProjectionTransformContext context); - static

ElasticsearchSearchProjection from(ElasticsearchSearchIndexScope scope, SearchProjection

projection) { + /** + * Transforms the extracted data and casts it to the right type. + *

+ * This should be used with care as it's unsafe. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + static Z transformUnsafe(Extractor extractor, LoadingResult loadingResult, + Object extractedData, ProjectionTransformContext context) { + return (Z) ( (Extractor) extractor ).transform( loadingResult, extractedData, context ); + } + } + + static

ElasticsearchSearchProjection

from(ElasticsearchSearchIndexScope scope, SearchProjection

projection) { if ( !( projection instanceof ElasticsearchSearchProjection ) ) { throw log.cannotMixElasticsearchSearchQueryWithOtherProjections( projection ); } @SuppressWarnings("unchecked") // Necessary for ecj (Eclipse compiler) - ElasticsearchSearchProjection casted = (ElasticsearchSearchProjection) projection; + ElasticsearchSearchProjection

casted = (ElasticsearchSearchProjection

) projection; if ( !scope.hibernateSearchIndexNames().equals( casted.indexNames() ) ) { throw log.projectionDefinedOnDifferentIndexes( projection, casted.indexNames(), scope.hibernateSearchIndexNames() ); @@ -74,14 +101,4 @@ static

ElasticsearchSearchProjection from(ElasticsearchSearchIndexScop return casted; } - /** - * Transform the extracted data and cast it to the right type. - *

- * This should be used with care as it's unsafe. - */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - static Z transformUnsafe(ElasticsearchSearchProjection projection, LoadingResult loadingResult, - Object extractedData, ProjectionTransformContext context) { - return (Z) ( (ElasticsearchSearchProjection) projection ).transform( loadingResult, extractedData, context ); - } } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchSearchProjectionBuilderFactory.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchSearchProjectionBuilderFactory.java index 71540ec864d..132daec45f1 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchSearchProjectionBuilderFactory.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchSearchProjectionBuilderFactory.java @@ -6,14 +6,8 @@ */ package org.hibernate.search.backend.elasticsearch.search.projection.impl; -import java.util.List; -import java.util.function.BiFunction; -import java.util.function.Function; - import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexScope; import org.hibernate.search.engine.search.common.spi.SearchIndexIdentifierContext; -import org.hibernate.search.engine.search.projection.SearchProjection; -import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; import org.hibernate.search.engine.search.projection.spi.DocumentReferenceProjectionBuilder; import org.hibernate.search.engine.search.projection.spi.EntityProjectionBuilder; @@ -22,7 +16,6 @@ import org.hibernate.search.engine.search.projection.spi.ScoreProjectionBuilder; import org.hibernate.search.engine.search.projection.spi.SearchProjectionBuilder; import org.hibernate.search.engine.search.projection.spi.SearchProjectionBuilderFactory; -import org.hibernate.search.util.common.function.TriFunction; import com.google.gson.JsonObject; @@ -67,39 +60,8 @@ public ScoreProjectionBuilder score() { } @Override - public CompositeProjectionBuilder composite(Function, V> transformer, - SearchProjection... projections) { - ElasticsearchSearchProjection[] typedProjections = new ElasticsearchSearchProjection[ projections.length ]; - for ( int i = 0; i < projections.length; i++ ) { - typedProjections[i] = toImplementation( projections[i] ); - } - return new ElasticsearchCompositeProjection.Builder<>( scope, - ProjectionCompositor.fromList( projections.length, transformer ), - typedProjections ); - } - - @Override - public CompositeProjectionBuilder composite(Function transformer, - SearchProjection projection) { - return new ElasticsearchCompositeProjection.Builder<>( scope, - ProjectionCompositor.from( transformer ), - toImplementation( projection ) ); - } - - @Override - public CompositeProjectionBuilder composite(BiFunction transformer, - SearchProjection projection1, SearchProjection projection2) { - return new ElasticsearchCompositeProjection.Builder<>( scope, - ProjectionCompositor.from( transformer ), - toImplementation( projection1 ), toImplementation( projection2 ) ); - } - - @Override - public CompositeProjectionBuilder composite(TriFunction transformer, - SearchProjection projection1, SearchProjection projection2, SearchProjection projection3) { - return new ElasticsearchCompositeProjection.Builder<>( scope, - ProjectionCompositor.from( transformer ), - toImplementation( projection1 ), toImplementation( projection2 ), toImplementation( projection3 ) ); + public CompositeProjectionBuilder composite() { + return new ElasticsearchCompositeProjection.Builder( scope ); } public SearchProjectionBuilder source() { @@ -114,7 +76,4 @@ public SearchProjectionBuilder jsonHit() { return new ElasticsearchJsonHitProjection.Builder( scope ); } - private ElasticsearchSearchProjection toImplementation(SearchProjection projection) { - return ElasticsearchSearchProjection.from( scope, projection ); - } } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchSourceProjection.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchSourceProjection.java index 68a6eb31910..04e0046f59f 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchSourceProjection.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ElasticsearchSourceProjection.java @@ -6,11 +6,8 @@ */ package org.hibernate.search.backend.elasticsearch.search.projection.impl; -import java.util.Optional; - import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; import org.hibernate.search.backend.elasticsearch.gson.impl.JsonArrayAccessor; -import org.hibernate.search.backend.elasticsearch.gson.impl.JsonObjectAccessor; import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexScope; import org.hibernate.search.engine.search.loading.spi.LoadingResult; import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; @@ -19,10 +16,10 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; -class ElasticsearchSourceProjection extends AbstractElasticsearchProjection { +class ElasticsearchSourceProjection extends AbstractElasticsearchProjection + implements ElasticsearchSearchProjection.Extractor { private static final JsonArrayAccessor REQUEST_SOURCE_ACCESSOR = JsonAccessor.root().property( "_source" ).asArray(); - private static final JsonObjectAccessor HIT_SOURCE_ACCESSOR = JsonAccessor.root().property( "_source" ).asObject(); private static final JsonPrimitive WILDCARD_ALL = new JsonPrimitive( "*" ); private ElasticsearchSourceProjection(ElasticsearchSearchIndexScope scope) { @@ -35,20 +32,15 @@ public String toString() { } @Override - public void request(JsonObject requestBody, ProjectionRequestContext context) { + public Extractor request(JsonObject requestBody, ProjectionRequestContext context) { REQUEST_SOURCE_ACCESSOR.addElementIfAbsent( requestBody, WILDCARD_ALL ); + return this; } @Override public JsonObject extract(ProjectionHitMapper projectionHitMapper, JsonObject hit, - ProjectionExtractContext context) { - Optional sourceElement = HIT_SOURCE_ACCESSOR.get( hit ); - if ( sourceElement.isPresent() ) { - return sourceElement.get(); - } - else { - return null; - } + JsonObject source, ProjectionExtractContext context) { + return source; } @Override diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/FieldProjectionRequestContext.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/FieldProjectionRequestContext.java new file mode 100644 index 00000000000..f66b9bd8f08 --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/FieldProjectionRequestContext.java @@ -0,0 +1,78 @@ +/* + * 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.backend.elasticsearch.search.projection.impl; + +import java.lang.invoke.MethodHandles; +import java.util.Arrays; + +import org.hibernate.search.backend.elasticsearch.logging.impl.Log; +import org.hibernate.search.backend.elasticsearch.lowlevel.syntax.search.impl.ElasticsearchSearchSyntax; +import org.hibernate.search.engine.backend.common.spi.FieldPaths; +import org.hibernate.search.engine.spatial.GeoPoint; +import org.hibernate.search.util.common.logging.impl.LoggerFactory; + +public class FieldProjectionRequestContext implements ProjectionRequestContext { + + private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + private final ProjectionRequestContext root; + private final String absoluteCurrentFieldPath; + private final String[] absoluteCurrentFieldPathComponents; + private final String[] relativeCurrentFieldPathComponents; + + public FieldProjectionRequestContext(ProjectionRequestContext root, + String absoluteCurrentFieldPath, String[] absoluteCurrentFieldPathComponents) { + this( root, absoluteCurrentFieldPath, absoluteCurrentFieldPathComponents, + absoluteCurrentFieldPathComponents + ); + } + + private FieldProjectionRequestContext(ProjectionRequestContext root, + String absoluteCurrentFieldPath, String[] absoluteCurrentFieldPathComponents, + String[] relativeCurrentFieldPathComponents) { + this.root = root; + this.absoluteCurrentFieldPath = absoluteCurrentFieldPath; + this.absoluteCurrentFieldPathComponents = absoluteCurrentFieldPathComponents; + this.relativeCurrentFieldPathComponents = relativeCurrentFieldPathComponents; + } + + @Override + public Integer getDistanceSortIndex(String absoluteFieldPath, GeoPoint location) { + return root.getDistanceSortIndex( absoluteFieldPath, location ); + } + + @Override + public ElasticsearchSearchSyntax getSearchSyntax() { + return root.getSearchSyntax(); + } + + @Override + public void checkValidField(String absoluteFieldPath) { + if ( !FieldPaths.isStrictPrefix( absoluteCurrentFieldPath, absoluteFieldPath ) ) { + throw log.invalidContextForProjectionOnField( absoluteFieldPath, absoluteCurrentFieldPath ); + } + } + + @Override + public ProjectionRequestContext forField(String absoluteFieldPath, String[] absoluteFieldPathComponents) { + checkValidField( absoluteFieldPath ); + String[] relativeFieldPathComponents = Arrays.copyOfRange( absoluteFieldPathComponents, + absoluteCurrentFieldPathComponents.length, absoluteFieldPathComponents.length ); + return new FieldProjectionRequestContext( root, absoluteFieldPath, absoluteFieldPathComponents, + relativeFieldPathComponents ); + } + + @Override + public String absoluteCurrentFieldPath() { + return absoluteCurrentFieldPath; + } + + @Override + public String[] relativeCurrentFieldPathComponents() { + return relativeCurrentFieldPathComponents; + } +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ProjectionRequestContext.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ProjectionRequestContext.java index fa0ed4cedf1..62c63770768 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ProjectionRequestContext.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/projection/impl/ProjectionRequestContext.java @@ -15,4 +15,12 @@ public interface ProjectionRequestContext { ElasticsearchSearchSyntax getSearchSyntax(); + void checkValidField(String absoluteFieldPath); + + ProjectionRequestContext forField(String absoluteFieldPath, String[] absoluteFieldPathComponents); + + String absoluteCurrentFieldPath(); + + String[] relativeCurrentFieldPathComponents(); + } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/dsl/impl/ElasticsearchSearchQuerySelectStepImpl.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/dsl/impl/ElasticsearchSearchQuerySelectStepImpl.java index 328e4e450f1..183b8b9e279 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/dsl/impl/ElasticsearchSearchQuerySelectStepImpl.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/dsl/impl/ElasticsearchSearchQuerySelectStepImpl.java @@ -22,6 +22,8 @@ import org.hibernate.search.engine.search.predicate.dsl.PredicateFinalStep; import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.dsl.ProjectionFinalStep; +import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; +import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; import org.hibernate.search.engine.search.query.dsl.spi.AbstractSearchQuerySelectStep; import org.hibernate.search.engine.search.query.spi.SearchQueryIndexScope; @@ -74,7 +76,9 @@ public

ElasticsearchSearchQueryWhereStep select(SearchProjection

@Override public ElasticsearchSearchQueryWhereStep, LOS> select(SearchProjection... projections) { - return select( scope.projectionBuilders().composite( Function.identity(), projections ).build() ); + return select( scope.projectionBuilders().composite() + .build( projections, ProjectionCompositor.fromList( projections.length ), + ProjectionAccumulator.single() ) ); } @Override diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch56SearchResultExtractor.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch56SearchResultExtractor.java index 1ee5f0fe10d..b068c38bde6 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch56SearchResultExtractor.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch56SearchResultExtractor.java @@ -33,9 +33,9 @@ class Elasticsearch56SearchResultExtractor extends Elasticsearch7SearchResult Elasticsearch56SearchResultExtractor( ElasticsearchSearchQueryRequestContext requestContext, - ElasticsearchSearchProjection rootProjection, + ElasticsearchSearchProjection.Extractor rootExtractor, Map, ElasticsearchSearchAggregation> aggregations) { - super( requestContext, rootProjection, aggregations ); + super( requestContext, rootExtractor, aggregations ); } @Override diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch56SearchResultExtractorFactory.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch56SearchResultExtractorFactory.java index 1e98390a293..741048061c3 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch56SearchResultExtractorFactory.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch56SearchResultExtractorFactory.java @@ -20,11 +20,11 @@ public class Elasticsearch56SearchResultExtractorFactory implements Elasticsearc @Override public ElasticsearchSearchResultExtractor> createResultExtractor( ElasticsearchSearchQueryRequestContext requestContext, - ElasticsearchSearchProjection rootProjection, + ElasticsearchSearchProjection.Extractor rootExtractor, Map, ElasticsearchSearchAggregation> aggregations) { return new Elasticsearch56SearchResultExtractor<>( requestContext, - rootProjection, aggregations + rootExtractor, aggregations ); } } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch7SearchResultExtractor.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch7SearchResultExtractor.java index 97535875f73..b802bea9a26 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch7SearchResultExtractor.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch7SearchResultExtractor.java @@ -55,19 +55,22 @@ class Elasticsearch7SearchResultExtractor implements ElasticsearchSearchResul private static final JsonAccessor SCROLL_ID_ACCESSOR = JsonAccessor.root().property( "_scroll_id" ).asString(); + private static final JsonObjectAccessor HIT_SOURCE_ACCESSOR = + JsonAccessor.root().property( "_source" ).asObject(); + private static final String HITS_TOTAL_RELATION_EXACT_VALUE = "eq"; private final ElasticsearchSearchQueryRequestContext requestContext; - private final ElasticsearchSearchProjection rootProjection; + private final ElasticsearchSearchProjection.Extractor rootExtractor; private final Map, ElasticsearchSearchAggregation> aggregations; Elasticsearch7SearchResultExtractor( ElasticsearchSearchQueryRequestContext requestContext, - ElasticsearchSearchProjection rootProjection, + ElasticsearchSearchProjection.Extractor rootExtractor, Map, ElasticsearchSearchAggregation> aggregations) { this.requestContext = requestContext; - this.rootProjection = rootProjection; + this.rootExtractor = rootExtractor; this.aggregations = aggregations; } @@ -98,7 +101,7 @@ public ElasticsearchLoadableSearchResult extract(JsonObject responseBody, return new ElasticsearchLoadableSearchResult<>( extractContext, - rootProjection, + rootExtractor, total, extractedHits, extractedAggregations, @@ -125,10 +128,9 @@ private List extractHits(ElasticsearchSearchQueryExtractContext extractC for ( JsonElement hit : jsonHits ) { JsonObject hitObject = hit.getAsJsonObject(); - - extractedData.add( rootProjection.extract( - hitMapper, hitObject, - projectionExtractContext + JsonObject source = HIT_SOURCE_ACCESSOR.get( hitObject ).orElse( null ); + extractedData.add( rootExtractor.extract( + hitMapper, hitObject, source, projectionExtractContext ) ); } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch7SearchResultExtractorFactory.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch7SearchResultExtractorFactory.java index 89c11025d0f..73d669808cc 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch7SearchResultExtractorFactory.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/Elasticsearch7SearchResultExtractorFactory.java @@ -20,11 +20,11 @@ public class Elasticsearch7SearchResultExtractorFactory implements Elasticsearch @Override public ElasticsearchSearchResultExtractor> createResultExtractor( ElasticsearchSearchQueryRequestContext requestContext, - ElasticsearchSearchProjection rootProjection, + ElasticsearchSearchProjection.Extractor rootExtractor, Map, ElasticsearchSearchAggregation> aggregations) { return new Elasticsearch7SearchResultExtractor<>( requestContext, - rootProjection, aggregations + rootExtractor, aggregations ); } } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchLoadableSearchResult.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchLoadableSearchResult.java index 56f2ad7886d..1e1b5424350 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchLoadableSearchResult.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchLoadableSearchResult.java @@ -6,8 +6,6 @@ */ package org.hibernate.search.backend.elasticsearch.search.query.impl; -import static org.hibernate.search.backend.elasticsearch.search.projection.impl.ElasticsearchSearchProjection.transformUnsafe; - import java.util.Collections; import java.util.List; import java.util.Map; @@ -32,7 +30,7 @@ */ public class ElasticsearchLoadableSearchResult { private final ElasticsearchSearchQueryExtractContext extractContext; - private final ElasticsearchSearchProjection rootProjection; + private final ElasticsearchSearchProjection.Extractor extractor; private final SearchResultTotal resultTotal; private List extractedHits; @@ -44,14 +42,14 @@ public class ElasticsearchLoadableSearchResult { private final Deadline deadline; ElasticsearchLoadableSearchResult(ElasticsearchSearchQueryExtractContext extractContext, - ElasticsearchSearchProjection rootProjection, + ElasticsearchSearchProjection.Extractor extractor, SearchResultTotal resultTotal, List extractedHits, Map, ?> extractedAggregations, Integer took, Boolean timedOut, String scrollId, Deadline deadline) { this.extractContext = extractContext; - this.rootProjection = rootProjection; + this.extractor = extractor; this.resultTotal = resultTotal; this.extractedHits = extractedHits; this.extractedAggregations = extractedAggregations; @@ -72,8 +70,8 @@ ElasticsearchSearchResultImpl loadBlocking() { int writeIndex = 0; for ( ; readIndex < extractedHits.size(); ++readIndex ) { transformContext.reset(); - H transformed = transformUnsafe( - rootProjection, loadingResult, extractedHits.get( readIndex ), transformContext + H transformed = ElasticsearchSearchProjection.Extractor.transformUnsafe( + extractor, loadingResult, extractedHits.get( readIndex ), transformContext ); if ( transformContext.hasFailedLoad() ) { diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchQueryBuilder.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchQueryBuilder.java index a6439b70905..7a229f30fa4 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchQueryBuilder.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchQueryBuilder.java @@ -65,7 +65,7 @@ public class ElasticsearchSearchQueryBuilder private final PredicateRequestContext rootPredicateContext; private final SearchLoadingContextBuilder loadingContextBuilder; - private final ElasticsearchSearchProjection rootProjection; + private final ElasticsearchSearchProjection rootProjection; private final Integer scrollTimeout; private final Set routingKeys; @@ -86,7 +86,7 @@ public ElasticsearchSearchQueryBuilder( ElasticsearchSearchIndexScope scope, BackendSessionContext sessionContext, SearchLoadingContextBuilder loadingContextBuilder, - ElasticsearchSearchProjection rootProjection, + ElasticsearchSearchProjection rootProjection, Integer scrollTimeout) { this.workFactory = workFactory; this.searchResultExtractorFactory = searchResultExtractorFactory; @@ -220,7 +220,7 @@ public ElasticsearchSearchQuery build() { scope, sessionContext, loadingContext, rootPredicateContext, distanceSorts ); - rootProjection.request( payload, requestContext ); + ElasticsearchSearchProjection.Extractor rootExtractor = rootProjection.request( payload, requestContext ); if ( aggregations != null ) { JsonObject jsonAggregations = new JsonObject(); @@ -242,7 +242,7 @@ public ElasticsearchSearchQuery build() { ElasticsearchSearchResultExtractor> searchResultExtractor = searchResultExtractorFactory.createResultExtractor( requestContext, - rootProjection, + rootExtractor, aggregations == null ? Collections.emptyMap() : aggregations ); diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchQueryRequestContext.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchQueryRequestContext.java index a7eb2f9e56c..f3d78813c2b 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchQueryRequestContext.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchQueryRequestContext.java @@ -13,6 +13,7 @@ import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexScope; import org.hibernate.search.backend.elasticsearch.search.predicate.impl.PredicateRequestContext; import org.hibernate.search.backend.elasticsearch.search.projection.impl.DistanceSortKey; +import org.hibernate.search.backend.elasticsearch.search.projection.impl.FieldProjectionRequestContext; import org.hibernate.search.backend.elasticsearch.search.projection.impl.ProjectionRequestContext; import org.hibernate.search.backend.elasticsearch.lowlevel.syntax.search.impl.ElasticsearchSearchSyntax; import org.hibernate.search.engine.backend.session.spi.BackendSessionContext; @@ -72,6 +73,26 @@ public ElasticsearchSearchSyntax getSearchSyntax() { return scope.searchSyntax(); } + @Override + public void checkValidField(String absoluteFieldPath) { + // All fields are valid at the root. + } + + @Override + public ProjectionRequestContext forField(String absoluteFieldPath, String[] absoluteFieldPathComponents) { + return new FieldProjectionRequestContext( this, absoluteFieldPath, absoluteFieldPathComponents ); + } + + @Override + public String absoluteCurrentFieldPath() { + return null; + } + + @Override + public String[] relativeCurrentFieldPathComponents() { + return null; + } + ElasticsearchSearchQueryExtractContext createExtractContext(JsonObject responseBody) { return new ElasticsearchSearchQueryExtractContext( this, diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchResultExtractorFactory.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchResultExtractorFactory.java index f8868317a38..0046c39e7b6 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchResultExtractorFactory.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/ElasticsearchSearchResultExtractorFactory.java @@ -17,7 +17,7 @@ public interface ElasticsearchSearchResultExtractorFactory { ElasticsearchSearchResultExtractor> createResultExtractor( ElasticsearchSearchQueryRequestContext requestContext, - ElasticsearchSearchProjection rootProjection, + ElasticsearchSearchProjection.Extractor rootExtractor, Map, ElasticsearchSearchAggregation> aggregations); } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/SearchBackendContext.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/SearchBackendContext.java index c68e1bb46dc..a1c13168e1a 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/SearchBackendContext.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/query/impl/SearchBackendContext.java @@ -38,6 +38,6 @@ ElasticsearchSearchQueryBuilder createSearchQueryBuilder( ElasticsearchSearchIndexScope scope, BackendSessionContext sessionContext, SearchLoadingContextBuilder loadingContextBuilder, - ElasticsearchSearchProjection rootProjection); + ElasticsearchSearchProjection rootProjection); } diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/types/impl/ElasticsearchIndexCompositeNodeType.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/types/impl/ElasticsearchIndexCompositeNodeType.java index 39af91b1b12..5ddc4daeaf0 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/types/impl/ElasticsearchIndexCompositeNodeType.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/types/impl/ElasticsearchIndexCompositeNodeType.java @@ -14,9 +14,11 @@ import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexScope; import org.hibernate.search.backend.elasticsearch.search.predicate.impl.ElasticsearchExistsPredicate; import org.hibernate.search.backend.elasticsearch.search.predicate.impl.ElasticsearchNestedPredicate; +import org.hibernate.search.backend.elasticsearch.search.projection.impl.ElasticsearchObjectProjection; import org.hibernate.search.engine.backend.types.ObjectStructure; import org.hibernate.search.engine.backend.types.spi.AbstractIndexCompositeNodeType; import org.hibernate.search.engine.search.predicate.spi.PredicateTypeKeys; +import org.hibernate.search.engine.search.projection.spi.ProjectionTypeKeys; public class ElasticsearchIndexCompositeNodeType extends AbstractIndexCompositeNodeType< @@ -42,6 +44,7 @@ public static class Builder public Builder(ObjectStructure objectStructure) { super( objectStructure ); queryElementFactory( PredicateTypeKeys.EXISTS, new ElasticsearchExistsPredicate.ObjectFieldFactory() ); + queryElementFactory( ProjectionTypeKeys.OBJECT, new ElasticsearchObjectProjection.Factory() ); if ( ObjectStructure.NESTED.equals( objectStructure ) ) { queryElementFactory( PredicateTypeKeys.NESTED, new ElasticsearchNestedPredicate.Factory() ); } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/index/impl/IndexManagerBackendContext.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/index/impl/IndexManagerBackendContext.java index 5f56f491759..3234443cc5e 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/index/impl/IndexManagerBackendContext.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/index/impl/IndexManagerBackendContext.java @@ -157,7 +157,7 @@ public LuceneSearchQueryBuilder createSearchQueryBuilder( LuceneSearchQueryIndexScope scope, BackendSessionContext sessionContext, SearchLoadingContextBuilder loadingContextBuilder, - LuceneSearchProjection rootProjection) { + LuceneSearchProjection rootProjection) { multiTenancyStrategy.checkTenantId( sessionContext.tenantIdentifier(), eventContext ); return new LuceneSearchQueryBuilder<>( diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/logging/impl/Log.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/logging/impl/Log.java index 1d99b1e9c67..06622bf3f04 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/logging/impl/Log.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/logging/impl/Log.java @@ -309,10 +309,6 @@ SearchException indexManagerUnwrappingWithUnknownType(@FormatWith(ClassFormatter SearchException unableToDeleteAllEntriesFromIndex(Query query, String causeMessage, @Param EventContext context, @Cause Exception cause); - @Message(id = ID_OFFSET + 69, - value = "Unable to explain search query: %1$s") - SearchException ioExceptionOnExplain(String causeMessage, @Cause IOException cause); - @Message(id = ID_OFFSET + 70, value = "Full-text features (analysis, fuzziness) are not supported for fields of this type.") SearchException fullTextFeaturesNotSupportedByFieldType(@Param EventContext context); @@ -592,4 +588,21 @@ SearchException indexSchemaNamedPredicateNameConflict(String relativeFilterName, value = "Offset + limit should be lower than Integer.MAX_VALUE, offset: '%1$s', limit: '%2$s'.") IOException offsetLimitExceedsMaxValue(int offset, Integer limit); + @Message(id = ID_OFFSET + 152, + value = "Invalid context for projection on field '%1$s': the surrounding projection" + + " is executed for each object in field '%2$s', which is not a parent of field '%1$s'." + + " Check the structure of your projections.") + SearchException invalidContextForProjectionOnField(String absolutePath, + String objectFieldAbsolutePath); + + @Message(id = ID_OFFSET + 153, + value = "Invalid cardinality for projection on field '%1$s': the projection is single-valued," + + " but this field is effectively multi-valued in this context," + + " because parent object field '%2$s' is multi-valued." + + " Either call '.multi()' when you create the projection on field '%1$s'," + + " or wrap that projection in an object projection like this:" + + " 'f.object(\"%2$s\").from().as(...).multi()'.") + SearchException invalidSingleValuedProjectionOnValueFieldInMultiValuedObjectField(String absolutePath, + String objectFieldAbsolutePath); + } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/CollectorExecutionContext.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/CollectorExecutionContext.java index 2122d34f780..bc591824246 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/CollectorExecutionContext.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/CollectorExecutionContext.java @@ -13,7 +13,7 @@ import org.apache.lucene.search.IndexSearcher; -public final class CollectorExecutionContext { +public class CollectorExecutionContext { private final IndexReaderMetadataResolver metadataResolver; @@ -37,8 +37,8 @@ public IndexSearcher getIndexSearcher() { return indexSearcher; } - public NestedDocsProvider createNestedDocsProvider(String nestedDocumentPath) { - return new NestedDocsProvider( nestedDocumentPath ); + public NestedDocsProvider createNestedDocsProvider(String parentDocumentPath, String nestedDocumentPath) { + return new NestedDocsProvider( parentDocumentPath, nestedDocumentPath ); } public NestedDocsProvider createNestedDocsProvider(Set nestedDocumentPaths) { diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/DocumentReferenceCollector.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/DocumentReferenceValues.java similarity index 50% rename from backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/DocumentReferenceCollector.java rename to backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/DocumentReferenceValues.java index c6d7444a097..30ec86100e5 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/DocumentReferenceCollector.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/DocumentReferenceValues.java @@ -8,69 +8,50 @@ import java.io.IOException; +import org.hibernate.search.backend.lucene.lowlevel.common.impl.MetadataFields; import org.hibernate.search.backend.lucene.lowlevel.reader.impl.IndexReaderMetadataResolver; import org.hibernate.search.backend.lucene.search.common.impl.LuceneDocumentReference; -import org.hibernate.search.backend.lucene.lowlevel.common.impl.MetadataFields; import org.hibernate.search.engine.backend.common.DocumentReference; -import com.carrotsearch.hppc.IntObjectHashMap; -import com.carrotsearch.hppc.IntObjectMap; import org.apache.lucene.index.BinaryDocValues; import org.apache.lucene.index.DocValues; import org.apache.lucene.index.LeafReaderContext; -import org.apache.lucene.search.ScoreMode; -import org.apache.lucene.search.SimpleCollector; - -public final class DocumentReferenceCollector extends SimpleCollector { - public static final CollectorKey KEY = CollectorKey.create(); +public abstract class DocumentReferenceValues implements Values { - public static final CollectorFactory FACTORY = new CollectorFactory() { - @Override - public DocumentReferenceCollector createCollector(CollectorExecutionContext context) { - return new DocumentReferenceCollector( context ); - } - - @Override - public CollectorKey getCollectorKey() { - return KEY; - } - }; + public static DocumentReferenceValues simple(CollectorExecutionContext executionContext) { + return new DocumentReferenceValues( executionContext ) { + @Override + protected DocumentReference toReference(String typeName, String identifier) { + return new LuceneDocumentReference( typeName, identifier ); + } + }; + } private final IndexReaderMetadataResolver metadataResolver; private String currentLeafMappedTypeName; private BinaryDocValues currentLeafIdDocValues; - private int currentLeafDocBase; - private final IntObjectMap collected = new IntObjectHashMap<>(); - - private DocumentReferenceCollector(CollectorExecutionContext executionContext) { + public DocumentReferenceValues(CollectorExecutionContext executionContext) { this.metadataResolver = executionContext.getMetadataResolver(); } @Override - public void collect(int doc) throws IOException { - currentLeafIdDocValues.advance( doc ); - collected.put( currentLeafDocBase + doc, new LuceneDocumentReference( - currentLeafMappedTypeName, - currentLeafIdDocValues.binaryValue().utf8ToString() - ) ); + public final void context(LeafReaderContext context) throws IOException { + this.currentLeafMappedTypeName = metadataResolver.resolveMappedTypeName( context ); + this.currentLeafIdDocValues = DocValues.getBinary( context.reader(), MetadataFields.idFieldName() ); } @Override - public ScoreMode scoreMode() { - return ScoreMode.COMPLETE_NO_SCORES; + public final R get(int doc) throws IOException { + currentLeafIdDocValues.advance( doc ); + return toReference( + currentLeafMappedTypeName, + currentLeafIdDocValues.binaryValue().utf8ToString() + ); } - public DocumentReference get(int doc) { - return collected.get( doc ); - } + protected abstract R toReference(String typeName, String identifier); - @Override - protected void doSetNextReader(LeafReaderContext context) throws IOException { - this.currentLeafMappedTypeName = metadataResolver.resolveMappedTypeName( context ); - this.currentLeafIdDocValues = DocValues.getBinary( context.reader(), MetadataFields.idFieldName() ); - this.currentLeafDocBase = context.docBase; - } } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/ExplanationValues.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/ExplanationValues.java new file mode 100644 index 00000000000..aeed2b55fd5 --- /dev/null +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/ExplanationValues.java @@ -0,0 +1,37 @@ +/* + * 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.backend.lucene.lowlevel.collector.impl; + +import java.io.IOException; + +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; + +public final class ExplanationValues implements Values { + + private final IndexSearcher indexSearcher; + private final Query luceneQuery; + + private int currentLeafDocBase; + + public ExplanationValues(TopDocsDataCollectorExecutionContext context) { + this.indexSearcher = context.getIndexSearcher(); + this.luceneQuery = context.executedQuery(); + } + + @Override + public void context(LeafReaderContext context) throws IOException { + this.currentLeafDocBase = context.docBase; + } + + @Override + public Explanation get(int doc) throws IOException { + return indexSearcher.explain( luceneQuery, currentLeafDocBase + doc ); + } +} diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/GeoPointDistanceCollector.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/GeoPointDistanceCollector.java deleted file mode 100644 index edc408a9b65..00000000000 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/GeoPointDistanceCollector.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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.backend.lucene.lowlevel.collector.impl; - -import java.io.IOException; -import java.util.ArrayList; - -import org.apache.lucene.index.LeafReaderContext; -import org.apache.lucene.search.Collector; -import org.apache.lucene.search.DoubleValues; -import org.apache.lucene.search.LeafCollector; -import org.apache.lucene.search.Scorable; -import org.apache.lucene.search.ScoreMode; - -import org.hibernate.search.backend.lucene.lowlevel.docvalues.impl.GeoPointDistanceMultiValuesToSingleValuesSource; -import org.hibernate.search.backend.lucene.lowlevel.docvalues.impl.MultiValueMode; -import org.hibernate.search.backend.lucene.lowlevel.join.impl.NestedDocsProvider; -import org.hibernate.search.engine.spatial.GeoPoint; -import org.hibernate.search.util.common.AssertionFailure; - -/** - * A Lucene distance {@code Collector} for spatial searches. - * - * @author Sanne Grinovero - * @author Nicolas Helleringer - */ -public class GeoPointDistanceCollector implements Collector { - - private static final double MISSING_VALUE_MARKER = Double.NEGATIVE_INFINITY; - - private final GeoPointDistanceMultiValuesToSingleValuesSource valuesSource; - - private final SpatialResultsCollector distances; - - public GeoPointDistanceCollector(String absoluteFieldPath, NestedDocsProvider nestedDocsProvider, - GeoPoint center, int hitsCount) { - // TODO HSEARCH-3391 project to multiple values instead of using the min - this.valuesSource = new GeoPointDistanceMultiValuesToSingleValuesSource( - absoluteFieldPath, MultiValueMode.MIN, nestedDocsProvider, center - ); - this.distances = new SpatialResultsCollector( hitsCount ); - } - - public Double getDistance(final int docId) { - return distances.get( docId ); - } - - @Override - public LeafCollector getLeafCollector(LeafReaderContext context) throws IOException { - return new DistanceLeafCollector( context.docBase, createDistanceDocValues( context ) ); - } - - @Override - public ScoreMode scoreMode() { - return ScoreMode.COMPLETE_NO_SCORES; - } - - private DoubleValues createDistanceDocValues(LeafReaderContext context) throws IOException { - return valuesSource.getValues( context, null ); - } - - /** - * We'll store matching hits in HitEntry instances so to allow retrieving results - * in a second phase after the Collector has run. - */ - private static class HitEntry { - private final int documentId; - private final Double distance; - - private HitEntry(int documentId, Double distance) { - this.documentId = documentId; - this.distance = distance; - } - } - - /** - * A custom structure to store all HitEntry instances. - * Based on an array, as in most cases we'll append sequentially and iterate the - * results in the same order too. The size is well known in most situations so we can - * also guess an appropriate allocation size. - * - * Iteration of the results will in practice follow a monotonic index, unless a non natural - * Sort is specified. So by keeping track of the position in the array of the last request, - * and look from that pointer first, the cost of get operations is O(1) in most common use - * cases. - */ - private static class SpatialResultsCollector { - final ArrayList orderedEntries; - int currentIterator = 0; - - private SpatialResultsCollector(int size) { - orderedEntries = new ArrayList<>( size ); - } - - public Double get(int docId) { - //Optimize for an iteration having a monotonic index: - int startingPoint = currentIterator; - for ( ; currentIterator < orderedEntries.size(); currentIterator++ ) { - HitEntry currentEntry = orderedEntries.get( currentIterator ); - if ( currentEntry == null ) { - break; - } - if ( currentEntry.documentId == docId ) { - return currentEntry.distance; - } - } - - //No match yet! scan the remaining section from the beginning: - for ( currentIterator = 0; currentIterator < startingPoint; currentIterator++ ) { - HitEntry currentEntry = orderedEntries.get( currentIterator ); - if ( currentEntry == null ) { - break; - } - if ( currentEntry.documentId == docId ) { - return currentEntry.distance; - } - } - - throw new AssertionFailure( "Unexpected Lucene docId: '%1$s'. No data was collected for this document." ); - } - - void put(int documentId, Double distance) { - orderedEntries.add( new HitEntry( documentId, distance ) ); - } - } - - private class DistanceLeafCollector implements LeafCollector { - - private final int docBase; - private final DoubleValues distanceDocValues; - - DistanceLeafCollector(int docBase, DoubleValues distanceDocValues) { - this.docBase = docBase; - this.distanceDocValues = distanceDocValues; - } - - @Override - public void setScorer(Scorable scorer) { - // we don't need any scorer - } - - @Override - public void collect(int docId) throws IOException { - final int absoluteDocId = docBase + docId; - Double distance = null; - if ( distanceDocValues.advanceExact( docId ) ) { - double distanceFromDocValues = distanceDocValues.doubleValue(); - distance = distanceFromDocValues == MISSING_VALUE_MARKER ? null : distanceFromDocValues; - } - distances.put( absoluteDocId, distance ); - } - } -} diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/IdentifierCollector.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/IdentifierCollector.java deleted file mode 100644 index 1d8d958383e..00000000000 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/IdentifierCollector.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.backend.lucene.lowlevel.collector.impl; - -import java.io.IOException; - -import org.hibernate.search.backend.lucene.lowlevel.common.impl.MetadataFields; - -import com.carrotsearch.hppc.IntObjectHashMap; -import com.carrotsearch.hppc.IntObjectMap; -import org.apache.lucene.index.BinaryDocValues; -import org.apache.lucene.index.DocValues; -import org.apache.lucene.index.LeafReaderContext; -import org.apache.lucene.search.ScoreMode; -import org.apache.lucene.search.SimpleCollector; - -public final class IdentifierCollector extends SimpleCollector { - - public static final CollectorKey KEY = CollectorKey.create(); - - public static final CollectorFactory FACTORY = new CollectorFactory() { - @Override - public IdentifierCollector createCollector(CollectorExecutionContext context) { - return new IdentifierCollector(); - } - - @Override - public CollectorKey getCollectorKey() { - return KEY; - } - }; - - private BinaryDocValues currentLeafIdDocValues; - private int currentLeafDocBase; - - private final IntObjectMap collected = new IntObjectHashMap<>(); - - private IdentifierCollector() { - } - - @Override - public void collect(int doc) throws IOException { - currentLeafIdDocValues.advance( doc ); - collected.put( currentLeafDocBase + doc, currentLeafIdDocValues.binaryValue().utf8ToString() ); - } - - @Override - public ScoreMode scoreMode() { - return ScoreMode.COMPLETE_NO_SCORES; - } - - public String get(int doc) { - return collected.get( doc ); - } - - @Override - protected void doSetNextReader(LeafReaderContext context) throws IOException { - this.currentLeafIdDocValues = DocValues.getBinary( context.reader(), MetadataFields.idFieldName() ); - this.currentLeafDocBase = context.docBase; - } -} diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/IdentifierValues.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/IdentifierValues.java new file mode 100644 index 00000000000..a9eba3d96f0 --- /dev/null +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/IdentifierValues.java @@ -0,0 +1,31 @@ +/* + * 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.backend.lucene.lowlevel.collector.impl; + +import java.io.IOException; + +import org.hibernate.search.backend.lucene.lowlevel.common.impl.MetadataFields; + +import org.apache.lucene.index.BinaryDocValues; +import org.apache.lucene.index.DocValues; +import org.apache.lucene.index.LeafReaderContext; + +public final class IdentifierValues implements Values { + + private BinaryDocValues currentLeafIdDocValues; + + @Override + public void context(LeafReaderContext context) throws IOException { + this.currentLeafIdDocValues = DocValues.getBinary( context.reader(), MetadataFields.idFieldName() ); + } + + @Override + public String get(int doc) throws IOException { + currentLeafIdDocValues.advance( doc ); + return currentLeafIdDocValues.binaryValue().utf8ToString(); + } +} diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/ScoreValues.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/ScoreValues.java new file mode 100644 index 00000000000..94eeb1f2696 --- /dev/null +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/ScoreValues.java @@ -0,0 +1,35 @@ +/* + * 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.backend.lucene.lowlevel.collector.impl; + +import java.io.IOException; + +import com.carrotsearch.hppc.IntIntMap; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.ScoreDoc; + +public class ScoreValues implements Values { + + private final IntIntMap docIdToScoreDocIndex; + private final ScoreDoc[] scoreDocs; + private int currentLeafDocBase; + + public ScoreValues(TopDocsDataCollectorExecutionContext context) { + this.docIdToScoreDocIndex = context.docIdToScoreDocIndex(); + this.scoreDocs = context.topDocs().scoreDocs; + } + + @Override + public void context(LeafReaderContext context) throws IOException { + this.currentLeafDocBase = context.docBase; + } + + @Override + public Float get(int doc) throws IOException { + return scoreDocs[docIdToScoreDocIndex.get( currentLeafDocBase + doc )].score; + } +} diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/StoredFieldsCollector.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/StoredFieldsCollector.java deleted file mode 100644 index 671161451c0..00000000000 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/StoredFieldsCollector.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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.backend.lucene.lowlevel.collector.impl; - -import java.io.IOException; -import java.util.Set; - -import org.hibernate.search.backend.lucene.lowlevel.join.impl.ChildDocIds; -import org.hibernate.search.backend.lucene.lowlevel.join.impl.NestedDocsProvider; -import org.hibernate.search.backend.lucene.search.extraction.impl.ReusableDocumentStoredFieldVisitor; -import org.hibernate.search.util.common.AssertionFailure; - -import com.carrotsearch.hppc.IntObjectHashMap; -import com.carrotsearch.hppc.IntObjectMap; -import org.apache.lucene.document.Document; -import org.apache.lucene.index.LeafReader; -import org.apache.lucene.index.LeafReaderContext; -import org.apache.lucene.search.DocIdSetIterator; -import org.apache.lucene.search.IndexSearcher; -import org.apache.lucene.search.ScoreMode; -import org.apache.lucene.search.SimpleCollector; -import org.apache.lucene.search.Weight; - -/** - * Collects stored fields as Document instances. - *

- * WARNING: this relies on reader.document() to load the value of stored field - * for each single matching document, - * Use with care. - */ -public class StoredFieldsCollector extends SimpleCollector { - - public static final CollectorKey KEY = CollectorKey.create(); - - public static CollectorFactory factory( - ReusableDocumentStoredFieldVisitor storedFieldVisitor, - Set requiredNestedDocumentPathsForStoredFields) { - return new CollectorFactory() { - @Override - public StoredFieldsCollector createCollector(CollectorExecutionContext context) throws IOException { - NestedDocsProvider nestedDocsProvider; - if ( requiredNestedDocumentPathsForStoredFields.isEmpty() ) { - nestedDocsProvider = null; - } - else { - nestedDocsProvider = context.createNestedDocsProvider( requiredNestedDocumentPathsForStoredFields ); - } - - return new StoredFieldsCollector( nestedDocsProvider, storedFieldVisitor, context.getIndexSearcher() ); - } - - @Override - public CollectorKey getCollectorKey() { - return KEY; - } - }; - } - - private final NestedDocsProvider nestedDocsProvider; - private final Weight childrenWeight; - private final ReusableDocumentStoredFieldVisitor storedFieldVisitor; - - private int currentLeafDocBase; - private int currentLeafLastSeenParentDoc; - private ChildDocIds currentLeafChildDocs; - private LeafReader currentLeafReader; - - private final IntObjectMap documents = new IntObjectHashMap<>(); - - public StoredFieldsCollector(NestedDocsProvider nestedDocsProvider, - ReusableDocumentStoredFieldVisitor storedFieldVisitor, - IndexSearcher indexSearcher) throws IOException { - this.childrenWeight = nestedDocsProvider == null ? null : nestedDocsProvider.childDocsWeight( indexSearcher ); - this.nestedDocsProvider = nestedDocsProvider; - this.storedFieldVisitor = storedFieldVisitor; - } - - @Override - public String toString() { - return "StoredFieldsCollector{" + "documents=" + documents + '}'; - } - - @Override - public void collect(int parentDoc) throws IOException { - // add nested documents contribution - if ( currentLeafChildDocs != null ) { - collectChildDocs( parentDoc ); - } - - // add root document contribution - currentLeafReader.document( parentDoc, storedFieldVisitor ); - - documents.put( currentLeafDocBase + parentDoc, storedFieldVisitor.getDocumentAndReset() ); - } - - private void collectChildDocs(int parentDoc) throws IOException { - if ( parentDoc < currentLeafLastSeenParentDoc ) { - throw new AssertionFailure( "Collector.collect called in unexpected order" ); - } - currentLeafLastSeenParentDoc = parentDoc; - - if ( !currentLeafChildDocs.advanceExactParent( parentDoc ) ) { - // No child - return; - } - - for ( int childDoc = currentLeafChildDocs.nextChild(); childDoc != DocIdSetIterator.NO_MORE_DOCS; - childDoc = currentLeafChildDocs.nextChild() ) { - currentLeafReader.document( childDoc, storedFieldVisitor ); - } - } - - @Override - public ScoreMode scoreMode() { - return ScoreMode.COMPLETE_NO_SCORES; - } - - public Document getDocument(int docId) { - return documents.get( docId ); - } - - @Override - protected void doSetNextReader(LeafReaderContext context) throws IOException { - this.currentLeafDocBase = context.docBase; - this.currentLeafLastSeenParentDoc = -1; - this.currentLeafReader = context.reader(); - - this.currentLeafChildDocs = nestedDocsProvider == null ? null - : nestedDocsProvider.childDocs( childrenWeight, context, null ); - } -} diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/StoredFieldsValuesDelegate.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/StoredFieldsValuesDelegate.java new file mode 100644 index 00000000000..f2f838867b2 --- /dev/null +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/StoredFieldsValuesDelegate.java @@ -0,0 +1,126 @@ +/* + * 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.backend.lucene.lowlevel.collector.impl; + +import java.io.IOException; +import java.util.Set; + +import org.hibernate.search.backend.lucene.lowlevel.join.impl.ChildDocIds; +import org.hibernate.search.backend.lucene.lowlevel.join.impl.NestedDocsProvider; +import org.hibernate.search.backend.lucene.search.extraction.impl.ReusableDocumentStoredFieldVisitor; +import org.hibernate.search.util.common.AssertionFailure; + +import com.carrotsearch.hppc.IntObjectHashMap; +import com.carrotsearch.hppc.IntObjectMap; +import org.apache.lucene.document.Document; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Weight; + +/** + * Collects values from stored fields, for use in {@link Values} implementations. + *

+ * WARNING: this relies on reader.document() to load the value of stored field + * for each single matching document, + * Use with care. + */ +public class StoredFieldsValuesDelegate { + public static class Factory { + private final ReusableDocumentStoredFieldVisitor storedFieldVisitor; + private final Set requiredNestedDocumentPathsForStoredFields; + + public Factory(ReusableDocumentStoredFieldVisitor storedFieldVisitor, + Set requiredNestedDocumentPathsForStoredFields) { + + this.storedFieldVisitor = storedFieldVisitor; + this.requiredNestedDocumentPathsForStoredFields = requiredNestedDocumentPathsForStoredFields; + } + + public StoredFieldsValuesDelegate create(CollectorExecutionContext context) throws IOException { + NestedDocsProvider nestedDocsProvider; + if ( requiredNestedDocumentPathsForStoredFields.isEmpty() ) { + nestedDocsProvider = null; + } + else { + nestedDocsProvider = context.createNestedDocsProvider( requiredNestedDocumentPathsForStoredFields ); + } + + return new StoredFieldsValuesDelegate( nestedDocsProvider, storedFieldVisitor, context.getIndexSearcher() ); + } + } + + private final NestedDocsProvider nestedDocsProvider; + private final Weight childrenWeight; + private final ReusableDocumentStoredFieldVisitor storedFieldVisitor; + + private ChildDocIds currentLeafChildDocs; + private LeafReader currentLeafReader; + + private int currentRootDoc; + private Document currentRootDocValue; + private final IntObjectMap currentChildDocValues; + + public StoredFieldsValuesDelegate(NestedDocsProvider nestedDocsProvider, + ReusableDocumentStoredFieldVisitor storedFieldVisitor, + IndexSearcher indexSearcher) throws IOException { + this.childrenWeight = nestedDocsProvider == null ? null : nestedDocsProvider.childDocsWeight( indexSearcher ); + this.nestedDocsProvider = nestedDocsProvider; + this.storedFieldVisitor = storedFieldVisitor; + this.currentChildDocValues = nestedDocsProvider == null ? null : new IntObjectHashMap<>(); + } + + @Override + public String toString() { + return "StoredFieldsValues{" + + "storedFieldVisitor=" + storedFieldVisitor + + '}'; + } + + void context(LeafReaderContext context) throws IOException { + this.currentLeafReader = context.reader(); + this.currentLeafChildDocs = nestedDocsProvider == null ? null + : nestedDocsProvider.childDocs( childrenWeight, context, null ); + + this.currentRootDoc = -1; + this.currentRootDocValue = null; + if ( currentChildDocValues != null ) { + this.currentChildDocValues.clear(); + } + } + + void collect(int parentDoc) throws IOException { + this.currentRootDoc = parentDoc; + + // collect child documents if necessary + if ( currentLeafChildDocs != null && currentLeafChildDocs.advanceExactParent( parentDoc ) ) { + for ( int childDoc = currentLeafChildDocs.nextChild(); childDoc != DocIdSetIterator.NO_MORE_DOCS; + childDoc = currentLeafChildDocs.nextChild() ) { + currentLeafReader.document( childDoc, storedFieldVisitor ); + currentChildDocValues.put( childDoc, storedFieldVisitor.getDocumentAndReset() ); + } + } + + // collect root document + currentLeafReader.document( parentDoc, storedFieldVisitor ); + this.currentRootDocValue = storedFieldVisitor.getDocumentAndReset(); + } + + public Document get(int docId) { + if ( docId == currentRootDoc ) { + return currentRootDocValue; + } + Document doc = currentChildDocValues.get( docId ); + if ( doc == null ) { + throw new AssertionFailure( "Getting value for " + docId + ", which is neither root document " + + currentRootDoc + " nor children " + currentChildDocValues.keys() ); + } + return doc; + } + +} diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/TopDocsDataCollector.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/TopDocsDataCollector.java new file mode 100644 index 00000000000..86d98ac07cb --- /dev/null +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/TopDocsDataCollector.java @@ -0,0 +1,67 @@ +/* + * 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.backend.lucene.lowlevel.collector.impl; + +import java.io.IOException; + +import com.carrotsearch.hppc.IntObjectHashMap; +import com.carrotsearch.hppc.IntObjectMap; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.ScoreMode; +import org.apache.lucene.search.SimpleCollector; + +/** + * The collector used when collecting data related to top docs. + * + * @param The type of value collected for each top doc. + */ +public class TopDocsDataCollector extends SimpleCollector { + + public interface Factory extends CollectorKey> { + + TopDocsDataCollector create(TopDocsDataCollectorExecutionContext context) throws IOException; + + } + + private final Values values; + private final StoredFieldsValuesDelegate storedFieldsValuesDelegate; + + private final IntObjectMap collected = new IntObjectHashMap<>(); + private int currentLeafDocBase; + + public TopDocsDataCollector(TopDocsDataCollectorExecutionContext context, Values values) { + this.values = values; + this.storedFieldsValuesDelegate = context.storedFieldsValuesDelegate(); + } + + @Override + protected void doSetNextReader(LeafReaderContext context) throws IOException { + if ( storedFieldsValuesDelegate != null ) { + storedFieldsValuesDelegate.context( context ); + } + values.context( context ); + this.currentLeafDocBase = context.docBase; + } + + @Override + public void collect(int doc) throws IOException { + if ( storedFieldsValuesDelegate != null ) { + // Pre-load the stored fields of the current document for use in our Values. + storedFieldsValuesDelegate.collect( doc ); + } + collected.put( currentLeafDocBase + doc, values.get( doc ) ); + } + + @Override + public ScoreMode scoreMode() { + return ScoreMode.COMPLETE_NO_SCORES; + } + + public T get(int doc) { + return collected.get( doc ); + } +} diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/TopDocsDataCollectorExecutionContext.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/TopDocsDataCollectorExecutionContext.java new file mode 100644 index 00000000000..03a03285b2c --- /dev/null +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/TopDocsDataCollectorExecutionContext.java @@ -0,0 +1,59 @@ +/* + * 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.backend.lucene.lowlevel.collector.impl; + +import java.io.IOException; + +import org.hibernate.search.backend.lucene.lowlevel.reader.impl.IndexReaderMetadataResolver; + +import com.carrotsearch.hppc.IntIntHashMap; +import com.carrotsearch.hppc.IntIntMap; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.TopDocs; + +public class TopDocsDataCollectorExecutionContext extends CollectorExecutionContext { + private final Query executedQuery; + private final TopDocs topDocs; + private final StoredFieldsValuesDelegate storedFieldsValuesDelegate; + + private IntIntMap docIdToScoreDocIndex; + + public TopDocsDataCollectorExecutionContext(IndexReaderMetadataResolver metadataResolver, + IndexSearcher indexSearcher, Query executedQuery, TopDocs topDocs, + StoredFieldsValuesDelegate.Factory storedFieldsValuesDelegateOrNull) throws IOException { + super( metadataResolver, indexSearcher, topDocs.scoreDocs.length ); + this.executedQuery = executedQuery; + this.topDocs = topDocs; + this.storedFieldsValuesDelegate = storedFieldsValuesDelegateOrNull == null ? null + : storedFieldsValuesDelegateOrNull.create( this ); + } + + public Query executedQuery() { + return executedQuery; + } + + public TopDocs topDocs() { + return topDocs; + } + + public IntIntMap docIdToScoreDocIndex() { + ScoreDoc[] scoreDocs = topDocs.scoreDocs; + if ( docIdToScoreDocIndex == null ) { + docIdToScoreDocIndex = new IntIntHashMap(); + for ( int i = 0; i < scoreDocs.length; i++ ) { + docIdToScoreDocIndex.put( scoreDocs[i].doc, i ); + } + } + return docIdToScoreDocIndex; + } + + public StoredFieldsValuesDelegate storedFieldsValuesDelegate() { + return storedFieldsValuesDelegate; + } +} diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/Values.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/Values.java new file mode 100644 index 00000000000..c6e1deff1da --- /dev/null +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/collector/impl/Values.java @@ -0,0 +1,33 @@ +/* + * 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.backend.lucene.lowlevel.collector.impl; + +import java.io.IOException; + +import org.apache.lucene.index.LeafReaderContext; + +/** + * A generic accessor to per-document values. + * + * @param The type of values. + */ +public interface Values { + + /** + * Sets the context to use for the next calls to {@link #get(int)}. + * @param context A {@link LeafReaderContext}. + * @throws IOException If an underlying I/O operation fails. + */ + void context(LeafReaderContext context) throws IOException; + + /** + * @return The value for the given document in the current leaf. + * @throws IOException If an underlying I/O operation fails. + */ + T get(int doc) throws IOException; + +} diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/join/impl/NestedDocsProvider.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/join/impl/NestedDocsProvider.java index 4a79cdb2750..82b34490897 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/join/impl/NestedDocsProvider.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/lowlevel/join/impl/NestedDocsProvider.java @@ -38,20 +38,20 @@ public class NestedDocsProvider { private final BitSetProducer parentFilter; private final Query childQuery; - public NestedDocsProvider(String nestedDocumentPath) { - this( Collections.singleton( nestedDocumentPath ), null ); + public NestedDocsProvider(String parentDocumentPath, String nestedDocumentPath) { + this( parentDocumentPath, Collections.singleton( nestedDocumentPath ), null ); } public NestedDocsProvider(String nestedDocumentPath, Query nestedFilter) { - this( Collections.singleton( nestedDocumentPath ), nestedFilter ); + this( null, Collections.singleton( nestedDocumentPath ), nestedFilter ); } public NestedDocsProvider(Set nestedDocumentPaths) { - this( nestedDocumentPaths, null ); + this( null, nestedDocumentPaths, null ); } - public NestedDocsProvider(Set nestedDocumentPaths, Query nestedFilter) { - Query parentsFilterQuery = Queries.parentsFilterQuery( null ); + public NestedDocsProvider(String parentDocumentPath, Set nestedDocumentPaths, Query nestedFilter) { + Query parentsFilterQuery = Queries.parentsFilterQuery( parentDocumentPath ); // Note: this filter should include *all* parents, not just the matched ones. // Otherwise we will not "see" non-matched parents, // and we will consider its matching children as children of the next matching parent. diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/common/impl/LuceneMultiIndexSearchIndexValueFieldContext.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/common/impl/LuceneMultiIndexSearchIndexValueFieldContext.java index 6c5d05efe37..7cb7dc306dc 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/common/impl/LuceneMultiIndexSearchIndexValueFieldContext.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/common/impl/LuceneMultiIndexSearchIndexValueFieldContext.java @@ -47,6 +47,11 @@ public LuceneSearchIndexCompositeNodeContext toComposite() { return SearchIndexSchemaElementContextHelper.throwingToComposite( this ); } + @Override + public LuceneSearchIndexCompositeNodeContext toObjectField() { + return SearchIndexSchemaElementContextHelper.throwingToObjectField( this ); + } + @Override public Analyzer searchAnalyzerOrNormalizer() { return fromTypeIfCompatible( LuceneSearchIndexValueFieldTypeContext::searchAnalyzerOrNormalizer, Object::equals, diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/common/impl/LuceneSearchIndexNodeContext.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/common/impl/LuceneSearchIndexNodeContext.java index 3019ed181d6..f2fb9212f65 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/common/impl/LuceneSearchIndexNodeContext.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/common/impl/LuceneSearchIndexNodeContext.java @@ -14,4 +14,7 @@ public interface LuceneSearchIndexNodeContext @Override LuceneSearchIndexCompositeNodeContext toComposite(); + @Override + LuceneSearchIndexCompositeNodeContext toObjectField(); + } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/extraction/impl/ExtractionRequirements.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/extraction/impl/ExtractionRequirements.java index b7185c47e38..1070315bbe0 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/extraction/impl/ExtractionRequirements.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/extraction/impl/ExtractionRequirements.java @@ -13,8 +13,8 @@ import org.hibernate.search.backend.lucene.lowlevel.collector.impl.CollectorExecutionContext; import org.hibernate.search.backend.lucene.lowlevel.collector.impl.CollectorFactory; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.StoredFieldsValuesDelegate; import org.hibernate.search.backend.lucene.lowlevel.reader.impl.IndexReaderMetadataResolver; -import org.hibernate.search.backend.lucene.lowlevel.collector.impl.StoredFieldsCollector; import org.hibernate.search.engine.search.timeout.spi.TimeoutManager; import org.apache.lucene.search.Collector; @@ -36,12 +36,12 @@ public final class ExtractionRequirements { private final boolean requireScore; private final Set> requiredCollectorForAllMatchingDocsFactories; - private final Set> requiredCollectorForTopDocsFactories; + private final StoredFieldsValuesDelegate.Factory storedFieldsSourceFactoryOrNull; private ExtractionRequirements(Builder builder) { requireScore = builder.requireScore; requiredCollectorForAllMatchingDocsFactories = builder.requiredCollectorForAllMatchingDocsFactories; - requiredCollectorForTopDocsFactories = builder.requiredCollectorForTopDocsFactories; + storedFieldsSourceFactoryOrNull = builder.createStoredFieldsSourceFactoryOrNull(); } public LuceneCollectors createCollectors(IndexSearcher indexSearcher, Query originalLuceneQuery, Sort sort, @@ -108,7 +108,7 @@ public LuceneCollectors createCollectors(IndexSearcher indexSearcher, Query orig rewrittenLuceneQuery, requireFieldDocRescoring, scoreSortFieldIndexForRescoring, collectorsForAllMatchingDocs, - requiredCollectorForTopDocsFactories, + storedFieldsSourceFactoryOrNull, timeoutManager ); } @@ -137,7 +137,6 @@ public static class Builder { private boolean requireScore; private final Set> requiredCollectorForAllMatchingDocsFactories = new LinkedHashSet<>(); - private final Set> requiredCollectorForTopDocsFactories = new LinkedHashSet<>(); private boolean requireAllStoredFields = false; private final Set requiredStoredFields = new HashSet<>(); @@ -151,10 +150,6 @@ public void requireCollectorForAllMatchingDocs(CollectorFa requiredCollectorForAllMatchingDocsFactories.add( collectorFactory ); } - public void requireCollectorForTopDocs(CollectorFactory collectorFactory) { - requiredCollectorForTopDocsFactories.add( collectorFactory ); - } - public void requireAllStoredFields() { requireAllStoredFields = true; requiredStoredFields.clear(); @@ -170,14 +165,10 @@ public void requireStoredField(String absoluteFieldPath, String nestedDocumentPa } public ExtractionRequirements build() { - CollectorFactory storedFieldCollectorFactory = createStoredFieldCollectorFactoryOrNull(); - if ( storedFieldCollectorFactory != null ) { - requiredCollectorForTopDocsFactories.add( storedFieldCollectorFactory ); - } return new ExtractionRequirements( this ); } - private CollectorFactory createStoredFieldCollectorFactoryOrNull() { + private StoredFieldsValuesDelegate.Factory createStoredFieldsSourceFactoryOrNull() { ReusableDocumentStoredFieldVisitor storedFieldVisitor; if ( requireAllStoredFields ) { storedFieldVisitor = new ReusableDocumentStoredFieldVisitor(); @@ -189,7 +180,7 @@ else if ( !requiredStoredFields.isEmpty() ) { return null; } - return StoredFieldsCollector.factory( storedFieldVisitor, requiredNestedDocumentPathsForStoredFields ); + return new StoredFieldsValuesDelegate.Factory( storedFieldVisitor, requiredNestedDocumentPathsForStoredFields ); } } } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/extraction/impl/LuceneCollectors.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/extraction/impl/LuceneCollectors.java index 4c6908922db..2db191cd598 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/extraction/impl/LuceneCollectors.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/extraction/impl/LuceneCollectors.java @@ -7,11 +7,13 @@ package org.hibernate.search.backend.lucene.search.extraction.impl; import java.io.IOException; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; -import org.hibernate.search.backend.lucene.lowlevel.collector.impl.CollectorExecutionContext; -import org.hibernate.search.backend.lucene.lowlevel.collector.impl.CollectorFactory; import org.hibernate.search.backend.lucene.lowlevel.collector.impl.CollectorKey; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.StoredFieldsValuesDelegate; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.TopDocsDataCollector; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.TopDocsDataCollectorExecutionContext; import org.hibernate.search.backend.lucene.lowlevel.query.impl.ExplicitDocIdsQuery; import org.hibernate.search.backend.lucene.lowlevel.reader.impl.IndexReaderMetadataResolver; import org.hibernate.search.engine.common.timing.Deadline; @@ -48,18 +50,18 @@ public class LuceneCollectors { private final Integer scoreSortFieldIndexForRescoring; private final CollectorSet collectorsForAllMatchingDocs; - private final Set> collectorsForTopDocsFactories; - private CollectorSet collectorsForTopDocs; + private final StoredFieldsValuesDelegate.Factory storedFieldsValuesDelegateOrNull; private final TimeoutManager timeoutManager; private SearchResultTotal resultTotal; private TopDocs topDocs = null; - LuceneCollectors(IndexReaderMetadataResolver metadataResolver, IndexSearcher indexSearcher, Query rewrittenLuceneQuery, + LuceneCollectors(IndexReaderMetadataResolver metadataResolver, IndexSearcher indexSearcher, + Query rewrittenLuceneQuery, boolean requireFieldDocRescoring, Integer scoreSortFieldIndexForRescoring, CollectorSet collectorsForAllMatchingDocs, - Set> collectorsForTopDocsFactories, + StoredFieldsValuesDelegate.Factory storedFieldsValuesDelegateOrNull, TimeoutManager timeoutManager) { this.metadataResolver = metadataResolver; this.indexSearcher = indexSearcher; @@ -67,7 +69,7 @@ public class LuceneCollectors { this.requireFieldDocRescoring = requireFieldDocRescoring; this.scoreSortFieldIndexForRescoring = scoreSortFieldIndexForRescoring; this.collectorsForAllMatchingDocs = collectorsForAllMatchingDocs; - this.collectorsForTopDocsFactories = collectorsForTopDocsFactories; + this.storedFieldsValuesDelegateOrNull = storedFieldsValuesDelegateOrNull; this.timeoutManager = timeoutManager; } @@ -149,21 +151,25 @@ public CollectorSet getCollectorsForAllMatchingDocs() { /** * Phase 2: collect data relative to top docs. * + * @param collectorFactory The factory to create a collector able to retrieve data for all top docs. * @param startInclusive The index of the first top doc whose data to collect. * @param endExclusive The index after the last top doc whose data to collect. + * @param The type of value collected for each top doc. * * @throws IOException If Lucene throws an {@link IOException}. */ - public void collectTopDocsData(int startInclusive, int endExclusive) throws IOException { - if ( collectorsForTopDocsFactories.isEmpty() || topDocs == null ) { - this.collectorsForTopDocs = null; - return; - } + public List collectTopDocsData(TopDocsDataCollector.Factory collectorFactory, + int startInclusive, int endExclusive) throws IOException { + List extractedData = new ArrayList<>( endExclusive - startInclusive ); try { ScoreDoc[] scoreDocs = topDocs.scoreDocs; ExplicitDocIdsQuery topDocsQuery = new ExplicitDocIdsQuery( scoreDocs, startInclusive, endExclusive ); - this.collectorsForTopDocs = buildTopDocsDataCollectors(); + CollectorSet collectorsForTopDocs = buildTopDocsDataCollectors( collectorFactory ); indexSearcher.search( topDocsQuery, collectorsForTopDocs.getComposed() ); + TopDocsDataCollector topDocsDataCollector = collectorsForTopDocs.get( collectorFactory ); + for ( int i = startInclusive; i < endExclusive; i++ ) { + extractedData.add( topDocsDataCollector.get( scoreDocs[i].doc ) ); + } } catch (TimeLimitingCollector.TimeExceededException e) { Deadline deadline = timeoutManager.deadlineOrNull(); @@ -172,10 +178,7 @@ public void collectTopDocsData(int startInclusive, int endExclusive) throws IOEx } deadline.forceTimeout( e ); } - } - - public CollectorSet getCollectorsForTopDocs() { - return collectorsForTopDocs; + return extractedData; } public SearchResultTotal getResultTotal() { @@ -225,16 +228,17 @@ private void handleRescoring() throws IOException { } } - private CollectorSet buildTopDocsDataCollectors() throws IOException { - CollectorExecutionContext executionContext = new CollectorExecutionContext( + private CollectorSet buildTopDocsDataCollectors(TopDocsDataCollector.Factory collectorFactory) throws IOException { + TopDocsDataCollectorExecutionContext executionContext = new TopDocsDataCollectorExecutionContext( metadataResolver, indexSearcher, - // Allocate just enough memory to handle the top documents. - topDocs.scoreDocs.length + rewrittenLuceneQuery, + topDocs, + storedFieldsValuesDelegateOrNull ); CollectorSet.Builder collectorForTopDocsBuilder = new CollectorSet.Builder( executionContext, timeoutManager ); - collectorForTopDocsBuilder.addAll( collectorsForTopDocsFactories ); + collectorForTopDocsBuilder.add( collectorFactory, collectorFactory.create( executionContext ) ); return collectorForTopDocsBuilder.build(); } } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/extraction/impl/LuceneResult.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/extraction/impl/LuceneResult.java deleted file mode 100644 index f7394e0e8e6..00000000000 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/extraction/impl/LuceneResult.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.backend.lucene.search.extraction.impl; - -import org.apache.lucene.document.Document; - -public class LuceneResult { - - private final Document document; - - private final int docId; - - private final float score; - - public LuceneResult(Document document, int docId, float score) { - this.document = document; - this.docId = docId; - this.score = score; - } - - public Document getDocument() { - return document; - } - - public int getDocId() { - return docId; - } - - public float getScore() { - return score; - } -} diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/AbstractLuceneProjection.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/AbstractLuceneProjection.java index e75c1ad93d8..bc484313044 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/AbstractLuceneProjection.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/AbstractLuceneProjection.java @@ -11,7 +11,7 @@ import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; import org.hibernate.search.engine.search.projection.spi.SearchProjectionBuilder; -abstract class AbstractLuceneProjection implements LuceneSearchProjection { +abstract class AbstractLuceneProjection

implements LuceneSearchProjection

{ private final Set indexNames; diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/AbstractNestingAwareAccumulatingValues.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/AbstractNestingAwareAccumulatingValues.java new file mode 100644 index 00000000000..bdbd07ccd9e --- /dev/null +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/AbstractNestingAwareAccumulatingValues.java @@ -0,0 +1,76 @@ +/* + * 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.backend.lucene.search.projection.impl; + +import java.io.IOException; + +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.TopDocsDataCollectorExecutionContext; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.Values; +import org.hibernate.search.backend.lucene.lowlevel.join.impl.ChildDocIds; +import org.hibernate.search.backend.lucene.lowlevel.join.impl.NestedDocsProvider; +import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; + +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.DocIdSetIterator; + +abstract class AbstractNestingAwareAccumulatingValues implements Values { + private final NestedDocsProvider nestedDocsProvider; + protected final ProjectionAccumulator accumulator; + + protected ChildDocIds currentLeafChildDocIds; + + AbstractNestingAwareAccumulatingValues(String parentDocumentPath, String nestedDocumentPath, + ProjectionAccumulator accumulator, TopDocsDataCollectorExecutionContext context) { + this.nestedDocsProvider = nestedDocumentPath == null || nestedDocumentPath.equals( parentDocumentPath ) + ? null + : context.createNestedDocsProvider( parentDocumentPath, nestedDocumentPath ); + this.accumulator = accumulator; + } + + @Override + public void context(LeafReaderContext context) throws IOException { + DocIdSetIterator valuesOrNull = doContext( context ); + if ( nestedDocsProvider != null ) { + currentLeafChildDocIds = nestedDocsProvider.childDocs( context, valuesOrNull ); + } + } + + protected DocIdSetIterator doContext(LeafReaderContext context) throws IOException { + // Nothing to do; to be overridden if necessary. + return null; + } + + @Override + public final A get(int parentDocId) throws IOException { + A accumulated = accumulator.createInitial(); + + if ( nestedDocsProvider == null ) { + // No nesting: we work directly on the parent doc. + accumulated = accumulate( accumulated, parentDocId ); + return accumulated; + } + if ( currentLeafChildDocIds == null ) { + // No child documents, hence no values, in the current leaf. + return accumulated; + } + + if ( !currentLeafChildDocIds.advanceExactParent( parentDocId ) ) { + return accumulated; + } + + for ( int currentChildDocId = currentLeafChildDocIds.nextChild(); + currentChildDocId != DocIdSetIterator.NO_MORE_DOCS; + currentChildDocId = currentLeafChildDocIds.nextChild() ) { + accumulated = accumulate( accumulated, currentChildDocId ); + } + + return accumulated; + } + + protected abstract A accumulate(A accumulated, int docId) throws IOException; + +} diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneCompositeProjection.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneCompositeProjection.java index 84e56b12d89..840875db8c9 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneCompositeProjection.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneCompositeProjection.java @@ -6,26 +6,42 @@ */ package org.hibernate.search.backend.lucene.search.projection.impl; +import java.io.IOException; import java.util.Arrays; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.Values; import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; -import org.hibernate.search.backend.lucene.search.extraction.impl.LuceneResult; import org.hibernate.search.engine.search.loading.spi.LoadingResult; -import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; import org.hibernate.search.engine.search.projection.SearchProjection; -import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; +import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; +import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; + +import org.apache.lucene.index.LeafReaderContext; -class LuceneCompositeProjection - extends AbstractLuceneProjection { +/** + * A projection that composes the result of multiple inner projections into a single value. + *

+ * Not to be confused with {@link LuceneObjectProjection}. + * + * @param The type of the temporary storage for component values. + * @param The type of a single composed value. + * @param The type of the temporary storage for accumulated values, before and after being composed. + * @param

The type of the final projection result representing an accumulation of composed values of type {@code V}. + */ +class LuceneCompositeProjection + extends AbstractLuceneProjection

{ + private final LuceneSearchProjection[] inners; private final ProjectionCompositor compositor; - private final LuceneSearchProjection[] inners; + private final ProjectionAccumulator accumulator; - private LuceneCompositeProjection(Builder builder) { + public LuceneCompositeProjection(Builder builder, LuceneSearchProjection[] inners, + ProjectionCompositor compositor, ProjectionAccumulator accumulator) { super( builder.scope ); - this.compositor = builder.compositor; - this.inners = builder.inners; + this.inners = inners; + this.compositor = compositor; + this.accumulator = accumulator; } @Override @@ -33,60 +49,109 @@ public String toString() { return getClass().getSimpleName() + "[" + "inners=" + Arrays.toString( inners ) + ", compositor=" + compositor + + ", accumulator=" + accumulator + "]"; } @Override - public void request(ProjectionRequestContext context) { - for ( LuceneSearchProjection inner : inners ) { - inner.request( context ); + public Extractor request(ProjectionRequestContext context) { + Extractor[] innerExtractors = new Extractor[inners.length]; + for ( int i = 0; i < inners.length; i++ ) { + innerExtractors[i] = inners[i].request( context ); } + return new CompositeExtractor( innerExtractors ); } - @Override - public final E extract(ProjectionHitMapper mapper, LuceneResult documentResult, - ProjectionExtractContext context) { - E extractedData = compositor.createInitial(); + private class CompositeExtractor implements Extractor { + private final Extractor[] inners; - for ( int i = 0; i < inners.length; i++ ) { - Object extractedDataForInner = inners[i].extract( mapper, documentResult, context ); - extractedData = compositor.set( extractedData, i, extractedDataForInner ); + private CompositeExtractor(Extractor[] inners) { + this.inners = inners; } - return extractedData; - } + @Override + public String toString() { + return getClass().getSimpleName() + "[" + + "inners=" + Arrays.toString( inners ) + + ", compositor=" + compositor + + ", accumulator=" + accumulator + + "]"; + } - @Override - public final V transform(LoadingResult loadingResult, E extractedData, - ProjectionTransformContext context) { - E transformedData = extractedData; - // Transform in-place - for ( int i = 0; i < inners.length; i++ ) { - Object extractedDataForInner = compositor.get( transformedData, i ); - Object transformedDataForInner = LuceneSearchProjection.transformUnsafe( inners[i], loadingResult, - extractedDataForInner, context ); - transformedData = compositor.set( transformedData, i, transformedDataForInner ); + @Override + public Values values(ProjectionExtractContext context) { + Values[] innerValues = new Values[inners.length]; + for ( int i = 0; i < inners.length; i++ ) { + innerValues[i] = inners[i].values( context ); + } + return new CompositeValues( innerValues ); } - return compositor.finish( transformedData ); + private class CompositeValues implements Values { + private final Values[] inners; + + private CompositeValues(Values[] inners) { + this.inners = inners; + } + + @Override + public void context(LeafReaderContext context) throws IOException { + for ( Values inner : inners ) { + inner.context( context ); + } + } + + @Override + public A get(int doc) throws IOException { + A accumulated = accumulator.createInitial(); + + E components = compositor.createInitial(); + for ( int i = 0; i < inners.length; i++ ) { + Object extractedDataForInner = inners[i].get( doc ); + components = compositor.set( components, i, extractedDataForInner ); + } + accumulated = accumulator.accumulate( accumulated, components ); + + return accumulated; + } + } + + @Override + public final P transform(LoadingResult loadingResult, A accumulated, + ProjectionTransformContext context) { + for ( int i = 0; i < accumulator.size( accumulated ); i++ ) { + E transformedData = accumulator.get( accumulated, i ); + // Transform in-place + for ( int j = 0; j < inners.length; j++ ) { + Object extractedDataForInner = compositor.get( transformedData, j ); + Object transformedDataForInner = Extractor.transformUnsafe( inners[j], loadingResult, + extractedDataForInner, context ); + transformedData = compositor.set( transformedData, j, transformedDataForInner ); + } + accumulated = accumulator.transform( accumulated, i, compositor.finish( transformedData ) ); + } + return accumulator.finish( accumulated ); + } } - static class Builder implements CompositeProjectionBuilder { + static class Builder implements CompositeProjectionBuilder { private final LuceneSearchIndexScope scope; - private final ProjectionCompositor compositor; - private final LuceneSearchProjection[] inners; - Builder(LuceneSearchIndexScope scope, ProjectionCompositor compositor, - LuceneSearchProjection ... inners) { + Builder(LuceneSearchIndexScope scope) { this.scope = scope; - this.compositor = compositor; - this.inners = inners; } @Override - public SearchProjection build() { - return new LuceneCompositeProjection<>( this ); + public SearchProjection

build(SearchProjection[] inners, ProjectionCompositor compositor, + ProjectionAccumulator.Provider accumulatorProvider) { + LuceneSearchProjection[] typedInners = + new LuceneSearchProjection[ inners.length ]; + for ( int i = 0; i < inners.length; i++ ) { + typedInners[i] = LuceneSearchProjection.from( scope, inners[i] ); + } + return new LuceneCompositeProjection<>( this, typedInners, + compositor, accumulatorProvider.get() ); } } } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneDistanceToFieldProjection.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneDistanceToFieldProjection.java index 789467a9232..677914bc10a 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneDistanceToFieldProjection.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneDistanceToFieldProjection.java @@ -6,23 +6,19 @@ */ package org.hibernate.search.backend.lucene.search.projection.impl; +import java.io.IOException; import java.lang.invoke.MethodHandles; -import java.util.Objects; import org.hibernate.search.backend.lucene.logging.impl.Log; -import org.hibernate.search.backend.lucene.lowlevel.collector.impl.CollectorExecutionContext; -import org.hibernate.search.backend.lucene.lowlevel.collector.impl.CollectorFactory; -import org.hibernate.search.backend.lucene.lowlevel.collector.impl.CollectorKey; -import org.hibernate.search.backend.lucene.lowlevel.collector.impl.GeoPointDistanceCollector; -import org.hibernate.search.backend.lucene.search.extraction.impl.LuceneResult; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.TopDocsDataCollectorExecutionContext; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.Values; +import org.hibernate.search.backend.lucene.lowlevel.docvalues.impl.GeoPointDistanceDocValues; import org.hibernate.search.backend.lucene.search.common.impl.AbstractLuceneCodecAwareSearchQueryElementFactory; import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexValueFieldContext; import org.hibernate.search.backend.lucene.types.codec.impl.LuceneFieldCodec; -import org.hibernate.search.engine.backend.types.converter.runtime.FromDocumentValueConvertContext; import org.hibernate.search.engine.backend.types.converter.spi.ProjectionConverter; import org.hibernate.search.engine.search.loading.spi.LoadingResult; -import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.spi.DistanceToFieldProjectionBuilder; import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; @@ -30,101 +26,149 @@ import org.hibernate.search.engine.spatial.GeoPoint; import org.hibernate.search.util.common.logging.impl.LoggerFactory; +import org.apache.lucene.index.DocValues; import org.apache.lucene.index.IndexableField; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.util.SloppyMath; /** * A projection on the distance from a given center to the GeoPoint defined in an index field. * - * @param The type of aggregated values extracted from the backend response (before conversion). - * @param

The type of aggregated values returned by the projection (after conversion). + * @param

The type of the final projection result representing accumulated distance values. */ -public class LuceneDistanceToFieldProjection extends AbstractLuceneProjection - implements CollectorFactory { +public class LuceneDistanceToFieldProjection

extends AbstractLuceneProjection

{ private static final ProjectionConverter NO_OP_DOUBLE_CONVERTER = ProjectionConverter.passThrough( Double.class ); private final String absoluteFieldPath; private final String nestedDocumentPath; + private final String requiredContextAbsoluteFieldPath; private final LuceneFieldCodec codec; private final GeoPoint center; private final DistanceUnit unit; - private final ProjectionAccumulator accumulator; + private final ProjectionAccumulator.Provider accumulatorProvider; - private final DistanceCollectorKey collectorKey; - private final LuceneFieldProjection fieldProjection; + private final LuceneFieldProjection fieldProjection; - private LuceneDistanceToFieldProjection(Builder builder, boolean singleValued, - ProjectionAccumulator accumulator) { + private LuceneDistanceToFieldProjection(Builder builder, + ProjectionAccumulator.Provider accumulatorProvider) { super( builder ); this.absoluteFieldPath = builder.field.absolutePath(); this.nestedDocumentPath = builder.field.nestedDocumentPath(); + this.requiredContextAbsoluteFieldPath = accumulatorProvider.isSingleValued() + ? builder.field.closestMultiValuedParentAbsolutePath() : null; this.codec = builder.codec; this.center = builder.center; this.unit = builder.unit; - this.accumulator = accumulator; - if ( singleValued ) { - // For single-valued fields, we can use the docvalues. - this.collectorKey = new DistanceCollectorKey( absoluteFieldPath, center ); - this.fieldProjection = null; - } - else { + this.accumulatorProvider = accumulatorProvider; + if ( builder.field.multiValued() ) { // For multi-valued fields, use a field projection, because we need order to be preserved. - this.collectorKey = null; this.fieldProjection = new LuceneFieldProjection<>( builder.scope, builder.field, - this::computeDistanceWithUnit, NO_OP_DOUBLE_CONVERTER, accumulator + this::computeDistanceWithUnit, NO_OP_DOUBLE_CONVERTER, accumulatorProvider ); } + else { + // For single-valued fields, we can use the docvalues. + this.fieldProjection = null; + } } @Override public String toString() { - StringBuilder sb = new StringBuilder( getClass().getSimpleName() ) - .append( "[" ) - .append( "absoluteFieldPath=" ).append( absoluteFieldPath ) - .append( ", center=" ).append( center ) - .append( ", accumulator=" ).append( accumulator ) - .append( "]" ); - return sb.toString(); + return getClass().getSimpleName() + "[" + + "absoluteFieldPath=" + absoluteFieldPath + + ", center=" + center + + ", accumulatorProvider=" + accumulatorProvider + + "]"; } @Override - public void request(ProjectionRequestContext context) { - if ( collectorKey != null ) { - context.requireCollector( this ); + public Extractor request(ProjectionRequestContext context) { + if ( fieldProjection != null ) { + return fieldProjection.request( context ); } else { - fieldProjection.request( context ); + context.checkValidField( absoluteFieldPath ); + if ( requiredContextAbsoluteFieldPath != null + && !requiredContextAbsoluteFieldPath.equals( context.absoluteCurrentFieldPath() ) ) { + throw log.invalidSingleValuedProjectionOnValueFieldInMultiValuedObjectField( + absoluteFieldPath, requiredContextAbsoluteFieldPath ); + } + return new DocValuesBasedDistanceExtractor<>( accumulatorProvider.get(), + context.absoluteCurrentFieldPath() ); } } - @Override - public E extract(ProjectionHitMapper mapper, LuceneResult documentResult, - ProjectionExtractContext context) { - if ( collectorKey != null ) { - E accumulated = accumulator.createInitial(); - GeoPointDistanceCollector distanceCollector = context.getCollector( collectorKey ); - Double distanceOrNull = distanceCollector.getDistance( documentResult.getDocId() ); - if ( distanceOrNull != null ) { - accumulated = accumulator.accumulate( accumulated, unit.fromMeters( distanceOrNull ) ); - } - return accumulated; + /** + * @param The type of the temporary storage for accumulated values, before and after being transformed. + */ + private class DocValuesBasedDistanceExtractor implements Extractor { + private final ProjectionAccumulator accumulator; + private final String contextAbsoluteFieldPath; + + private DocValuesBasedDistanceExtractor(ProjectionAccumulator accumulator, + String contextAbsoluteFieldPath) { + this.accumulator = accumulator; + this.contextAbsoluteFieldPath = contextAbsoluteFieldPath; } - else { - return fieldProjection.extract( mapper, documentResult, context ); + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + + "absoluteFieldPath=" + absoluteFieldPath + + ", center=" + center + + ", accumulator=" + accumulator + + "]"; } - } - @Override - public P transform(LoadingResult loadingResult, E extractedData, - ProjectionTransformContext context) { - FromDocumentValueConvertContext convertContext = context.fromDocumentValueConvertContext(); - return accumulator.finish( extractedData, NO_OP_DOUBLE_CONVERTER, convertContext ); + @Override + public Values values(ProjectionExtractContext context) { + // Note that if this method gets called, we're dealing with a single-valued projection, + // so we don't care that the order of doc values is not the same + // as the order of values in the original document. + return new DocValuesBasedDistanceValues( context.collectorExecutionContext() ); + } + + private class DocValuesBasedDistanceValues + extends AbstractNestingAwareAccumulatingValues { + private GeoPointDistanceDocValues currentLeafValues; + + public DocValuesBasedDistanceValues(TopDocsDataCollectorExecutionContext context) { + super( contextAbsoluteFieldPath, nestedDocumentPath, + DocValuesBasedDistanceExtractor.this.accumulator, context ); + } + + @Override + protected DocIdSetIterator doContext(LeafReaderContext context) throws IOException { + currentLeafValues = new GeoPointDistanceDocValues( + DocValues.getSortedNumeric( context.reader(), absoluteFieldPath ), center ); + return currentLeafValues; + } + + @Override + protected A accumulate(A accumulated, int docId) throws IOException { + if ( currentLeafValues.advanceExact( docId ) ) { + for ( int i = 0; i < currentLeafValues.docValueCount(); i++ ) { + Double distanceOrNull = currentLeafValues.nextValue(); + accumulated = accumulator.accumulate( accumulated, unit.fromMeters( distanceOrNull ) ); + } + } + return accumulated; + } + } + + @Override + public P transform(LoadingResult loadingResult, A extractedData, + ProjectionTransformContext context) { + // Nothing to transform: we take the values as they are. + return accumulator.finish( extractedData ); + } } private Double computeDistanceWithUnit(IndexableField field) { @@ -139,52 +183,6 @@ private Double computeDistanceWithUnit(IndexableField field) { return unit.fromMeters( distanceInMeters ); } - @Override - public GeoPointDistanceCollector createCollector(CollectorExecutionContext context) { - return new GeoPointDistanceCollector( - absoluteFieldPath, - nestedDocumentPath == null ? null : context.createNestedDocsProvider( nestedDocumentPath ), - center, context.getMaxDocs() - ); - } - - @Override - public CollectorKey getCollectorKey() { - return collectorKey; - } - - /** - * Necessary in order to share a single collector if there are multiple similar projections. - * See {@link #createCollector(CollectorExecutionContext)}, {@link #request(ProjectionRequestContext)}. - */ - private static final class DistanceCollectorKey implements CollectorKey { - - private final String absoluteFieldPath; - private final GeoPoint center; - - private DistanceCollectorKey(String absoluteFieldPath, GeoPoint center) { - this.absoluteFieldPath = absoluteFieldPath; - this.center = center; - } - - @Override - public boolean equals(Object obj) { - if ( obj == this ) { - return true; - } - if ( obj == null || !obj.getClass().equals( getClass() ) ) { - return false; - } - DistanceCollectorKey other = (DistanceCollectorKey) obj; - return absoluteFieldPath.equals( other.absoluteFieldPath ) && center.equals( other.center ); - } - - @Override - public int hashCode() { - return Objects.hash( absoluteFieldPath, center ); - } - } - public static class Factory extends AbstractLuceneCodecAwareSearchQueryElementFactory> { @@ -231,11 +229,10 @@ public void unit(DistanceUnit unit) { @Override public

SearchProjection

build(ProjectionAccumulator.Provider accumulatorProvider) { - if ( accumulatorProvider.isSingleValued() && field.multiValuedInRoot() ) { + if ( accumulatorProvider.isSingleValued() && field.multiValued() ) { throw log.invalidSingleValuedProjectionOnMultiValuedField( field.absolutePath(), field.eventContext() ); } - return new LuceneDistanceToFieldProjection<>( this, accumulatorProvider.isSingleValued(), - accumulatorProvider.get() ); + return new LuceneDistanceToFieldProjection<>( this, accumulatorProvider ); } } } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneDocumentProjection.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneDocumentProjection.java index f879007cbcb..f20bfbe368b 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneDocumentProjection.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneDocumentProjection.java @@ -6,16 +6,18 @@ */ package org.hibernate.search.backend.lucene.search.projection.impl; -import org.hibernate.search.backend.lucene.search.extraction.impl.LuceneResult; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.StoredFieldsValuesDelegate; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.Values; import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; import org.hibernate.search.engine.search.loading.spi.LoadingResult; -import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.spi.SearchProjectionBuilder; import org.apache.lucene.document.Document; +import org.apache.lucene.index.LeafReaderContext; -class LuceneDocumentProjection extends AbstractLuceneProjection { +class LuceneDocumentProjection extends AbstractLuceneProjection + implements LuceneSearchProjection.Extractor { private LuceneDocumentProjection(LuceneSearchIndexScope scope) { super( scope ); @@ -27,14 +29,25 @@ public String toString() { } @Override - public void request(ProjectionRequestContext context) { + public Extractor request(ProjectionRequestContext context) { context.requireAllStoredFields(); + return this; } @Override - public Document extract(ProjectionHitMapper mapper, LuceneResult documentResult, - ProjectionExtractContext context) { - return documentResult.getDocument(); + public Values values(ProjectionExtractContext context) { + StoredFieldsValuesDelegate delegate = context.collectorExecutionContext().storedFieldsValuesDelegate(); + return new Values() { + @Override + public void context(LeafReaderContext context) { + // Nothing to do + } + + @Override + public Document get(int doc) { + return delegate.get( doc ); + } + }; } @Override diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneDocumentReferenceProjection.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneDocumentReferenceProjection.java index e0d70f787c3..4b1789d3f47 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneDocumentReferenceProjection.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneDocumentReferenceProjection.java @@ -6,16 +6,16 @@ */ package org.hibernate.search.backend.lucene.search.projection.impl; -import org.hibernate.search.backend.lucene.lowlevel.collector.impl.DocumentReferenceCollector; -import org.hibernate.search.backend.lucene.search.extraction.impl.LuceneResult; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.DocumentReferenceValues; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.Values; import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; import org.hibernate.search.engine.backend.common.DocumentReference; import org.hibernate.search.engine.search.loading.spi.LoadingResult; -import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.spi.DocumentReferenceProjectionBuilder; -class LuceneDocumentReferenceProjection extends AbstractLuceneProjection { +class LuceneDocumentReferenceProjection extends AbstractLuceneProjection + implements LuceneSearchProjection.Extractor { private LuceneDocumentReferenceProjection(LuceneSearchIndexScope scope) { super( scope ); @@ -27,14 +27,13 @@ public String toString() { } @Override - public void request(ProjectionRequestContext context) { - context.requireCollector( DocumentReferenceCollector.FACTORY ); + public Extractor request(ProjectionRequestContext context) { + return this; } @Override - public DocumentReference extract(ProjectionHitMapper mapper, LuceneResult documentResult, - ProjectionExtractContext context) { - return context.getCollector( DocumentReferenceCollector.KEY ).get( documentResult.getDocId() ); + public Values values(ProjectionExtractContext context) { + return DocumentReferenceValues.simple( context.collectorExecutionContext() ); } @Override diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneEntityProjection.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneEntityProjection.java index e4b7a4cdb56..97d4af2459b 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneEntityProjection.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneEntityProjection.java @@ -6,16 +6,17 @@ */ package org.hibernate.search.backend.lucene.search.projection.impl; -import org.hibernate.search.backend.lucene.lowlevel.collector.impl.DocumentReferenceCollector; -import org.hibernate.search.backend.lucene.search.extraction.impl.LuceneResult; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.DocumentReferenceValues; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.Values; +import org.hibernate.search.backend.lucene.search.common.impl.LuceneDocumentReference; import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; -import org.hibernate.search.engine.backend.common.DocumentReference; import org.hibernate.search.engine.search.loading.spi.LoadingResult; import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.spi.EntityProjectionBuilder; -public class LuceneEntityProjection extends AbstractLuceneProjection { +public class LuceneEntityProjection extends AbstractLuceneProjection + implements LuceneSearchProjection.Extractor { private LuceneEntityProjection(LuceneSearchIndexScope scope) { super( scope ); @@ -27,16 +28,19 @@ public String toString() { } @Override - public void request(ProjectionRequestContext context) { - context.requireCollector( DocumentReferenceCollector.FACTORY ); + public Extractor request(ProjectionRequestContext context) { + return this; } @Override - public Object extract(ProjectionHitMapper mapper, LuceneResult documentResult, - ProjectionExtractContext context) { - DocumentReference documentReference = - context.getCollector( DocumentReferenceCollector.KEY ).get( documentResult.getDocId() ); - return mapper.planLoading( documentReference ); + public Values values(ProjectionExtractContext context) { + ProjectionHitMapper mapper = context.projectionHitMapper(); + return new DocumentReferenceValues( context.collectorExecutionContext() ) { + @Override + protected Object toReference(String typeName, String identifier) { + return mapper.planLoading( new LuceneDocumentReference( typeName, identifier ) ); + } + }; } @SuppressWarnings("unchecked") diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneEntityReferenceProjection.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneEntityReferenceProjection.java index 829e86c32eb..4cf04f4549a 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneEntityReferenceProjection.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneEntityReferenceProjection.java @@ -6,16 +6,16 @@ */ package org.hibernate.search.backend.lucene.search.projection.impl; -import org.hibernate.search.backend.lucene.lowlevel.collector.impl.DocumentReferenceCollector; -import org.hibernate.search.backend.lucene.search.extraction.impl.LuceneResult; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.DocumentReferenceValues; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.Values; import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; import org.hibernate.search.engine.backend.common.DocumentReference; import org.hibernate.search.engine.search.loading.spi.LoadingResult; -import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.spi.EntityReferenceProjectionBuilder; -public class LuceneEntityReferenceProjection extends AbstractLuceneProjection { +public class LuceneEntityReferenceProjection extends AbstractLuceneProjection + implements LuceneSearchProjection.Extractor { private LuceneEntityReferenceProjection(LuceneSearchIndexScope scope) { super( scope ); @@ -27,15 +27,13 @@ public String toString() { } @Override - public void request(ProjectionRequestContext context) { - context.requireCollector( DocumentReferenceCollector.FACTORY ); + public Extractor request(ProjectionRequestContext context) { + return this; } - @SuppressWarnings("unchecked") @Override - public DocumentReference extract(ProjectionHitMapper mapper, LuceneResult documentResult, - ProjectionExtractContext context) { - return context.getCollector( DocumentReferenceCollector.KEY ).get( documentResult.getDocId() ); + public Values values(ProjectionExtractContext context) { + return DocumentReferenceValues.simple( context.collectorExecutionContext() ); } @Override diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneExplanationProjection.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneExplanationProjection.java index 69956b5a569..1a10246c924 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneExplanationProjection.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneExplanationProjection.java @@ -6,30 +6,30 @@ */ package org.hibernate.search.backend.lucene.search.projection.impl; -import org.hibernate.search.backend.lucene.search.extraction.impl.LuceneResult; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.ExplanationValues; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.Values; import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; import org.hibernate.search.engine.search.loading.spi.LoadingResult; -import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.spi.SearchProjectionBuilder; import org.apache.lucene.search.Explanation; -class LuceneExplanationProjection extends AbstractLuceneProjection { +class LuceneExplanationProjection extends AbstractLuceneProjection + implements LuceneSearchProjection.Extractor { private LuceneExplanationProjection(LuceneSearchIndexScope scope) { super( scope ); } @Override - public void request(ProjectionRequestContext context) { - // We do not need anything specific. + public Extractor request(ProjectionRequestContext context) { + return this; } @Override - public Explanation extract(ProjectionHitMapper mapper, LuceneResult documentResult, - ProjectionExtractContext context) { - return context.explain( documentResult.getDocId() ); + public Values values(ProjectionExtractContext context) { + return new ExplanationValues( context.collectorExecutionContext() ); } @Override diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneFieldProjection.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneFieldProjection.java index 4824cda0f95..718d17cda95 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneFieldProjection.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneFieldProjection.java @@ -10,87 +10,143 @@ import java.util.function.Function; import org.hibernate.search.backend.lucene.logging.impl.Log; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.StoredFieldsValuesDelegate; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.TopDocsDataCollectorExecutionContext; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.Values; import org.hibernate.search.backend.lucene.search.common.impl.AbstractLuceneCodecAwareSearchQueryElementFactory; import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexValueFieldContext; -import org.hibernate.search.backend.lucene.search.extraction.impl.LuceneResult; import org.hibernate.search.backend.lucene.types.codec.impl.LuceneFieldCodec; import org.hibernate.search.engine.backend.types.converter.runtime.FromDocumentValueConvertContext; import org.hibernate.search.engine.backend.types.converter.spi.ProjectionConverter; import org.hibernate.search.engine.search.common.ValueConvert; import org.hibernate.search.engine.search.loading.spi.LoadingResult; -import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.spi.FieldProjectionBuilder; import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; import org.hibernate.search.util.common.logging.impl.LoggerFactory; +import org.apache.lucene.document.Document; import org.apache.lucene.index.IndexableField; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.DocIdSetIterator; /** * A projection on the values of an index field. * - * @param The type of the aggregated value extracted from the Lucene index (before conversion). - * @param

The type of the aggregated value returned by the projection (after conversion). * @param The type of individual field values obtained from the backend (before conversion). * @param The type of individual field values after conversion. + * @param

The type of the final projection result representing accumulated values of type {@code V}. */ -public class LuceneFieldProjection extends AbstractLuceneProjection { +public class LuceneFieldProjection extends AbstractLuceneProjection

{ private final String absoluteFieldPath; private final String nestedDocumentPath; + private final String requiredContextAbsoluteFieldPath; private final Function decodeFunction; private final ProjectionConverter converter; - private final ProjectionAccumulator accumulator; + private final ProjectionAccumulator.Provider accumulatorProvider; - private LuceneFieldProjection(Builder builder, ProjectionAccumulator accumulator) { - this( builder.scope, builder.field, builder.codec::decode, builder.converter, accumulator ); + private LuceneFieldProjection(Builder builder, ProjectionAccumulator.Provider accumulatorProvider) { + this( builder.scope, builder.field, builder.codec::decode, builder.converter, accumulatorProvider ); } LuceneFieldProjection(LuceneSearchIndexScope scope, LuceneSearchIndexValueFieldContext field, Function decodeFunction, ProjectionConverter converter, - ProjectionAccumulator accumulator) { + ProjectionAccumulator.Provider accumulatorProvider) { super( scope ); this.absoluteFieldPath = field.absolutePath(); this.nestedDocumentPath = field.nestedDocumentPath(); + this.requiredContextAbsoluteFieldPath = accumulatorProvider.isSingleValued() + ? field.closestMultiValuedParentAbsolutePath() : null; this.decodeFunction = decodeFunction; this.converter = converter; - this.accumulator = accumulator; + this.accumulatorProvider = accumulatorProvider; } @Override public String toString() { return getClass().getSimpleName() + "[" + "absoluteFieldPath=" + absoluteFieldPath - + ", accumulator=" + accumulator + + ", accumulatorProvider=" + accumulatorProvider + "]"; } @Override - public void request(ProjectionRequestContext context) { + public ValueFieldExtractor request(ProjectionRequestContext context) { + context.checkValidField( absoluteFieldPath ); + if ( requiredContextAbsoluteFieldPath != null + && !requiredContextAbsoluteFieldPath.equals( context.absoluteCurrentFieldPath() ) ) { + throw log.invalidSingleValuedProjectionOnValueFieldInMultiValuedObjectField( + absoluteFieldPath, requiredContextAbsoluteFieldPath ); + } context.requireStoredField( absoluteFieldPath, nestedDocumentPath ); + return new ValueFieldExtractor<>( context.absoluteCurrentFieldPath(), accumulatorProvider.get() ); } - @Override - public E extract(ProjectionHitMapper mapper, LuceneResult documentResult, - ProjectionExtractContext context) { - E extracted = accumulator.createInitial(); - for ( IndexableField field : documentResult.getDocument().getFields() ) { - if ( field.name().equals( absoluteFieldPath ) ) { - F decoded = decodeFunction.apply( field ); - extracted = accumulator.accumulate( extracted, decoded ); + /** + * @param The type of the temporary storage for accumulated values, before and after being transformed. + */ + private class ValueFieldExtractor implements LuceneSearchProjection.Extractor { + + private final String contextAbsoluteFieldPath; + private final ProjectionAccumulator accumulator; + + public ValueFieldExtractor(String contextAbsoluteFieldPath, ProjectionAccumulator accumulator) { + this.accumulator = accumulator; + this.contextAbsoluteFieldPath = contextAbsoluteFieldPath; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + + "absoluteFieldPath=" + absoluteFieldPath + + ", accumulator=" + accumulator + + "]"; + } + + @Override + public Values values(ProjectionExtractContext context) { + return new StoredFieldValues( accumulator, context.collectorExecutionContext() ); + } + + private class StoredFieldValues extends AbstractNestingAwareAccumulatingValues { + private final StoredFieldsValuesDelegate delegate; + + public StoredFieldValues(ProjectionAccumulator accumulator, + TopDocsDataCollectorExecutionContext context) { + super( contextAbsoluteFieldPath, nestedDocumentPath, accumulator, context ); + this.delegate = context.storedFieldsValuesDelegate(); + } + + @Override + protected DocIdSetIterator doContext(LeafReaderContext context) { + // We don't have a cost-effective way to iterate on children that have values for our stored field. + return null; + } + + @Override + protected A accumulate(A accumulated, int docId) { + Document document = delegate.get( docId ); + for ( IndexableField field : document.getFields() ) { + if ( field.name().equals( absoluteFieldPath ) ) { + F decoded = decodeFunction.apply( field ); + accumulated = accumulator.accumulate( accumulated, decoded ); + } + } + return accumulated; } } - return extracted; - } - @Override - public P transform(LoadingResult loadingResult, E extractedData, - ProjectionTransformContext context) { - FromDocumentValueConvertContext convertContext = context.fromDocumentValueConvertContext(); - return accumulator.finish( extractedData, converter, convertContext ); + @Override + public P transform(LoadingResult loadingResult, A extractedData, + ProjectionTransformContext context) { + FromDocumentValueConvertContext convertContext = context.fromDocumentValueConvertContext(); + A transformedData = accumulator.transformAll( extractedData, converter, convertContext ); + return accumulator.finish( transformedData ); + } } public static class Factory @@ -148,10 +204,10 @@ private Builder(LuceneFieldCodec codec, LuceneSearchIndexScope scope, @Override public

SearchProjection

build(ProjectionAccumulator.Provider accumulatorProvider) { - if ( accumulatorProvider.isSingleValued() && field.multiValuedInRoot() ) { + if ( accumulatorProvider.isSingleValued() && field.multiValued() ) { throw log.invalidSingleValuedProjectionOnMultiValuedField( field.absolutePath(), field.eventContext() ); } - return new LuceneFieldProjection<>( this, accumulatorProvider.get() ); + return new LuceneFieldProjection<>( this, accumulatorProvider ); } } } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneIdProjection.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneIdProjection.java index 9cd5ff2385b..216d07d0af6 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneIdProjection.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneIdProjection.java @@ -6,16 +6,16 @@ */ package org.hibernate.search.backend.lucene.search.projection.impl; -import org.hibernate.search.backend.lucene.lowlevel.collector.impl.IdentifierCollector; -import org.hibernate.search.backend.lucene.search.extraction.impl.LuceneResult; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.IdentifierValues; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.Values; import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; import org.hibernate.search.engine.backend.types.converter.spi.ProjectionConverter; import org.hibernate.search.engine.search.loading.spi.LoadingResult; -import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.spi.IdProjectionBuilder; -public class LuceneIdProjection extends AbstractLuceneProjection { +public class LuceneIdProjection extends AbstractLuceneProjection + implements LuceneSearchProjection.Extractor { private final ProjectionConverter converter; @@ -30,14 +30,13 @@ public String toString() { } @Override - public void request(ProjectionRequestContext context) { - context.requireCollector( IdentifierCollector.FACTORY ); + public Extractor request(ProjectionRequestContext context) { + return this; } @Override - public String extract(ProjectionHitMapper mapper, LuceneResult documentResult, - ProjectionExtractContext context) { - return context.getCollector( IdentifierCollector.KEY ).get( documentResult.getDocId() ); + public Values values(ProjectionExtractContext context) { + return new IdentifierValues(); } @Override diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneObjectProjection.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneObjectProjection.java new file mode 100644 index 00000000000..dce2b8c6dfb --- /dev/null +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneObjectProjection.java @@ -0,0 +1,193 @@ +/* + * 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.backend.lucene.search.projection.impl; + +import java.io.IOException; +import java.util.Arrays; + +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.TopDocsDataCollectorExecutionContext; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.Values; +import org.hibernate.search.backend.lucene.search.common.impl.AbstractLuceneCompositeNodeSearchQueryElementFactory; +import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexCompositeNodeContext; +import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; +import org.hibernate.search.engine.search.loading.spi.LoadingResult; +import org.hibernate.search.engine.search.projection.SearchProjection; +import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; +import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; +import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; + +import org.apache.lucene.index.LeafReaderContext; + +/** + * A projection that yields one composite value per object in a given object field. + *

+ * Not to be confused with {@link LuceneCompositeProjection}. + * + * @param The type of the temporary storage for component values. + * @param The type of a single composed value. + * @param

The type of the final projection result representing an accumulation of composed values of type {@code V}. + */ +public class LuceneObjectProjection + extends AbstractLuceneProjection

{ + + private final String absoluteFieldPath; + private final String nestedDocumentPath; + private final String requiredContextAbsoluteFieldPath; + private final LuceneSearchProjection[] inners; + private final ProjectionCompositor compositor; + private final ProjectionAccumulator.Provider accumulatorProvider; + + public LuceneObjectProjection(Builder builder, LuceneSearchProjection[] inners, + ProjectionCompositor compositor, ProjectionAccumulator.Provider accumulatorProvider) { + super( builder.scope ); + this.absoluteFieldPath = builder.objectField.absolutePath(); + this.nestedDocumentPath = builder.objectField.nestedDocumentPath(); + this.requiredContextAbsoluteFieldPath = accumulatorProvider.isSingleValued() + ? builder.objectField.closestMultiValuedParentAbsolutePath() : null; + this.inners = inners; + this.compositor = compositor; + this.accumulatorProvider = accumulatorProvider; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + + "inners=" + Arrays.toString( inners ) + + ", compositor=" + compositor + + ", accumulatorProvider=" + accumulatorProvider + + "]"; + } + + @Override + public Extractor request(ProjectionRequestContext context) { + ProjectionRequestContext innerContext = context.forField( absoluteFieldPath ); + if ( requiredContextAbsoluteFieldPath != null + && !requiredContextAbsoluteFieldPath.equals( context.absoluteCurrentFieldPath() ) ) { + throw log.invalidSingleValuedProjectionOnValueFieldInMultiValuedObjectField( + absoluteFieldPath, requiredContextAbsoluteFieldPath ); + } + Extractor[] innerExtractors = new Extractor[inners.length]; + for ( int i = 0; i < inners.length; i++ ) { + innerExtractors[i] = inners[i].request( innerContext ); + } + return new ObjectFieldExtractor<>( context.absoluteCurrentFieldPath(), innerExtractors, + accumulatorProvider.get() ); + } + + /** + * @param The type of the temporary storage for accumulated values, before and after being composed. + */ + private class ObjectFieldExtractor implements Extractor { + private final String contextAbsoluteFieldPath; + private final Extractor[] inners; + private final ProjectionAccumulator accumulator; + + private ObjectFieldExtractor(String contextAbsoluteFieldPath, + Extractor[] inners, ProjectionAccumulator accumulator) { + this.contextAbsoluteFieldPath = contextAbsoluteFieldPath; + this.inners = inners; + this.accumulator = accumulator; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + + "inners=" + Arrays.toString( inners ) + + ", compositor=" + compositor + + ", accumulator=" + accumulator + + "]"; + } + + @Override + public Values values(ProjectionExtractContext context) { + Values[] innerValues = new Values[inners.length]; + for ( int i = 0; i < inners.length; i++ ) { + innerValues[i] = inners[i].values( context ); + } + return new ObjectFieldValues( context.collectorExecutionContext(), innerValues ); + } + + private class ObjectFieldValues extends AbstractNestingAwareAccumulatingValues { + private final Values[] inners; + + private ObjectFieldValues(TopDocsDataCollectorExecutionContext context, Values[] inners) { + super( contextAbsoluteFieldPath, nestedDocumentPath, ObjectFieldExtractor.this.accumulator, context ); + this.inners = inners; + } + + @Override + public void context(LeafReaderContext context) throws IOException { + super.context( context ); + for ( Values inner : inners ) { + inner.context( context ); + } + } + + @Override + protected A accumulate(A accumulated, int docId) throws IOException { + E components = compositor.createInitial(); + for ( int i = 0; i < inners.length; i++ ) { + Object extractedDataForInner = inners[i].get( docId ); + components = compositor.set( components, i, extractedDataForInner ); + } + return accumulator.accumulate( accumulated, components ); + } + } + + @Override + public final P transform(LoadingResult loadingResult, A accumulated, + ProjectionTransformContext context) { + for ( int i = 0; i < accumulator.size( accumulated ); i++ ) { + E transformedData = accumulator.get( accumulated, i ); + // Transform in-place + for ( int j = 0; j < inners.length; j++ ) { + Object extractedDataForInner = compositor.get( transformedData, j ); + Object transformedDataForInner = Extractor.transformUnsafe( inners[j], loadingResult, + extractedDataForInner, context ); + transformedData = compositor.set( transformedData, j, transformedDataForInner ); + } + accumulated = accumulator.transform( accumulated, i, compositor.finish( transformedData ) ); + } + return accumulator.finish( accumulated ); + } + } + + public static class Factory + extends AbstractLuceneCompositeNodeSearchQueryElementFactory { + @Override + public Builder create(LuceneSearchIndexScope scope, LuceneSearchIndexCompositeNodeContext node) { + return new Builder( scope, node ); + } + } + + static class Builder implements CompositeProjectionBuilder { + + private final LuceneSearchIndexScope scope; + private final LuceneSearchIndexCompositeNodeContext objectField; + + Builder(LuceneSearchIndexScope scope, LuceneSearchIndexCompositeNodeContext objectField) { + this.scope = scope; + this.objectField = objectField; + } + + @Override + public SearchProjection

build(SearchProjection[] inners, ProjectionCompositor compositor, + ProjectionAccumulator.Provider accumulatorProvider) { + if ( accumulatorProvider.isSingleValued() && objectField.multiValued() ) { + throw log.invalidSingleValuedProjectionOnMultiValuedField( objectField.absolutePath(), + objectField.eventContext() ); + } + LuceneSearchProjection[] typedInners = + new LuceneSearchProjection[ inners.length ]; + for ( int i = 0; i < inners.length; i++ ) { + typedInners[i] = LuceneSearchProjection.from( scope, inners[i] ); + } + return new LuceneObjectProjection<>( this, typedInners, + compositor, accumulatorProvider ); + } + } +} diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneScoreProjection.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneScoreProjection.java index 4c5960221a1..64b3888a11f 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneScoreProjection.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneScoreProjection.java @@ -6,14 +6,15 @@ */ package org.hibernate.search.backend.lucene.search.projection.impl; -import org.hibernate.search.backend.lucene.search.extraction.impl.LuceneResult; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.ScoreValues; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.Values; import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; import org.hibernate.search.engine.search.loading.spi.LoadingResult; -import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.spi.ScoreProjectionBuilder; -class LuceneScoreProjection extends AbstractLuceneProjection { +class LuceneScoreProjection extends AbstractLuceneProjection + implements LuceneSearchProjection.Extractor { private LuceneScoreProjection(LuceneSearchIndexScope scope) { super( scope ); @@ -25,14 +26,14 @@ public String toString() { } @Override - public void request(ProjectionRequestContext context) { + public Extractor request(ProjectionRequestContext context) { context.requireScore(); + return this; } @Override - public Float extract(ProjectionHitMapper mapper, LuceneResult documentResult, - ProjectionExtractContext context) { - return documentResult.getScore(); + public Values values(ProjectionExtractContext context) { + return new ScoreValues( context.collectorExecutionContext() ); } @Override diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneSearchProjection.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneSearchProjection.java index 12c10fb1c59..ce59a87c075 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneSearchProjection.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneSearchProjection.java @@ -10,14 +10,15 @@ import java.util.Set; import org.hibernate.search.backend.lucene.logging.impl.Log; -import org.hibernate.search.backend.lucene.search.extraction.impl.LuceneResult; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.TopDocsDataCollector; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.Values; import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; import org.hibernate.search.engine.search.loading.spi.LoadingResult; import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.util.common.logging.impl.LoggerFactory; -public interface LuceneSearchProjection extends SearchProjection

{ +public interface LuceneSearchProjection

extends SearchProjection

{ Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() ); @@ -25,50 +26,66 @@ public interface LuceneSearchProjection extends SearchProjection

{ /** * Request the collection of per-document data that will be used in - * {@link #extract(ProjectionHitMapper, LuceneResult, ProjectionExtractContext)}, + * {@link Extractor#values(ProjectionExtractContext)}, * making sure that the requirements for this projection are met. * - * @param context A context that will share its state with the context passed to - * {@link #extract(ProjectionHitMapper, LuceneResult, ProjectionExtractContext)} . + * @param context An execution context for the request. + * @return An {@link Extractor}, to extract the result of the projection from the Elasticsearch response. */ - void request(ProjectionRequestContext context); + Extractor request(ProjectionRequestContext context); /** - * Perform hit extraction. - *

- * Implementations should only perform operations relative to extracting content from the index, - * delaying operations that rely on the mapper until - * {@link #transform(LoadingResult, Object, ProjectionTransformContext)} is called, - * so that blocking mapper operations (if any) do not pollute backend threads. + * An object responsible for extracting data from the Lucene Searcher, + * to implement a projection. * - * @param projectionHitMapper The projection hit mapper used to transform hits to entities. - * @param luceneResult A wrapper on top of the Lucene document extracted from the index. - * @param context An execution context for the extraction. - * @return The element extracted from the hit. Might be a key referring to an object that will be loaded by the - * {@link ProjectionHitMapper}. This returned object will be passed to {@link #transform(LoadingResult, Object, ProjectionTransformContext)}. + * @param The type of temporary values extracted from the response. May be the same as {@link P}, or not, + * depending on implementation. + * @param

The type of projected values. */ - E extract(ProjectionHitMapper projectionHitMapper, LuceneResult luceneResult, - ProjectionExtractContext context); + interface Extractor { + /** + * Creates low-level values for use in a {@link TopDocsDataCollector}. + *

+ * The returned {@link Values} should only perform operations relative to extracting content from the index, + * delaying operations that rely on the mapper until + * {@link #transform(LoadingResult, Object, ProjectionTransformContext)} is called, + * so that blocking mapper operations (if any) do not pollute backend threads. + * @param context An execution context for the extraction. + * @return The {@link Values} to use during Lucene collection of top docs data. + */ + Values values(ProjectionExtractContext context); - /** - * Transform the extracted data to the actual projection result. - * - * @param loadingResult Container containing all the entities that have been loaded by the - * {@link ProjectionHitMapper}. - * @param extractedData The extracted data to transform, coming from the - * {@link #extract(ProjectionHitMapper, LuceneResult, ProjectionExtractContext)} method. - * @param context An execution context for the transforming. - * @return The final result considered as a hit. - */ - P transform(LoadingResult loadingResult, E extractedData, - ProjectionTransformContext context); + /** + * Transforms the extracted data to the actual projection result. + * + * @param loadingResult Container containing all the entities that have been loaded by the + * {@link ProjectionHitMapper}. + * @param extractedData The extracted data to transform, returned by the + * {@link #values(ProjectionExtractContext) value source}. + * @param context An execution context for the transforming. + * @return The final result considered as a hit. + */ + P transform(LoadingResult loadingResult, E extractedData, + ProjectionTransformContext context); - static

LuceneSearchProjection from(LuceneSearchIndexScope scope, SearchProjection

projection) { + /** + * Transforms the extracted data and casts it to the right type. + *

+ * This should be used with care as it's unsafe. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + static Z transformUnsafe(Extractor extractor, LoadingResult loadingResult, + Object extractedData, ProjectionTransformContext context) { + return (Z) ( (Extractor) extractor ).transform( loadingResult, extractedData, context ); + } + } + + static

LuceneSearchProjection

from(LuceneSearchIndexScope scope, SearchProjection

projection) { if ( !( projection instanceof LuceneSearchProjection ) ) { throw log.cannotMixLuceneSearchQueryWithOtherProjections( projection ); } @SuppressWarnings("unchecked") // Necessary for ecj (Eclipse compiler) - LuceneSearchProjection casted = (LuceneSearchProjection) projection; + LuceneSearchProjection

casted = (LuceneSearchProjection

) projection; if ( !scope.hibernateSearchIndexNames().equals( casted.indexNames() ) ) { throw log.projectionDefinedOnDifferentIndexes( projection, casted.indexNames(), scope.hibernateSearchIndexNames() ); @@ -76,14 +93,4 @@ static

LuceneSearchProjection from(LuceneSearchIndexScope scope, Se return casted; } - /** - * Transform the extracted data and cast it to the right type. - *

- * This should be used with care as it's unsafe. - */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - static Z transformUnsafe(LuceneSearchProjection projection, LoadingResult loadingResult, - Object extractedData, ProjectionTransformContext context) { - return (Z) ( (LuceneSearchProjection) projection ).transform( loadingResult, extractedData, context ); - } } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneSearchProjectionBuilderFactory.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneSearchProjectionBuilderFactory.java index d647306f976..1fd3122ee4e 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneSearchProjectionBuilderFactory.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/LuceneSearchProjectionBuilderFactory.java @@ -6,14 +6,8 @@ */ package org.hibernate.search.backend.lucene.search.projection.impl; -import java.util.List; -import java.util.function.BiFunction; -import java.util.function.Function; - import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; import org.hibernate.search.engine.search.common.spi.SearchIndexIdentifierContext; -import org.hibernate.search.engine.search.projection.SearchProjection; -import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; import org.hibernate.search.engine.search.projection.spi.DocumentReferenceProjectionBuilder; import org.hibernate.search.engine.search.projection.spi.EntityProjectionBuilder; @@ -22,7 +16,6 @@ import org.hibernate.search.engine.search.projection.spi.ScoreProjectionBuilder; import org.hibernate.search.engine.search.projection.spi.SearchProjectionBuilder; import org.hibernate.search.engine.search.projection.spi.SearchProjectionBuilderFactory; -import org.hibernate.search.util.common.function.TriFunction; import org.apache.lucene.document.Document; import org.apache.lucene.search.Explanation; @@ -63,39 +56,8 @@ public ScoreProjectionBuilder score() { } @Override - public CompositeProjectionBuilder composite(Function, V> transformer, - SearchProjection... projections) { - LuceneSearchProjection[] typedProjections = new LuceneSearchProjection[ projections.length ]; - for ( int i = 0; i < projections.length; i++ ) { - typedProjections[i] = toImplementation( projections[i] ); - } - return new LuceneCompositeProjection.Builder<>( scope, - ProjectionCompositor.fromList( projections.length, transformer ), - typedProjections ); - } - - @Override - public CompositeProjectionBuilder composite(Function transformer, - SearchProjection projection) { - return new LuceneCompositeProjection.Builder<>( scope, - ProjectionCompositor.from( transformer ), - toImplementation( projection ) ); - } - - @Override - public CompositeProjectionBuilder composite(BiFunction transformer, - SearchProjection projection1, SearchProjection projection2) { - return new LuceneCompositeProjection.Builder<>( scope, - ProjectionCompositor.from( transformer ), - toImplementation( projection1 ), toImplementation( projection2 ) ); - } - - @Override - public CompositeProjectionBuilder composite(TriFunction transformer, - SearchProjection projection1, SearchProjection projection2, SearchProjection projection3) { - return new LuceneCompositeProjection.Builder<>( scope, - ProjectionCompositor.from( transformer ), - toImplementation( projection1 ), toImplementation( projection2 ), toImplementation( projection3 ) ); + public CompositeProjectionBuilder composite() { + return new LuceneCompositeProjection.Builder( scope ); } public SearchProjectionBuilder document() { @@ -106,7 +68,4 @@ public SearchProjectionBuilder explanation() { return new LuceneExplanationProjection.Builder( scope ); } - private LuceneSearchProjection toImplementation(SearchProjection projection) { - return LuceneSearchProjection.from( scope, projection ); - } } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/ProjectionExtractContext.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/ProjectionExtractContext.java index dc1026c56e3..f164a650a0b 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/ProjectionExtractContext.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/ProjectionExtractContext.java @@ -6,45 +6,25 @@ */ package org.hibernate.search.backend.lucene.search.projection.impl; -import java.io.IOException; -import java.lang.invoke.MethodHandles; - -import org.hibernate.search.backend.lucene.logging.impl.Log; -import org.hibernate.search.backend.lucene.lowlevel.collector.impl.CollectorKey; -import org.hibernate.search.backend.lucene.search.extraction.impl.CollectorSet; -import org.hibernate.search.util.common.logging.impl.LoggerFactory; - -import org.apache.lucene.search.Collector; -import org.apache.lucene.search.Explanation; -import org.apache.lucene.search.IndexSearcher; -import org.apache.lucene.search.Query; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.TopDocsDataCollectorExecutionContext; +import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; public class ProjectionExtractContext { - private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + private final TopDocsDataCollectorExecutionContext collectorExecutionContext; + private final ProjectionHitMapper projectionHitMapper; - private final IndexSearcher indexSearcher; - private final Query luceneQuery; - private final CollectorSet collectors; - - public ProjectionExtractContext(IndexSearcher indexSearcher, Query luceneQuery, - CollectorSet collectors) { - this.indexSearcher = indexSearcher; - this.luceneQuery = luceneQuery; - this.collectors = collectors; + public ProjectionExtractContext(TopDocsDataCollectorExecutionContext collectorExecutionContext, + ProjectionHitMapper projectionHitMapper) { + this.collectorExecutionContext = collectorExecutionContext; + this.projectionHitMapper = projectionHitMapper; } - public Explanation explain(int docId) { - try { - return indexSearcher.explain( luceneQuery, docId ); - } - catch (IOException e) { - throw log.ioExceptionOnExplain( e.getMessage(), e ); - } + public TopDocsDataCollectorExecutionContext collectorExecutionContext() { + return collectorExecutionContext; } - public C getCollector(CollectorKey key) { - return collectors == null ? null : collectors.get( key ); + public ProjectionHitMapper projectionHitMapper() { + return projectionHitMapper; } - } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/ProjectionRequestContext.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/ProjectionRequestContext.java index bc0d3f09a3c..e45efa43922 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/ProjectionRequestContext.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/projection/impl/ProjectionRequestContext.java @@ -6,17 +6,28 @@ */ package org.hibernate.search.backend.lucene.search.projection.impl; -import org.hibernate.search.backend.lucene.lowlevel.collector.impl.CollectorFactory; -import org.hibernate.search.backend.lucene.search.extraction.impl.ExtractionRequirements; +import java.lang.invoke.MethodHandles; -import org.apache.lucene.search.Collector; +import org.hibernate.search.backend.lucene.logging.impl.Log; +import org.hibernate.search.backend.lucene.search.extraction.impl.ExtractionRequirements; +import org.hibernate.search.engine.backend.common.spi.FieldPaths; +import org.hibernate.search.util.common.logging.impl.LoggerFactory; public final class ProjectionRequestContext { + private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + private final ExtractionRequirements.Builder extractionRequirementsBuilder; + private final String absoluteCurrentFieldPath; public ProjectionRequestContext(ExtractionRequirements.Builder extractionRequirementsBuilder) { + this( extractionRequirementsBuilder, null ); + } + + private ProjectionRequestContext(ExtractionRequirements.Builder extractionRequirementsBuilder, + String absoluteCurrentFieldPath) { this.extractionRequirementsBuilder = extractionRequirementsBuilder; + this.absoluteCurrentFieldPath = absoluteCurrentFieldPath; } public void requireAllStoredFields() { @@ -31,7 +42,19 @@ public void requireScore() { extractionRequirementsBuilder.requireScore(); } - public void requireCollector(CollectorFactory collectorFactory) { - extractionRequirementsBuilder.requireCollectorForTopDocs( collectorFactory ); + public void checkValidField(String absoluteFieldPath) { + if ( !FieldPaths.isStrictPrefix( absoluteCurrentFieldPath, absoluteFieldPath ) ) { + throw log.invalidContextForProjectionOnField( absoluteFieldPath, absoluteCurrentFieldPath ); + } } + + public ProjectionRequestContext forField(String absoluteFieldPath) { + checkValidField( absoluteFieldPath ); + return new ProjectionRequestContext( extractionRequirementsBuilder, absoluteFieldPath ); + } + + public String absoluteCurrentFieldPath() { + return absoluteCurrentFieldPath; + } + } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/dsl/impl/LuceneSearchQuerySelectStepImpl.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/dsl/impl/LuceneSearchQuerySelectStepImpl.java index 94ab3cf1919..10017a2c46d 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/dsl/impl/LuceneSearchQuerySelectStepImpl.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/dsl/impl/LuceneSearchQuerySelectStepImpl.java @@ -22,6 +22,8 @@ import org.hibernate.search.engine.search.predicate.dsl.PredicateFinalStep; import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.dsl.ProjectionFinalStep; +import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; +import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; import org.hibernate.search.engine.search.query.dsl.spi.AbstractSearchQuerySelectStep; public class LuceneSearchQuerySelectStepImpl @@ -73,7 +75,9 @@ public

LuceneSearchQueryWhereStep select(SearchProjection

project @Override public LuceneSearchQueryWhereStep, LOS> select(SearchProjection... projections) { - return select( scope.projectionBuilders().composite( Function.identity(), projections ).build() ); + return select( scope.projectionBuilders().composite() + .build( projections, ProjectionCompositor.fromList( projections.length ), + ProjectionAccumulator.single() ) ); } @Override diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneExtractableSearchResult.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneExtractableSearchResult.java index d55278a3aea..70c52be7796 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneExtractableSearchResult.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneExtractableSearchResult.java @@ -7,17 +7,16 @@ package org.hibernate.search.backend.lucene.search.query.impl; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import org.hibernate.search.backend.lucene.lowlevel.collector.impl.StoredFieldsCollector; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.TopDocsDataCollector; +import org.hibernate.search.backend.lucene.lowlevel.collector.impl.TopDocsDataCollectorExecutionContext; import org.hibernate.search.backend.lucene.search.aggregation.impl.AggregationExtractContext; import org.hibernate.search.backend.lucene.search.aggregation.impl.LuceneSearchAggregation; import org.hibernate.search.backend.lucene.search.extraction.impl.LuceneCollectors; -import org.hibernate.search.backend.lucene.search.extraction.impl.LuceneResult; import org.hibernate.search.backend.lucene.search.projection.impl.LuceneSearchProjection; import org.hibernate.search.backend.lucene.search.projection.impl.ProjectionExtractContext; import org.hibernate.search.engine.backend.types.converter.runtime.FromDocumentValueConvertContext; @@ -27,7 +26,6 @@ import org.hibernate.search.engine.search.query.SearchResultTotal; import org.hibernate.search.engine.search.timeout.spi.TimeoutManager; -import org.apache.lucene.document.Document; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; @@ -38,20 +36,20 @@ public class LuceneExtractableSearchResult { private final FromDocumentValueConvertContext fromDocumentValueConvertContext; private final IndexSearcher indexSearcher; private final LuceneCollectors luceneCollectors; - private final LuceneSearchProjection rootProjection; + private final LuceneSearchProjection.Extractor rootExtractor; private final Map, LuceneSearchAggregation> aggregations; private final TimeoutManager timeoutManager; public LuceneExtractableSearchResult(LuceneSearchQueryRequestContext requestContext, IndexSearcher indexSearcher, LuceneCollectors luceneCollectors, - LuceneSearchProjection rootProjection, + LuceneSearchProjection.Extractor rootExtractor, Map, LuceneSearchAggregation> aggregations, TimeoutManager timeoutManager) { this.requestContext = requestContext; this.fromDocumentValueConvertContext = new FromDocumentValueConvertContextImpl( requestContext.getSessionContext() ); this.indexSearcher = indexSearcher; this.luceneCollectors = luceneCollectors; - this.rootProjection = rootProjection; + this.rootExtractor = rootExtractor; this.aggregations = aggregations; this.timeoutManager = timeoutManager; } @@ -72,8 +70,6 @@ public LuceneLoadableSearchResult extract(int startInclusive, int endExclusiv endExclusive = Math.min( endExclusive, scoreDocs.length ); } - luceneCollectors.collectTopDocsData( startInclusive, endExclusive ); - ProjectionHitMapper projectionHitMapper = requestContext.getLoadingContext().createProjectionHitMapper(); List extractedData = extractHits( projectionHitMapper, startInclusive, endExclusive ); @@ -81,7 +77,7 @@ public LuceneLoadableSearchResult extract(int startInclusive, int endExclusiv Collections.emptyMap() : extractAggregations(); return new LuceneLoadableSearchResult<>( - fromDocumentValueConvertContext, rootProjection, + fromDocumentValueConvertContext, rootExtractor, luceneCollectors.getResultTotal(), luceneCollectors.getTopDocs(), extractedData, extractedAggregations, projectionHitMapper, timeoutManager.tookTime(), @@ -100,38 +96,15 @@ SearchResultTotal total() { } private List extractHits(ProjectionHitMapper projectionHitMapper, int startInclusive, - int endExclusive) { + int endExclusive) throws IOException { TopDocs topDocs = luceneCollectors.getTopDocs(); if ( topDocs == null ) { return Collections.emptyList(); } - List extractedData = new ArrayList<>( topDocs.scoreDocs.length ); - - ProjectionExtractContext projectionExtractContext = new ProjectionExtractContext( - indexSearcher, requestContext.getLuceneQuery(), - luceneCollectors.getCollectorsForTopDocs() - ); - - StoredFieldsCollector storedFieldsCollector = - projectionExtractContext.getCollector( StoredFieldsCollector.KEY ); - - for ( int i = startInclusive; i < endExclusive; i++ ) { - // Check for timeout every 16 elements. - // Do this *before* the element, so that we don't fail after the last element. - if ( i % 16 == 0 && timeoutManager.checkTimedOut() ) { - break; - } - - ScoreDoc hit = topDocs.scoreDocs[i]; - Document document = storedFieldsCollector == null ? null : storedFieldsCollector.getDocument( hit.doc ); + TopDocsDataCollectorFactory collectorFactory = new TopDocsDataCollectorFactory( projectionHitMapper ); - LuceneResult luceneResult = new LuceneResult( document, hit.doc, hit.score ); - - extractedData.add( rootProjection.extract( projectionHitMapper, luceneResult, projectionExtractContext ) ); - } - - return extractedData; + return luceneCollectors.collectTopDocsData( collectorFactory, startInclusive, endExclusive ); } private Map, ?> extractAggregations() throws IOException { @@ -159,4 +132,19 @@ private List extractHits(ProjectionHitMapper projectionHitMapper, return extractedMap; } + + private class TopDocsDataCollectorFactory implements TopDocsDataCollector.Factory { + private final ProjectionHitMapper projectionHitMapper; + + public TopDocsDataCollectorFactory(ProjectionHitMapper projectionHitMapper) { + this.projectionHitMapper = projectionHitMapper; + } + + @Override + public TopDocsDataCollector create(TopDocsDataCollectorExecutionContext context) throws IOException { + ProjectionExtractContext projectionExtractContext = + new ProjectionExtractContext( context, projectionHitMapper ); + return new TopDocsDataCollector<>( context, rootExtractor.values( projectionExtractContext ) ); + } + } } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneLoadableSearchResult.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneLoadableSearchResult.java index adf425bfaa2..27f0445212a 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneLoadableSearchResult.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneLoadableSearchResult.java @@ -6,7 +6,7 @@ */ package org.hibernate.search.backend.lucene.search.query.impl; -import static org.hibernate.search.backend.lucene.search.projection.impl.LuceneSearchProjection.transformUnsafe; +import static org.hibernate.search.backend.lucene.search.projection.impl.LuceneSearchProjection.Extractor.transformUnsafe; import java.time.Duration; import java.util.Collections; @@ -38,7 +38,7 @@ */ public class LuceneLoadableSearchResult { private final FromDocumentValueConvertContext fromDocumentValueConvertContext; - private final LuceneSearchProjection rootProjection; + private final LuceneSearchProjection.Extractor rootExtractor; private final SearchResultTotal resultTotal; private final TopDocs topDocs; @@ -51,13 +51,13 @@ public class LuceneLoadableSearchResult { private final TimeoutManager timeoutManager; LuceneLoadableSearchResult(FromDocumentValueConvertContext fromDocumentValueConvertContext, - LuceneSearchProjection rootProjection, + LuceneSearchProjection.Extractor rootExtractor, SearchResultTotal resultTotal, TopDocs topDocs, List extractedData, Map, ?> extractedAggregations, ProjectionHitMapper projectionHitMapper, Duration took, boolean timedOut, TimeoutManager timeoutManager) { this.fromDocumentValueConvertContext = fromDocumentValueConvertContext; - this.rootProjection = rootProjection; + this.rootExtractor = rootExtractor; this.resultTotal = resultTotal; this.topDocs = topDocs; this.extractedData = extractedData; @@ -78,7 +78,7 @@ LuceneSearchResult loadBlocking() { for ( ; readIndex < extractedData.size(); ++readIndex ) { transformContext.reset(); H transformed = transformUnsafe( - rootProjection, loadingResult, extractedData.get( readIndex ), transformContext + rootExtractor, loadingResult, extractedData.get( readIndex ), transformContext ); if ( transformContext.hasFailedLoad() ) { diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneSearchQueryBuilder.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneSearchQueryBuilder.java index 29a9564cc9c..a1fe191855a 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneSearchQueryBuilder.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneSearchQueryBuilder.java @@ -62,7 +62,7 @@ public class LuceneSearchQueryBuilder implements SearchQueryBuilder, Lucen private final Set routingKeys; private final SearchLoadingContextBuilder loadingContextBuilder; - private final LuceneSearchProjection rootProjection; + private final LuceneSearchProjection rootProjection; private Query luceneQuery; private List sortFields; @@ -78,7 +78,7 @@ public LuceneSearchQueryBuilder( LuceneSearchQueryIndexScope scope, BackendSessionContext sessionContext, SearchLoadingContextBuilder loadingContextBuilder, - LuceneSearchProjection rootProjection) { + LuceneSearchProjection rootProjection) { this.workFactory = workFactory; this.queryOrchestrator = queryOrchestrator; @@ -209,7 +209,8 @@ public LuceneSearchQuery build() { ExtractionRequirements.Builder extractionRequirementsBuilder = new ExtractionRequirements.Builder(); ProjectionRequestContext projectionRequestContext = new ProjectionRequestContext( extractionRequirementsBuilder ); - rootProjection.request( projectionRequestContext ); + LuceneSearchProjection.Extractor rootExtractor = + rootProjection.request( projectionRequestContext ); if ( aggregations != null ) { AggregationRequestContext aggregationRequestContext = new AggregationRequestContext( extractionRequirementsBuilder ); @@ -223,7 +224,7 @@ public LuceneSearchQuery build() { LuceneSearcherImpl searcher = new LuceneSearcherImpl<>( requestContext, - rootProjection, + rootExtractor, aggregations == null ? Collections.emptyMap() : aggregations, extractionRequirements, timeoutManager diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneSearchQueryImpl.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneSearchQueryImpl.java index c15afc7c374..3af5623ca43 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneSearchQueryImpl.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneSearchQueryImpl.java @@ -85,7 +85,7 @@ public String queryString() { @Override public String toString() { - return getClass().getSimpleName() + "[query=" + queryString() + ", sort=" + luceneSort + "]"; + return getClass().getSimpleName() + "[searcher=" + searcher + "]"; } @Override diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneSearcherImpl.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneSearcherImpl.java index b84396e5698..3c79bce9cd3 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneSearcherImpl.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/LuceneSearcherImpl.java @@ -39,19 +39,19 @@ class LuceneSearcherImpl implements LuceneSearcher rootProjection; + private final LuceneSearchProjection.Extractor rootExtractor; private final Map, LuceneSearchAggregation> aggregations; private final ExtractionRequirements extractionRequirements; private TimeoutManager timeoutManager; LuceneSearcherImpl(LuceneSearchQueryRequestContext requestContext, - LuceneSearchProjection rootProjection, + LuceneSearchProjection.Extractor rootExtractor, Map, LuceneSearchAggregation> aggregations, ExtractionRequirements extractionRequirements, TimeoutManager timeoutManager) { this.requestContext = requestContext; - this.rootProjection = rootProjection; + this.rootExtractor = rootExtractor; this.aggregations = aggregations; this.extractionRequirements = extractionRequirements; this.timeoutManager = timeoutManager; @@ -59,12 +59,12 @@ class LuceneSearcherImpl implements LuceneSearcher doSearch(IndexSearcher indexSearcher, collectMatchingDocsWithPrefetch( indexSearcher, metadataResolver, offset, limit, maxDocs, totalHitCountThreshold ); return new LuceneExtractableSearchResult<>( requestContext, indexSearcher, luceneCollectors, - rootProjection, aggregations, timeoutManager ); + rootExtractor, aggregations, timeoutManager ); } @Override diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/SearchBackendContext.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/SearchBackendContext.java index c20d1b9fdf6..6064948e5a0 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/SearchBackendContext.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/query/impl/SearchBackendContext.java @@ -34,6 +34,6 @@ LuceneSearchQueryBuilder createSearchQueryBuilder( LuceneSearchQueryIndexScope scope, BackendSessionContext sessionContext, SearchLoadingContextBuilder loadingContextBuilder, - LuceneSearchProjection rootProjection); + LuceneSearchProjection rootProjection); } diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/types/impl/LuceneIndexCompositeNodeType.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/types/impl/LuceneIndexCompositeNodeType.java index 0612265498a..3068d25f45b 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/types/impl/LuceneIndexCompositeNodeType.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/types/impl/LuceneIndexCompositeNodeType.java @@ -10,10 +10,12 @@ import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexCompositeNodeTypeContext; import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; import org.hibernate.search.backend.lucene.search.predicate.impl.LuceneNestedPredicate; +import org.hibernate.search.backend.lucene.search.projection.impl.LuceneObjectProjection; import org.hibernate.search.backend.lucene.types.predicate.impl.LuceneObjectExistsPredicate; import org.hibernate.search.engine.backend.types.ObjectStructure; import org.hibernate.search.engine.backend.types.spi.AbstractIndexCompositeNodeType; import org.hibernate.search.engine.search.predicate.spi.PredicateTypeKeys; +import org.hibernate.search.engine.search.projection.spi.ProjectionTypeKeys; public class LuceneIndexCompositeNodeType extends AbstractIndexCompositeNodeType< @@ -34,6 +36,7 @@ public Builder(ObjectStructure objectStructure) { queryElementFactory( PredicateTypeKeys.EXISTS, LuceneObjectExistsPredicate.Factory.INSTANCE ); if ( ObjectStructure.NESTED.equals( objectStructure ) ) { queryElementFactory( PredicateTypeKeys.NESTED, LuceneNestedPredicate.Factory.INSTANCE ); + queryElementFactory( ProjectionTypeKeys.OBJECT, new LuceneObjectProjection.Factory() ); } } diff --git a/documentation/src/main/asciidoc/reference/mapper-orm-mapping-indexedembedded.asciidoc b/documentation/src/main/asciidoc/reference/mapper-orm-mapping-indexedembedded.asciidoc index 9252f556c08..4b588aec05b 100644 --- a/documentation/src/main/asciidoc/reference/mapper-orm-mapping-indexedembedded.asciidoc +++ b/documentation/src/main/asciidoc/reference/mapper-orm-mapping-indexedembedded.asciidoc @@ -492,6 +492,9 @@ include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/indexedembedd <3> The hits will *not* include a book whose authors are "Ty Daniel" and "Frank Abraham". ==== +NOTE: With the <>, the nested structure is also necessary if +you want to perform <>. + [[mapper-orm-indexedembedded-programmatic]] == Programmatic mapping diff --git a/documentation/src/main/asciidoc/reference/search-dsl-projection.asciidoc b/documentation/src/main/asciidoc/reference/search-dsl-projection.asciidoc index 810a1503dbf..9b8f4728aaa 100644 --- a/documentation/src/main/asciidoc/reference/search-dsl-projection.asciidoc +++ b/documentation/src/main/asciidoc/reference/search-dsl-projection.asciidoc @@ -289,6 +289,9 @@ include::{sourcedir}/org/hibernate/search/documentation/search/projection/Projec [[search-dsl-projection-composite]] == `composite`: combine projections +[[search-dsl-projection-composite-basics]] +=== Basics + The `composite` projection applies multiple projections and combines their results, either as a `List` or as a single object generated using a custom transformer. @@ -311,11 +314,14 @@ include::{sourcedir}/org/hibernate/search/documentation/search/projection/Projec The constructor of `MyPair` will be called for each matched document, with the value of the `title` field as its first argument, and the value of the `genre` field as its second argument. -<5> The hits will be the result of calling the constructor for each matched document, -in this case `MyPair` instances. +<5> Each hit will be an instance of `MyPair`. +Thus, the list of hits will be an instance of `List`. ==== -If you pass more than 3 projections as arguments, +[[search-dsl-projection-composite-more-inners]] +=== Composing more than 3 inner projections + +If you pass more than 3 projections as arguments to `from(...)`, then the transform function will have to take a `List` as an argument, and will be set using `asList(...)` instead of `as(..,)`: @@ -330,13 +336,16 @@ include::{sourcedir}/org/hibernate/search/documentation/search/projection/Projec <3> Define the second inner projection as a projection on the `genre` field. <4> Define the third inner projection as a projection on the `pageCount` field. <5> Define the fourth inner projection as a projection on the `description` field. -<6> Define the result of the composite projection as the result of calling a lambda. +<6> Define the result of the object projection as the result of calling a lambda. The lambda will take elements of the list (the results of projections defined above, in order), cast them, and pass them to the constructor of a custom class, `MyTuple4`. -<7> The hits will be the result of calling the lambda for each matched document, -in this case `MyTuple4` instances. +<7> Each hit will be an instance of `MyTuple4`. +Thus, the list of hits will be an instance of `List`. ==== +[[search-dsl-projection-composite-as-list]] +=== Projecting to a `List` + If you don't mind receiving the result of inner projections as a `List`, you can do without the transformer by calling `asList()`: @@ -352,7 +361,9 @@ include::{sourcedir}/org/hibernate/search/documentation/search/projection/Projec <4> Define the result of the projection as a list, meaning the hits will be `List` instances with the value of the `title` field of the matched document at index `0`, and the value of the `genre` field of the matched document at index `1`. -<5> The hits will be `List` instances holding the result of the inner projections, in the given order, for each matched document. +<5> Each hit will be an instance of `List`: +a list containing the result of the inner projections, in the given order. +Thus, the list of hits will be an instance of `List>`. ==== Alternatively, to get the result as a `List`, @@ -367,9 +378,9 @@ include::{sourcedir}/org/hibernate/search/documentation/search/projection/Projec <1> Call `.composite(...)`. <2> Define the first projection to combine as a projection on the `title` field. <3> Define the second projection to combine as a projection on the `genre` field. -<4> The hits will be `List` instances holding the result of the given projections, in the given order, for each matched document, -meaning the hits will be `List` instances with the value of the `title` field of the matched document at index `0`, -and the value of the `genre` field of the matched document at index `1`. +<4> Each hit will be an instance of `List`: +a list containing the result of the inner projections, in the given order. +Thus, the list of hits will be an instance of `List>`. ==== [[search-dsl-projection-composite-deprecated-variants]] @@ -394,10 +405,127 @@ with the value of the `title` field as its first argument. <4> Define the second projection to combine as a projection on the `genre` field, meaning the constructor of `MyPair` will be called for each matched document with the value of the `genre` field as its second argument. -<5> The hits will be the result of calling the transformer for each matched document, -in this case `MyPair` instances. +<5> Each hit will be an instance of `MyPair`. +Thus, the list of hits will be an instance of `List`. +==== + +[[search-dsl-projection-object]] +== `object`: return one value per object in an object field + +The `object` projection yields one projected value for each object in a given object field, +the value being generated by applying multiple inner projections and combining their results +either as a `List` or as a single object generated using a custom transformer. + +[NOTE] +==== +The `object` projection may seem very similar to the <>, +and its definition via the Search DSL certainly is indeed similar. + +However, there are two key differences: + +1. The `object` projection will yield `null` when projecting on a single-valued object field + if the object was null when indexing. +2. The `object` projection will yield multiple values when projecting on a multivalued object field + if there were multiple objects when indexing. ==== +[WARNING] +==== +With the <>, the object projection has a few limitations: + +1. It is only available for object fields with a +<>. +2. It will never yield `null` objects for multi-valued object fields. +The Lucene backend does not index `null` objects, +and thus cannot find them when searching. + +These limitations do not apply to the <>. +==== + +[[search-dsl-projection-object-syntax]] +=== Syntax + +To preserve type-safety, you can provide a custom transformer. +The transformer can be a `Function`, a `BiFunction`, +or a `org.hibernate.search.util.common.function.TriFunction`, +depending on the number of inner projections. +It will receive values returned by inner projections and return an object combining these values. + +.Returning custom objects created from multiple projected values with `.object(...).from(...).as(...)` +==== +[source, JAVA, indent=0, subs="+callouts"] +---- +include::{sourcedir}/org/hibernate/search/documentation/search/projection/ProjectionDslIT.java[tags=object-customObject] +---- +<1> Call `.object( "authors" )`. +<2> Define the first inner projection as a projection on the `firstName` field of `authors`. +<3> Define the second inner projection as a projection on the `lastName` field of `authors`. +<4> Define the result of the object projection as the result of calling the constructor of a custom object, `MyAuthorName`. +The constructor of `MyAuthorName` will be called for each object in the `authors` object field, +with the value of the `authors.firstName` field as its first argument, +and the value of the `authors.lastName` field as its second argument. +<5> Define the projection as multivalued, +meaning it will yield values of type `List`: +one `MyAuthorName` per object in the `authors` object field. +<6> Each hit will be an instance of `List`. +Thus, the list of hits will be an instance of `List>`. +==== + +[[search-dsl-projection-object-more-inners]] +=== Composing more than 3 inner projections + +If you pass more than 3 projections as arguments, +then the transform function will have to take a `List` as an argument, +and will be set using `asList(...)` instead of `as(..,)`: + +.Returning custom objects created from multiple projected values with `.object(...).from(...).asList(...)` +==== +[source, JAVA, indent=0, subs="+callouts"] +---- +include::{sourcedir}/org/hibernate/search/documentation/search/projection/ProjectionDslIT.java[tags=object-customObject-asList] +---- +<1> Call `.object( "authors" )`. +<2> Define the first inner projection as a projection on the `firstName` field of `authors`. +<3> Define the second inner projection as a projection on the `lastName` field of `authors`. +<4> Define the third inner projection as a projection on the `birthDate` field of `authors`. +<5> Define the fourth inner projection as a <> +on the `placeOfBirth` field with the given center and unit. +<6> Define the result of the object projection as the result of calling a lambda. +The lambda will take elements of the list (the results of projections defined above, in order), +cast them, and pass them to the constructor of a custom class, `MyAuthorNameAndBirthDateAndPlaceOfBirthDistance`. +<7> Define the projection as multivalued, +meaning it will yield values of type `List`: +one `MyAuthorNameAndBirthDateAndPlaceOfBirthDistance` per object in the `authors` object field. +instead of just `MyAuthorNameAndBirthDateAndPlaceOfBirthDistance`. +<8> Each hit will be an instance of `List`. +Thus, the list of hits will be an instance of `List>`. +==== + +[[search-dsl-projection-object-as-list]] +=== Projecting to a `List` + +If you don't mind receiving the result of inner projections as a `List`, +you can do without the transformer by calling `asList()`: + +.Returning a `List` of projected values with `.object(...).add(...).asList()` +==== +[source, JAVA, indent=0, subs="+callouts"] +---- +include::{sourcedir}/org/hibernate/search/documentation/search/projection/ProjectionDslIT.java[tags=object-list] +---- +<1> Call `.object( "authors" )`. +<2> Define the first inner projection as a projection on the `firstName` field of `authors`. +<3> Define the second inner projection as a projection on the `lastName` field of `authors`. +<4> Define the result of the projection as a list, +meaning the hits will be `List` instances with at index `0` the value of the `firstName` field of `authors` , +and at index `1` the value of the `lastName` field of `authors`. +<5> Define the projection as multivalued, +meaning it will yield values of type `List`: +one `List` per object in the `authors` object field. +<6> Each hit will be an instance of `List>`: +a list containing one list per author, which in turns contains the result of the inner projections, in the given order. +Thus, the list of hits will be an instance of `List>>`. +==== [[search-dsl-projection-extensions]] == Backend-specific extensions diff --git a/documentation/src/test/java/org/hibernate/search/documentation/search/projection/Author.java b/documentation/src/test/java/org/hibernate/search/documentation/search/projection/Author.java index a746ecf685c..37a880482b5 100644 --- a/documentation/src/test/java/org/hibernate/search/documentation/search/projection/Author.java +++ b/documentation/src/test/java/org/hibernate/search/documentation/search/projection/Author.java @@ -6,6 +6,7 @@ */ package org.hibernate.search.documentation.search.projection; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import javax.persistence.Embedded; @@ -31,6 +32,9 @@ public class Author { @FullTextField(analyzer = "name", projectable = Projectable.YES) private String lastName; + @GenericField(projectable = Projectable.YES) + private LocalDate birthDate; + @Embedded @GenericField(projectable = Projectable.YES) private EmbeddableGeoPoint placeOfBirth; @@ -65,6 +69,14 @@ public void setLastName(String lastName) { this.lastName = lastName; } + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } + public EmbeddableGeoPoint getPlaceOfBirth() { return placeOfBirth; } diff --git a/documentation/src/test/java/org/hibernate/search/documentation/search/projection/ProjectionDslIT.java b/documentation/src/test/java/org/hibernate/search/documentation/search/projection/ProjectionDslIT.java index 0f6965b6ecc..ddc18581826 100644 --- a/documentation/src/test/java/org/hibernate/search/documentation/search/projection/ProjectionDslIT.java +++ b/documentation/src/test/java/org/hibernate/search/documentation/search/projection/ProjectionDslIT.java @@ -9,7 +9,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.search.util.impl.integrationtest.common.assertion.SearchHitsAssert.assertThatHits; +import java.time.LocalDate; +import java.time.Month; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -29,12 +32,15 @@ import org.hibernate.search.mapper.orm.common.impl.EntityReferenceImpl; import org.hibernate.search.mapper.orm.scope.SearchScope; import org.hibernate.search.mapper.orm.session.SearchSession; +import org.hibernate.search.util.impl.integrationtest.common.assertion.TestComparators; import org.hibernate.search.util.impl.integrationtest.mapper.orm.OrmUtils; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration; + public class ProjectionDslIT { private static final int ASIMOV_ID = 1; @@ -502,6 +508,150 @@ public void composite_singleStep() { } ); } + @Test + public void object() { + withinSearchSession( searchSession -> { + // tag::object-customObject[] + List> hits = searchSession.search( Book.class ) + .select( f -> f.object( "authors" ) // <1> + .from( f.field( "authors.firstName", String.class ), // <2> + f.field( "authors.lastName", String.class ) ) // <3> + .as( MyAuthorName::new ) // <4> + .multi() ) // <5> + .where( f -> f.matchAll() ) + .fetchHits( 20 ); // <6> + // end::object-customObject[] + Session session = searchSession.toOrmSession(); + assertThat( hits ).usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder( + Collections.singletonList( + new MyAuthorName( + session.getReference( Book.class, BOOK1_ID ).getAuthors().get( 0 ).getFirstName(), + session.getReference( Book.class, BOOK1_ID ).getAuthors().get( 0 ).getLastName() + ) + ), + Collections.singletonList( + new MyAuthorName( + session.getReference( Book.class, BOOK2_ID ).getAuthors().get( 0 ).getFirstName(), + session.getReference( Book.class, BOOK2_ID ).getAuthors().get( 0 ).getLastName() + ) + ), + Collections.singletonList( + new MyAuthorName( + session.getReference( Book.class, BOOK3_ID ).getAuthors().get( 0 ).getFirstName(), + session.getReference( Book.class, BOOK3_ID ).getAuthors().get( 0 ).getLastName() + ) + ), + Collections.singletonList( + new MyAuthorName( + session.getReference( Book.class, BOOK4_ID ).getAuthors().get( 0 ).getFirstName(), + session.getReference( Book.class, BOOK4_ID ).getAuthors().get( 0 ).getLastName() + ) + ) + ); + } ); + + withinSearchSession( searchSession -> { + // tag::object-customObject-asList[] + GeoPoint center = GeoPoint.of( 53.970000, 32.150000 ); + List> hits = searchSession + .search( Book.class ) + .select( f -> f.object( "authors" ) // <1> + .from( f.field( "authors.firstName", String.class ), // <2> + f.field( "authors.lastName", String.class ), // <3> + f.field( "authors.birthDate", LocalDate.class ), // <4> + f.distance( "authors.placeOfBirth", center ) // <5> + .unit( DistanceUnit.KILOMETERS ) ) + .asList( list -> // <6> + new MyAuthorNameAndBirthDateAndPlaceOfBirthDistance( + (String) list.get( 0 ), (String) list.get( 1 ), + (LocalDate) list.get( 2 ), (Double) list.get( 3 ) ) ) + .multi() ) // <7> + .where( f -> f.matchAll() ) + .fetchHits( 20 ); // <8> + // end::object-customObject-asList[] + Session session = searchSession.toOrmSession(); + assertThat( hits ) + .usingRecursiveFieldByFieldElementComparator( RecursiveComparisonConfiguration.builder() + .withComparatorForType( TestComparators.APPROX_KM_COMPARATOR, Double.class ) + .build() ) + .containsExactlyInAnyOrder( + Collections.singletonList( + new MyAuthorNameAndBirthDateAndPlaceOfBirthDistance( + session.getReference( Book.class, BOOK1_ID ).getAuthors().get( 0 ).getFirstName(), + session.getReference( Book.class, BOOK1_ID ).getAuthors().get( 0 ).getLastName(), + session.getReference( Book.class, BOOK1_ID ).getAuthors().get( 0 ).getBirthDate(), + 0.888 + ) + ), + Collections.singletonList( + new MyAuthorNameAndBirthDateAndPlaceOfBirthDistance( + session.getReference( Book.class, BOOK2_ID ).getAuthors().get( 0 ).getFirstName(), + session.getReference( Book.class, BOOK2_ID ).getAuthors().get( 0 ).getLastName(), + session.getReference( Book.class, BOOK2_ID ).getAuthors().get( 0 ).getBirthDate(), + 0.888 + ) + ), + Collections.singletonList( + new MyAuthorNameAndBirthDateAndPlaceOfBirthDistance( + session.getReference( Book.class, BOOK3_ID ).getAuthors().get( 0 ).getFirstName(), + session.getReference( Book.class, BOOK3_ID ).getAuthors().get( 0 ).getLastName(), + session.getReference( Book.class, BOOK3_ID ).getAuthors().get( 0 ).getBirthDate(), + 0.888 + ) + ), + Collections.singletonList( + new MyAuthorNameAndBirthDateAndPlaceOfBirthDistance( + session.getReference( Book.class, BOOK4_ID ).getAuthors().get( 0 ).getFirstName(), + session.getReference( Book.class, BOOK4_ID ).getAuthors().get( 0 ).getLastName(), + session.getReference( Book.class, BOOK4_ID ).getAuthors().get( 0 ).getBirthDate(), + 9680.93 + ) + ) + ); + } ); + + withinSearchSession( searchSession -> { + // tag::object-list[] + List>> hits = searchSession.search( Book.class ) + .select( f -> f.object( "authors" ) // <1> + .from( f.field( "authors.firstName", String.class ), // <2> + f.field( "authors.lastName", String.class ) ) // <3> + .asList() // <4> + .multi() ) // <5> + .where( f -> f.matchAll() ) + .fetchHits( 20 ); // <6> + // end::object-list[] + Session session = searchSession.toOrmSession(); + assertThat( hits ).containsExactlyInAnyOrder( + Collections.singletonList( + Arrays.asList( + session.getReference( Book.class, BOOK1_ID ).getAuthors().get( 0 ).getFirstName(), + session.getReference( Book.class, BOOK1_ID ).getAuthors().get( 0 ).getLastName() + ) + ), + Collections.singletonList( + Arrays.asList( + session.getReference( Book.class, BOOK2_ID ).getAuthors().get( 0 ).getFirstName(), + session.getReference( Book.class, BOOK2_ID ).getAuthors().get( 0 ).getLastName() + ) + ), + Collections.singletonList( + Arrays.asList( + session.getReference( Book.class, BOOK3_ID ).getAuthors().get( 0 ).getFirstName(), + session.getReference( Book.class, BOOK3_ID ).getAuthors().get( 0 ).getLastName() + ) + ), + Collections.singletonList( + Arrays.asList( + session.getReference( Book.class, BOOK4_ID ).getAuthors().get( 0 ).getFirstName(), + session.getReference( Book.class, BOOK4_ID ).getAuthors().get( 0 ).getLastName() + ) + ) + ); + } ); + } + private void withinSearchSession(Consumer action) { OrmUtils.withinJPATransaction( entityManagerFactory, entityManager -> { SearchSession searchSession = Search.session( entityManager ); @@ -515,12 +665,14 @@ private void initData() { isaacAsimov.setId( ASIMOV_ID ); isaacAsimov.setFirstName( "Isaac" ); isaacAsimov.setLastName( "Asimov" ); + isaacAsimov.setBirthDate( LocalDate.of( 1920, Month.JANUARY, 2 ) ); isaacAsimov.setPlaceOfBirth( EmbeddableGeoPoint.of( 53.976177, 32.158627 ) ); Author aLeeMartinez = new Author(); aLeeMartinez.setId( MARTINEZ_ID ); aLeeMartinez.setFirstName( "A. Lee" ); aLeeMartinez.setLastName( "Martinez" ); + aLeeMartinez.setBirthDate( LocalDate.of( 1973, Month.JANUARY, 12 ) ); aLeeMartinez.setPlaceOfBirth( EmbeddableGeoPoint.of( 31.814315, -106.475524 ) ); Book book1 = new Book(); @@ -620,4 +772,48 @@ public int hashCode() { return Objects.hash( first, second, third, fourth ); } } + + private static class MyAuthorName { + private final String firstName; + private final String lastName; + + MyAuthorName(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + @Override + public String toString() { + return "MyAuthorName{" + + "firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + '}'; + } + } + + private static class MyAuthorNameAndBirthDateAndPlaceOfBirthDistance { + private final String firstName; + private final String lastName; + private final LocalDate birthDate; + private final Double placeOfBirthDistance; + + private MyAuthorNameAndBirthDateAndPlaceOfBirthDistance(String firstName, String lastName, + LocalDate birthDate, Double placeOfBirthDistance) { + this.firstName = firstName; + this.lastName = lastName; + this.birthDate = birthDate; + this.placeOfBirthDistance = placeOfBirthDistance; + } + + @Override + public String toString() { + return "MyAuthorNameAndBirthDateAndPlaceOfBirthDistance{" + + "firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", birthDate=" + birthDate + + ", placeOfBirthDistance=" + placeOfBirthDistance + + '}'; + } + } + } diff --git a/engine/src/main/java/org/hibernate/search/engine/backend/common/spi/FieldPaths.java b/engine/src/main/java/org/hibernate/search/engine/backend/common/spi/FieldPaths.java index e562a20ac88..ae6011d7c25 100644 --- a/engine/src/main/java/org/hibernate/search/engine/backend/common/spi/FieldPaths.java +++ b/engine/src/main/java/org/hibernate/search/engine/backend/common/spi/FieldPaths.java @@ -67,6 +67,17 @@ public static RelativizedPath relativize(String absolutePath) { ); } + public static boolean isStrictPrefix(String prefixCandidatePath, String path) { + if ( prefixCandidatePath == null ) { + return !path.isEmpty(); + } + if ( prefixCandidatePath.length() >= path.length() ) { + return false; + } + return path.startsWith( prefixCandidatePath ) + && path.charAt( prefixCandidatePath.length() ) == PATH_SEPARATOR; + } + public static String[] split(String absoluteFieldPath) { return absoluteFieldPath.split( PATH_SEPARATOR_REGEX_STRING ); } diff --git a/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/AbstractIndexField.java b/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/AbstractIndexField.java index aecf81a6b86..a6f4d43eb0e 100644 --- a/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/AbstractIndexField.java +++ b/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/AbstractIndexField.java @@ -26,7 +26,7 @@ public abstract class AbstractIndexField< protected final String relativeName; protected final IndexFieldInclusion inclusion; protected final boolean multiValued; - protected final boolean multiValuedInRoot; + private final String closestMultiValuedParentAbsolutePath; public AbstractIndexField(C parent, String relativeFieldName, NT type, IndexFieldInclusion inclusion, boolean multiValued) { @@ -37,7 +37,8 @@ public AbstractIndexField(C parent, String relativeFieldName, NT type, IndexFiel this.relativeName = relativeFieldName; this.inclusion = inclusion; this.multiValued = multiValued; - this.multiValuedInRoot = multiValued || parent.multiValuedInRoot(); + this.closestMultiValuedParentAbsolutePath = parent.multiValued() ? parent.absolutePath() + : parent.closestMultiValuedParentAbsolutePath(); } @Override @@ -81,8 +82,13 @@ public final boolean multiValued() { } @Override - public final boolean multiValuedInRoot() { - return multiValuedInRoot; + public boolean multiValuedInRoot() { + return multiValued || closestMultiValuedParentAbsolutePath != null; + } + + @Override + public String closestMultiValuedParentAbsolutePath() { + return closestMultiValuedParentAbsolutePath; } } diff --git a/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/AbstractIndexRoot.java b/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/AbstractIndexRoot.java index 35c824f24d9..2e32886456d 100644 --- a/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/AbstractIndexRoot.java +++ b/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/AbstractIndexRoot.java @@ -103,11 +103,21 @@ public final Map staticChildrenByName() { return staticChildrenByName; } + @Override + public final boolean multiValued() { + return false; + } + @Override public boolean multiValuedInRoot() { return false; } + @Override + public final String closestMultiValuedParentAbsolutePath() { + return null; + } + @Override final SearchIndexSchemaElementContextHelper helper() { return SearchIndexSchemaElementContextHelper.COMPOSITE; diff --git a/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/IndexNode.java b/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/IndexNode.java index 2afbf96fa0d..cc5703e90dd 100644 --- a/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/IndexNode.java +++ b/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/IndexNode.java @@ -15,13 +15,12 @@ public interface IndexNode> @Override IndexCompositeNode toComposite(); + @Override IndexObjectField toObjectField(); @Override IndexValueField toValueField(); - boolean multiValuedInRoot(); - IndexFieldInclusion inclusion(); } diff --git a/engine/src/main/java/org/hibernate/search/engine/logging/impl/Log.java b/engine/src/main/java/org/hibernate/search/engine/logging/impl/Log.java index c39ab6d6854..68d3788ad02 100644 --- a/engine/src/main/java/org/hibernate/search/engine/logging/impl/Log.java +++ b/engine/src/main/java/org/hibernate/search/engine/logging/impl/Log.java @@ -452,9 +452,10 @@ SearchException cannotUseQueryElementForIndexNode( + " If it already is, then '%1$s' is not available for fields of this type.") String missingSupportHintForValueField(SearchQueryElementTypeKey key); - @Message(value = "If you are trying to use the 'nested' predicate, set the field structure to 'NESTED' and reindex all your data." - + " If you are trying to use another predicate, it probably isn't available for this field") - String missingSupportHintForCompositNode(); + @Message(value = "Some object field features require a nested structure;" + + " try setting the field structure to 'NESTED' and reindexing all your data." + + " If you are trying to use another feature, it probably isn't available for this field.") + String missingSupportHintForCompositeNode(); @Message(id = ID_OFFSET + 105, value = "Cannot use '%2$s' on %1$s: %3$s") SearchException cannotUseQueryElementForIndexElementBecauseCreationException( diff --git a/engine/src/main/java/org/hibernate/search/engine/search/common/spi/AbstractMultiIndexSearchIndexCompositeNodeContext.java b/engine/src/main/java/org/hibernate/search/engine/search/common/spi/AbstractMultiIndexSearchIndexCompositeNodeContext.java index be209953899..ecff73abb30 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/common/spi/AbstractMultiIndexSearchIndexCompositeNodeContext.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/common/spi/AbstractMultiIndexSearchIndexCompositeNodeContext.java @@ -45,6 +45,11 @@ public final boolean isComposite() { return true; } + @Override + public boolean isObjectField() { + return absolutePath != null; + } + @Override public final boolean isValueField() { return false; @@ -55,6 +60,16 @@ public final S toComposite() { return self(); } + @Override + public S toObjectField() { + if ( isObjectField() ) { + return self(); + } + else { + return SearchIndexSchemaElementContextHelper.throwingToObjectField( this ); + } + } + @Override public SearchIndexValueFieldContext toValueField() { return SearchIndexSchemaElementContextHelper.throwingToValueField( this ); diff --git a/engine/src/main/java/org/hibernate/search/engine/search/common/spi/AbstractMultiIndexSearchIndexNodeContext.java b/engine/src/main/java/org/hibernate/search/engine/search/common/spi/AbstractMultiIndexSearchIndexNodeContext.java index 321cdd4c72f..ce322fdbb12 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/common/spi/AbstractMultiIndexSearchIndexNodeContext.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/common/spi/AbstractMultiIndexSearchIndexNodeContext.java @@ -8,6 +8,7 @@ import java.lang.invoke.MethodHandles; import java.util.List; +import java.util.Objects; import java.util.function.BiPredicate; import java.util.function.Function; @@ -64,6 +65,32 @@ public String nestedDocumentPath() { Object::equals, "nestedDocumentPath" ); } + @Override + public String closestMultiValuedParentAbsolutePath() { + return fromNodeIfCompatible( SearchIndexNodeContext::closestMultiValuedParentAbsolutePath, + Objects::equals, "closestMultiValuedParentAbsolutePath" ); + } + + @Override + public boolean multiValued() { + for ( S field : nodeForEachIndex ) { + if ( field.multiValued() ) { + return true; + } + } + return false; + } + + @Override + public final boolean multiValuedInRoot() { + for ( S field : nodeForEachIndex ) { + if ( field.multiValuedInRoot() ) { + return true; + } + } + return false; + } + @Override public final EventContext eventContext() { return indexesEventContext().append( relativeEventContext() ); diff --git a/engine/src/main/java/org/hibernate/search/engine/search/common/spi/AbstractMultiIndexSearchIndexValueFieldContext.java b/engine/src/main/java/org/hibernate/search/engine/search/common/spi/AbstractMultiIndexSearchIndexValueFieldContext.java index 088f5fa6a36..eddace8756d 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/common/spi/AbstractMultiIndexSearchIndexValueFieldContext.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/common/spi/AbstractMultiIndexSearchIndexValueFieldContext.java @@ -35,6 +35,11 @@ public final boolean isComposite() { return false; } + @Override + public boolean isObjectField() { + return false; + } + @Override public final boolean isValueField() { return true; @@ -46,18 +51,13 @@ public SearchIndexCompositeNodeContext toComposite() { } @Override - public final S toValueField() { - return self(); + public SearchIndexCompositeNodeContext toObjectField() { + return SearchIndexSchemaElementContextHelper.throwingToObjectField( this ); } @Override - public final boolean multiValuedInRoot() { - for ( S field : nodeForEachIndex ) { - if ( field.multiValuedInRoot() ) { - return true; - } - } - return false; + public final S toValueField() { + return self(); } @Override diff --git a/engine/src/main/java/org/hibernate/search/engine/search/common/spi/SearchIndexNodeContext.java b/engine/src/main/java/org/hibernate/search/engine/search/common/spi/SearchIndexNodeContext.java index 6f6d4c018fe..913ca9efae6 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/common/spi/SearchIndexNodeContext.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/common/spi/SearchIndexNodeContext.java @@ -26,10 +26,14 @@ public interface SearchIndexNodeContext> boolean isComposite(); + boolean isObjectField(); + boolean isValueField(); SearchIndexCompositeNodeContext toComposite(); + SearchIndexCompositeNodeContext toObjectField(); + SearchIndexValueFieldContext toValueField(); String absolutePath(); @@ -45,6 +49,12 @@ default String nestedDocumentPath() { hierarchy.get( hierarchy.size() - 1 ); } + String closestMultiValuedParentAbsolutePath(); + + boolean multiValued(); + + boolean multiValuedInRoot(); + // Query elements: predicates, sorts, projections, aggregations, ... T queryElement(SearchQueryElementTypeKey key, SC searchContext); diff --git a/engine/src/main/java/org/hibernate/search/engine/search/common/spi/SearchIndexSchemaElementContextHelper.java b/engine/src/main/java/org/hibernate/search/engine/search/common/spi/SearchIndexSchemaElementContextHelper.java index 1692b7b13cd..c92b04275c0 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/common/spi/SearchIndexSchemaElementContextHelper.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/common/spi/SearchIndexSchemaElementContextHelper.java @@ -64,7 +64,7 @@ public String partialSupportHint() { public static final SearchIndexSchemaElementContextHelper COMPOSITE = new SearchIndexSchemaElementContextHelper() { @Override protected String missingSupportHint(SearchQueryElementTypeKey key) { - return log.missingSupportHintForCompositNode(); + return log.missingSupportHintForCompositeNode(); } @Override diff --git a/engine/src/main/java/org/hibernate/search/engine/search/common/spi/SearchIndexValueFieldContext.java b/engine/src/main/java/org/hibernate/search/engine/search/common/spi/SearchIndexValueFieldContext.java index f05cc99927e..cf89cfd6928 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/common/spi/SearchIndexValueFieldContext.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/common/spi/SearchIndexValueFieldContext.java @@ -15,8 +15,6 @@ public interface SearchIndexValueFieldContext> extends SearchIndexNodeContext { - boolean multiValuedInRoot(); - SearchIndexValueFieldTypeContext type(); } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionAsStep.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionAsStep.java index 32bf269db6c..0bad11389f6 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionAsStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionAsStep.java @@ -22,7 +22,7 @@ public interface CompositeProjectionAsStep { * * @return The next DSL step. */ - CompositeProjectionOptionsStep> asList(); + CompositeProjectionValueStep> asList(); /** * Defines the result of the composite projection @@ -33,6 +33,6 @@ public interface CompositeProjectionAsStep { * @return The next DSL step. * @param The type of values returned by the transformer. */ - CompositeProjectionOptionsStep asList(Function, V> transformer); + CompositeProjectionValueStep asList(Function, V> transformer); } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionFrom1AsStep.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionFrom1AsStep.java index 2cdf6242637..0a01618559d 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionFrom1AsStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionFrom1AsStep.java @@ -26,6 +26,6 @@ public interface CompositeProjectionFrom1AsStep * @return The next DSL step. * @param The type of values returned by the transformer. */ - CompositeProjectionOptionsStep as(Function transformer); + CompositeProjectionValueStep as(Function transformer); } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionFrom2AsStep.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionFrom2AsStep.java index 94af16585f9..f5a35c5468c 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionFrom2AsStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionFrom2AsStep.java @@ -27,6 +27,6 @@ public interface CompositeProjectionFrom2AsStep * @return The next DSL step. * @param The type of values returned by the transformer. */ - CompositeProjectionOptionsStep as(BiFunction transformer); + CompositeProjectionValueStep as(BiFunction transformer); } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionFrom3AsStep.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionFrom3AsStep.java index d3388e2d023..bf8d530d669 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionFrom3AsStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionFrom3AsStep.java @@ -28,6 +28,6 @@ public interface CompositeProjectionFrom3AsStep * @return The next DSL step. * @param The type of values returned by the transformer. */ - CompositeProjectionOptionsStep as(TriFunction transformer); + CompositeProjectionValueStep as(TriFunction transformer); } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionValueStep.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionValueStep.java new file mode 100644 index 00000000000..9e767bd1bf9 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/CompositeProjectionValueStep.java @@ -0,0 +1,40 @@ +/* + * 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.engine.search.projection.dsl; + +import java.util.List; + +/** + * The step in a composite projection definition + * where the projection (optionally) can be marked as multi-valued (returning Lists), + * and where optional parameters can be set. + *

+ * By default (if {@link #multi()} is not called), the projection is single-valued. + * + * @param The next step if a method other than {@link #multi()} is called, + * i.e. the return type of methods defined in {@link CompositeProjectionOptionsStep} + * when called directly on this object. + * @param The type of composed projections. + */ +public interface CompositeProjectionValueStep, T> + extends CompositeProjectionOptionsStep { + + /** + * Defines the projection as multi-valued, i.e. returning {@code List} instead of {@code T}. + *

+ * Calling {@link #multi()} is mandatory for {@link SearchProjectionFactory#object(String) object projections} + * on multi-valued object fields, + * otherwise the projection will throw an exception upon creating the search query. + *

+ * Calling {@link #multi()} on {@link SearchProjectionFactory#composite() basic composite projections} + * is generally not useful: the only effect is that projected values will be wrapped in a one-element {@link List}. + * + * @return A new step to define optional parameters for the projection. + */ + CompositeProjectionOptionsStep> multi(); + +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/SearchProjectionFactory.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/SearchProjectionFactory.java index 38f61ec51dc..bd0bc9678d6 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/SearchProjectionFactory.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/SearchProjectionFactory.java @@ -158,9 +158,31 @@ default FieldProjectionValueStep field(String fieldPath) { */ DistanceToFieldProjectionValueStep distance(String fieldPath, GeoPoint center); + /** + * Starts the definition of an object projection, + * which will yield one value per object in a given object field, + * the value being the result of combining multiple given projections + * (usually on fields within the object field). + *

+ * Compared to the basic {@link #composite() composite projection}, + * an object projection is bound to a specific object field, + * and thus it yield zero, one or many values, as many as there are objects in the targeted object field. + * Therefore, you must take care of calling {@link CompositeProjectionValueStep#multi()} + * if the object field is multi-valued. + * + * @param objectFieldPath The path to the object field whose object(s) will be extracted. + * @return A DSL step where the "composite" projection can be defined in more details. + */ + CompositeProjectionFromStep object(String objectFieldPath); + /** * Starts the definition of a composite projection, * which will combine multiple given projections. + *

+ * On contrary to the {@link #object(String) object projection}, + * a composite projection is not bound to a specific object field, + * and thus it will always yield one and only one value, + * regardless of whether {@link CompositeProjectionValueStep#multi()} is called. * * @return A DSL step where the "composite" projection can be defined in more details. */ @@ -172,7 +194,7 @@ default FieldProjectionValueStep field(String fieldPath) { * @param projections The projections used to populate the list, in order. * @return A DSL step where the "composite" projection can be defined in more details. */ - CompositeProjectionOptionsStep> composite(SearchProjection... projections); + CompositeProjectionValueStep> composite(SearchProjection... projections); /** * Create a projection that will compose a {@link List} based on the given almost-built projections. @@ -180,7 +202,7 @@ default FieldProjectionValueStep field(String fieldPath) { * @param dslFinalSteps The final steps in the projection DSL allowing the retrieval of {@link SearchProjection}s. * @return A DSL step where the "composite" projection can be defined in more details. */ - default CompositeProjectionOptionsStep> composite(ProjectionFinalStep... dslFinalSteps) { + default CompositeProjectionValueStep> composite(ProjectionFinalStep... dslFinalSteps) { SearchProjection[] projections = new SearchProjection[dslFinalSteps.length]; for ( int i = 0; i < dslFinalSteps.length; i++ ) { projections[i] = dslFinalSteps[i].toProjection(); diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/AbstractCompositeProjectionAsStep.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/AbstractCompositeProjectionAsStep.java index 1d951866482..fc0f72d29f7 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/AbstractCompositeProjectionAsStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/AbstractCompositeProjectionAsStep.java @@ -11,26 +11,29 @@ import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionAsStep; -import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionOptionsStep; -import org.hibernate.search.engine.search.projection.dsl.spi.SearchProjectionDslContext; +import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionValueStep; +import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; +import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; abstract class AbstractCompositeProjectionAsStep implements CompositeProjectionAsStep { - final SearchProjectionDslContext dslContext; + final CompositeProjectionBuilder builder; - public AbstractCompositeProjectionAsStep(SearchProjectionDslContext dslContext) { - this.dslContext = dslContext; + public AbstractCompositeProjectionAsStep(CompositeProjectionBuilder builder) { + this.builder = builder; } @Override - public final CompositeProjectionOptionsStep> asList() { + public final CompositeProjectionValueStep> asList() { return asList( Function.identity() ); } @Override - public final CompositeProjectionOptionsStep asList(Function, V> transformer) { - return new CompositeProjectionOptionsStepImpl<>( dslContext, transformer, toProjectionArray() ); + public final CompositeProjectionValueStep asList(Function, V> transformer) { + SearchProjection[] inners = toProjectionArray(); + return new CompositeProjectionValueStepImpl<>( builder, inners, + ProjectionCompositor.fromList( inners.length, transformer ) ); } abstract SearchProjection[] toProjectionArray(); diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFrom1AsStepImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFrom1AsStepImpl.java index 5ad28872b26..efcfdd44f47 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFrom1AsStepImpl.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFrom1AsStepImpl.java @@ -10,23 +10,26 @@ import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionFrom1AsStep; -import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionOptionsStep; -import org.hibernate.search.engine.search.projection.dsl.spi.SearchProjectionDslContext; +import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionValueStep; +import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; +import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; class CompositeProjectionFrom1AsStepImpl extends AbstractCompositeProjectionAsStep implements CompositeProjectionFrom1AsStep { final SearchProjection inner1; - public CompositeProjectionFrom1AsStepImpl(SearchProjectionDslContext dslContext, + public CompositeProjectionFrom1AsStepImpl(CompositeProjectionBuilder builder, SearchProjection inner1) { - super( dslContext ); + super( builder ); this.inner1 = inner1; } @Override - public CompositeProjectionOptionsStep as(Function transformer) { - return new CompositeProjectionOptionsStepImpl<>( dslContext, transformer, inner1 ); + public CompositeProjectionValueStep as(Function transformer) { + return new CompositeProjectionValueStepImpl<>( builder, toProjectionArray(), + ProjectionCompositor.from( transformer ) + ); } @Override diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFrom2AsStepImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFrom2AsStepImpl.java index 63301f593c8..2550daded31 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFrom2AsStepImpl.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFrom2AsStepImpl.java @@ -10,8 +10,9 @@ import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionFrom2AsStep; -import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionOptionsStep; -import org.hibernate.search.engine.search.projection.dsl.spi.SearchProjectionDslContext; +import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionValueStep; +import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; +import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; class CompositeProjectionFrom2AsStepImpl extends AbstractCompositeProjectionAsStep @@ -20,16 +21,18 @@ class CompositeProjectionFrom2AsStepImpl final SearchProjection inner1; final SearchProjection inner2; - public CompositeProjectionFrom2AsStepImpl(SearchProjectionDslContext dslContext, + public CompositeProjectionFrom2AsStepImpl(CompositeProjectionBuilder builder, SearchProjection inner1, SearchProjection inner2) { - super( dslContext ); + super( builder ); this.inner1 = inner1; this.inner2 = inner2; } @Override - public CompositeProjectionOptionsStep as(BiFunction transformer) { - return new CompositeProjectionOptionsStepImpl<>( dslContext, transformer, inner1, inner2 ); + public CompositeProjectionValueStep as(BiFunction transformer) { + return new CompositeProjectionValueStepImpl<>( builder, toProjectionArray(), + ProjectionCompositor.from( transformer ) + ); } @Override diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFrom3AsStepImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFrom3AsStepImpl.java index 390dc664cc0..fe99a6fd4ac 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFrom3AsStepImpl.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFrom3AsStepImpl.java @@ -8,8 +8,9 @@ import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionFrom3AsStep; -import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionOptionsStep; -import org.hibernate.search.engine.search.projection.dsl.spi.SearchProjectionDslContext; +import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionValueStep; +import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; +import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; import org.hibernate.search.util.common.function.TriFunction; class CompositeProjectionFrom3AsStepImpl @@ -20,17 +21,19 @@ class CompositeProjectionFrom3AsStepImpl final SearchProjection inner2; final SearchProjection inner3; - public CompositeProjectionFrom3AsStepImpl(SearchProjectionDslContext dslContext, + public CompositeProjectionFrom3AsStepImpl(CompositeProjectionBuilder builder, SearchProjection inner1, SearchProjection inner2, SearchProjection inner3) { - super( dslContext ); + super( builder ); this.inner1 = inner1; this.inner2 = inner2; this.inner3 = inner3; } @Override - public CompositeProjectionOptionsStep as(TriFunction transformer) { - return new CompositeProjectionOptionsStepImpl<>( dslContext, transformer, inner1, inner2, inner3 ); + public CompositeProjectionValueStep as(TriFunction transformer) { + return new CompositeProjectionValueStepImpl<>( builder, toProjectionArray(), + ProjectionCompositor.from( transformer ) + ); } @Override diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFromAnyNumberAsStep.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFromAnyNumberAsStep.java index 158c8f12eaa..71c6f6cf7a7 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFromAnyNumberAsStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFromAnyNumberAsStep.java @@ -8,16 +8,16 @@ import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionAsStep; -import org.hibernate.search.engine.search.projection.dsl.spi.SearchProjectionDslContext; +import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; class CompositeProjectionFromAnyNumberAsStep extends AbstractCompositeProjectionAsStep implements CompositeProjectionAsStep { final SearchProjection[] inner; - public CompositeProjectionFromAnyNumberAsStep(SearchProjectionDslContext dslContext, + public CompositeProjectionFromAnyNumberAsStep(CompositeProjectionBuilder builder, SearchProjection[] inner) { - super( dslContext ); + super( builder ); this.inner = inner; } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFromStepImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFromStepImpl.java index 2d5301a3ddb..ae129e7da2d 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFromStepImpl.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionFromStepImpl.java @@ -14,37 +14,43 @@ import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionFromStep; import org.hibernate.search.engine.search.projection.dsl.ProjectionFinalStep; import org.hibernate.search.engine.search.projection.dsl.spi.SearchProjectionDslContext; +import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; +import org.hibernate.search.engine.search.projection.spi.ProjectionTypeKeys; import org.hibernate.search.util.common.impl.Contracts; public class CompositeProjectionFromStepImpl implements CompositeProjectionFromStep { - private final SearchProjectionDslContext dslContext; + private final CompositeProjectionBuilder builder; public CompositeProjectionFromStepImpl(SearchProjectionDslContext dslContext) { - this.dslContext = dslContext; + this.builder = dslContext.scope().projectionBuilders().composite(); + } + + public CompositeProjectionFromStepImpl(SearchProjectionDslContext dslContext, String objectFieldPath) { + this.builder = dslContext.scope().fieldQueryElement( objectFieldPath, ProjectionTypeKeys.OBJECT ); } @Override public CompositeProjectionFrom1AsStep from(SearchProjection projection) { - return new CompositeProjectionFrom1AsStepImpl<>( dslContext, projection ); + return new CompositeProjectionFrom1AsStepImpl<>( builder, projection ); } @Override public CompositeProjectionFrom2AsStep from(SearchProjection projection1, SearchProjection projection2) { - return new CompositeProjectionFrom2AsStepImpl<>( dslContext, projection1, projection2 ); + return new CompositeProjectionFrom2AsStepImpl<>( builder, projection1, projection2 ); } @Override public CompositeProjectionFrom3AsStep from(SearchProjection projection1, SearchProjection projection2, SearchProjection projection3) { - return new CompositeProjectionFrom3AsStepImpl<>( dslContext, projection1, projection2, projection3 ); + return new CompositeProjectionFrom3AsStepImpl<>( builder, projection1, projection2, projection3 ); } @Override public CompositeProjectionAsStep from(SearchProjection... projections) { Contracts.assertNotNullNorEmpty( projections, "projections" ); - return new CompositeProjectionFromAnyNumberAsStep( dslContext, projections ); + return new CompositeProjectionFromAnyNumberAsStep( builder, projections ); } @Override diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionOptionsStepImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionOptionsStepImpl.java index f016857e97c..d35f19a59ce 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionOptionsStepImpl.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionOptionsStepImpl.java @@ -6,54 +6,32 @@ */ package org.hibernate.search.engine.search.projection.dsl.impl; -import java.util.List; -import java.util.function.BiFunction; -import java.util.function.Function; - import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionOptionsStep; -import org.hibernate.search.engine.search.projection.dsl.spi.SearchProjectionDslContext; import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; -import org.hibernate.search.util.common.function.TriFunction; - - -public class CompositeProjectionOptionsStepImpl - implements CompositeProjectionOptionsStep, T> { - - private final CompositeProjectionBuilder compositeProjectionBuilder; +import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; +import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; - public CompositeProjectionOptionsStepImpl(SearchProjectionDslContext dslContext, - Function, T> transformer, - SearchProjection[] projections) { - this.compositeProjectionBuilder = dslContext.scope().projectionBuilders().composite( transformer, projections ); - } - public

CompositeProjectionOptionsStepImpl(SearchProjectionDslContext dslContext, - Function transformer, - SearchProjection

projection) { - this.compositeProjectionBuilder = dslContext.scope().projectionBuilders().composite( transformer, projection ); - } +public class CompositeProjectionOptionsStepImpl + implements CompositeProjectionOptionsStep, P> { - public CompositeProjectionOptionsStepImpl(SearchProjectionDslContext dslContext, - BiFunction transformer, - SearchProjection projection1, - SearchProjection projection2) { - this.compositeProjectionBuilder = dslContext.scope().projectionBuilders() - .composite( transformer, projection1, projection2 ); - } + final CompositeProjectionBuilder builder; + final SearchProjection[] inners; + final ProjectionCompositor compositor; + private final ProjectionAccumulator.Provider accumulatorProvider; - public CompositeProjectionOptionsStepImpl(SearchProjectionDslContext dslContext, - TriFunction transformer, - SearchProjection projection1, - SearchProjection projection2, - SearchProjection projection3) { - this.compositeProjectionBuilder = dslContext.scope().projectionBuilders() - .composite( transformer, projection1, projection2, projection3 ); + public CompositeProjectionOptionsStepImpl(CompositeProjectionBuilder builder, + SearchProjection[] inners, ProjectionCompositor compositor, + ProjectionAccumulator.Provider accumulatorProvider) { + this.builder = builder; + this.inners = inners; + this.compositor = compositor; + this.accumulatorProvider = accumulatorProvider; } @Override - public SearchProjection toProjection() { - return compositeProjectionBuilder.build(); + public SearchProjection

toProjection() { + return builder.build( inners, compositor, accumulatorProvider ); } - } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionValueStepImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionValueStepImpl.java new file mode 100644 index 00000000000..dd87aef63f4 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/CompositeProjectionValueStepImpl.java @@ -0,0 +1,33 @@ +/* + * 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.engine.search.projection.dsl.impl; + +import java.util.List; + +import org.hibernate.search.engine.search.projection.SearchProjection; +import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionOptionsStep; +import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionValueStep; +import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; +import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; +import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; + + +public class CompositeProjectionValueStepImpl + extends CompositeProjectionOptionsStepImpl + implements CompositeProjectionValueStep, T> { + + public CompositeProjectionValueStepImpl(CompositeProjectionBuilder builder, + SearchProjection[] inners, ProjectionCompositor compositor) { + super( builder, inners, compositor, ProjectionAccumulator.single() ); + } + + @Override + public CompositeProjectionOptionsStep> multi() { + return new CompositeProjectionOptionsStepImpl<>( builder, inners, compositor, + ProjectionAccumulator.list() ); + } +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/DistanceToFieldProjectionValueStepImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/DistanceToFieldProjectionValueStepImpl.java index f3c27898188..7718874f0b2 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/DistanceToFieldProjectionValueStepImpl.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/DistanceToFieldProjectionValueStepImpl.java @@ -11,9 +11,8 @@ import org.hibernate.search.engine.search.projection.dsl.DistanceToFieldProjectionOptionsStep; import org.hibernate.search.engine.search.projection.dsl.DistanceToFieldProjectionValueStep; import org.hibernate.search.engine.search.projection.dsl.spi.SearchProjectionDslContext; -import org.hibernate.search.engine.search.projection.spi.ListProjectionAccumulator; +import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; import org.hibernate.search.engine.search.projection.spi.ProjectionTypeKeys; -import org.hibernate.search.engine.search.projection.spi.SingleValuedProjectionAccumulator; import org.hibernate.search.engine.spatial.GeoPoint; public final class DistanceToFieldProjectionValueStepImpl @@ -23,14 +22,14 @@ public final class DistanceToFieldProjectionValueStepImpl public DistanceToFieldProjectionValueStepImpl(SearchProjectionDslContext dslContext, String fieldPath, GeoPoint center) { super( dslContext.scope().fieldQueryElement( fieldPath, ProjectionTypeKeys.DISTANCE ), - SingleValuedProjectionAccumulator.provider() ); + ProjectionAccumulator.single() ); distanceFieldProjectionBuilder.center( center ); } @Override public DistanceToFieldProjectionOptionsStep> multi() { return new DistanceToFieldProjectionOptionsStepImpl<>( distanceFieldProjectionBuilder, - ListProjectionAccumulator.provider() ); + ProjectionAccumulator.list() ); } } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/FieldProjectionValueStepImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/FieldProjectionValueStepImpl.java index bca376938cd..69d00803825 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/FieldProjectionValueStepImpl.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/impl/FieldProjectionValueStepImpl.java @@ -13,9 +13,8 @@ import org.hibernate.search.engine.search.projection.dsl.FieldProjectionOptionsStep; import org.hibernate.search.engine.search.projection.dsl.FieldProjectionValueStep; import org.hibernate.search.engine.search.projection.dsl.spi.SearchProjectionDslContext; -import org.hibernate.search.engine.search.projection.spi.ListProjectionAccumulator; +import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; import org.hibernate.search.engine.search.projection.spi.ProjectionTypeKeys; -import org.hibernate.search.engine.search.projection.spi.SingleValuedProjectionAccumulator; public final class FieldProjectionValueStepImpl @@ -27,12 +26,12 @@ public FieldProjectionValueStepImpl(SearchProjectionDslContext dslContext, St ValueConvert convert) { super( dslContext.scope().fieldQueryElement( fieldPath, ProjectionTypeKeys.FIELD ) .type( clazz, convert ), - SingleValuedProjectionAccumulator.provider() ); + ProjectionAccumulator.single() ); } @Override public FieldProjectionOptionsStep> multi() { - return new FieldProjectionOptionsStepImpl<>( fieldProjectionBuilder, ListProjectionAccumulator.provider() ); + return new FieldProjectionOptionsStepImpl<>( fieldProjectionBuilder, ProjectionAccumulator.list() ); } @Override diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/spi/AbstractSearchProjectionFactory.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/spi/AbstractSearchProjectionFactory.java index 87c2701be18..4a2741cfea3 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/spi/AbstractSearchProjectionFactory.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/spi/AbstractSearchProjectionFactory.java @@ -7,13 +7,12 @@ package org.hibernate.search.engine.search.projection.dsl.spi; import java.util.List; -import java.util.function.Function; import org.hibernate.search.engine.common.dsl.spi.DslExtensionState; import org.hibernate.search.engine.search.common.ValueConvert; import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionFromStep; -import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionOptionsStep; +import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionValueStep; import org.hibernate.search.engine.search.projection.dsl.DistanceToFieldProjectionValueStep; import org.hibernate.search.engine.search.projection.dsl.DocumentReferenceProjectionOptionsStep; import org.hibernate.search.engine.search.projection.dsl.EntityProjectionOptionsStep; @@ -24,8 +23,8 @@ import org.hibernate.search.engine.search.projection.dsl.ScoreProjectionOptionsStep; import org.hibernate.search.engine.search.projection.dsl.SearchProjectionFactoryExtension; import org.hibernate.search.engine.search.projection.dsl.SearchProjectionFactoryExtensionIfSupportedStep; -import org.hibernate.search.engine.search.projection.dsl.impl.CompositeProjectionOptionsStepImpl; import org.hibernate.search.engine.search.projection.dsl.impl.CompositeProjectionFromStepImpl; +import org.hibernate.search.engine.search.projection.dsl.impl.CompositeProjectionValueStepImpl; import org.hibernate.search.engine.search.projection.dsl.impl.DistanceToFieldProjectionValueStepImpl; import org.hibernate.search.engine.search.projection.dsl.impl.DocumentReferenceProjectionOptionsStepImpl; import org.hibernate.search.engine.search.projection.dsl.impl.EntityProjectionOptionsStepImpl; @@ -34,6 +33,7 @@ import org.hibernate.search.engine.search.projection.dsl.impl.IdProjectionOptionsStepImpl; import org.hibernate.search.engine.search.projection.dsl.impl.ScoreProjectionOptionsStepImpl; import org.hibernate.search.engine.search.projection.dsl.impl.SearchProjectionFactoryExtensionStep; +import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; import org.hibernate.search.engine.search.projection.spi.SearchProjectionIndexScope; import org.hibernate.search.engine.spatial.GeoPoint; import org.hibernate.search.util.common.impl.Contracts; @@ -96,14 +96,21 @@ public DistanceToFieldProjectionValueStep distance(String fieldPath, return new DistanceToFieldProjectionValueStepImpl( dslContext, fieldPath, center ); } + @Override + public CompositeProjectionFromStep object(String objectFieldPath) { + Contracts.assertNotNull( objectFieldPath, "objectFieldPath" ); + return new CompositeProjectionFromStepImpl( dslContext, objectFieldPath ); + } + @Override public CompositeProjectionFromStep composite() { return new CompositeProjectionFromStepImpl( dslContext ); } @Override - public CompositeProjectionOptionsStep> composite(SearchProjection... projections) { - return new CompositeProjectionOptionsStepImpl<>( dslContext, Function.identity(), projections ); + public CompositeProjectionValueStep> composite(SearchProjection... projections) { + return new CompositeProjectionValueStepImpl<>( dslContext.scope().projectionBuilders().composite(), + projections, ProjectionCompositor.fromList( projections.length ) ); } @Override diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/CompositeProjectionBuilder.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/CompositeProjectionBuilder.java index 48eca1086bd..4a831132804 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/CompositeProjectionBuilder.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/CompositeProjectionBuilder.java @@ -6,6 +6,11 @@ */ package org.hibernate.search.engine.search.projection.spi; -public interface CompositeProjectionBuilder extends SearchProjectionBuilder { +import org.hibernate.search.engine.search.projection.SearchProjection; + +public interface CompositeProjectionBuilder { + + SearchProjection

build(SearchProjection[] inners, ProjectionCompositor compositor, + ProjectionAccumulator.Provider accumulatorProvider); } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/DistanceToFieldProjectionBuilder.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/DistanceToFieldProjectionBuilder.java index 593b11759d6..a808f73ca15 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/DistanceToFieldProjectionBuilder.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/DistanceToFieldProjectionBuilder.java @@ -18,7 +18,7 @@ public interface DistanceToFieldProjectionBuilder extends SearchProjectionBuilde @Override default SearchProjection build() { - return build( SingleValuedProjectionAccumulator.provider() ); + return build( ProjectionAccumulator.single() ); }

SearchProjection

build(ProjectionAccumulator.Provider accumulatorProvider); diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/FieldProjectionBuilder.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/FieldProjectionBuilder.java index a567a639131..6e0a2d2372a 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/FieldProjectionBuilder.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/FieldProjectionBuilder.java @@ -17,7 +17,7 @@ interface TypeSelector { @Override default SearchProjection build() { - return build( SingleValuedProjectionAccumulator.provider() ); + return build( ProjectionAccumulator.single() ); }

SearchProjection

build(ProjectionAccumulator.Provider accumulatorProvider); diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ListProjectionAccumulator.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ListProjectionAccumulator.java index 26b3830f3f2..e23a7d42d72 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ListProjectionAccumulator.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ListProjectionAccumulator.java @@ -8,21 +8,17 @@ import java.util.ArrayList; import java.util.List; -import java.util.ListIterator; - -import org.hibernate.search.engine.backend.types.converter.runtime.FromDocumentValueConvertContext; -import org.hibernate.search.engine.backend.types.converter.spi.ProjectionConverter; /** * A {@link ProjectionAccumulator} that can accumulate any number of values into a {@link java.util.List}. * - * @param The type of (unconverted) field values. - * @param The type of field values after the projection converter was applied. + * @param The type of extracted values to accumulate before being transformed. + * @param The type of values to accumulate obtained by transforming extracted values ({@code E}). */ -public final class ListProjectionAccumulator implements ProjectionAccumulator, List> { +final class ListProjectionAccumulator implements ProjectionAccumulator, List> { @SuppressWarnings("rawtypes") - private static final Provider PROVIDER = new Provider() { + static final Provider PROVIDER = new Provider() { private final ListProjectionAccumulator instance = new ListProjectionAccumulator(); @Override public ProjectionAccumulator get() { @@ -34,11 +30,6 @@ public boolean isSingleValued() { } }; - @SuppressWarnings("unchecked") // PROVIDER works for any V. - public static Provider> provider() { - return PROVIDER; - } - private ListProjectionAccumulator() { } @@ -48,29 +39,39 @@ public String toString() { } @Override - public List createInitial() { + public List createInitial() { return new ArrayList<>(); } @Override - public List accumulate(List accumulated, F value) { + public List accumulate(List accumulated, E value) { accumulated.add( value ); return accumulated; } + @Override + public int size(List accumulated) { + return accumulated.size(); + } + + @Override + @SuppressWarnings("unchecked") + public E get(List accumulated, int index) { + return (E) accumulated.get( index ); + } + + @Override + public List transform(List accumulated, int index, V transformed) { + accumulated.set( index, transformed ); + return accumulated; + } + @Override @SuppressWarnings({ "unchecked", "rawtypes" }) - public List finish(List accumulated, ProjectionConverter converter, - FromDocumentValueConvertContext context) { - // Hack to avoid instantiating another list: we convert a List into a List just by replacing its elements. + public List finish(List accumulated) { + // Hack to avoid instantiating another list: we convert a List into a List just by replacing its elements. // It works *only* because we know the actual underlying type of the list, - // and we know it can work just as well with V as with F. - ListIterator iterator = accumulated.listIterator(); - while ( iterator.hasNext() ) { - F fieldValue = iterator.next(); - V convertedValue = converter.fromDocumentValue( fieldValue, context ); - ( (ListIterator) iterator ).set( convertedValue ); - } + // and we know it can work just as well with U as with Object. return (List) (List) accumulated; } } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ProjectionAccumulator.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ProjectionAccumulator.java index eff24905d97..6167d6a7b5f 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ProjectionAccumulator.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ProjectionAccumulator.java @@ -6,6 +6,8 @@ */ package org.hibernate.search.engine.search.projection.spi; +import java.util.List; + import org.hibernate.search.engine.backend.types.converter.runtime.FromDocumentValueConvertContext; import org.hibernate.search.engine.backend.types.converter.spi.ProjectionConverter; @@ -16,15 +18,31 @@ *
    *
  • There is no concept of parallel execution.
  • *
  • All operations are expected to be non-blocking, - * except for {@link #finish(Object, ProjectionConverter, FromDocumentValueConvertContext)}
  • + * except for {@link #transformAll(Object, ProjectionConverter, FromDocumentValueConvertContext)} + * and {@link #finish(Object)}. + *
  • Values to accumulate are expected to be {@link #transform(Object, int, Object) transformed} exactly once + * after accumulation, changing their type from {@link E} to {@link V}. + * Clients are responsible for ensuring values to accumulate have been transformed + * upon calling {@link #finish(Object)}. *
* - * @param The type of (unconverted) field values. - * @param The type of field values after the projection converter was applied. - * @param The type of the temporary storage for collected field values of type {@code F}. + * @param The type of extracted values to accumulate before being transformed. + * @param The type of values to accumulate obtained by transforming extracted values ({@code E}). + * @param The type of the temporary storage for accumulated values, + * before and after being transformed. * @param The type of the final result containing values of type {@code V}. */ -public interface ProjectionAccumulator { +public interface ProjectionAccumulator { + + @SuppressWarnings("unchecked") // PROVIDER works for any V. + static ProjectionAccumulator.Provider single() { + return SingleValuedProjectionAccumulator.PROVIDER; + } + + @SuppressWarnings("unchecked") // PROVIDER works for any V. + static Provider> list() { + return ListProjectionAccumulator.PROVIDER; + } /** * Creates the initial accumulated container. @@ -34,7 +52,7 @@ public interface ProjectionAccumulator { * @return The initial accumulated container, * to pass to the first call to {@link #accumulate(Object, Object)}. */ - U createInitial(); + A createInitial(); /** * Folds a new value in the given accumulated container. @@ -47,37 +65,90 @@ public interface ProjectionAccumulator { * @param value The value to accumulate. * @return The new accumulated value. */ - U accumulate(U accumulated, F value); + A accumulate(A accumulated, E value); + + /** + * @param accumulated The accumulated value so far, + * returned by the last call to {@link #accumulate(Object, Object)}. + * @return The number of elements in the accumulated value. + */ + int size(A accumulated); /** - * Finishes the collecting, converting the accumulated container into the final result. + * Retrieves the value at the given index. + *

+ * This operation should be non-blocking. + * + * @param accumulated The accumulated value so far, + * returned by the last call to {@link #accumulate(Object, Object)}. + * @param index The index of the value to retrieve. + * @return The value at the given index. + */ + E get(A accumulated, int index); + + /** + * Transforms the value at the given index, + * replacing it with the given transformed value. + *

+ * This operation should be non-blocking. + * + * @param accumulated The accumulated value so far, + * returned by the last call to {@link #accumulate(Object, Object)}. + * @param index The index of the value being transformed. + * @param transformed The transformed value. + * @return The new accumulated value. + */ + A transform(A accumulated, int index, V transformed); + + /** + * Transforms all values with the given converter and the given context. *

* This operation may be blocking. * - * @param accumulated The temporary storage created by {@link #createInitial()} and populated - * by successive calls to {@link #accumulate(Object, Object)}. + * @param accumulated The accumulated value so far, + * returned by the last call to {@link #accumulate(Object, Object)}. * @param converter The projection converter (from {@code F} to {@code V}). * @param context The context to be passed to the projection converter. - * @return The final result of the collecting. + * @return The new accumulated value. + */ + default A transformAll(A accumulated, ProjectionConverter converter, + FromDocumentValueConvertContext context) { + for ( int i = 0; i < size( accumulated ); i++ ) { + E initial = get( accumulated, i ); + V transformed = converter.fromDocumentValue( initial, context ); + accumulated = transform( accumulated, i, transformed ); + } + return accumulated; + } + + /** + * Finishes the accumulation, converting the accumulated container into the final result. + *

+ * This operation may be blocking. + * + * @param accumulated The temporary storage created by {@link #createInitial()}, + * then populated by successive calls to {@link #accumulate(Object, Object)}, + * then transformed by a single call to {@link #transformAll(Object, ProjectionConverter, FromDocumentValueConvertContext)} + * or by successive calls to {@link #transform(Object, int, Object)}. + * @return The final result of the accumulation. */ - R finish(U accumulated, ProjectionConverter converter, - FromDocumentValueConvertContext context); + R finish(A accumulated); /** - * Provides an accumulator for a given underlying field type ({@code F}). + * Provides an accumulator for a given type of values to accumulate ({@code T}). *

* The provider may always return the same accumulator, * if generics are irrelevant and it's safe to do so. * - * @param The type of field values after the projection converter was applied. + * @param The type of values to accumulate after being transformed. * @param The type of the final result containing values of type {@code V}. */ - interface Provider { + interface Provider { /** - * @param The type of field values. + * @param The type of values to accumulate before being transformed. * @return An accumulator for the given type. */ - ProjectionAccumulator get(); + ProjectionAccumulator get(); /** * @return {@code true} if accumulators returned by {@link #get()} can only accept a single value, diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ProjectionCompositor.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ProjectionCompositor.java index e6bae82d30c..adeab4f6aff 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ProjectionCompositor.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ProjectionCompositor.java @@ -116,6 +116,10 @@ protected Object transformer() { }; } + static ProjectionCompositor> fromList(int size) { + return fromList( size, Function.identity() ); + } + static ProjectionCompositor fromList(int size, Function, V> transformer) { return new ObjectArrayProjectionCompositor( size ) { @Override diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ProjectionTypeKeys.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ProjectionTypeKeys.java index 63302242aab..6daa7a126be 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ProjectionTypeKeys.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/ProjectionTypeKeys.java @@ -19,5 +19,6 @@ public static SearchQueryElementTypeKey key(String name) { public static final SearchQueryElementTypeKey FIELD = key( "field" ); public static final SearchQueryElementTypeKey DISTANCE = key( "distance" ); + public static final SearchQueryElementTypeKey OBJECT = key( "object" ); } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/SearchProjectionBuilderFactory.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/SearchProjectionBuilderFactory.java index d51746f9065..c7a0f69796b 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/SearchProjectionBuilderFactory.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/SearchProjectionBuilderFactory.java @@ -6,13 +6,6 @@ */ package org.hibernate.search.engine.search.projection.spi; -import java.util.List; -import java.util.function.BiFunction; -import java.util.function.Function; - -import org.hibernate.search.engine.search.projection.SearchProjection; -import org.hibernate.search.util.common.function.TriFunction; - /** * A factory for search projection builders. *

@@ -31,13 +24,6 @@ public interface SearchProjectionBuilderFactory { ScoreProjectionBuilder score(); - CompositeProjectionBuilder composite(Function, V> transformer, SearchProjection... projections); - - CompositeProjectionBuilder composite(Function transformer, SearchProjection projection); - - CompositeProjectionBuilder composite(BiFunction transformer, - SearchProjection projection1, SearchProjection projection2); + CompositeProjectionBuilder composite(); - CompositeProjectionBuilder composite(TriFunction transformer, - SearchProjection projection1, SearchProjection projection2, SearchProjection projection3); } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/SingleValuedProjectionAccumulator.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/SingleValuedProjectionAccumulator.java index 19e47f1d82f..047fb2c6265 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/SingleValuedProjectionAccumulator.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/spi/SingleValuedProjectionAccumulator.java @@ -16,15 +16,15 @@ /** * A {@link ProjectionAccumulator} that can accumulate up to one value, and will throw an exception beyond that. * - * @param The type of (unconverted) field values. - * @param The type of field values after the projection converter was applied. + * @param The type of extracted values to accumulate before being transformed. + * @param The type of values to accumulate obtained by transforming extracted values ({@code E}). */ -public final class SingleValuedProjectionAccumulator implements ProjectionAccumulator { +final class SingleValuedProjectionAccumulator implements ProjectionAccumulator { private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() ); @SuppressWarnings("rawtypes") - private static final ProjectionAccumulator.Provider PROVIDER = new ProjectionAccumulator.Provider() { + static final ProjectionAccumulator.Provider PROVIDER = new ProjectionAccumulator.Provider() { private final SingleValuedProjectionAccumulator instance = new SingleValuedProjectionAccumulator(); @Override public ProjectionAccumulator get() { @@ -36,11 +36,6 @@ public boolean isSingleValued() { } }; - @SuppressWarnings("unchecked") // PROVIDER works for any V. - public static ProjectionAccumulator.Provider provider() { - return PROVIDER; - } - private SingleValuedProjectionAccumulator() { } @@ -50,12 +45,12 @@ public String toString() { } @Override - public F createInitial() { + public E createInitial() { return null; } @Override - public F accumulate(F accumulated, F value) { + public E accumulate(Object accumulated, E value) { if ( accumulated != null ) { throw log.unexpectedMultiValuedField( accumulated, value ); } @@ -63,8 +58,34 @@ public F accumulate(F accumulated, F value) { } @Override - public V finish(F accumulated, ProjectionConverter converter, + public int size(Object accumulated) { + return accumulated == null ? 0 : 1; + } + + @Override + @SuppressWarnings("unchecked") + public E get(Object accumulated, int index) { + return (E) accumulated; + } + + @Override + public Object transform(Object accumulated, int index, V transformed) { + if ( index != 0 ) { + throw new IndexOutOfBoundsException( "Invalid index passed to " + this + ": " + index ); + } + return transformed; + } + + @Override + @SuppressWarnings("unchecked") + public Object transformAll(Object accumulated, ProjectionConverter converter, FromDocumentValueConvertContext context) { - return converter.fromDocumentValue( accumulated, context ); + return converter.fromDocumentValue( (E) accumulated, context ); + } + + @Override + @SuppressWarnings("unchecked") + public V finish(Object accumulated) { + return (V) accumulated; } } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/query/dsl/impl/DefaultSearchQuerySelectStep.java b/engine/src/main/java/org/hibernate/search/engine/search/query/dsl/impl/DefaultSearchQuerySelectStep.java index ac8b4acb4e9..055ec7db38e 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/query/dsl/impl/DefaultSearchQuerySelectStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/query/dsl/impl/DefaultSearchQuerySelectStep.java @@ -17,6 +17,8 @@ import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.dsl.ProjectionFinalStep; import org.hibernate.search.engine.search.projection.dsl.SearchProjectionFactory; +import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; +import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; import org.hibernate.search.engine.search.query.dsl.SearchQueryOptionsStep; import org.hibernate.search.engine.search.query.dsl.spi.AbstractSearchQuerySelectStep; import org.hibernate.search.engine.search.query.spi.SearchQueryBuilder; @@ -68,7 +70,9 @@ public

DefaultSearchQueryOptionsStep select(SearchProjection

proj @Override public DefaultSearchQueryOptionsStep, LOS> select(SearchProjection... projections) { - return select( scope.projectionBuilders().composite( Function.identity(), projections ).build() ); + return select( scope.projectionBuilders().composite() + .build( projections, ProjectionCompositor.fromList( projections.length ), + ProjectionAccumulator.single() ) ); } @Override diff --git a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchTckBackendFeatures.java b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchTckBackendFeatures.java index 5a563d2854d..7769bab5090 100644 --- a/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchTckBackendFeatures.java +++ b/integrationtest/backend/elasticsearch/src/test/java/org/hibernate/search/integrationtest/backend/elasticsearch/testsupport/util/ElasticsearchTckBackendFeatures.java @@ -203,4 +203,9 @@ public boolean supportsFieldSortWhenNestedFieldMissingInSomeTargetIndexes() { // Not supported in older versions of Elasticsearch return dialect.ignoresFieldSortWhenNestedFieldMissing(); } + + @Override + public boolean reliesOnNestedDocumentsForObjectProjection() { + return false; + } } diff --git a/integrationtest/backend/lucene/src/test/java/org/hibernate/search/integrationtest/backend/lucene/testsupport/util/LuceneTckBackendFeatures.java b/integrationtest/backend/lucene/src/test/java/org/hibernate/search/integrationtest/backend/lucene/testsupport/util/LuceneTckBackendFeatures.java index 6e1763a3458..c44cbd15fa7 100644 --- a/integrationtest/backend/lucene/src/test/java/org/hibernate/search/integrationtest/backend/lucene/testsupport/util/LuceneTckBackendFeatures.java +++ b/integrationtest/backend/lucene/src/test/java/org/hibernate/search/integrationtest/backend/lucene/testsupport/util/LuceneTckBackendFeatures.java @@ -20,4 +20,9 @@ public boolean nonDefaultOrderInTermsAggregations() { public boolean projectionPreservesNulls() { return false; } + + @Override + public boolean reliesOnNestedDocumentsForObjectProjection() { + return true; + } } diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/ObjectStructureIT.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/ObjectStructureIT.java index f9cc6338d90..fa49b6eba05 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/ObjectStructureIT.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/ObjectStructureIT.java @@ -174,7 +174,8 @@ public void search_error_nonNestedField() { ) .isInstanceOf( SearchException.class ) .hasMessageContainingAll( "Cannot use 'predicate:nested' on field 'flattenedObject'.", - "If you are trying to use the 'nested' predicate, set the field structure to 'NESTED' and reindex all your data" ); + "Some object field features require a nested structure; " + + "try setting the field structure to 'NESTED' and reindexing all your data" ); } @Test diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/dynamic/ObjectFieldTemplateIT.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/dynamic/ObjectFieldTemplateIT.java index 14911ad057a..5b87c41c4a1 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/dynamic/ObjectFieldTemplateIT.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/dynamic/ObjectFieldTemplateIT.java @@ -512,7 +512,8 @@ private void checkFlattened(String objectFieldPath) { ) ) ) .hasMessageContainingAll( "Cannot use 'predicate:nested' on field '" + objectFieldPath + "'.", - "If you are trying to use the 'nested' predicate, set the field structure to 'NESTED' and reindex all your data" ); + "Some object field features require a nested structure; " + + "try setting the field structure to 'NESTED' and reindexing all your data" ); assertThatQuery( query( f -> f.bool() .must( f.match().field( objectFieldPath + "." + FIRSTNAME_FIELD ) diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/AbstractPerFieldTypeProjectionDataSet.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/AbstractPerFieldTypeProjectionDataSet.java new file mode 100644 index 00000000000..8394330d9f7 --- /dev/null +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/AbstractPerFieldTypeProjectionDataSet.java @@ -0,0 +1,23 @@ +/* + * 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.integrationtest.backend.tck.search.projection; + +import org.hibernate.search.integrationtest.backend.tck.testsupport.types.FieldTypeDescriptor; + +public abstract class AbstractPerFieldTypeProjectionDataSet> + extends AbstractProjectionDataSet { + + protected final FieldTypeDescriptor fieldType; + protected final V values; + + protected AbstractPerFieldTypeProjectionDataSet(String routingKey, V values) { + super( routingKey ); + fieldType = values.fieldType(); + this.values = values; + } + +} diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/AbstractProjectionDataSet.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/AbstractProjectionDataSet.java new file mode 100644 index 00000000000..f3a3c5a4e6a --- /dev/null +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/AbstractProjectionDataSet.java @@ -0,0 +1,29 @@ +/* + * 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.integrationtest.backend.tck.search.projection; + +public abstract class AbstractProjectionDataSet { + + protected final String routingKey; + + protected AbstractProjectionDataSet(String routingKey) { + this.routingKey = routingKey; + } + + @Override + public String toString() { + if ( routingKey != null ) { + // Pretty rendering of the dataset as a parameter of the Parameterized runner + return routingKey; + } + else { + // Probably not used as a parameter of the Parameterized runner + return getClass().getName(); + } + } + +} diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/AbstractProjectionInObjectProjectionIT.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/AbstractProjectionInObjectProjectionIT.java new file mode 100644 index 00000000000..1ed42135a8e --- /dev/null +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/AbstractProjectionInObjectProjectionIT.java @@ -0,0 +1,1487 @@ +/* + * 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.integrationtest.backend.tck.search.projection; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.hibernate.search.util.impl.integrationtest.common.assertion.SearchResultAssert.assertThatQuery; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.hibernate.search.engine.backend.common.spi.FieldPaths; +import org.hibernate.search.engine.backend.document.DocumentElement; +import org.hibernate.search.engine.backend.document.IndexObjectFieldReference; +import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement; +import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaObjectField; +import org.hibernate.search.engine.backend.types.ObjectStructure; +import org.hibernate.search.engine.backend.types.Projectable; +import org.hibernate.search.engine.search.projection.dsl.ProjectionFinalStep; +import org.hibernate.search.engine.search.projection.dsl.SearchProjectionFactory; +import org.hibernate.search.integrationtest.backend.tck.testsupport.types.FieldTypeDescriptor; +import org.hibernate.search.integrationtest.backend.tck.testsupport.util.SimpleFieldModelsByType; +import org.hibernate.search.integrationtest.backend.tck.testsupport.util.TckConfiguration; +import org.hibernate.search.util.common.SearchException; +import org.hibernate.search.util.impl.integrationtest.mapper.stub.BulkIndexer; +import org.hibernate.search.util.impl.integrationtest.mapper.stub.SimpleMappedIndex; +import org.hibernate.search.util.impl.integrationtest.mapper.stub.StubMappingScope; + +import org.junit.Test; + +import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration; + +public abstract class AbstractProjectionInObjectProjectionIT> { + + private static final String FULL_DOCUMENT_ID = "fullDocId"; + private static final String SINGLE_VALUED_DOCUMENT_ID = "singleValuedDocId"; + private static final String LEVEL1_SINGLE_OBJECT_DOCUMENT_ID = "level1SingleObjectDocId"; + private static final String LEVEL1_SINGLE_EMPTY_OBJECT_DOCUMENT_ID = "level1SingleEmptyObjectDocId"; + private static final String LEVEL1_SINGLE_NULL_OBJECT_DOCUMENT_ID = "level1SingleNullObjectDocId"; + private static final String LEVEL1_NO_OBJECT_DOCUMENT_ID = "level1NoObjectDocId"; + private static final String LEVEL2_SINGLE_OBJECT_DOCUMENT_ID = "level2SingleObjectDocId"; + private static final String LEVEL2_SINGLE_EMPTY_OBJECT_DOCUMENT_ID = "level2SingleEmptyObjectDocId"; + private static final String LEVEL2_NO_OBJECT_DOCUMENT_ID = "level2NoObjectDocId"; + + private static final String MISSING_LEVEL1_DOCUMENT_ID = "missingLevel1DocId"; + private static final String MISSING_LEVEL1_SINGLE_VALUED_FIELD_DOCUMENT_ID = "missingLevel1SVFieldDocId"; + private static final String MISSING_LEVEL2_DOCUMENT_ID = "missingLevel2DocId"; + private static final String MISSING_LEVEL2_SINGLE_VALUED_FIELD_DOCUMENT_ID = "missingLevel2SVFieldDocId"; + + private final SimpleMappedIndex mainIndex; + private final SimpleMappedIndex missingLevel1Index; + private final SimpleMappedIndex missingLevel1SingleValuedFieldIndex; + private final SimpleMappedIndex missingLevel2Index; + private final SimpleMappedIndex missingLevel2SingleValuedFieldIndex; + protected final DataSet dataSet; + private final RecursiveComparisonConfiguration recursiveComparisonConfig; + + public AbstractProjectionInObjectProjectionIT(SimpleMappedIndex mainIndex, + SimpleMappedIndex missingLevel1Index, + SimpleMappedIndex missingLevel1SingleValuedFieldIndex, + SimpleMappedIndex missingLevel2Index, + SimpleMappedIndex missingLevel2SingleValuedFieldIndex, + DataSet dataSet) { + this.mainIndex = mainIndex; + this.missingLevel1Index = missingLevel1Index; + this.missingLevel1SingleValuedFieldIndex = missingLevel1SingleValuedFieldIndex; + this.missingLevel2Index = missingLevel2Index; + this.missingLevel2SingleValuedFieldIndex = missingLevel2SingleValuedFieldIndex; + this.dataSet = dataSet; + this.recursiveComparisonConfig = configureRecursiveComparison( RecursiveComparisonConfiguration.builder() ) + .build(); + } + + protected RecursiveComparisonConfiguration.Builder configureRecursiveComparison(RecursiveComparisonConfiguration.Builder builder) { + return builder; + } + + @Test + public void objectOnLevel1AndObjectOnLevel2() { + assertThatQuery( mainIndex.query() + .select( f -> f.composite() + .from( + f.id( String.class ), + f.object( level1Path() ) + .from( + singleValuedProjection( f, level1SingleValuedFieldPath() ), + multiValuedProjection( f, level1MultiValuedFieldPath() ), + f.object( level2Path() ) + .from( + singleValuedProjection( f, level2SingleValuedFieldPath() ), + multiValuedProjection( f, level2MultiValuedFieldPath() ) + ) + .as( ObjectDto::new ) + .multi() + ) + .as( Level1ObjectDto::new ) + .multi() + ) + .as( IdAndObjectDto::new ) ) + .where( f -> f.matchAll() ) + .routing( dataSet.routingKey ) ) + .hits().asIs().usingRecursiveFieldByFieldElementComparator( recursiveComparisonConfig ) + .containsExactlyInAnyOrder( + hit( FULL_DOCUMENT_ID, listMaybeWithNull( + null, + new Level1ObjectDto<>( + dataSet.values.projectedValue( 0 ), + dataSet.values.projectedValues( 1, 2 ), + listMaybeWithNull( + null, + new ObjectDto<>( + dataSet.values.projectedValue( 3 ), + dataSet.values.projectedValues( 4, 5 ) + ), + new ObjectDto<>( + dataSet.values.projectedValue( 6 ), + dataSet.values.projectedValues( 7, 8 ) + ) + ) + ), + new Level1ObjectDto<>( + dataSet.values.projectedValue( 9 ), + dataSet.values.projectedValues( 10, 11 ), + listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 12 ), + dataSet.values.projectedValues( 13, 14 ) + ), + null, + new ObjectDto<>( + dataSet.values.projectedValue( 15 ), + dataSet.values.projectedValues( 16, 17 ) + ) + ) + ) + ) ), + hit( SINGLE_VALUED_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectDto<>( + dataSet.values.projectedValue( 0 ), + dataSet.values.projectedValues( 1 ), + listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 2 ), + dataSet.values.projectedValues( 3 ) + ) + ) + ) + ) ), + hit( LEVEL1_SINGLE_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectDto<>( + dataSet.values.projectedValue( 0 ), + dataSet.values.projectedValues( 1, 2 ), + listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 3 ), + dataSet.values.projectedValues( 4, 5 ) + ), + new ObjectDto<>( + dataSet.values.projectedValue( 6 ), + dataSet.values.projectedValues( 7, 8 ) + ) + ) + ) + ) ), + hit( LEVEL1_SINGLE_EMPTY_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectDto<>( + null, + Collections.emptyList(), + Collections.emptyList() + ) + ) ), + hit( LEVEL1_SINGLE_NULL_OBJECT_DOCUMENT_ID, listMaybeWithNull( + (Level1ObjectDto

) null + ) ), + hit( LEVEL1_NO_OBJECT_DOCUMENT_ID, Collections.emptyList() ), + hit( LEVEL2_SINGLE_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectDto<>( + null, + Collections.emptyList(), + listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 0 ), + dataSet.values.projectedValues( 1, 2 ) + ) + ) + ), + new Level1ObjectDto<>( + null, + Collections.emptyList(), + listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 3 ), + dataSet.values.projectedValues( 4, 5 ) + ) + ) + ) + ) ), + hit( LEVEL2_SINGLE_EMPTY_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectDto<>( + null, + Collections.emptyList(), + listMaybeWithNull( + new ObjectDto<>( + null, + Collections.emptyList() + ) + ) + ), + new Level1ObjectDto<>( + null, + Collections.emptyList(), + listMaybeWithNull( + new ObjectDto<>( + null, + Collections.emptyList() + ) + ) + ) + ) ), + hit( LEVEL2_NO_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectDto<>( + null, + Collections.emptyList(), + Collections.emptyList() + ), + new Level1ObjectDto<>( + null, + Collections.emptyList(), + Collections.emptyList() + ) + ) ) + ); + } + + @Test + public void objectOnLevel1AndNoLevel2() { + assertThatQuery( mainIndex.query() + .select( f -> f.composite() + .from( + f.id( String.class ), + f.object( level1Path() ) + .from( + singleValuedProjection( f, level1SingleValuedFieldPath() ), + multiValuedProjection( f, level1MultiValuedFieldPath() ) + ) + .as( ObjectDto::new ) + .multi() + ) + .as( IdAndObjectDto::new ) ) + .where( f -> f.matchAll() ) + .routing( dataSet.routingKey ) ) + .hits().asIs().usingRecursiveFieldByFieldElementComparator( recursiveComparisonConfig ) + .containsExactlyInAnyOrder( + hit( FULL_DOCUMENT_ID, listMaybeWithNull( + null, + new ObjectDto<>( + dataSet.values.projectedValue( 0 ), + dataSet.values.projectedValues( 1, 2 ) + ), + new ObjectDto<>( + dataSet.values.projectedValue( 9 ), + dataSet.values.projectedValues( 10, 11 ) + ) + ) ), + hit( SINGLE_VALUED_DOCUMENT_ID, listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 0 ), + dataSet.values.projectedValues( 1 ) + ) + ) ), + hit( LEVEL1_SINGLE_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 0 ), + dataSet.values.projectedValues( 1, 2 ) + ) + ) ), + hit( LEVEL1_SINGLE_EMPTY_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new ObjectDto<>( + null, + Collections.emptyList() + ) + ) ), + hit( LEVEL1_SINGLE_NULL_OBJECT_DOCUMENT_ID, listMaybeWithNull( + (ObjectDto

) null + ) ), + hit( LEVEL1_NO_OBJECT_DOCUMENT_ID, Collections.emptyList() ), + hit( LEVEL2_SINGLE_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new ObjectDto<>( + null, + Collections.emptyList() + ), + new ObjectDto<>( + null, + Collections.emptyList() + ) + ) ), + hit( LEVEL2_SINGLE_EMPTY_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new ObjectDto<>( + null, + Collections.emptyList() + ), + new ObjectDto<>( + null, + Collections.emptyList() + ) + ) ), + hit( LEVEL2_NO_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new ObjectDto<>( + null, + Collections.emptyList() + ), + new ObjectDto<>( + null, + Collections.emptyList() + ) + ) ) + ); + } + + @Test + public void objectOnLevel1AndFlattenedLevel2() { + assertThatQuery( mainIndex.query() + .select( f -> f.composite() + .from( + f.id( String.class ), + f.object( level1Path() ) + .from( + singleValuedProjection( f, level1SingleValuedFieldPath() ), + multiValuedProjection( f, level1MultiValuedFieldPath() ), + f.composite() + .from( + multiValuedProjection( f, level2SingleValuedFieldPath() ), + multiValuedProjection( f, level2MultiValuedFieldPath() ) + ) + .as( FlattenedObjectDto::new ) + ) + .as( Level1ObjectWithFlattenedLevel2Dto::new ) + .multi() + ) + .as( IdAndObjectDto::new ) ) + .where( f -> f.matchAll() ) + .routing( dataSet.routingKey ) ) + .hits().asIs().usingRecursiveFieldByFieldElementComparator( recursiveComparisonConfig ) + .containsExactlyInAnyOrder( + hit( FULL_DOCUMENT_ID, listMaybeWithNull( + null, + new Level1ObjectWithFlattenedLevel2Dto<>( + dataSet.values.projectedValue( 0 ), + dataSet.values.projectedValues( 1, 2 ), + new FlattenedObjectDto<>( + dataSet.values.projectedValues( 3, 6 ), + dataSet.values.projectedValues( 4, 5, 7, 8 ) + ) + ), + new Level1ObjectWithFlattenedLevel2Dto<>( + dataSet.values.projectedValue( 9 ), + dataSet.values.projectedValues( 10, 11 ), + new FlattenedObjectDto<>( + dataSet.values.projectedValues( 12, 15 ), + dataSet.values.projectedValues( 13, 14, 16, 17 ) + ) + ) + ) ), + hit( SINGLE_VALUED_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectWithFlattenedLevel2Dto<>( + dataSet.values.projectedValue( 0 ), + dataSet.values.projectedValues( 1 ), + new FlattenedObjectDto<>( + dataSet.values.projectedValues( 2 ), + dataSet.values.projectedValues( 3 ) + ) + ) + ) ), + hit( LEVEL1_SINGLE_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectWithFlattenedLevel2Dto<>( + dataSet.values.projectedValue( 0 ), + dataSet.values.projectedValues( 1, 2 ), + new FlattenedObjectDto<>( + dataSet.values.projectedValues( 3, 6 ), + dataSet.values.projectedValues( 4, 5, 7, 8 ) + ) + ) + ) ), + hit( LEVEL1_SINGLE_EMPTY_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectWithFlattenedLevel2Dto<>( + null, + Collections.emptyList(), + new FlattenedObjectDto<>( + Collections.emptyList(), + Collections.emptyList() + ) + ) + ) ), + hit( LEVEL1_SINGLE_NULL_OBJECT_DOCUMENT_ID, listMaybeWithNull( + (Level1ObjectWithFlattenedLevel2Dto

) null + ) ), + hit( LEVEL1_NO_OBJECT_DOCUMENT_ID, Collections.emptyList() ), + hit( LEVEL2_SINGLE_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectWithFlattenedLevel2Dto<>( + null, + Collections.emptyList(), + new FlattenedObjectDto<>( + dataSet.values.projectedValues( 0 ), + dataSet.values.projectedValues( 1, 2 ) + ) + ), + new Level1ObjectWithFlattenedLevel2Dto<>( + null, + Collections.emptyList(), + new FlattenedObjectDto<>( + dataSet.values.projectedValues( 3 ), + dataSet.values.projectedValues( 4, 5 ) + ) + ) + ) ), + hit( LEVEL2_SINGLE_EMPTY_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectWithFlattenedLevel2Dto<>( + null, + Collections.emptyList(), + new FlattenedObjectDto<>( + Collections.emptyList(), + Collections.emptyList() + ) + ), + new Level1ObjectWithFlattenedLevel2Dto<>( + null, + Collections.emptyList(), + new FlattenedObjectDto<>( + Collections.emptyList(), + Collections.emptyList() + ) + ) + ) ), + hit( LEVEL2_NO_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectWithFlattenedLevel2Dto<>( + null, + Collections.emptyList(), + new FlattenedObjectDto<>( + Collections.emptyList(), + Collections.emptyList() + ) + ), + new Level1ObjectWithFlattenedLevel2Dto<>( + null, + Collections.emptyList(), + new FlattenedObjectDto<>( + Collections.emptyList(), + Collections.emptyList() + ) + ) + ) ) + ); + } + + @Test + public void objectOnSingleValuedLevel1AndNoLevel2() { + assertThatQuery( mainIndex.query() + .select( f -> f.composite() + .from( + f.id( String.class ), + f.object( singleValuedLevel1Path() ) + .from( + singleValuedProjection( f, singleValuedLevel1SingleValuedFieldPath() ), + multiValuedProjection( f, singleValuedLevel1MultiValuedFieldPath() ) + ) + .as( ObjectDto::new ) + ) + .as( IdAndObjectDto::new ) ) + .where( f -> f.matchAll() ) + .routing( dataSet.routingKey ) ) + .hits().asIs().usingRecursiveFieldByFieldElementComparator( recursiveComparisonConfig ) + .containsExactlyInAnyOrder( + hit( FULL_DOCUMENT_ID, new ObjectDto<>( + dataSet.values.projectedValue( 18 ), + dataSet.values.projectedValues( 19, 20 ) + ) ), + hit( SINGLE_VALUED_DOCUMENT_ID, new ObjectDto<>( + dataSet.values.projectedValue( 4 ), + dataSet.values.projectedValues( 5 ) + ) ), + hit( LEVEL1_SINGLE_OBJECT_DOCUMENT_ID, new ObjectDto<>( + dataSet.values.projectedValue( 9 ), + dataSet.values.projectedValues( 10, 11 ) + ) ), + hit( LEVEL1_SINGLE_NULL_OBJECT_DOCUMENT_ID, null ), + hit( LEVEL1_SINGLE_EMPTY_OBJECT_DOCUMENT_ID, new ObjectDto<>( + null, + Collections.emptyList() + ) ), + hit( LEVEL1_NO_OBJECT_DOCUMENT_ID, null ), + hit( LEVEL2_SINGLE_OBJECT_DOCUMENT_ID, null ), + hit( LEVEL2_SINGLE_EMPTY_OBJECT_DOCUMENT_ID, null ), + hit( LEVEL2_NO_OBJECT_DOCUMENT_ID, null ) + ); + } + + @Test + public void noLevel1AndObjectOnLevel2() { + assertThatQuery( mainIndex.query() + .select( f -> f.composite() + .from( + f.id( String.class ), + f.object( level2Path() ) + .from( + singleValuedProjection( f, level2SingleValuedFieldPath() ), + multiValuedProjection( f, level2MultiValuedFieldPath() ) + ) + .as( ObjectDto::new ) + .multi() + ) + .as( IdAndObjectDto::new ) ) + .where( f -> f.matchAll() ) + .routing( dataSet.routingKey ) ) + .hits().asIs().usingRecursiveFieldByFieldElementComparator( recursiveComparisonConfig ) + .containsExactlyInAnyOrder( + hit( FULL_DOCUMENT_ID, listMaybeWithNull( + null, + new ObjectDto<>( + dataSet.values.projectedValue( 3 ), + dataSet.values.projectedValues( 4, 5 ) + ), + new ObjectDto<>( + dataSet.values.projectedValue( 6 ), + dataSet.values.projectedValues( 7, 8 ) + ), + new ObjectDto<>( + dataSet.values.projectedValue( 12 ), + dataSet.values.projectedValues( 13, 14 ) + ), + null, + new ObjectDto<>( + dataSet.values.projectedValue( 15 ), + dataSet.values.projectedValues( 16, 17 ) + ) + ) ), + hit( SINGLE_VALUED_DOCUMENT_ID, listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 2 ), + dataSet.values.projectedValues( 3 ) + ) + ) ), + hit( LEVEL1_SINGLE_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 3 ), + dataSet.values.projectedValues( 4, 5 ) + ), + new ObjectDto<>( + dataSet.values.projectedValue( 6 ), + dataSet.values.projectedValues( 7, 8 ) + ) + ) ), + hit( LEVEL1_SINGLE_NULL_OBJECT_DOCUMENT_ID, Collections.emptyList() ), + hit( LEVEL1_SINGLE_EMPTY_OBJECT_DOCUMENT_ID, Collections.emptyList() ), + hit( LEVEL1_NO_OBJECT_DOCUMENT_ID, Collections.emptyList() ), + hit( LEVEL2_SINGLE_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 0 ), + dataSet.values.projectedValues( 1, 2 ) + ), + new ObjectDto<>( + dataSet.values.projectedValue( 3 ), + dataSet.values.projectedValues( 4, 5 ) + ) + ) ), + hit( LEVEL2_SINGLE_EMPTY_OBJECT_DOCUMENT_ID, listMaybeWithNull( + new ObjectDto<>( + null, + Collections.emptyList() + ), + new ObjectDto<>( + null, + Collections.emptyList() + ) + ) ), + hit( LEVEL2_NO_OBJECT_DOCUMENT_ID, Collections.emptyList() ) + ); + } + + @Test + public void flattenedLevel1AndObjectOnLevel2() { + assertThatQuery( mainIndex.query() + .select( f -> f.composite() + .from( + f.id( String.class ), + f.composite() + .from( + multiValuedProjection( f, level1SingleValuedFieldPath() ), + multiValuedProjection( f, level1MultiValuedFieldPath() ), + f.object( level2Path() ) + .from( + singleValuedProjection( f, level2SingleValuedFieldPath() ), + multiValuedProjection( f, level2MultiValuedFieldPath() ) + ) + .as( ObjectDto::new ) + .multi() + ) + .as( FlattenedLevel1ObjectDto::new ) + ) + .as( IdAndObjectDto::new ) ) + .where( f -> f.matchAll() ) + .routing( dataSet.routingKey ) ) + .hits().asIs().usingRecursiveFieldByFieldElementComparator( recursiveComparisonConfig ) + .containsExactlyInAnyOrder( + hit( FULL_DOCUMENT_ID, new FlattenedLevel1ObjectDto<>( + dataSet.values.projectedValues( 0, 9 ), + dataSet.values.projectedValues( 1, 2, 10, 11 ), + listMaybeWithNull( + null, + new ObjectDto<>( + dataSet.values.projectedValue( 3 ), + dataSet.values.projectedValues( 4, 5 ) + ), + new ObjectDto<>( + dataSet.values.projectedValue( 6 ), + dataSet.values.projectedValues( 7, 8 ) + ), + new ObjectDto<>( + dataSet.values.projectedValue( 12 ), + dataSet.values.projectedValues( 13, 14 ) + ), + null, + new ObjectDto<>( + dataSet.values.projectedValue( 15 ), + dataSet.values.projectedValues( 16, 17 ) + ) + ) + ) ), + hit( SINGLE_VALUED_DOCUMENT_ID, new FlattenedLevel1ObjectDto<>( + dataSet.values.projectedValues( 0 ), + dataSet.values.projectedValues( 1 ), + listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 2 ), + dataSet.values.projectedValues( 3 ) + ) + ) + ) ), + hit( LEVEL1_SINGLE_OBJECT_DOCUMENT_ID, new FlattenedLevel1ObjectDto<>( + dataSet.values.projectedValues( 0 ), + dataSet.values.projectedValues( 1, 2 ), + listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 3 ), + dataSet.values.projectedValues( 4, 5 ) + ), + new ObjectDto<>( + dataSet.values.projectedValue( 6 ), + dataSet.values.projectedValues( 7, 8 ) + ) + ) + ) ), + hit( LEVEL1_SINGLE_NULL_OBJECT_DOCUMENT_ID, new FlattenedLevel1ObjectDto<>( + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + ) ), + hit( LEVEL1_SINGLE_EMPTY_OBJECT_DOCUMENT_ID, new FlattenedLevel1ObjectDto<>( + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + ) ), + hit( LEVEL1_NO_OBJECT_DOCUMENT_ID, new FlattenedLevel1ObjectDto<>( + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + ) ), + hit( LEVEL2_SINGLE_OBJECT_DOCUMENT_ID, new FlattenedLevel1ObjectDto<>( + Collections.emptyList(), + Collections.emptyList(), + listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 0 ), + dataSet.values.projectedValues( 1, 2 ) + ), + new ObjectDto<>( + dataSet.values.projectedValue( 3 ), + dataSet.values.projectedValues( 4, 5 ) + ) + ) + ) ), + hit( LEVEL2_SINGLE_EMPTY_OBJECT_DOCUMENT_ID, new FlattenedLevel1ObjectDto<>( + Collections.emptyList(), + Collections.emptyList(), + listMaybeWithNull( + new ObjectDto<>( + null, + Collections.emptyList() + ), + new ObjectDto<>( + null, + Collections.emptyList() + ) + ) + ) ), + hit( LEVEL2_NO_OBJECT_DOCUMENT_ID, new FlattenedLevel1ObjectDto<>( + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + ) ) + ); + } + + @Test + public void fieldOutsideObjectFieldTree() { + assertThatThrownBy( () -> mainIndex.query() + .select( f -> f.object( level2Path() ) + .from( + // This is incorrect: the inner projection uses fields from "level1", + // which won't be present in "level1.level2". + singleValuedProjection( f, level1SingleValuedFieldPath() ) + ) + .asList() + .multi() ) + .where( f -> f.matchAll() ) + .routing( dataSet.routingKey ) + .toQuery() ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid context for projection on field '" + level1SingleValuedFieldPath() + "'", + "the surrounding projection is executed for each object in field '" + level2Path() + "'," + + " which is not a parent of field '" + level1SingleValuedFieldPath() + "'", + "Check the structure of your projections" + ); + } + + @Test + public void singleValuedField_effectivelyMultiValuedInContext() { + assertThatThrownBy( () -> mainIndex.query() + .select( f -> f.object( level1Path() ) + .from( + singleValuedProjection( f, level1SingleValuedFieldPath() ), + multiValuedProjection( f, level1MultiValuedFieldPath() ), + f.composite() + .from( + // This is incorrect: we don't use object( "level1.level2" ), + // so this field is multi-valued, because it's collected + // for each "level1" object, and "level1.level2" is multi-valued. + singleValuedProjection( f, level2SingleValuedFieldPath() ), + multiValuedProjection( f, level2MultiValuedFieldPath() ) + ) + .asList() + ) + .asList() + .multi() ) + .where( f -> f.matchAll() ) + .routing( dataSet.routingKey ) + .toQuery() ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid cardinality for projection on field '" + level2SingleValuedFieldPath() + "'", + "the projection is single-valued, but this field is effectively multi-valued in this context", + "because parent object field '" + level2Path() + "' is multi-valued", + "call '.multi()' when you create the projection on field '" + level2SingleValuedFieldPath() + "'", + "or wrap that projection in an object projection like this:" + + " 'f.object(\"" + level2Path() + "\").from().as(...).multi()'." + ); + } + + @Test + public void missingFields() { + StubMappingScope scope = mainIndex.createScope( missingLevel1Index, missingLevel1SingleValuedFieldIndex, + missingLevel2Index, missingLevel2SingleValuedFieldIndex + ); + assertThatQuery( scope.query() + .select( f -> f.composite() + .from( + f.id( String.class ), + f.object( level1Path() ) + .from( + singleValuedProjection( f, level1SingleValuedFieldPath() ), + multiValuedProjection( f, level1MultiValuedFieldPath() ), + f.object( level2Path() ) + .from( + singleValuedProjection( f, level2SingleValuedFieldPath() ), + multiValuedProjection( f, level2MultiValuedFieldPath() ) + ) + .as( ObjectDto::new ) + .multi() + ) + .as( Level1ObjectDto::new ) + .multi() + ) + .as( IdAndObjectDto::new ) ) + .where( f -> f.matchAll() ) + .routing( dataSet.routingKey ) ) + .hits().asIs().usingRecursiveFieldByFieldElementComparator( recursiveComparisonConfig ) + .contains( + hit( MISSING_LEVEL1_DOCUMENT_ID, Collections.emptyList() ), + hit( MISSING_LEVEL1_SINGLE_VALUED_FIELD_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectDto<>( + null, + dataSet.values.projectedValues( 0, 1 ), + listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 2 ), + dataSet.values.projectedValues( 3, 4 ) + ), + new ObjectDto<>( + dataSet.values.projectedValue( 5 ), + dataSet.values.projectedValues( 6, 7 ) + ) + ) + ), + new Level1ObjectDto<>( + null, + dataSet.values.projectedValues( 8, 9 ), + listMaybeWithNull( + new ObjectDto<>( + dataSet.values.projectedValue( 10 ), + dataSet.values.projectedValues( 11, 12 ) + ), + new ObjectDto<>( + dataSet.values.projectedValue( 13 ), + dataSet.values.projectedValues( 14, 15 ) + ) + ) + ) + ) ), + hit( MISSING_LEVEL2_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectDto<>( + dataSet.values.projectedValue( 0 ), + dataSet.values.projectedValues( 1, 2 ), + Collections.emptyList() + ), + new Level1ObjectDto<>( + dataSet.values.projectedValue( 3 ), + dataSet.values.projectedValues( 4, 5 ), + Collections.emptyList() + ) + ) ), + hit( MISSING_LEVEL2_SINGLE_VALUED_FIELD_DOCUMENT_ID, listMaybeWithNull( + new Level1ObjectDto<>( + dataSet.values.projectedValue( 0 ), + dataSet.values.projectedValues( 1, 2 ), + listMaybeWithNull( + new ObjectDto<>( + null, + dataSet.values.projectedValues( 3, 4 ) + ), + new ObjectDto<>( + null, + dataSet.values.projectedValues( 5, 6 ) + ) + ) + ), + new Level1ObjectDto<>( + dataSet.values.projectedValue( 7 ), + dataSet.values.projectedValues( 8, 9 ), + listMaybeWithNull( + new ObjectDto<>( + null, + dataSet.values.projectedValues( 10, 11 ) + ), + new ObjectDto<>( + null, + dataSet.values.projectedValues( 12, 13 ) + ) + ) + ) + ) ) + ); + } + + @SafeVarargs + private static List listMaybeWithNull(T ... values) { + List list = new ArrayList<>(); + Collections.addAll( list, values ); + if ( !TckConfiguration.get().getBackendFeatures().projectionPreservesNulls() ) { + list.removeIf( Objects::isNull ); + } + return list; + } + + private IdAndObjectDto hit(String docIdConstant, T object) { + return new IdAndObjectDto<>( dataSet.docId( docIdConstant ), object ); + } + + private String level1Path() { + return mainIndex.binding().level1( dataSet.structure ).absolutePath; + } + + private String level1SingleValuedFieldPath() { + return mainIndex.binding().level1( dataSet.structure ) + .singleValuedFieldAbsolutePath( dataSet.fieldType ); + } + + private String level1MultiValuedFieldPath() { + return mainIndex.binding().level1( dataSet.structure ) + .multiValuedFieldAbsolutePath( dataSet.fieldType ); + } + + private String singleValuedLevel1Path() { + return mainIndex.binding().singleValuedLevel1( dataSet.structure ).absolutePath; + } + + private String singleValuedLevel1SingleValuedFieldPath() { + return mainIndex.binding().singleValuedLevel1( dataSet.structure ) + .singleValuedFieldAbsolutePath( dataSet.fieldType ); + } + + private String singleValuedLevel1MultiValuedFieldPath() { + return mainIndex.binding().singleValuedLevel1( dataSet.structure ) + .multiValuedFieldAbsolutePath( dataSet.fieldType ); + } + + private String level2Path() { + return mainIndex.binding().level1( dataSet.structure ).level2.absolutePath; + } + + private String level2SingleValuedFieldPath() { + return mainIndex.binding().level1( dataSet.structure ).level2 + .singleValuedFieldAbsolutePath( dataSet.fieldType ); + } + + private String level2MultiValuedFieldPath() { + return mainIndex.binding().level1( dataSet.structure ).level2 + .multiValuedFieldAbsolutePath( dataSet.fieldType ); + } + + protected abstract ProjectionFinalStep

singleValuedProjection(SearchProjectionFactory f, String absoluteFieldPath); + + protected abstract ProjectionFinalStep> multiValuedProjection(SearchProjectionFactory f, String absoluteFieldPath); + + public static final class DataSet> + extends AbstractPerFieldTypeProjectionDataSet { + private final ObjectStructure structure; + + public DataSet(V values, ObjectStructure structure) { + super( values.fieldType.getUniqueName() + "/" + structure.name(), values ); + this.structure = structure; + } + + public String docId(String docIdConstant) { + return docIdConstant + "_" + routingKey; + } + + public void contribute(SimpleMappedIndex mainIndex, BulkIndexer mainIndexer, + SimpleMappedIndex missingLevel1Index, BulkIndexer missingLevel1Indexer, + SimpleMappedIndex missingLevel1SingleValuedFieldIndex, + BulkIndexer missingLevel1SingleValuedFieldIndexer, + SimpleMappedIndex missingLevel2Index, BulkIndexer missingLevel2Indexer, + SimpleMappedIndex missingLevel2SingleValuedFieldIndex, + BulkIndexer missingLevel2SingleValuedFieldIndexer) { + mainIndexer + .add( docId( FULL_DOCUMENT_ID ), routingKey, document -> { + Level1ObjectFieldBinding level1; + ObjectFieldBinding level2; + Level1ObjectFieldBinding singleValuedLevel1; + DocumentElement level1Object; + DocumentElement level2Object; + DocumentElement singleValuedLevel1Object; + + level1 = mainIndex.binding().level1( structure ); + level2 = level1.level2; + singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( structure ); + + document.addNullObject( level1.reference ); + + level1Object = document.addObject( level1.reference ); + level1Object.addValue( level1.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 0 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 1 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 2 ) ); + level1Object.addNullObject( level2.reference ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 3 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 4 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 5 ) ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 6 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 7 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 8 ) ); + + level1Object = document.addObject( level1.reference ); + level1Object.addValue( level1.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 9 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 10 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 11 ) ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 12 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 13 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 14 ) ); + level1Object.addNullObject( level2.reference ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 15 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 16 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 17 ) ); + + singleValuedLevel1Object = document.addObject( singleValuedLevel1.reference ); + singleValuedLevel1Object.addValue( singleValuedLevel1.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 18 ) ); + singleValuedLevel1Object.addValue( singleValuedLevel1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 19 ) ); + singleValuedLevel1Object.addValue( singleValuedLevel1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 20 ) ); + } ) + .add( docId( SINGLE_VALUED_DOCUMENT_ID ), routingKey, document -> { + Level1ObjectFieldBinding level1; + ObjectFieldBinding level2; + Level1ObjectFieldBinding singleValuedLevel1; + DocumentElement level1Object; + DocumentElement level2Object; + DocumentElement singleValuedLevel1Object; + + level1 = mainIndex.binding().level1( structure ); + level2 = level1.level2; + singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( structure ); + + level1Object = document.addObject( level1.reference ); + level1Object.addValue( level1.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 0 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 1 ) ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 2 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 3 ) ); + + singleValuedLevel1Object = document.addObject( singleValuedLevel1.reference ); + singleValuedLevel1Object.addValue( singleValuedLevel1.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 4 ) ); + singleValuedLevel1Object.addValue( singleValuedLevel1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 5 ) ); + } ) + .add( docId( LEVEL1_SINGLE_OBJECT_DOCUMENT_ID ), routingKey, document -> { + Level1ObjectFieldBinding level1; + ObjectFieldBinding level2; + Level1ObjectFieldBinding singleValuedLevel1; + DocumentElement level1Object; + DocumentElement level2Object; + DocumentElement singleValuedLevel1Object; + + level1 = mainIndex.binding().level1( structure ); + level2 = level1.level2; + singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( structure ); + + level1Object = document.addObject( level1.reference ); + level1Object.addValue( level1.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 0 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 1 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 2 ) ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 3 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 4 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 5 ) ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 6 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 7 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 8 ) ); + + singleValuedLevel1Object = document.addObject( singleValuedLevel1.reference ); + singleValuedLevel1Object.addValue( singleValuedLevel1.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 9 ) ); + singleValuedLevel1Object.addValue( singleValuedLevel1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 10 ) ); + singleValuedLevel1Object.addValue( singleValuedLevel1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 11 ) ); + } ) + .add( docId( LEVEL1_SINGLE_EMPTY_OBJECT_DOCUMENT_ID ), routingKey, document -> { + Level1ObjectFieldBinding level1; + Level1ObjectFieldBinding singleValuedLevel1; + + level1 = mainIndex.binding().level1( structure ); + singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( structure ); + + document.addObject( level1.reference ); + document.addObject( singleValuedLevel1.reference ); + } ) + .add( docId( LEVEL1_SINGLE_NULL_OBJECT_DOCUMENT_ID ), routingKey, document -> { + Level1ObjectFieldBinding level1; + Level1ObjectFieldBinding singleValuedLevel1; + + level1 = mainIndex.binding().level1( structure ); + singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( structure ); + + document.addNullObject( level1.reference ); + document.addNullObject( singleValuedLevel1.reference ); + } ) + .add( docId( LEVEL1_NO_OBJECT_DOCUMENT_ID ), routingKey, document -> { + } ) + .add( docId( LEVEL2_SINGLE_OBJECT_DOCUMENT_ID ), routingKey, document -> { + Level1ObjectFieldBinding level1; + ObjectFieldBinding level2; + DocumentElement level1Object; + DocumentElement level2Object; + + level1 = mainIndex.binding().level1( structure ); + level2 = level1.level2; + + level1Object = document.addObject( level1.reference ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 0 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 1 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 2 ) ); + + level1Object = document.addObject( level1.reference ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 3 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 4 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 5 ) ); + } ) + .add( docId( LEVEL2_SINGLE_EMPTY_OBJECT_DOCUMENT_ID ), routingKey, document -> { + Level1ObjectFieldBinding level1; + ObjectFieldBinding level2; + DocumentElement level1Object; + + level1 = mainIndex.binding().level1( structure ); + level2 = level1.level2; + + level1Object = document.addObject( level1.reference ); + level1Object.addObject( level2.reference ); + + level1Object = document.addObject( level1.reference ); + level1Object.addObject( level2.reference ); + } ) + .add( docId( LEVEL2_NO_OBJECT_DOCUMENT_ID ), routingKey, document -> { + Level1ObjectFieldBinding level1; + + level1 = mainIndex.binding().level1( structure ); + + document.addObject( level1.reference ); + + document.addObject( level1.reference ); + } ); + missingLevel1Indexer.add( docId( MISSING_LEVEL1_DOCUMENT_ID ), routingKey, document -> { } ); + missingLevel1SingleValuedFieldIndexer.add( docId( MISSING_LEVEL1_SINGLE_VALUED_FIELD_DOCUMENT_ID ), routingKey, document -> { + Level1ObjectFieldBindingWithoutSingleValuedField level1; + ObjectFieldBinding level2; + DocumentElement level1Object; + DocumentElement level2Object; + + level1 = missingLevel1SingleValuedFieldIndex.binding().level1( structure ); + level2 = level1.level2; + + level1Object = document.addObject( level1.reference ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 0 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 1 ) ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 2 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 3 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 4 ) ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 5 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 6 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 7 ) ); + + level1Object = document.addObject( level1.reference ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 8 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 9 ) ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 10 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 11 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 12 ) ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 13 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 14 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 15 ) ); + } ); + missingLevel2Indexer.add( docId( MISSING_LEVEL2_DOCUMENT_ID ), routingKey, document -> { + ObjectFieldBinding level1; + DocumentElement level1Object; + + level1 = missingLevel2Index.binding().level1( structure ); + + level1Object = document.addObject( level1.reference ); + level1Object.addValue( level1.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 0 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 1 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 2 ) ); + + level1Object = document.addObject( level1.reference ); + level1Object.addValue( level1.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 3 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 4 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 5 ) ); + } ); + missingLevel2SingleValuedFieldIndexer.add( docId( MISSING_LEVEL2_SINGLE_VALUED_FIELD_DOCUMENT_ID ), routingKey, document -> { + Level1ObjectFieldBindingWithoutLevel2SingleValuedField level1; + ObjectFieldBindingWithoutSingleValuedField level2; + DocumentElement level1Object; + DocumentElement level2Object; + + level1 = missingLevel2SingleValuedFieldIndex.binding().level1( structure ); + level2 = level1.level2; + + level1Object = document.addObject( level1.reference ); + level1Object.addValue( level1.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 0 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 1 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 2 ) ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 3 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 4 ) ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 5 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 6 ) ); + + level1Object = document.addObject( level1.reference ); + level1Object.addValue( level1.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 7 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 8 ) ); + level1Object.addValue( level1.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 9 ) ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 10 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 11 ) ); + level2Object = level1Object.addObject( level2.reference ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 12 ) ); + level2Object.addValue( level2.multiValuedField.get( values.fieldType ).reference, values.fieldValue( 13 ) ); + } ); + } + } + + static class IdAndObjectDto { + final String id; + final T object; + + IdAndObjectDto(String id, T object) { + this.id = id; + this.object = object; + } + + @Override + public String toString() { + return "IdAndObjectDto{" + + "id='" + id + '\'' + + ", object=" + object + + '}'; + } + } + + static class ObjectDto { + final T singleValuedField; + final List multiValuedField; + + ObjectDto(T singleValuedField, List multiValuedField) { + this.singleValuedField = singleValuedField; + this.multiValuedField = multiValuedField; + } + + @Override + public String toString() { + return "ObjectDto{" + + "singleValuedField=" + singleValuedField + + ", multiValuedField=" + multiValuedField + + '}'; + } + } + + static class Level1ObjectDto extends ObjectDto { + final List> level2; + + Level1ObjectDto(T singleValuedField, List multiValuedField, List> level2) { + super( singleValuedField, multiValuedField ); + this.level2 = level2; + } + + @Override + public String toString() { + return "Level1ObjectDto{" + + "singleValuedField=" + singleValuedField + + ", multiValuedField=" + multiValuedField + + ", level2=" + level2 + + "}"; + } + } + + static class FlattenedObjectDto { + final List singleValuedField; + final List multiValuedField; + + FlattenedObjectDto(List singleValuedField, List multiValuedField) { + this.singleValuedField = singleValuedField; + this.multiValuedField = multiValuedField; + } + + @Override + public String toString() { + return "FlattenedObjectDto{" + + "singleValuedField=" + singleValuedField + + ", multiValuedField=" + multiValuedField + + '}'; + } + } + + static class Level1ObjectWithFlattenedLevel2Dto extends ObjectDto { + final FlattenedObjectDto level2; + + Level1ObjectWithFlattenedLevel2Dto(T singleValuedField, List multiValuedField, FlattenedObjectDto level2) { + super( singleValuedField, multiValuedField ); + this.level2 = level2; + } + + @Override + public String toString() { + return "Level1ObjectWithFlattenedLevel2Dto{" + + "singleValuedField=" + singleValuedField + + ", multiValuedField=" + multiValuedField + + ", level2=" + level2 + + "}"; + } + } + + static class FlattenedLevel1ObjectDto extends FlattenedObjectDto { + final List> level2; + + FlattenedLevel1ObjectDto(List singleValuedField, List multiValuedField, List> level2) { + super( singleValuedField, multiValuedField ); + this.level2 = level2; + } + + @Override + public String toString() { + return "FlattenedLevel1ObjectDto{" + + "singleValuedField=" + singleValuedField + + ", multiValuedField=" + multiValuedField + + ", level2=" + level2 + + '}'; + } + } + + protected static class IndexBinding { + private final Level1ObjectFieldBinding level1_nested; + private final Level1ObjectFieldBinding level1_flattened; + private final Level1ObjectFieldBinding singleValuedLevel1_nested; + private final Level1ObjectFieldBinding singleValuedLevel1_flattened; + + IndexBinding(IndexSchemaElement root, Collection> fieldTypes) { + IndexSchemaObjectField level1NestedObjectField = + root.objectField( "level1_nested", ObjectStructure.NESTED ).multiValued(); + level1_nested = new Level1ObjectFieldBinding( level1NestedObjectField, null, + "level1_nested", ObjectStructure.NESTED, fieldTypes ); + IndexSchemaObjectField level1FlattenedObjectField = + root.objectField( "level1_flattened", ObjectStructure.FLATTENED ).multiValued(); + level1_flattened = new Level1ObjectFieldBinding( level1FlattenedObjectField, null, + "level1_flattened", ObjectStructure.FLATTENED, fieldTypes ); + IndexSchemaObjectField singleValuedLevel1NestedObjectField = + root.objectField( "singleValuedLevel1_nested", ObjectStructure.NESTED ); + singleValuedLevel1_nested = new Level1ObjectFieldBinding( singleValuedLevel1NestedObjectField, null, + "singleValuedLevel1_nested", ObjectStructure.NESTED, fieldTypes ); + IndexSchemaObjectField singleValuedLevel1FlattenedObjectField = + root.objectField( "singleValuedLevel1_flattened", ObjectStructure.FLATTENED ); + singleValuedLevel1_flattened = new Level1ObjectFieldBinding( singleValuedLevel1FlattenedObjectField, null, + "singleValuedLevel1_flattened", ObjectStructure.FLATTENED, fieldTypes ); + } + + public Level1ObjectFieldBinding level1(ObjectStructure structure) { + return ObjectStructure.NESTED.equals( structure ) ? level1_nested : level1_flattened; + } + + public Level1ObjectFieldBinding singleValuedLevel1(ObjectStructure structure) { + return ObjectStructure.NESTED.equals( structure ) ? singleValuedLevel1_nested : singleValuedLevel1_flattened; + } + } + + protected static class MissingLevel1IndexBinding { + MissingLevel1IndexBinding(IndexSchemaElement root) { + } + } + + protected static class MissingLevel1SingleValuedFieldIndexBinding { + final Level1ObjectFieldBindingWithoutSingleValuedField level1_nested; + final Level1ObjectFieldBindingWithoutSingleValuedField level1_flattened; + + MissingLevel1SingleValuedFieldIndexBinding(IndexSchemaElement root, + Collection> fieldTypes) { + IndexSchemaObjectField level1NestedObjectField = + root.objectField( "level1_nested", ObjectStructure.NESTED ).multiValued(); + level1_nested = new Level1ObjectFieldBindingWithoutSingleValuedField( level1NestedObjectField, null, + "level1_nested", ObjectStructure.NESTED, fieldTypes ); + IndexSchemaObjectField level1FlattenedObjectField = + root.objectField( "level1_flattened", ObjectStructure.FLATTENED ).multiValued(); + level1_flattened = new Level1ObjectFieldBindingWithoutSingleValuedField( level1FlattenedObjectField, null, + "level1_flattened", ObjectStructure.FLATTENED, fieldTypes ); + } + + public Level1ObjectFieldBindingWithoutSingleValuedField level1(ObjectStructure structure) { + return ObjectStructure.NESTED.equals( structure ) ? level1_nested : level1_flattened; + } + } + + protected static class MissingLevel2IndexBinding { + final ObjectFieldBinding level1_nested; + final ObjectFieldBinding level1_flattened; + + MissingLevel2IndexBinding(IndexSchemaElement root, Collection> fieldTypes) { + IndexSchemaObjectField level1NestedObjectField = + root.objectField( "level1_nested", ObjectStructure.NESTED ).multiValued(); + level1_nested = new ObjectFieldBinding( level1NestedObjectField, null, + "level1_nested", fieldTypes ); + IndexSchemaObjectField level1FlattenedObjectField = + root.objectField( "level1_flattened", ObjectStructure.FLATTENED ).multiValued(); + level1_flattened = new ObjectFieldBinding( level1FlattenedObjectField, null, + "level1_flattened", fieldTypes ); + } + + public ObjectFieldBinding level1(ObjectStructure structure) { + return ObjectStructure.NESTED.equals( structure ) ? level1_nested : level1_flattened; + } + } + + protected static class MissingLevel2SingleValuedFieldIndexBinding { + final Level1ObjectFieldBindingWithoutLevel2SingleValuedField level1_nested; + final Level1ObjectFieldBindingWithoutLevel2SingleValuedField level1_flattened; + + MissingLevel2SingleValuedFieldIndexBinding(IndexSchemaElement root, + Collection> fieldTypes) { + IndexSchemaObjectField level1NestedObjectField = + root.objectField( "level1_nested", ObjectStructure.NESTED ).multiValued(); + level1_nested = new Level1ObjectFieldBindingWithoutLevel2SingleValuedField( level1NestedObjectField, null, + "level1_nested", ObjectStructure.NESTED, fieldTypes ); + IndexSchemaObjectField level1FlattenedObjectField = + root.objectField( "level1_flattened", ObjectStructure.FLATTENED ).multiValued(); + level1_flattened = new Level1ObjectFieldBindingWithoutLevel2SingleValuedField( level1FlattenedObjectField, null, + "level1_flattened", ObjectStructure.FLATTENED, fieldTypes ); + } + + public Level1ObjectFieldBindingWithoutLevel2SingleValuedField level1(ObjectStructure structure) { + return ObjectStructure.NESTED.equals( structure ) ? level1_nested : level1_flattened; + } + } + + protected static class ObjectFieldBinding { + final String absolutePath; + final SimpleFieldModelsByType singleValuedField; + final SimpleFieldModelsByType multiValuedField; + final IndexObjectFieldReference reference; + + ObjectFieldBinding(IndexSchemaObjectField objectField, String parentAbsolutePath, String relativeFieldName, + Collection> fieldTypes) { + absolutePath = FieldPaths.compose( parentAbsolutePath, relativeFieldName ); + singleValuedField = SimpleFieldModelsByType.mapAll( fieldTypes, objectField, "singleValuedField_", + b -> b.projectable( Projectable.YES ) ); + multiValuedField = SimpleFieldModelsByType.mapAllMultiValued( fieldTypes, objectField, "multiValuedField_", + b -> b.projectable( Projectable.YES ) ); + reference = objectField.toReference(); + } + + String singleValuedFieldAbsolutePath(FieldTypeDescriptor fieldType) { + return FieldPaths.compose( absolutePath, singleValuedField.get( fieldType ).relativeFieldName ); + } + + String multiValuedFieldAbsolutePath(FieldTypeDescriptor fieldType) { + return FieldPaths.compose( absolutePath, multiValuedField.get( fieldType ).relativeFieldName ); + } + } + + protected static class Level1ObjectFieldBinding extends ObjectFieldBinding { + final ObjectFieldBinding level2; + + Level1ObjectFieldBinding(IndexSchemaObjectField objectField, String parentAbsolutePath, String relativeFieldName, + ObjectStructure structure, Collection> fieldTypes) { + super( objectField, parentAbsolutePath, relativeFieldName, fieldTypes ); + IndexSchemaObjectField level2ObjectField = objectField.objectField( "level2", structure ) + .multiValued(); + level2 = new ObjectFieldBinding( level2ObjectField, this.absolutePath, "level2", fieldTypes ); + } + } + + protected static class ObjectFieldBindingWithoutSingleValuedField { + final String absolutePath; + final SimpleFieldModelsByType multiValuedField; + final IndexObjectFieldReference reference; + + ObjectFieldBindingWithoutSingleValuedField(IndexSchemaObjectField objectField, String parentAbsolutePath, + String relativeFieldName, Collection> fieldTypes) { + absolutePath = FieldPaths.compose( parentAbsolutePath, relativeFieldName ); + multiValuedField = SimpleFieldModelsByType.mapAllMultiValued( fieldTypes, objectField, "multiValuedField_", + b -> b.projectable( Projectable.YES ) ); + reference = objectField.toReference(); + } + } + + protected static class Level1ObjectFieldBindingWithoutSingleValuedField + extends ObjectFieldBindingWithoutSingleValuedField { + final ObjectFieldBinding level2; + + Level1ObjectFieldBindingWithoutSingleValuedField(IndexSchemaObjectField objectField, String parentAbsolutePath, + String relativeFieldName, + ObjectStructure structure, Collection> fieldTypes) { + super( objectField, parentAbsolutePath, relativeFieldName, fieldTypes ); + IndexSchemaObjectField level2ObjectField = objectField.objectField( "level2", structure ) + .multiValued(); + level2 = new ObjectFieldBinding( level2ObjectField, this.absolutePath, "level2", fieldTypes ); + } + } + + protected static class Level1ObjectFieldBindingWithoutLevel2SingleValuedField extends ObjectFieldBinding { + final ObjectFieldBindingWithoutSingleValuedField level2; + + Level1ObjectFieldBindingWithoutLevel2SingleValuedField(IndexSchemaObjectField objectField, String parentAbsolutePath, + String relativeFieldName, + ObjectStructure structure, Collection> fieldTypes) { + super( objectField, parentAbsolutePath, relativeFieldName, fieldTypes ); + IndexSchemaObjectField level2ObjectField = objectField.objectField( "level2", structure ) + .multiValued(); + level2 = new ObjectFieldBindingWithoutSingleValuedField( level2ObjectField, this.absolutePath, "level2", fieldTypes ); + } + } +} diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/AbstractProjectionTestValues.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/AbstractProjectionTestValues.java new file mode 100644 index 00000000000..05dc9211ecf --- /dev/null +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/AbstractProjectionTestValues.java @@ -0,0 +1,40 @@ +/* + * 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.integrationtest.backend.tck.search.projection; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.hibernate.search.integrationtest.backend.tck.testsupport.types.FieldTypeDescriptor; + +/** + * @param The type of field values. + * @param

The type of projected values. + */ +public abstract class AbstractProjectionTestValues { + + protected final FieldTypeDescriptor fieldType; + + protected AbstractProjectionTestValues(FieldTypeDescriptor fieldType) { + this.fieldType = fieldType; + } + + public FieldTypeDescriptor fieldType() { + return fieldType; + } + + public abstract F fieldValue(int ordinal); + + public abstract P projectedValue(int ordinal); + + public List

projectedValues(int ... ordinals) { + return IntStream.of( ordinals ).mapToObj( this::projectedValue ) + .collect( Collectors.toList() ); + } + +} diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/DistanceProjectionBaseIT.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/DistanceProjectionBaseIT.java new file mode 100644 index 00000000000..27c076ed282 --- /dev/null +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/DistanceProjectionBaseIT.java @@ -0,0 +1,137 @@ +/* + * 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.integrationtest.backend.tck.search.projection; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.hibernate.search.engine.backend.types.ObjectStructure; +import org.hibernate.search.engine.search.projection.dsl.ProjectionFinalStep; +import org.hibernate.search.engine.search.projection.dsl.SearchProjectionFactory; +import org.hibernate.search.engine.spatial.GeoPoint; +import org.hibernate.search.integrationtest.backend.tck.testsupport.types.FieldTypeDescriptor; +import org.hibernate.search.integrationtest.backend.tck.testsupport.types.GeoPointFieldTypeDescriptor; +import org.hibernate.search.integrationtest.backend.tck.testsupport.util.TckConfiguration; +import org.hibernate.search.integrationtest.backend.tck.testsupport.util.rule.SearchSetupHelper; +import org.hibernate.search.util.impl.integrationtest.common.assertion.TestComparators; +import org.hibernate.search.util.impl.integrationtest.mapper.stub.BulkIndexer; +import org.hibernate.search.util.impl.integrationtest.mapper.stub.SimpleMappedIndex; + +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration; + +@RunWith(Enclosed.class) +public class DistanceProjectionBaseIT { + + @ClassRule + public static final SearchSetupHelper setupHelper = new SearchSetupHelper(); + + private static DistanceProjectionTestValues testValues() { + return new DistanceProjectionTestValues(); + } + + @BeforeClass + public static void setup() { + setupHelper.start() + .withIndexes( InObjectProjectionIT.mainIndex, InObjectProjectionIT.missingLevel1Index, + InObjectProjectionIT.missingLevel1SingleValuedFieldIndex, + InObjectProjectionIT.missingLevel2Index, InObjectProjectionIT.missingLevel2SingleValuedFieldIndex ) + .setup(); + + BulkIndexer compositeForEachMainIndexer = InObjectProjectionIT.mainIndex.bulkIndexer(); + BulkIndexer compositeForEachMissingLevel1Indexer = InObjectProjectionIT.missingLevel1Index.bulkIndexer(); + BulkIndexer compositeForEachMissingLevel1SingleValuedFieldIndexer = InObjectProjectionIT.missingLevel1SingleValuedFieldIndex.bulkIndexer(); + BulkIndexer compositeForEachMissingLevel2Indexer = InObjectProjectionIT.missingLevel2Index.bulkIndexer(); + BulkIndexer compositeForEachMissingLevel2SingleValuedFieldIndexer = InObjectProjectionIT.missingLevel2SingleValuedFieldIndex.bulkIndexer(); + InObjectProjectionIT.dataSets.forEach( d -> d.contribute( InObjectProjectionIT.mainIndex, compositeForEachMainIndexer, + InObjectProjectionIT.missingLevel1Index, compositeForEachMissingLevel1Indexer, + InObjectProjectionIT.missingLevel1SingleValuedFieldIndex, compositeForEachMissingLevel1SingleValuedFieldIndexer, + InObjectProjectionIT.missingLevel2Index, compositeForEachMissingLevel2Indexer, + InObjectProjectionIT.missingLevel2SingleValuedFieldIndex, compositeForEachMissingLevel2SingleValuedFieldIndexer ) ); + + compositeForEachMainIndexer.join( compositeForEachMissingLevel1Indexer, + compositeForEachMissingLevel1SingleValuedFieldIndexer, compositeForEachMissingLevel2Indexer, + compositeForEachMissingLevel2SingleValuedFieldIndexer ); + } + + @Test + public void takariCpSuiteWorkaround() { + // Workaround to get Takari-CPSuite to run this test. + } + + @RunWith(Parameterized.class) + public static class InObjectProjectionIT + extends AbstractProjectionInObjectProjectionIT { + private static final List> supportedFieldTypes = + Arrays.asList( GeoPointFieldTypeDescriptor.INSTANCE ); + private static final List> dataSets = new ArrayList<>(); + private static final List parameters = new ArrayList<>(); + static { + for ( ObjectStructure structure : + TckConfiguration.get().getBackendFeatures().reliesOnNestedDocumentsForObjectProjection() + ? new ObjectStructure[] { ObjectStructure.NESTED } + : new ObjectStructure[] { ObjectStructure.FLATTENED, ObjectStructure.NESTED } ) { + DataSet dataSet = new DataSet<>( testValues(), structure ); + dataSets.add( dataSet ); + parameters.add( new Object[] { dataSet } ); + } + } + + private static final SimpleMappedIndex mainIndex = + SimpleMappedIndex.of( root -> new IndexBinding( root, supportedFieldTypes ) ) + .name( "main" ); + private static final SimpleMappedIndex missingLevel1Index = + SimpleMappedIndex.of( MissingLevel1IndexBinding::new ) + .name( "missingLevel1" ); + private static final SimpleMappedIndex missingLevel1SingleValuedFieldIndex = + SimpleMappedIndex.of( root -> new MissingLevel1SingleValuedFieldIndexBinding( root, supportedFieldTypes ) ) + .name( "missingLevel1Field1" ); + private static final SimpleMappedIndex missingLevel2Index = + SimpleMappedIndex.of( root -> new MissingLevel2IndexBinding( root, supportedFieldTypes ) ) + .name( "missingLevel2" ); + private static final SimpleMappedIndex missingLevel2SingleValuedFieldIndex = + SimpleMappedIndex.of( root -> new MissingLevel2SingleValuedFieldIndexBinding( root, supportedFieldTypes ) ) + .name( "missingLevel2Field1" ); + + @Parameterized.Parameters(name = "{0}") + public static List parameters() { + return parameters; + } + + public InObjectProjectionIT(DataSet dataSet) { + super( mainIndex, missingLevel1Index, missingLevel1SingleValuedFieldIndex, missingLevel2Index, + missingLevel2SingleValuedFieldIndex, + dataSet ); + } + + @Override + protected RecursiveComparisonConfiguration.Builder configureRecursiveComparison( + RecursiveComparisonConfiguration.Builder builder) { + return builder.withComparatorForType( TestComparators.APPROX_M_COMPARATOR, Double.class ); + } + + @Override + protected ProjectionFinalStep singleValuedProjection(SearchProjectionFactory f, + String absoluteFieldPath) { + return f.distance( absoluteFieldPath, dataSet.values.projectionCenterPoint() ); + } + + @Override + protected ProjectionFinalStep> multiValuedProjection(SearchProjectionFactory f, + String absoluteFieldPath) { + return f.distance( absoluteFieldPath, dataSet.values.projectionCenterPoint() ).multi(); + } + + } +} diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/DistanceProjectionTestValues.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/DistanceProjectionTestValues.java new file mode 100644 index 00000000000..98fdc606c16 --- /dev/null +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/DistanceProjectionTestValues.java @@ -0,0 +1,32 @@ +/* + * 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.integrationtest.backend.tck.search.projection; + +import org.hibernate.search.engine.spatial.GeoPoint; +import org.hibernate.search.integrationtest.backend.tck.testsupport.types.GeoPointFieldTypeDescriptor; +import org.hibernate.search.integrationtest.backend.tck.testsupport.types.values.IndexableGeoPointWithDistanceFromCenterValues; + +public class DistanceProjectionTestValues extends AbstractProjectionTestValues { + protected DistanceProjectionTestValues() { + super( GeoPointFieldTypeDescriptor.INSTANCE ); + } + + @Override + public GeoPoint fieldValue(int ordinal) { + return IndexableGeoPointWithDistanceFromCenterValues.INSTANCE.getSingle().get( ordinal ); + } + + @Override + public Double projectedValue(int ordinal) { + return IndexableGeoPointWithDistanceFromCenterValues.INSTANCE + .getSingleDistancesFromCenterPoint1().get( ordinal ); + } + + public GeoPoint projectionCenterPoint() { + return IndexableGeoPointWithDistanceFromCenterValues.CENTER_POINT_1; + } +} diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/DistanceProjectionTypeCheckingAndConversionIT.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/DistanceProjectionTypeCheckingAndConversionIT.java index 4d72fa339df..a3d4fd13521 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/DistanceProjectionTypeCheckingAndConversionIT.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/DistanceProjectionTypeCheckingAndConversionIT.java @@ -115,13 +115,13 @@ public void multiValuedField_singleValuedProjection() { @Test @TestForIssue(jiraKey = "HSEARCH-3391") public void singleValuedFieldInMultiValuedObjectField_flattened_singleValuedProjection() { - StubMappingScope scope = mainIndex.createScope(); - String fieldPath = mainIndex.binding().flattenedObjectWithMultipleValues.relativeFieldName + "." + mainIndex.binding().flattenedObjectWithMultipleValues.fieldModel.relativeFieldName; - assertThatThrownBy( () -> scope.projection() - .field( fieldPath, fieldType.getJavaType() ).toProjection() ) + assertThatThrownBy( () -> mainIndex.query() + .select( f -> f.field( fieldPath, fieldType.getJavaType() ) ) + .where( f -> f.matchAll() ) + .toQuery() ) .isInstanceOf( SearchException.class ) .hasMessageContaining( "Invalid cardinality for projection on field '" + fieldPath + "'", @@ -133,13 +133,13 @@ public void singleValuedFieldInMultiValuedObjectField_flattened_singleValuedProj @Test @TestForIssue(jiraKey = "HSEARCH-3391") public void singleValuedFieldInMultiValuedObjectField_nested_singleValuedProjection() { - StubMappingScope scope = mainIndex.createScope(); - String fieldPath = mainIndex.binding().nestedObjectWithMultipleValues.relativeFieldName + "." + mainIndex.binding().nestedObjectWithMultipleValues.fieldModel.relativeFieldName; - assertThatThrownBy( () -> scope.projection() - .field( fieldPath, fieldType.getJavaType() ).toProjection() ) + assertThatThrownBy( () -> mainIndex.query() + .select( f -> f.field( fieldPath, fieldType.getJavaType() ) ) + .where( f -> f.matchAll() ) + .toQuery() ) .isInstanceOf( SearchException.class ) .hasMessageContaining( "Invalid cardinality for projection on field '" + fieldPath + "'", diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/FieldProjectionBaseIT.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/FieldProjectionBaseIT.java new file mode 100644 index 00000000000..b8f881ac5ed --- /dev/null +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/FieldProjectionBaseIT.java @@ -0,0 +1,136 @@ +/* + * 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.integrationtest.backend.tck.search.projection; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.hibernate.search.engine.backend.types.ObjectStructure; +import org.hibernate.search.engine.search.projection.dsl.ProjectionFinalStep; +import org.hibernate.search.engine.search.projection.dsl.SearchProjectionFactory; +import org.hibernate.search.integrationtest.backend.tck.testsupport.types.FieldTypeDescriptor; +import org.hibernate.search.integrationtest.backend.tck.testsupport.util.TckConfiguration; +import org.hibernate.search.integrationtest.backend.tck.testsupport.util.rule.SearchSetupHelper; +import org.hibernate.search.util.impl.integrationtest.mapper.stub.BulkIndexer; +import org.hibernate.search.util.impl.integrationtest.mapper.stub.SimpleMappedIndex; + +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration; + +@RunWith(Enclosed.class) +public class FieldProjectionBaseIT { + + @ClassRule + public static final SearchSetupHelper setupHelper = new SearchSetupHelper(); + + private static FieldProjectionTestValues testValues(FieldTypeDescriptor fieldType) { + return new FieldProjectionTestValues<>( fieldType ); + } + + @BeforeClass + public static void setup() { + setupHelper.start() + .withIndexes( InObjectProjectionIT.mainIndex, InObjectProjectionIT.missingLevel1Index, + InObjectProjectionIT.missingLevel1SingleValuedFieldIndex, + InObjectProjectionIT.missingLevel2Index, InObjectProjectionIT.missingLevel2SingleValuedFieldIndex ) + .setup(); + + BulkIndexer compositeForEachMainIndexer = InObjectProjectionIT.mainIndex.bulkIndexer(); + BulkIndexer compositeForEachMissingLevel1Indexer = InObjectProjectionIT.missingLevel1Index.bulkIndexer(); + BulkIndexer compositeForEachMissingLevel1SingleValuedFieldIndexer = InObjectProjectionIT.missingLevel1SingleValuedFieldIndex.bulkIndexer(); + BulkIndexer compositeForEachMissingLevel2Indexer = InObjectProjectionIT.missingLevel2Index.bulkIndexer(); + BulkIndexer compositeForEachMissingLevel2SingleValuedFieldIndexer = InObjectProjectionIT.missingLevel2SingleValuedFieldIndex.bulkIndexer(); + InObjectProjectionIT.dataSets.forEach( d -> d.contribute( InObjectProjectionIT.mainIndex, compositeForEachMainIndexer, + InObjectProjectionIT.missingLevel1Index, compositeForEachMissingLevel1Indexer, + InObjectProjectionIT.missingLevel1SingleValuedFieldIndex, compositeForEachMissingLevel1SingleValuedFieldIndexer, + InObjectProjectionIT.missingLevel2Index, compositeForEachMissingLevel2Indexer, + InObjectProjectionIT.missingLevel2SingleValuedFieldIndex, compositeForEachMissingLevel2SingleValuedFieldIndexer ) ); + + compositeForEachMainIndexer.join( compositeForEachMissingLevel1Indexer, + compositeForEachMissingLevel1SingleValuedFieldIndexer, compositeForEachMissingLevel2Indexer, + compositeForEachMissingLevel2SingleValuedFieldIndexer ); + } + + @Test + public void takariCpSuiteWorkaround() { + // Workaround to get Takari-CPSuite to run this test. + } + + @RunWith(Parameterized.class) + public static class InObjectProjectionIT + extends AbstractProjectionInObjectProjectionIT> { + private static final List> supportedFieldTypes = FieldTypeDescriptor.getAll(); + private static final List> dataSets = new ArrayList<>(); + private static final List parameters = new ArrayList<>(); + static { + for ( FieldTypeDescriptor fieldType : supportedFieldTypes ) { + for ( ObjectStructure structure : + TckConfiguration.get().getBackendFeatures().reliesOnNestedDocumentsForObjectProjection() + ? new ObjectStructure[] { ObjectStructure.NESTED } + : new ObjectStructure[] { ObjectStructure.FLATTENED, ObjectStructure.NESTED } ) { + DataSet dataSet = new DataSet<>( testValues( fieldType ), structure ); + dataSets.add( dataSet ); + parameters.add( new Object[] { dataSet } ); + } + } + } + + private static final SimpleMappedIndex mainIndex = + SimpleMappedIndex.of( root -> new IndexBinding( root, supportedFieldTypes ) ) + .name( "main" ); + private static final SimpleMappedIndex missingLevel1Index = + SimpleMappedIndex.of( MissingLevel1IndexBinding::new ) + .name( "missingLevel1" ); + private static final SimpleMappedIndex missingLevel1SingleValuedFieldIndex = + SimpleMappedIndex.of( root -> new MissingLevel1SingleValuedFieldIndexBinding( root, supportedFieldTypes ) ) + .name( "missingLevel1Field1" ); + private static final SimpleMappedIndex missingLevel2Index = + SimpleMappedIndex.of( root -> new MissingLevel2IndexBinding( root, supportedFieldTypes ) ) + .name( "missingLevel2" ); + private static final SimpleMappedIndex missingLevel2SingleValuedFieldIndex = + SimpleMappedIndex.of( root -> new MissingLevel2SingleValuedFieldIndexBinding( root, supportedFieldTypes ) ) + .name( "missingLevel2Field1" ); + + @Parameterized.Parameters(name = "{0}") + public static List parameters() { + return parameters; + } + + public InObjectProjectionIT(DataSet> dataSet) { + super( mainIndex, missingLevel1Index, missingLevel1SingleValuedFieldIndex, missingLevel2Index, + missingLevel2SingleValuedFieldIndex, + dataSet ); + } + + @Override + protected RecursiveComparisonConfiguration.Builder configureRecursiveComparison( + RecursiveComparisonConfiguration.Builder builder) { + return builder.withComparatorForType( Comparator.nullsFirst( Comparator.naturalOrder() ), BigDecimal.class ); + } + + @Override + protected ProjectionFinalStep singleValuedProjection(SearchProjectionFactory f, + String absoluteFieldPath) { + return f.field( absoluteFieldPath, dataSet.fieldType.getJavaType() ); + } + + @Override + protected ProjectionFinalStep> multiValuedProjection(SearchProjectionFactory f, + String absoluteFieldPath) { + return f.field( absoluteFieldPath, dataSet.fieldType.getJavaType() ).multi(); + } + + } +} diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/FieldProjectionTestValues.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/FieldProjectionTestValues.java new file mode 100644 index 00000000000..08868be1f55 --- /dev/null +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/FieldProjectionTestValues.java @@ -0,0 +1,25 @@ +/* + * 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.integrationtest.backend.tck.search.projection; + +import org.hibernate.search.integrationtest.backend.tck.testsupport.types.FieldTypeDescriptor; + +public class FieldProjectionTestValues extends AbstractProjectionTestValues { + protected FieldProjectionTestValues(FieldTypeDescriptor fieldType) { + super( fieldType ); + } + + @Override + public F fieldValue(int ordinal) { + return fieldType.valueFromInteger( ordinal ); + } + + @Override + public F projectedValue(int ordinal) { + return fieldType.valueFromInteger( ordinal ); + } +} diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/FieldProjectionTypeCheckingAndConversionIT.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/FieldProjectionTypeCheckingAndConversionIT.java index 51b522de772..d1864c80936 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/FieldProjectionTypeCheckingAndConversionIT.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/FieldProjectionTypeCheckingAndConversionIT.java @@ -191,13 +191,13 @@ public void multiValuedField_singleValuedProjection() { @Test @TestForIssue(jiraKey = "HSEARCH-3391") public void singleValuedFieldInMultiValuedObjectField_flattened_singleValuedProjection() { - StubMappingScope scope = mainIndex.createScope(); - String fieldPath = mainIndex.binding().flattenedObjectWithMultipleValues.relativeFieldName + "." + mainIndex.binding().flattenedObjectWithMultipleValues.fieldModels.get( fieldType ).relativeFieldName; - assertThatThrownBy( () -> scope.projection() - .field( fieldPath, fieldType.getJavaType() ).toProjection() ) + assertThatThrownBy( () -> mainIndex.query() + .select( f -> f.field( fieldPath, fieldType.getJavaType() ) ) + .where( f -> f.matchAll() ) + .toQuery() ) .isInstanceOf( SearchException.class ) .hasMessageContaining( "Invalid cardinality for projection on field '" + fieldPath + "'", @@ -209,13 +209,13 @@ public void singleValuedFieldInMultiValuedObjectField_flattened_singleValuedProj @Test @TestForIssue(jiraKey = "HSEARCH-3391") public void singleValuedFieldInMultiValuedObjectField_nested_singleValuedProjection() { - StubMappingScope scope = mainIndex.createScope(); - String fieldPath = mainIndex.binding().nestedObjectWithMultipleValues.relativeFieldName + "." + mainIndex.binding().nestedObjectWithMultipleValues.fieldModels.get( fieldType ).relativeFieldName; - assertThatThrownBy( () -> scope.projection() - .field( fieldPath, fieldType.getJavaType() ).toProjection() ) + assertThatThrownBy( () -> mainIndex.query() + .select( f -> f.field( fieldPath, fieldType.getJavaType() ) ) + .where( f -> f.matchAll() ) + .toQuery() ) .isInstanceOf( SearchException.class ) .hasMessageContaining( "Invalid cardinality for projection on field '" + fieldPath + "'", diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/ObjectProjectionSpecificsIT.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/ObjectProjectionSpecificsIT.java new file mode 100644 index 00000000000..2a7ed11e9bc --- /dev/null +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/ObjectProjectionSpecificsIT.java @@ -0,0 +1,191 @@ +/* + * 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.integrationtest.backend.tck.search.projection; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assume.assumeTrue; + +import org.hibernate.search.engine.backend.document.IndexFieldReference; +import org.hibernate.search.engine.backend.document.IndexObjectFieldReference; +import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement; +import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaObjectField; +import org.hibernate.search.engine.backend.types.ObjectStructure; +import org.hibernate.search.engine.backend.types.Projectable; +import org.hibernate.search.engine.search.projection.dsl.SearchProjectionFactory; +import org.hibernate.search.integrationtest.backend.tck.testsupport.util.TckConfiguration; +import org.hibernate.search.integrationtest.backend.tck.testsupport.util.rule.SearchSetupHelper; +import org.hibernate.search.util.common.SearchException; +import org.hibernate.search.util.impl.integrationtest.mapper.stub.SimpleMappedIndex; + +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +public class ObjectProjectionSpecificsIT { + + @ClassRule + public static final SearchSetupHelper setupHelper = new SearchSetupHelper(); + + private static final SimpleMappedIndex index = SimpleMappedIndex.of( IndexBinding::new ); + + @BeforeClass + public static void setup() { + setupHelper.start() + .withIndexes( index ) + .setup(); + } + + @Test + public void nullFieldPath() { + assertThatThrownBy( () -> index.createScope().projection().object( null ) ) + .isInstanceOf( IllegalArgumentException.class ) + .hasMessageContaining( "'objectFieldPath' must not be null" ); + } + + @Test + public void unknownFieldPath() { + assertThatThrownBy( () -> index.createScope().projection().object( "unknownField" ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( "Unknown field 'unknownField'" ); + } + + @Test + public void nonObjectFieldPath() { + assertThatThrownBy( () -> index.createScope().projection().object( "level1.field1" ) ) + .hasMessageContainingAll( "Cannot use 'projection:object' on field 'level1.field1'." ); + } + + @Test + public void innerObjectProjectionOnFieldOutsideOuterObjectProjectionFieldTree() { + assertThatThrownBy( () -> index.query() + .select( f -> f.object( "level1.level2" ) + .from( + // This is incorrect: the inner projection is executed for each object in field "level1", + // which won't be present in "level1.level2". + f.object( "level1" ) + .from( f.field( "level1.field1" ) ) + .asList() + .multi() + ) + .asList() + .multi() ) + .where( f -> f.matchAll() ) + .toQuery() ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid context for projection on field 'level1'", + "the surrounding projection is executed for each object in field 'level1.level2'," + + " which is not a parent of field 'level1'", + "Check the structure of your projections" + ); + } + + @Test + public void flattenedObjectField_unsupported() { + assumeTrue( + "This test is only relevant if the backend relies on nested documents to implement object projections", + TckConfiguration.get().getBackendFeatures().reliesOnNestedDocumentsForObjectProjection() + ); + SearchProjectionFactory f = index.createScope().projection(); + assertThatThrownBy( () -> f.object( "flattenedLevel1" ) ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( "Cannot use 'projection:object' on field 'flattenedLevel1'.", + "Some object field features require a nested structure;" + + " try setting the field structure to 'NESTED' and reindexing all your data" ); + } + + @Test + public void multiValuedObjectField_singleValuedObjectProjection() { + SearchProjectionFactory f = index.createScope().projection(); + assertThatThrownBy( () -> f.object( "level1" ) + .from( f.field( "level1.field1" ) ) + .asList() + // A call to .multi() is missing here + .toProjection() + ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid cardinality for projection on field 'level1'", + "the projection is single-valued, but this field is multi-valued", + "Make sure to call '.multi()' when you create the projection" + ); + } + + @Test + public void singleValuedObjectField_effectivelyMultiValuedInContext() { + assertThatThrownBy( () -> index.query() + .select( f -> f.object( "level1WithSingleValuedLevel2.level2" ) + .from( f.field( "level1WithSingleValuedLevel2.level2.field1" ) ) + .asList() + // A call to .multi() is missing here + ) + .where( f -> f.matchAll() ) + .toQuery() ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "Invalid cardinality for projection on field 'level1WithSingleValuedLevel2.level2'", + "the projection is single-valued, but this field is effectively multi-valued in this context", + "because parent object field 'level1WithSingleValuedLevel2' is multi-valued", + "call '.multi()' when you create the projection on field 'level1WithSingleValuedLevel2.level2'", + "or wrap that projection in an object projection like this:" + + " 'f.object(\"level1WithSingleValuedLevel2\").from().as(...).multi()'." + ); + } + + private static class IndexBinding { + final Level1ObjectBinding level1; + final Level1ObjectBinding flattenedLevel1; + final Level1ObjectBindingWithSingleValuedLevel2 level1WithSingleValuedLevel2; + + IndexBinding(IndexSchemaElement root) { + IndexSchemaObjectField nestedObjectField = root.objectField( "level1", ObjectStructure.NESTED ) + .multiValued(); + level1 = new Level1ObjectBinding( nestedObjectField ); + IndexSchemaObjectField flattenedObjectField = root.objectField( "flattenedLevel1", ObjectStructure.FLATTENED ) + .multiValued(); + flattenedLevel1 = new Level1ObjectBinding( flattenedObjectField ); + IndexSchemaObjectField otherNestedObjectField = root.objectField( "level1WithSingleValuedLevel2", ObjectStructure.NESTED ) + .multiValued(); + level1WithSingleValuedLevel2 = new Level1ObjectBindingWithSingleValuedLevel2( otherNestedObjectField ); + } + } + + private static class ObjectBinding { + final IndexObjectFieldReference self; + final IndexFieldReference field1; + final IndexFieldReference field2; + + ObjectBinding(IndexSchemaObjectField objectField) { + self = objectField.toReference(); + field1 = objectField.field( "field1", f -> f.asString().projectable( Projectable.YES ) ) + .toReference(); + field2 = objectField.field( "field2", f -> f.asString().projectable( Projectable.YES ) ) + .multiValued().toReference(); + } + } + + private static class Level1ObjectBinding extends ObjectBinding { + final ObjectBinding level2; + + Level1ObjectBinding(IndexSchemaObjectField objectField) { + super( objectField ); + IndexSchemaObjectField nestedObjectField = objectField.objectField( "level2", ObjectStructure.NESTED ) + .multiValued(); + level2 = new ObjectBinding( nestedObjectField ); + } + } + + private static class Level1ObjectBindingWithSingleValuedLevel2 extends ObjectBinding { + final ObjectBinding level2; + + Level1ObjectBindingWithSingleValuedLevel2(IndexSchemaObjectField objectField) { + super( objectField ); + IndexSchemaObjectField nestedObjectField = objectField.objectField( "level2", ObjectStructure.NESTED ); + level2 = new ObjectBinding( nestedObjectField ); + } + } +} diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/AnalyzedStringFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/AnalyzedStringFieldTypeDescriptor.java index 80e334bb8e4..593ed59c933 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/AnalyzedStringFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/AnalyzedStringFieldTypeDescriptor.java @@ -106,6 +106,11 @@ protected List createNonMatchingValues() { ); } + @Override + public String valueFromInteger(int integer) { + return "string_" + integer; + } + @Override public boolean isFieldSortSupported() { return false; diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/BigDecimalFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/BigDecimalFieldTypeDescriptor.java index e68bdff27dd..e1d5380e072 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/BigDecimalFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/BigDecimalFieldTypeDescriptor.java @@ -108,6 +108,11 @@ protected List createNonMatchingValues() { ); } + @Override + public BigDecimal valueFromInteger(int integer) { + return new BigDecimal( BigInteger.valueOf( integer ), 2 ); + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/BigIntegerFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/BigIntegerFieldTypeDescriptor.java index 30079ae8bd9..358153384dd 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/BigIntegerFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/BigIntegerFieldTypeDescriptor.java @@ -110,6 +110,11 @@ protected List createNonMatchingValues() { ); } + @Override + public BigInteger valueFromInteger(int integer) { + return BigInteger.valueOf( integer ); + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/BooleanFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/BooleanFieldTypeDescriptor.java index 09b8d673b2c..6edfb4066d7 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/BooleanFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/BooleanFieldTypeDescriptor.java @@ -75,6 +75,11 @@ protected List createNonMatchingValues() { return Collections.emptyList(); } + @Override + public Boolean valueFromInteger(int integer) { + return integer % 2 == 0; + } + @Override public boolean isFieldSortSupported() { return false; diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/ByteFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/ByteFieldTypeDescriptor.java index c05163b5d97..3a851f23ae9 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/ByteFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/ByteFieldTypeDescriptor.java @@ -84,6 +84,11 @@ protected List createNonMatchingValues() { ); } + @Override + public Byte valueFromInteger(int integer) { + return (byte) integer; + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/DoubleFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/DoubleFieldTypeDescriptor.java index 5d0773bb304..68da3749be4 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/DoubleFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/DoubleFieldTypeDescriptor.java @@ -83,6 +83,11 @@ protected List createNonMatchingValues() { ); } + @Override + public Double valueFromInteger(int integer) { + return (double) integer; + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/FieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/FieldTypeDescriptor.java index 330debf76b4..1951def644a 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/FieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/FieldTypeDescriptor.java @@ -149,6 +149,8 @@ public final List getNonMatchingValues() { protected abstract List createNonMatchingValues(); + public abstract F valueFromInteger(int integer); + public boolean isFieldSortSupported() { // Assume supported by default: this way, we'll get test failures if we forget to override this method. return true; diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/FloatFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/FloatFieldTypeDescriptor.java index 6d04206ba8c..d64f155bb64 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/FloatFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/FloatFieldTypeDescriptor.java @@ -83,6 +83,11 @@ protected List createNonMatchingValues() { ); } + @Override + public Float valueFromInteger(int integer) { + return (float) integer; + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/GeoPointFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/GeoPointFieldTypeDescriptor.java index 228b302df92..908d641c948 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/GeoPointFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/GeoPointFieldTypeDescriptor.java @@ -75,6 +75,11 @@ protected List createNonMatchingValues() { ); } + @Override + public GeoPoint valueFromInteger(int integer) { + return GeoPoint.of( 0, integer ); + } + @Override public boolean isFieldSortSupported() { return false; diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/InstantFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/InstantFieldTypeDescriptor.java index bce03ff8ae1..38c7422e2ff 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/InstantFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/InstantFieldTypeDescriptor.java @@ -89,6 +89,12 @@ protected List createNonMatchingValues() { return new ArrayList<>( values ); } + @Override + public Instant valueFromInteger(int integer) { + return LocalDateTimeFieldTypeDescriptor.INSTANCE.valueFromInteger( integer ) + .atOffset( ZoneOffset.UTC ).toInstant(); + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/IntegerFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/IntegerFieldTypeDescriptor.java index fefc408b398..b595bc55b61 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/IntegerFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/IntegerFieldTypeDescriptor.java @@ -77,6 +77,11 @@ protected List createNonMatchingValues() { ); } + @Override + public Integer valueFromInteger(int integer) { + return integer; + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/KeywordStringFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/KeywordStringFieldTypeDescriptor.java index 7166b151c70..a0e70b15b5f 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/KeywordStringFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/KeywordStringFieldTypeDescriptor.java @@ -91,6 +91,11 @@ protected List createNonMatchingValues() { ); } + @Override + public String valueFromInteger(int integer) { + return "string_" + integer; + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LocalDateFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LocalDateFieldTypeDescriptor.java index 67cf43553f8..f51bcc0013c 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LocalDateFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LocalDateFieldTypeDescriptor.java @@ -88,6 +88,11 @@ protected List createNonMatchingValues() { ); } + @Override + public LocalDate valueFromInteger(int integer) { + return LocalDate.of( 2017, 11, 1 ).plus( integer, ChronoUnit.DAYS ); + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LocalDateTimeFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LocalDateTimeFieldTypeDescriptor.java index 76000ba1b4f..0edd834ef13 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LocalDateTimeFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LocalDateTimeFieldTypeDescriptor.java @@ -103,6 +103,12 @@ protected List createNonMatchingValues() { ); } + @Override + public LocalDateTime valueFromInteger(int integer) { + return LocalDateTime.of( 2017, 11, 1, 0, 0, 0 ) + .plus( integer, ChronoUnit.SECONDS ); + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LocalTimeFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LocalTimeFieldTypeDescriptor.java index 9e66cf431fe..3e2e2fd0ea6 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LocalTimeFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LocalTimeFieldTypeDescriptor.java @@ -87,6 +87,12 @@ protected List createNonMatchingValues() { ); } + @Override + public LocalTime valueFromInteger(int integer) { + return LocalTime.of( 0, 0, 0 ) + .plus( integer, ChronoUnit.SECONDS ); + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LongFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LongFieldTypeDescriptor.java index 22b213816ca..b4a45e4d337 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LongFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/LongFieldTypeDescriptor.java @@ -77,6 +77,11 @@ protected List createNonMatchingValues() { ); } + @Override + public Long valueFromInteger(int integer) { + return (long) integer; + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/MonthDayFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/MonthDayFieldTypeDescriptor.java index 46dae2fcbe4..c8b985f662b 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/MonthDayFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/MonthDayFieldTypeDescriptor.java @@ -99,6 +99,11 @@ protected List createNonMatchingValues() { return values; } + @Override + public MonthDay valueFromInteger(int integer) { + return MonthDay.of( Month.JANUARY, integer + 1 ); + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/NormalizedStringFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/NormalizedStringFieldTypeDescriptor.java index 7b41dca45f2..907cb12b659 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/NormalizedStringFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/NormalizedStringFieldTypeDescriptor.java @@ -105,6 +105,11 @@ protected List createNonMatchingValues() { ); } + @Override + public String valueFromInteger(int integer) { + return "string_" + integer; + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/OffsetDateTimeFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/OffsetDateTimeFieldTypeDescriptor.java index 9442ea408ef..90df99c32b5 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/OffsetDateTimeFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/OffsetDateTimeFieldTypeDescriptor.java @@ -126,6 +126,11 @@ List createIndexableOffsetList() { ); } + @Override + public OffsetDateTime valueFromInteger(int integer) { + return LocalDateTimeFieldTypeDescriptor.INSTANCE.valueFromInteger( integer ).atOffset( ZoneOffset.ofHours( 2 ) ); + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/OffsetTimeFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/OffsetTimeFieldTypeDescriptor.java index 2f01fdb36fb..d874cdcc11d 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/OffsetTimeFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/OffsetTimeFieldTypeDescriptor.java @@ -106,6 +106,11 @@ protected List createNonMatchingValues() { ); } + @Override + public OffsetTime valueFromInteger(int integer) { + return LocalTimeFieldTypeDescriptor.INSTANCE.valueFromInteger( integer ).atOffset( ZoneOffset.ofHours( 2 ) ); + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/ShortFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/ShortFieldTypeDescriptor.java index f5e610a50ea..910c95f9826 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/ShortFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/ShortFieldTypeDescriptor.java @@ -83,6 +83,11 @@ protected List createNonMatchingValues() { ); } + @Override + public Short valueFromInteger(int integer) { + return (short) integer; + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/YearFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/YearFieldTypeDescriptor.java index c711a020ddc..0df6437199e 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/YearFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/YearFieldTypeDescriptor.java @@ -91,6 +91,11 @@ protected List createNonMatchingValues() { ); } + @Override + public Year valueFromInteger(int integer) { + return Year.of( 2000 + integer ); + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/YearMonthFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/YearMonthFieldTypeDescriptor.java index 12c57ae4dce..8dc8642092a 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/YearMonthFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/YearMonthFieldTypeDescriptor.java @@ -88,6 +88,11 @@ protected List createNonMatchingValues() { return values; } + @Override + public YearMonth valueFromInteger(int integer) { + return YearMonth.of( 2000 + integer, Month.SEPTEMBER ); + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/ZonedDateTimeFieldTypeDescriptor.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/ZonedDateTimeFieldTypeDescriptor.java index d70d003a4ab..260cab95785 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/ZonedDateTimeFieldTypeDescriptor.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/ZonedDateTimeFieldTypeDescriptor.java @@ -158,6 +158,12 @@ private List createIndexableZoneIdList() { ); } + @Override + public ZonedDateTime valueFromInteger(int integer) { + return LocalDateTimeFieldTypeDescriptor.INSTANCE.valueFromInteger( integer ) + .atZone( ZoneId.of( "Europe/Paris" ) ); + } + @Override public Optional> getIndexNullAsMatchPredicateExpectations() { return Optional.of( new IndexNullAsMatchPredicateExpectactions<>( diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/values/IndexableGeoPointWithDistanceFromCenterValues.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/values/IndexableGeoPointWithDistanceFromCenterValues.java index 763dcfae7c4..f2322b6686b 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/values/IndexableGeoPointWithDistanceFromCenterValues.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/types/values/IndexableGeoPointWithDistanceFromCenterValues.java @@ -46,14 +46,29 @@ public List> getMultiDistancesFromCenterPoint2() { @Override protected List createSingle() { return asList( - CENTER_POINT_1, // ~0km / ~1km - CENTER_POINT_2, // ~1km / ~0km - GeoPoint.of( 46.059852, 3.978235 ), // ~2km / ~2km - GeoPoint.of( 46.039763, 3.914977 ), // ~5km / ~4km - GeoPoint.of( 46.000833, 3.931265 ), // ~5.5km / ~5km - GeoPoint.of( 46.094712, 4.044507 ), // ~8km / ~9km - GeoPoint.of( 46.018378, 4.196792 ), // ~14km / ~13km - GeoPoint.of( 46.123025, 3.845305 ) // ~17km / ~18km + CENTER_POINT_1, // ~ 0km / ~ 1km + CENTER_POINT_2, // ~ 1km / ~ 0km + GeoPoint.of( 46.059852, 3.978235 ), // ~ 2km / ~ 3km + GeoPoint.of( 46.039763, 3.914977 ), // ~ 5km / ~ 4km + GeoPoint.of( 46.000833, 3.931265 ), // ~ 6km / ~ 5km + GeoPoint.of( 46.094712, 4.044507 ), // ~ 8km / ~ 9km + GeoPoint.of( 46.018378, 4.196792 ), // ~17km / ~18km + GeoPoint.of( 46.123025, 3.845305 ), // ~14km / ~13km + GeoPoint.of( 46.018378, 4.226792 ), // ~19km / ~20km + GeoPoint.of( 46.018378, 4.286792 ), // ~24km / ~25km + GeoPoint.of( 46.018378, 4.326792 ), // ~27km / ~28km + GeoPoint.of( 46.018378, 4.386792 ), // ~32km / ~33km + GeoPoint.of( 46.018378, 4.446792 ), // ~36km / ~37km + GeoPoint.of( 46.018378, 4.506792 ), // ~41km / ~42km + GeoPoint.of( 46.018378, 4.566792 ), // ~45km / ~47km + GeoPoint.of( 46.018378, 4.626792 ), // ~50km / ~51km + GeoPoint.of( 46.018378, 4.686792 ), // ~55km / ~56km + GeoPoint.of( 46.018378, 4.746792 ), // ~59km / ~60km + GeoPoint.of( 46.018378, 4.806792 ), // ~64km / ~65km + GeoPoint.of( 46.018378, 4.866792 ), // ~69km / ~70km + GeoPoint.of( 46.018378, 4.926792 ), // ~73km / ~74km + GeoPoint.of( 46.018378, 4.986792 ), // ~78km / ~79km + GeoPoint.of( 46.018378, 5.046792 ) // ~83km / ~84km ); } @@ -62,11 +77,26 @@ private List createDistancesFromCenterPoint1() { 0.0, 1_073.8, 2_355.1, - 4_909.5, + 4_909.6, 5_571.5, - 8_044.3, - 13_914.5, - 16_998.3 + 8_044.4, + 16_998.3, + 13_914.6, + 19_296.4, + 23_902.9, + 26_978.8, + 31_597.1, + 36_218.9, + 40_843.1, + 45_468.8, + 50_095.8, + 54_723.6, + 59_352.1, + 63_981.1, + 68_610.5, + 73_240.2, + 77_870.2, + 82_500.5 ); } @@ -78,8 +108,23 @@ private List createDistancesFromCenterPoint2() { 3_836.2, 4_935.5, 8_761.8, + 18_063.5, 13_141.2, - 18_063.5 + 20_363.5, + 24_972.4, + 28_049.2, + 32_668.4, + 37_290.9, + 41_915.5, + 46_541.6, + 51_168.7, + 55_796.7, + 60_425.3, + 65_054.4, + 69_683.9, + 74_313.7, + 78_943.8, + 83_574.0 ); } diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/util/TckBackendFeatures.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/util/TckBackendFeatures.java index 22974bc2cbe..7837295a0eb 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/util/TckBackendFeatures.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/util/TckBackendFeatures.java @@ -8,7 +8,7 @@ import org.hibernate.search.engine.search.common.SortMode; -public class TckBackendFeatures { +public abstract class TckBackendFeatures { public boolean geoPointIndexNullAs() { return true; @@ -106,4 +106,7 @@ public boolean supportsFieldSortWhenFieldMissingInSomeTargetIndexes(Class fie public boolean supportsFieldSortWhenNestedFieldMissingInSomeTargetIndexes() { return true; } + + public abstract boolean reliesOnNestedDocumentsForObjectProjection(); + } diff --git a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubCompositeProjection.java b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubCompositeProjection.java index 459359b2aa2..bee30ea2b19 100644 --- a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubCompositeProjection.java +++ b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubCompositeProjection.java @@ -13,16 +13,18 @@ import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.spi.CompositeProjectionBuilder; +import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; +import org.hibernate.search.util.common.AssertionFailure; class StubCompositeProjection implements StubSearchProjection { - private final ProjectionCompositor compositor; private final StubSearchProjection[] inners; + private final ProjectionCompositor compositor; - private StubCompositeProjection(Builder builder) { - this.compositor = builder.compositor; - this.inners = builder.inners; + private StubCompositeProjection(StubSearchProjection[] inners, ProjectionCompositor compositor) { + this.inners = inners; + this.compositor = compositor; } @Override @@ -61,19 +63,24 @@ public V transform(LoadingResult loadingResult, Object extractedData, Stub return compositor.finish( transformedData ); } - static class Builder implements CompositeProjectionBuilder { - - private final ProjectionCompositor compositor; - private final StubSearchProjection[] inners; + static class Builder implements CompositeProjectionBuilder { - Builder(ProjectionCompositor compositor, StubSearchProjection ... inners) { - this.compositor = compositor; - this.inners = inners; + Builder() { } @Override - public SearchProjection build() { - return new StubCompositeProjection<>( this ); + @SuppressWarnings("unchecked") + public SearchProjection

build(SearchProjection[] inners, ProjectionCompositor compositor, + ProjectionAccumulator.Provider accumulatorProvider) { + if ( !accumulatorProvider.isSingleValued() ) { + throw new AssertionFailure( "Multi-valued projections are not supported in the stub backend." ); + } + StubSearchProjection[] typedInners = + new StubSearchProjection[ inners.length ]; + for ( int i = 0; i < inners.length; i++ ) { + typedInners[i] = StubSearchProjection.from( inners[i] ); + } + return (SearchProjection

) new StubCompositeProjection<>( typedInners, compositor ); } } } diff --git a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubFieldProjection.java b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubFieldProjection.java index 6504690c4e0..18e5292fe5b 100644 --- a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubFieldProjection.java +++ b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubFieldProjection.java @@ -13,7 +13,6 @@ import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.projection.spi.FieldProjectionBuilder; import org.hibernate.search.engine.search.projection.spi.ProjectionAccumulator; -import org.hibernate.search.engine.search.projection.spi.SingleValuedProjectionAccumulator; import org.hibernate.search.util.common.AssertionFailure; import org.hibernate.search.util.impl.integrationtest.common.stub.backend.search.common.impl.StubSearchIndexNodeContext; import org.hibernate.search.util.impl.integrationtest.common.stub.backend.search.common.impl.StubSearchIndexValueFieldContext; @@ -82,7 +81,7 @@ public SearchProjection build() { @Override @SuppressWarnings("unchecked") public

SearchProjection

build(ProjectionAccumulator.Provider accumulatorProvider) { - if ( accumulatorProvider == SingleValuedProjectionAccumulator.provider() ) { + if ( accumulatorProvider.isSingleValued() ) { return (SearchProjection

) build(); } else { diff --git a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubSearchProjection.java b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubSearchProjection.java index 160f5b4baf5..9bd77813d00 100644 --- a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubSearchProjection.java +++ b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubSearchProjection.java @@ -9,6 +9,7 @@ import org.hibernate.search.engine.search.projection.SearchProjection; import org.hibernate.search.engine.search.loading.spi.LoadingResult; import org.hibernate.search.engine.search.loading.spi.ProjectionHitMapper; +import org.hibernate.search.util.common.AssertionFailure; public interface StubSearchProjection

extends SearchProjection

{ @@ -16,4 +17,11 @@ Object extract(ProjectionHitMapper projectionHitMapper, Object projectionF StubSearchProjectionContext context); P transform(LoadingResult loadingResult, Object extractedData, StubSearchProjectionContext context); + + static StubSearchProjection from(SearchProjection projection) { + if ( !( projection instanceof StubSearchProjection ) ) { + throw new AssertionFailure( "Projection " + projection + " must be a StubSearchProjection" ); + } + return (StubSearchProjection) projection; + } } diff --git a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubSearchProjectionBuilderFactory.java b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubSearchProjectionBuilderFactory.java index 91b3ba8c0bb..1fa7c7b86c7 100644 --- a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubSearchProjectionBuilderFactory.java +++ b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/projection/impl/StubSearchProjectionBuilderFactory.java @@ -6,10 +6,6 @@ */ package org.hibernate.search.util.impl.integrationtest.common.stub.backend.search.projection.impl; -import java.util.List; -import java.util.function.BiFunction; -import java.util.function.Function; - import org.hibernate.search.engine.backend.common.DocumentReference; import org.hibernate.search.engine.search.common.spi.SearchIndexIdentifierContext; import org.hibernate.search.engine.search.projection.SearchProjection; @@ -18,11 +14,8 @@ import org.hibernate.search.engine.search.projection.spi.EntityProjectionBuilder; import org.hibernate.search.engine.search.projection.spi.EntityReferenceProjectionBuilder; import org.hibernate.search.engine.search.projection.spi.IdProjectionBuilder; -import org.hibernate.search.engine.search.projection.spi.ProjectionCompositor; import org.hibernate.search.engine.search.projection.spi.ScoreProjectionBuilder; import org.hibernate.search.engine.search.projection.spi.SearchProjectionBuilderFactory; -import org.hibernate.search.util.common.AssertionFailure; -import org.hibernate.search.util.common.function.TriFunction; import org.hibernate.search.util.impl.integrationtest.common.stub.backend.search.common.impl.StubSearchIndexScope; public class StubSearchProjectionBuilderFactory implements SearchProjectionBuilderFactory { @@ -71,42 +64,8 @@ public SearchProjection build() { } @Override - public CompositeProjectionBuilder composite(Function, V> transformer, - SearchProjection... projections) { - StubSearchProjection[] typedProjections = new StubSearchProjection[ projections.length ]; - for ( int i = 0; i < projections.length; i++ ) { - typedProjections[i] = toImplementation( projections[i] ); - } - return new StubCompositeProjection.Builder<>( ProjectionCompositor.fromList( projections.length, transformer ), - typedProjections ); - } - - @Override - public CompositeProjectionBuilder composite(Function transformer, - SearchProjection projection) { - return new StubCompositeProjection.Builder<>( ProjectionCompositor.from( transformer ), - toImplementation( projection ) ); - } - - @Override - public CompositeProjectionBuilder composite(BiFunction transformer, - SearchProjection projection1, SearchProjection projection2) { - return new StubCompositeProjection.Builder<>( ProjectionCompositor.from( transformer ), - toImplementation( projection1 ), toImplementation( projection2 ) ); - } - - @Override - public CompositeProjectionBuilder composite(TriFunction transformer, - SearchProjection projection1, SearchProjection projection2, SearchProjection projection3) { - return new StubCompositeProjection.Builder<>( ProjectionCompositor.from( transformer ), - toImplementation( projection1 ), toImplementation( projection2 ), toImplementation( projection3 ) ); - } - - private StubSearchProjection toImplementation(SearchProjection projection) { - if ( !( projection instanceof StubSearchProjection ) ) { - throw new AssertionFailure( "Projection " + projection + " must be a StubSearchProjection" ); - } - return (StubSearchProjection) projection; + public CompositeProjectionBuilder composite() { + return new StubCompositeProjection.Builder(); } }