From 55d1c050cc57fdadbf5027211816f70535220e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Tue, 30 Aug 2022 13:16:09 +0200 Subject: [PATCH] HSEARCH-4591 Allow object projections on single-valued, flattened object fields with the Lucene backend --- .../backend/lucene/logging/impl/Log.java | 6 ++ .../impl/LuceneObjectProjection.java | 58 +++++++++++++- .../impl/LuceneIndexCompositeNodeType.java | 2 +- .../document/model/spi/AbstractIndexNode.java | 6 ++ .../search/engine/logging/impl/Log.java | 10 +-- ...tractMultiIndexSearchIndexNodeContext.java | 5 ++ .../common/spi/SearchIndexNodeContext.java | 3 + ...SearchIndexSchemaElementContextHelper.java | 15 ++-- .../util/ElasticsearchTckBackendFeatures.java | 2 +- .../util/LuceneTckBackendFeatures.java | 11 ++- .../backend/tck/ObjectStructureIT.java | 8 +- .../tck/dynamic/ObjectFieldTemplateIT.java | 2 +- ...bstractProjectionInObjectProjectionIT.java | 76 ++++++++++--------- .../projection/DistanceProjectionBaseIT.java | 14 ++-- .../projection/FieldProjectionBaseIT.java | 13 ++-- .../projection/ObjectProjectionBaseIT.java | 6 +- .../ObjectProjectionSpecificsIT.java | 16 ++-- .../testsupport/util/TckBackendFeatures.java | 7 +- .../projection/ProjectionConversionTest.java | 2 +- 19 files changed, 181 insertions(+), 81 deletions(-) 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 0464cdd003a..2da50c9959a 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 @@ -614,4 +614,10 @@ SearchException invalidSingleValuedProjectionOnValueFieldInMultiValuedObjectFiel + " The document was probably indexed with a different configuration: full reindexing is necessary.") SearchException unexpectedMappedTypeNameForByMappedTypeProjection(String typeName, Set expectedTypeNames); + @Message(value = "This multi-valued field has a 'FLATTENED' structure," + + " which means the structure of objects is not preserved upon indexing," + + " making object projections impossible." + + " Try setting the field structure to 'NESTED' and reindexing all your data.") + String missingSupportHintForObjectProjectionOnMultiValuedFlattenedObjectNode(); + } 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 index dce2b8c6dfb..15baeec62fc 100644 --- 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 @@ -14,13 +14,21 @@ 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.backend.lucene.search.predicate.impl.LuceneSearchPredicate; +import org.hibernate.search.backend.lucene.search.predicate.impl.PredicateRequestContext; import org.hibernate.search.engine.search.loading.spi.LoadingResult; +import org.hibernate.search.engine.search.predicate.spi.PredicateTypeKeys; 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.engine.search.projection.spi.ProjectionTypeKeys; +import org.hibernate.search.util.common.SearchException; import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.join.QueryBitSetProducer; +import org.apache.lucene.util.BitSet; /** * A projection that yields one composite value per object in a given object field. @@ -35,6 +43,8 @@ public class LuceneObjectProjection extends AbstractLuceneProjection

{ private final String absoluteFieldPath; + private final boolean nested; + private final Query filter; private final String nestedDocumentPath; private final String requiredContextAbsoluteFieldPath; private final LuceneSearchProjection[] inners; @@ -45,6 +55,8 @@ public LuceneObjectProjection(Builder builder, LuceneSearchProjection[] inner ProjectionCompositor compositor, ProjectionAccumulator.Provider accumulatorProvider) { super( builder.scope ); this.absoluteFieldPath = builder.objectField.absolutePath(); + this.nested = builder.objectField.type().nested(); + this.filter = builder.filter; this.nestedDocumentPath = builder.objectField.nestedDocumentPath(); this.requiredContextAbsoluteFieldPath = accumulatorProvider.isSingleValued() ? builder.objectField.closestMultiValuedParentAbsolutePath() : null; @@ -64,7 +76,14 @@ public String toString() { @Override public Extractor request(ProjectionRequestContext context) { - ProjectionRequestContext innerContext = context.forField( absoluteFieldPath ); + ProjectionRequestContext innerContext; + if ( nested ) { + innerContext = context.forField( absoluteFieldPath ); + } + else { + context.checkValidField( absoluteFieldPath ); + innerContext = context; + } if ( requiredContextAbsoluteFieldPath != null && !requiredContextAbsoluteFieldPath.equals( context.absoluteCurrentFieldPath() ) ) { throw log.invalidSingleValuedProjectionOnValueFieldInMultiValuedObjectField( @@ -113,15 +132,23 @@ public Values values(ProjectionExtractContext context) { private class ObjectFieldValues extends AbstractNestingAwareAccumulatingValues { private final Values[] inners; + private final QueryBitSetProducer filterBitSetProducer; + + private BitSet filterMatchedBitSet; private ObjectFieldValues(TopDocsDataCollectorExecutionContext context, Values[] inners) { super( contextAbsoluteFieldPath, nestedDocumentPath, ObjectFieldExtractor.this.accumulator, context ); this.inners = inners; + this.filterBitSetProducer = filter == null ? null : new QueryBitSetProducer( filter ); + } @Override public void context(LeafReaderContext context) throws IOException { super.context( context ); + if ( filterBitSetProducer != null ) { + filterMatchedBitSet = filterBitSetProducer.getBitSet( context ); + } for ( Values inner : inners ) { inner.context( context ); } @@ -129,6 +156,11 @@ public void context(LeafReaderContext context) throws IOException { @Override protected A accumulate(A accumulated, int docId) throws IOException { + if ( filterBitSetProducer != null && ( filterMatchedBitSet == null || !filterMatchedBitSet.get( docId ) ) ) { + // The object didn't match the given filter: act as if it didn't exist. + // Note that filters are used to detect flattened objects that were null upon indexing. + return accumulated; + } E components = compositor.createInitial(); for ( int i = 0; i < inners.length; i++ ) { Object extractedDataForInner = inners[i].get( docId ); @@ -160,7 +192,25 @@ public static class Factory extends AbstractLuceneCompositeNodeSearchQueryElementFactory { @Override public Builder create(LuceneSearchIndexScope scope, LuceneSearchIndexCompositeNodeContext node) { - return new Builder( scope, node ); + Query filter = null; + if ( !node.type().nested() ) { + if ( node.multiValued() ) { + throw node.cannotUseQueryElement( ProjectionTypeKeys.OBJECT, + log.missingSupportHintForObjectProjectionOnMultiValuedFlattenedObjectNode(), null ); + } + try { + filter = LuceneSearchPredicate.from( scope, node.queryElement( PredicateTypeKeys.EXISTS, scope ).build() ) + .toQuery( PredicateRequestContext.root() ); + } + catch (SearchException e) { + throw node.cannotUseQueryElement( ProjectionTypeKeys.OBJECT, e.getMessage(), e ); + } + } + if ( node.multiValued() && !node.type().nested() ) { + throw node.cannotUseQueryElement( ProjectionTypeKeys.OBJECT, + log.missingSupportHintForObjectProjectionOnMultiValuedFlattenedObjectNode(), null ); + } + return new Builder( scope, node, filter ); } } @@ -168,10 +218,12 @@ static class Builder implements CompositeProjectionBuilder { private final LuceneSearchIndexScope scope; private final LuceneSearchIndexCompositeNodeContext objectField; + private final Query filter; - Builder(LuceneSearchIndexScope scope, LuceneSearchIndexCompositeNodeContext objectField) { + Builder(LuceneSearchIndexScope scope, LuceneSearchIndexCompositeNodeContext objectField, Query filter) { this.scope = scope; this.objectField = objectField; + this.filter = filter; } @Override 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 3068d25f45b..593c57e640b 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 @@ -34,9 +34,9 @@ public static class Builder public Builder(ObjectStructure objectStructure) { super( objectStructure ); queryElementFactory( PredicateTypeKeys.EXISTS, LuceneObjectExistsPredicate.Factory.INSTANCE ); + queryElementFactory( ProjectionTypeKeys.OBJECT, new LuceneObjectProjection.Factory() ); if ( ObjectStructure.NESTED.equals( objectStructure ) ) { queryElementFactory( PredicateTypeKeys.NESTED, LuceneNestedPredicate.Factory.INSTANCE ); - queryElementFactory( ProjectionTypeKeys.OBJECT, new LuceneObjectProjection.Factory() ); } } diff --git a/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/AbstractIndexNode.java b/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/AbstractIndexNode.java index a555f6a4b53..48e23d1d2d2 100644 --- a/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/AbstractIndexNode.java +++ b/engine/src/main/java/org/hibernate/search/engine/backend/document/model/spi/AbstractIndexNode.java @@ -11,6 +11,7 @@ import org.hibernate.search.engine.search.common.spi.SearchIndexScope; import org.hibernate.search.engine.search.common.spi.SearchQueryElementFactory; import org.hibernate.search.engine.search.common.spi.SearchQueryElementTypeKey; +import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.common.reporting.EventContext; public abstract class AbstractIndexNode< @@ -42,6 +43,11 @@ public final T queryElement(SearchQueryElementTypeKey key, SC scope) { return helper().queryElement( key, factory, scope, self() ); } + @Override + public SearchException cannotUseQueryElement(SearchQueryElementTypeKey key, String hint, Exception causeOrNull) { + return helper().cannotUseQueryElement( key, self(), hint, causeOrNull ); + } + abstract SearchIndexSchemaElementContextHelper helper(); } 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 081b24ffc10..628ce09ec6b 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 @@ -468,10 +468,10 @@ SearchException inconsistentSupportForQueryElement(SearchQueryElementTypeKey value = "Attribute '%1$s' differs: '%2$s' vs. '%3$s'.") SearchException differentAttribute(String attributeName, Object component1, Object component2); - @Message(id = ID_OFFSET + 104, value = "Cannot use '%2$s' on %1$s. %3$s") + @Message(id = ID_OFFSET + 104, value = "Cannot use '%2$s' on %1$s: %3$s") SearchException cannotUseQueryElementForIndexNode( @FormatWith(EventContextNoPrefixFormatter.class) EventContext elementContext, - SearchQueryElementTypeKey key, String hint, @Param EventContext context); + SearchQueryElementTypeKey key, String hint, @Param EventContext context, @Cause Exception cause); @Message(value = "Make sure the field is marked as searchable/sortable/projectable/aggregable (whichever is relevant)." + " If it already is, then '%1$s' is not available for fields of this type.") @@ -482,12 +482,6 @@ SearchException cannotUseQueryElementForIndexNode( + " 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( - @FormatWith(EventContextNoPrefixFormatter.class) EventContext elementContext, - SearchQueryElementTypeKey key, String causeMessage, @Cause SearchException cause, - @Param EventContext elementContextAsParam); - @Message(id = ID_OFFSET + 106, value = "'%1$s' can be used in some of the targeted indexes, but not all of them. %2$s") SearchException partialSupportForQueryElement(SearchQueryElementTypeKey key, String hint); 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 ce322fdbb12..24c004d2ab6 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 @@ -112,6 +112,11 @@ public final T queryElement(SearchQueryElementTypeKey key, SC scope) { return helper().queryElement( key, factory, scope, self() ); } + @Override + public SearchException cannotUseQueryElement(SearchQueryElementTypeKey key, String hint, Exception causeOrNull) { + return helper().cannotUseQueryElement( key, self(), hint, causeOrNull ); + } + abstract SearchIndexSchemaElementContextHelper helper(); @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 913ca9efae6..f62ad726074 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 @@ -8,6 +8,7 @@ import java.util.List; +import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.common.reporting.EventContext; import org.hibernate.search.util.common.reporting.spi.EventContextProvider; @@ -59,4 +60,6 @@ default String nestedDocumentPath() { T queryElement(SearchQueryElementTypeKey key, SC searchContext); + SearchException cannotUseQueryElement(SearchQueryElementTypeKey key, String hint, Exception causeOrNull); + } 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 c92b04275c0..aa722a391ca 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 @@ -73,21 +73,24 @@ public String partialSupportHint() { } }; + public , N extends SearchIndexNodeContext> + SearchException cannotUseQueryElement(SearchQueryElementTypeKey key, N node, String hint, + Exception causeOrNull) { + throw log.cannotUseQueryElementForIndexNode( node.relativeEventContext(), key, + hint, node.eventContext(), causeOrNull ); + } + public , N extends SearchIndexNodeContext> T queryElement(SearchQueryElementTypeKey key, SearchQueryElementFactory factory, SC scope, N node) { if ( factory == null ) { - throw log.cannotUseQueryElementForIndexNode( node.relativeEventContext(), key, - missingSupportHint( key ), node.eventContext() - ); + throw cannotUseQueryElement( key, node, missingSupportHint( key ), null ); } try { return factory.create( scope, node ); } catch (SearchException e) { - throw log.cannotUseQueryElementForIndexElementBecauseCreationException( node.relativeEventContext(), - key, e.getMessage(), e, node.eventContext() - ); + throw cannotUseQueryElement( key, node, e.getMessage(), e ); } } 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 e6e18d200b8..c7e1be65662 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 @@ -211,7 +211,7 @@ public boolean supportsFieldSortWhenNestedFieldMissingInSomeTargetIndexes() { } @Override - public boolean reliesOnNestedDocumentsForObjectProjection() { + public boolean reliesOnNestedDocumentsForMultiValuedObjectProjection() { 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 06dd220a47e..ff72285d959 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 @@ -6,6 +6,7 @@ */ package org.hibernate.search.integrationtest.backend.lucene.testsupport.util; +import org.hibernate.search.engine.backend.types.ObjectStructure; import org.hibernate.search.integrationtest.backend.tck.testsupport.util.TckBackendFeatures; class LuceneTckBackendFeatures extends TckBackendFeatures { @@ -27,7 +28,15 @@ public boolean fieldsProjectableByDefault() { } @Override - public boolean reliesOnNestedDocumentsForObjectProjection() { + public boolean projectionPreservesEmptySingleValuedObject(ObjectStructure structure) { + // For single-valued, flattened object fields, + // we cannot distinguish between an empty object (non-null object, but no subfield carries a value) + // and an empty object. + return ObjectStructure.NESTED.equals( structure ); + } + + @Override + public boolean reliesOnNestedDocumentsForMultiValuedObjectProjection() { 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 4365043aca0..cbd75322961 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 @@ -170,9 +170,9 @@ public void search_error_nonNestedField() { scope.predicate().nested( "flattenedObject" ) ) .isInstanceOf( SearchException.class ) - .hasMessageContainingAll( "Cannot use 'predicate:nested' on field 'flattenedObject'.", - "Some object field features require a nested structure; " - + "try setting the field structure to 'NESTED' and reindexing all your data" ); + .hasMessageContainingAll( "Cannot use 'predicate:nested' on field 'flattenedObject'", + "Some object field features require a nested structure", + "try setting the field structure to 'NESTED' and reindexing all your data" ); } @Test @@ -183,7 +183,7 @@ public void search_error_nonObjectField() { scope.predicate().nested( "flattenedObject.string" ) ) .isInstanceOf( SearchException.class ) - .hasMessageContainingAll( "Cannot use 'predicate:nested' on field 'flattenedObject.string'.", + .hasMessageContainingAll( "Cannot use 'predicate:nested' on field 'flattenedObject.string'", "'predicate:nested' is not available for fields of this type" ); } 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 f6d401c7505..8a90fbb0e16 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 @@ -507,7 +507,7 @@ private void checkFlattened(String objectFieldPath) { .add( f.match().field( objectFieldPath + "." + LASTNAME_FIELD ) .matching( LASTNAME_1 ) ) ) ) - .hasMessageContainingAll( "Cannot use 'predicate:nested' on field '" + objectFieldPath + "'.", + .hasMessageContainingAll( "Cannot use 'predicate:nested' on field '" + objectFieldPath + "'", "Some object field features require a nested structure; " + "try setting the field structure to 'NESTED' and reindexing all your data" ); assertThatQuery( query( 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 index 1ed42135a8e..54821a96484 100644 --- 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 @@ -489,10 +489,14 @@ public void objectOnSingleValuedLevel1AndNoLevel2() { 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_SINGLE_EMPTY_OBJECT_DOCUMENT_ID, + TckConfiguration.get().getBackendFeatures() + .projectionPreservesEmptySingleValuedObject( dataSet.singleValuedObjectStructure ) + ? new ObjectDto<>( + null, + Collections.emptyList() ) + : null + ), hit( LEVEL1_NO_OBJECT_DOCUMENT_ID, null ), hit( LEVEL2_SINGLE_OBJECT_DOCUMENT_ID, null ), hit( LEVEL2_SINGLE_EMPTY_OBJECT_DOCUMENT_ID, null ), @@ -882,44 +886,44 @@ private IdAndObjectDto hit(String docIdConstant, T object) { } private String level1Path() { - return mainIndex.binding().level1( dataSet.structure ).absolutePath; + return mainIndex.binding().level1( dataSet.multiValuedObjectStructure ).absolutePath; } private String level1SingleValuedFieldPath() { - return mainIndex.binding().level1( dataSet.structure ) + return mainIndex.binding().level1( dataSet.multiValuedObjectStructure ) .singleValuedFieldAbsolutePath( dataSet.fieldType ); } private String level1MultiValuedFieldPath() { - return mainIndex.binding().level1( dataSet.structure ) + return mainIndex.binding().level1( dataSet.multiValuedObjectStructure ) .multiValuedFieldAbsolutePath( dataSet.fieldType ); } private String singleValuedLevel1Path() { - return mainIndex.binding().singleValuedLevel1( dataSet.structure ).absolutePath; + return mainIndex.binding().singleValuedLevel1( dataSet.singleValuedObjectStructure ).absolutePath; } private String singleValuedLevel1SingleValuedFieldPath() { - return mainIndex.binding().singleValuedLevel1( dataSet.structure ) + return mainIndex.binding().singleValuedLevel1( dataSet.singleValuedObjectStructure ) .singleValuedFieldAbsolutePath( dataSet.fieldType ); } private String singleValuedLevel1MultiValuedFieldPath() { - return mainIndex.binding().singleValuedLevel1( dataSet.structure ) + return mainIndex.binding().singleValuedLevel1( dataSet.singleValuedObjectStructure ) .multiValuedFieldAbsolutePath( dataSet.fieldType ); } private String level2Path() { - return mainIndex.binding().level1( dataSet.structure ).level2.absolutePath; + return mainIndex.binding().level1( dataSet.multiValuedObjectStructure ).level2.absolutePath; } private String level2SingleValuedFieldPath() { - return mainIndex.binding().level1( dataSet.structure ).level2 + return mainIndex.binding().level1( dataSet.multiValuedObjectStructure ).level2 .singleValuedFieldAbsolutePath( dataSet.fieldType ); } private String level2MultiValuedFieldPath() { - return mainIndex.binding().level1( dataSet.structure ).level2 + return mainIndex.binding().level1( dataSet.multiValuedObjectStructure ).level2 .multiValuedFieldAbsolutePath( dataSet.fieldType ); } @@ -929,11 +933,15 @@ private String level2MultiValuedFieldPath() { 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; + private final ObjectStructure singleValuedObjectStructure; + private final ObjectStructure multiValuedObjectStructure; + + public DataSet(V values, ObjectStructure singleValuedObjectStructure, + ObjectStructure multiValuedObjectStructure) { + super( values.fieldType.getUniqueName() + "/single=" + singleValuedObjectStructure.name() + + "/multi=" + multiValuedObjectStructure, values ); + this.singleValuedObjectStructure = singleValuedObjectStructure; + this.multiValuedObjectStructure = multiValuedObjectStructure; } public String docId(String docIdConstant) { @@ -956,9 +964,9 @@ public void contribute(SimpleMappedIndex mainIndex, BulkIndexer ma DocumentElement level2Object; DocumentElement singleValuedLevel1Object; - level1 = mainIndex.binding().level1( structure ); + level1 = mainIndex.binding().level1( multiValuedObjectStructure ); level2 = level1.level2; - singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( structure ); + singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( singleValuedObjectStructure ); document.addNullObject( level1.reference ); @@ -1003,9 +1011,9 @@ public void contribute(SimpleMappedIndex mainIndex, BulkIndexer ma DocumentElement level2Object; DocumentElement singleValuedLevel1Object; - level1 = mainIndex.binding().level1( structure ); + level1 = mainIndex.binding().level1( multiValuedObjectStructure ); level2 = level1.level2; - singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( structure ); + singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( singleValuedObjectStructure ); level1Object = document.addObject( level1.reference ); level1Object.addValue( level1.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 0 ) ); @@ -1026,9 +1034,9 @@ public void contribute(SimpleMappedIndex mainIndex, BulkIndexer ma DocumentElement level2Object; DocumentElement singleValuedLevel1Object; - level1 = mainIndex.binding().level1( structure ); + level1 = mainIndex.binding().level1( multiValuedObjectStructure ); level2 = level1.level2; - singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( structure ); + singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( singleValuedObjectStructure ); level1Object = document.addObject( level1.reference ); level1Object.addValue( level1.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 0 ) ); @@ -1052,8 +1060,8 @@ public void contribute(SimpleMappedIndex mainIndex, BulkIndexer ma Level1ObjectFieldBinding level1; Level1ObjectFieldBinding singleValuedLevel1; - level1 = mainIndex.binding().level1( structure ); - singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( structure ); + level1 = mainIndex.binding().level1( multiValuedObjectStructure ); + singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( singleValuedObjectStructure ); document.addObject( level1.reference ); document.addObject( singleValuedLevel1.reference ); @@ -1062,8 +1070,8 @@ public void contribute(SimpleMappedIndex mainIndex, BulkIndexer ma Level1ObjectFieldBinding level1; Level1ObjectFieldBinding singleValuedLevel1; - level1 = mainIndex.binding().level1( structure ); - singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( structure ); + level1 = mainIndex.binding().level1( multiValuedObjectStructure ); + singleValuedLevel1 = mainIndex.binding().singleValuedLevel1( singleValuedObjectStructure ); document.addNullObject( level1.reference ); document.addNullObject( singleValuedLevel1.reference ); @@ -1076,7 +1084,7 @@ public void contribute(SimpleMappedIndex mainIndex, BulkIndexer ma DocumentElement level1Object; DocumentElement level2Object; - level1 = mainIndex.binding().level1( structure ); + level1 = mainIndex.binding().level1( multiValuedObjectStructure ); level2 = level1.level2; level1Object = document.addObject( level1.reference ); @@ -1096,7 +1104,7 @@ public void contribute(SimpleMappedIndex mainIndex, BulkIndexer ma ObjectFieldBinding level2; DocumentElement level1Object; - level1 = mainIndex.binding().level1( structure ); + level1 = mainIndex.binding().level1( multiValuedObjectStructure ); level2 = level1.level2; level1Object = document.addObject( level1.reference ); @@ -1108,7 +1116,7 @@ public void contribute(SimpleMappedIndex mainIndex, BulkIndexer ma .add( docId( LEVEL2_NO_OBJECT_DOCUMENT_ID ), routingKey, document -> { Level1ObjectFieldBinding level1; - level1 = mainIndex.binding().level1( structure ); + level1 = mainIndex.binding().level1( multiValuedObjectStructure ); document.addObject( level1.reference ); @@ -1121,7 +1129,7 @@ public void contribute(SimpleMappedIndex mainIndex, BulkIndexer ma DocumentElement level1Object; DocumentElement level2Object; - level1 = missingLevel1SingleValuedFieldIndex.binding().level1( structure ); + level1 = missingLevel1SingleValuedFieldIndex.binding().level1( multiValuedObjectStructure ); level2 = level1.level2; level1Object = document.addObject( level1.reference ); @@ -1152,7 +1160,7 @@ public void contribute(SimpleMappedIndex mainIndex, BulkIndexer ma ObjectFieldBinding level1; DocumentElement level1Object; - level1 = missingLevel2Index.binding().level1( structure ); + level1 = missingLevel2Index.binding().level1( multiValuedObjectStructure ); level1Object = document.addObject( level1.reference ); level1Object.addValue( level1.singleValuedField.get( values.fieldType ).reference, values.fieldValue( 0 ) ); @@ -1170,7 +1178,7 @@ public void contribute(SimpleMappedIndex mainIndex, BulkIndexer ma DocumentElement level1Object; DocumentElement level2Object; - level1 = missingLevel2SingleValuedFieldIndex.binding().level1( structure ); + level1 = missingLevel2SingleValuedFieldIndex.binding().level1( multiValuedObjectStructure ); level2 = level1.level2; level1Object = document.addObject( level1.reference ); 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 index b8b6c8f650e..9ed93231472 100644 --- 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 @@ -80,11 +80,15 @@ public static class InObjectProjectionIT 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 ); + for ( ObjectStructure singleValuedObjectStructure : + new ObjectStructure[] { ObjectStructure.FLATTENED, ObjectStructure.NESTED } ) { + ObjectStructure multiValuedObjectStructure = + ObjectStructure.NESTED.equals( singleValuedObjectStructure ) + || TckConfiguration.get().getBackendFeatures().reliesOnNestedDocumentsForMultiValuedObjectProjection() + ? ObjectStructure.NESTED + : ObjectStructure.FLATTENED; + DataSet dataSet = new DataSet<>( testValues(), + singleValuedObjectStructure, multiValuedObjectStructure ); dataSets.add( dataSet ); parameters.add( new Object[] { dataSet } ); } 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 index faa23bc34a2..b2a5b192946 100644 --- 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 @@ -78,11 +78,14 @@ public static class InObjectProjectionIT 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 ); + for ( ObjectStructure singleValuedObjectStructure : + new ObjectStructure[] { ObjectStructure.FLATTENED, ObjectStructure.NESTED } ) { + ObjectStructure multiValuedObjectStructure = + ObjectStructure.NESTED.equals( singleValuedObjectStructure ) + || TckConfiguration.get().getBackendFeatures().reliesOnNestedDocumentsForMultiValuedObjectProjection() + ? ObjectStructure.NESTED + : ObjectStructure.FLATTENED; + DataSet dataSet = new DataSet<>( testValues( fieldType ), singleValuedObjectStructure, multiValuedObjectStructure ); dataSets.add( dataSet ); parameters.add( new Object[] { dataSet } ); } diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/ObjectProjectionBaseIT.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/ObjectProjectionBaseIT.java index a8c510b5a72..83334206251 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/ObjectProjectionBaseIT.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/ObjectProjectionBaseIT.java @@ -50,8 +50,8 @@ public void takariCpSuiteWorkaround() { // Workaround to get Takari-CPSuite to run this test. } - private static ObjectStructure requiredObjectStructure() { - return TckConfiguration.get().getBackendFeatures().reliesOnNestedDocumentsForObjectProjection() + private static ObjectStructure requiredObjectStructure(boolean multivalued) { + return multivalued && TckConfiguration.get().getBackendFeatures().reliesOnNestedDocumentsForMultiValuedObjectProjection() ? ObjectStructure.NESTED : ObjectStructure.DEFAULT; } @@ -100,7 +100,7 @@ CompositeBinding compositeForMulti() { private static class ObjectBinding extends AbstractCompositeProjectionFromAsIT.CompositeBinding { public static ObjectBinding create(IndexSchemaElement parent, String relativeName, boolean multiValued) { - IndexSchemaObjectField objectField = parent.objectField( relativeName, requiredObjectStructure() ); + IndexSchemaObjectField objectField = parent.objectField( relativeName, requiredObjectStructure( multiValued ) ); if ( multiValued ) { objectField.multiValued(); } 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 index 2a7ed11e9bc..b840694ba26 100644 --- 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 @@ -56,7 +56,7 @@ public void unknownFieldPath() { @Test public void nonObjectFieldPath() { assertThatThrownBy( () -> index.createScope().projection().object( "level1.field1" ) ) - .hasMessageContainingAll( "Cannot use 'projection:object' on field 'level1.field1'." ); + .hasMessageContainingAll( "Cannot use 'projection:object' on field 'level1.field1'" ); } @Test @@ -85,17 +85,19 @@ public void innerObjectProjectionOnFieldOutsideOuterObjectProjectionFieldTree() } @Test - public void flattenedObjectField_unsupported() { + public void multiValuedObjectField_flattened_unsupported() { assumeTrue( - "This test is only relevant if the backend relies on nested documents to implement object projections", - TckConfiguration.get().getBackendFeatures().reliesOnNestedDocumentsForObjectProjection() + "This test is only relevant if the backend relies on nested documents to implement object projections on multi-valued fields", + TckConfiguration.get().getBackendFeatures().reliesOnNestedDocumentsForMultiValuedObjectProjection() ); 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" ); + .hasMessageContainingAll( "Cannot use 'projection:object' on field 'flattenedLevel1'", + "This multi-valued field has a 'FLATTENED' structure," + + " which means the structure of objects is not preserved upon indexing," + + " making object projections impossible", + "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/testsupport/util/TckBackendFeatures.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/testsupport/util/TckBackendFeatures.java index 340b0091156..028a0938f31 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 @@ -6,6 +6,7 @@ */ package org.hibernate.search.integrationtest.backend.tck.testsupport.util; +import org.hibernate.search.engine.backend.types.ObjectStructure; import org.hibernate.search.engine.search.common.SortMode; public abstract class TckBackendFeatures { @@ -109,6 +110,10 @@ public boolean supportsFieldSortWhenNestedFieldMissingInSomeTargetIndexes() { return true; } - public abstract boolean reliesOnNestedDocumentsForObjectProjection(); + public boolean projectionPreservesEmptySingleValuedObject(ObjectStructure structure) { + return true; + } + + public abstract boolean reliesOnNestedDocumentsForMultiValuedObjectProjection(); } diff --git a/integrationtest/v5migrationhelper/engine/src/test/java/org/hibernate/search/test/projection/ProjectionConversionTest.java b/integrationtest/v5migrationhelper/engine/src/test/java/org/hibernate/search/test/projection/ProjectionConversionTest.java index 66f8a2a5cd7..cf52ae51db8 100644 --- a/integrationtest/v5migrationhelper/engine/src/test/java/org/hibernate/search/test/projection/ProjectionConversionTest.java +++ b/integrationtest/v5migrationhelper/engine/src/test/java/org/hibernate/search/test/projection/ProjectionConversionTest.java @@ -92,7 +92,7 @@ public void projectingIntegerField() { @Category(ElasticsearchSupportInProgress.class) // HSEARCH-2423 Projecting an unstored field should raise an exception public void projectingUnstoredField() { thrown.expect( SearchException.class ); - thrown.expectMessage( "Cannot use 'projection:field' on field 'unstoredField'." ); + thrown.expectMessage( "Cannot use 'projection:field' on field 'unstoredField'" ); thrown.expectMessage( "Make sure the field is marked as searchable/sortable/projectable/aggregable (whichever is relevant)." ); projectionTestHelper( "unstoredField", null );