Skip to content

Commit 169c8ad

Browse files
committed
HSEARCH-5033 Better tests and documentation of limitations around nested fields for highlighting
1 parent eefa048 commit 169c8ad

File tree

3 files changed

+283
-227
lines changed

3 files changed

+283
-227
lines changed

documentation/src/main/asciidoc/public/reference/_search-dsl-projection.adoc

+12-1
Original file line numberDiff line numberDiff line change
@@ -1378,7 +1378,6 @@ The <<mapping-directfieldmapping-highlightable,highlightable>> default may alrea
13781378
in some cases. For more details see <<mapping-directfieldmapping-highlightable-default, how the `DEFAULT` highlightable value behaves>>.
13791379
====
13801380

1381-
13821381
[[search-dsl-projection-highlight-syntax]]
13831382
=== Syntax
13841383

@@ -1515,6 +1514,18 @@ include::{sourcedirJava17}/org/hibernate/search/documentation/search/projection/
15151514
----
15161515
====
15171516

1517+
[[search-dsl-projection-highlight-limitations]]
1518+
=== Highlight limitations
1519+
1520+
When it comes to highlighting of fields located in <<mapping-indexedembedded-structure-nested,nested>>
1521+
or <<mapping-indexedembedded-structure-flattened,flattened>> objects, Hibernate Search will not allow it in most scenarios right now.
1522+
Trying to do so will result in an exception being thrown.
1523+
The only exception to this is highlighting a field of a <<mapping-indexedembedded-structure-flattened,flattened>> object
1524+
that is a property of a root entity, i.e. the one having a path `[flattenedObjectPropertyName].[propertyName]`.
1525+
Trying to add a <<search-dsl-projection-highlight,highlight projection>> to an <<search-dsl-projection-object, object projection>>
1526+
will also result in an exception.
1527+
The possibility to do highlighting within nested objects should be opened up (at least partially) once link:{hibernateSearchJiraUrl}/HSEARCH-4841[HSEARCH-4841] is implemented.
1528+
15181529
[[search-dsl-projection-extensions]]
15191530
== Backend-specific extensions
15201531

integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/highlight/AbstractHighlighterIT.java

+3-208
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@
66
*/
77
package org.hibernate.search.integrationtest.backend.tck.search.highlight;
88

9-
import static org.assertj.core.api.Assertions.assertThat;
109
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1110
import static org.hibernate.search.util.impl.integrationtest.common.assertion.SearchHitsAssert.assertThatHits;
1211
import static org.junit.jupiter.api.Assumptions.assumeFalse;
1312
import static org.junit.jupiter.api.Assumptions.assumeTrue;
1413

15-
import java.time.LocalDate;
1614
import java.util.Arrays;
1715
import java.util.Collections;
1816
import java.util.List;
@@ -22,9 +20,6 @@
2220
import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement;
2321
import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaObjectField;
2422
import org.hibernate.search.engine.backend.types.Highlightable;
25-
import org.hibernate.search.engine.backend.types.IndexFieldTraits;
26-
import org.hibernate.search.engine.backend.types.ObjectStructure;
27-
import org.hibernate.search.engine.backend.types.Projectable;
2823
import org.hibernate.search.engine.backend.types.TermVector;
2924
import org.hibernate.search.engine.search.highlighter.SearchHighlighter;
3025
import org.hibernate.search.engine.search.highlighter.dsl.HighlighterEncoder;
@@ -54,20 +49,15 @@ abstract class AbstractHighlighterIT {
5449
public final ExpectedLog4jLog logged = ExpectedLog4jLog.create();
5550

5651
protected static final SimpleMappedIndex<IndexBinding> index = SimpleMappedIndex.of( IndexBinding::new );
57-
protected static final SimpleMappedIndex<IndexBinding> matchingIndex = SimpleMappedIndex.of( IndexBinding::new )
58-
.name( "matchingIndex" );
59-
protected static final SimpleMappedIndex<NotMatchingTypeIndexBinding> notMatchingTypeIndex =
52+
private static final SimpleMappedIndex<NotMatchingTypeIndexBinding> notMatchingTypeIndex =
6053
SimpleMappedIndex.of( NotMatchingTypeIndexBinding::new )
6154
.name( "notMatchingTypeIndex" );
62-
protected static final SimpleMappedIndex<NestedIndexBinding> nestedIndex = SimpleMappedIndex.of( NestedIndexBinding::new )
63-
.name( "nestedIndex" );
6455

6556
@BeforeAll
6657
static void setup() {
6758
setupHelper.start().withIndex( index )
68-
.withIndex( matchingIndex )
6959
.withIndex( notMatchingTypeIndex )
70-
.withIndex( nestedIndex ).setup();
60+
.setup();
7161

7262
index.bulkIndexer()
7363
.add( "1", d -> d.addValue( "string", "some value" ) )
@@ -118,63 +108,12 @@ static void setup() {
118108
} )
119109
.add( "12", d -> {
120110
d.addValue( "stringNoTermVector", "boo and boo and boo much more times" );
121-
d.addValue( "stringNotProjectable", "The quick brown fox jumps right over the little lazy dog" );
122111
} )
123112
.join();
124-
125-
matchingIndex.bulkIndexer()
126-
.add( "100", d -> d.addValue( "string", "string with dog" ) )
127-
.join();
128113
}
129114

130115
abstract HighlighterOptionsStep<?> highlighter(SearchHighlighterFactory factory);
131116

132-
@Test
133-
void highlightable_enabled_trait() {
134-
assertThat( Arrays.asList( "string", "objectFlattened.string" ) )
135-
.allSatisfy( fieldPath -> assertThat( index.toApi().descriptor().field( fieldPath ) )
136-
.hasValueSatisfying( fieldDescriptor -> assertThat( fieldDescriptor.type().traits() )
137-
.as( "traits of field '" + fieldPath + "'" )
138-
.contains( "projection:highlight" ) ) );
139-
}
140-
141-
@Test
142-
void projectable_no_trait() {
143-
String fieldPath = "stringNotProjectable";
144-
if ( TckConfiguration.get().getBackendFeatures().supportsHighlightableWithoutProjectable() ) {
145-
assertThat( index.toApi().descriptor().field( fieldPath ) )
146-
.hasValueSatisfying( fieldDescriptor -> assertThat( fieldDescriptor.type().traits() )
147-
.as( "traits of field '" + fieldPath + "'" )
148-
.contains( IndexFieldTraits.Projections.HIGHLIGHT ) );
149-
}
150-
else {
151-
assertThat( index.toApi().descriptor().field( fieldPath ) )
152-
.hasValueSatisfying( fieldDescriptor -> assertThat( fieldDescriptor.type().traits() )
153-
.as( "traits of field '" + fieldPath + "'" )
154-
.doesNotContain( IndexFieldTraits.Projections.HIGHLIGHT ) );
155-
}
156-
}
157-
158-
@Test
159-
void highlightable_enabled_trait_nested() {
160-
assertThat( Arrays.asList(
161-
"objectNested.string",
162-
"objectNested.level2objectDefault.string",
163-
"objectNested.level2objectNested.string",
164-
"objectNested.level2objectFlattened.string",
165-
"objectDefault.level2objectNested.string",
166-
"objectFlattened.level2objectNested.string"
167-
) )
168-
.allSatisfy( inObjectFieldPath -> assertThat( nestedIndex.toApi().descriptor().field( inObjectFieldPath ) )
169-
.hasValueSatisfying( fieldDescriptor -> assertThat( fieldDescriptor.type().traits() )
170-
.as( "traits of field '" + inObjectFieldPath + "'" )
171-
// See HSEARCH-4841: highlighting is forbidden on nested fields...
172-
// but here we're inspecting the field *type*, which unfortunately
173-
// is independent of the field structure and thus doesn't know
174-
// highlighting is not available.
175-
.contains( IndexFieldTraits.Projections.HIGHLIGHT ) ) );
176-
}
177-
178117
@Test
179118
void highlighterNoConfigurationAtAll() {
180119
StubMappingScope scope = index.createScope();
@@ -790,67 +729,6 @@ void unknownNamedHighlighter() {
790729
);
791730
}
792731

793-
@Test
794-
void highlightNonAnalyzedField() {
795-
assertThatThrownBy(
796-
() -> index.createScope().query().select(
797-
f -> f.highlight( "notAnalyzedString" )
798-
).where( f -> f.matchAll() )
799-
.toQuery()
800-
).isInstanceOf( SearchException.class )
801-
.hasMessageContainingAll(
802-
"Cannot use 'projection:highlight' on field 'notAnalyzedString':",
803-
"Make sure the field is marked as searchable/sortable/projectable/aggregable/highlightable (whichever is relevant).",
804-
"If it already is, then 'projection:highlight' is not available for fields of this type."
805-
);
806-
}
807-
808-
@Test
809-
void multipleIndexesScopeIncompatibleTypes() {
810-
assertThatThrownBy(
811-
() -> index.createScope( notMatchingTypeIndex ).query().select(
812-
f -> f.highlight( "string" )
813-
).where( f -> f.matchAll() )
814-
.toQuery()
815-
).isInstanceOf( SearchException.class )
816-
.hasMessageContainingAll(
817-
"Inconsistent support for 'projection:highlight'",
818-
"'projection:highlight' can be used in some of the targeted indexes, but not all of them.",
819-
"Make sure the field is marked as searchable/sortable/projectable/aggregable/highlightable (whichever is relevant) in all indexes, and that the field has the same type in all indexes"
820-
);
821-
}
822-
823-
@Test
824-
void multipleIndexesScopeIncompatibleTypesInObjectField() {
825-
assertThatThrownBy(
826-
() -> index.createScope( notMatchingTypeIndex ).query().select(
827-
f -> f.highlight( "objectFlattened.string" )
828-
).where( f -> f.matchAll() )
829-
.toQuery()
830-
).isInstanceOf( SearchException.class )
831-
.hasMessageContainingAll(
832-
"Inconsistent support for 'projection:highlight'",
833-
"'projection:highlight' can be used in some of the targeted indexes, but not all of them.",
834-
"Make sure the field is marked as searchable/sortable/projectable/aggregable/highlightable (whichever is relevant) in all indexes, and that the field has the same type in all indexes."
835-
);
836-
}
837-
838-
@Test
839-
void multipleIndexesScopeCompatibleTypes() {
840-
SearchQuery<List<String>> highlights = index.createScope( matchingIndex ).query().select(
841-
f -> f.highlight( "string" )
842-
).where( f -> f.match().field( "string" ).matching( "dog" ) )
843-
.highlighter( h -> highlighter( h ) )
844-
.toQuery();
845-
846-
assertThatHits( highlights.fetchAllHits() )
847-
.hasHitsAnyOrder( Arrays.asList(
848-
Collections.singletonList( "string with <em>dog</em>" ),
849-
Collections.singletonList( "This string mentions a <em>dog</em>" ),
850-
Collections.singletonList( "This string mentions a <em>dog</em> too" )
851-
) );
852-
}
853-
854732
@Test
855733
void prebuiltHighlighter() {
856734
SearchHighlighter highlighter = highlighter( index.createScope().highlighter() ).tag( "---", "---" )
@@ -908,36 +786,6 @@ void prebuiltHighlighterWrongScope() {
908786
);
909787
}
910788

911-
@Test
912-
void inObjectProjection() {
913-
List<String> objects = Arrays.asList( "objectDefault", "objectFlattened" );
914-
for ( String object : objects ) {
915-
for ( String level2 : objects ) {
916-
assertThatThrownBy( () -> nestedIndex.query().select(
917-
f -> f.object( object )
918-
.from(
919-
f.composite().from(
920-
f.field( object + ".string" ),
921-
f.object( object + ".level2" + level2 )
922-
.from( f.highlight( object + ".level2" + level2 + ".string" ) )
923-
.asList()
924-
).asList()
925-
)
926-
.asList()
927-
)
928-
.where( f -> f.matchAll() )
929-
.toQuery() )
930-
.as( object )
931-
.isInstanceOf( SearchException.class )
932-
.hasMessageContainingAll(
933-
"Highlight projection cannot be applied within nested context of",
934-
object,
935-
level2
936-
);
937-
}
938-
}
939-
}
940-
941789
@Test
942790
void phraseMatching() {
943791
SearchQuery<List<String>> highlights = index.createScope().query().select(
@@ -971,7 +819,7 @@ private List<List<String>> phraseMatchingResult() {
971819
}
972820
}
973821

974-
private static class IndexBinding {
822+
protected static class IndexBinding {
975823
final IndexFieldReference<String> stringField;
976824
final IndexFieldReference<String> anotherStringField;
977825
final IndexFieldReference<String> objectFlattenedString;
@@ -980,7 +828,6 @@ private static class IndexBinding {
980828
final IndexObjectFieldReference objectFlattened;
981829
final IndexFieldReference<String> stringNoTermVectorField;
982830
final IndexFieldReference<Integer> intField;
983-
final IndexFieldReference<String> stringNotProjectableField;
984831

985832
IndexBinding(IndexSchemaElement root) {
986833
stringField = root.field( "string", f -> f.asString()
@@ -1016,66 +863,14 @@ private static class IndexBinding {
1016863
).toReference();
1017864

1018865
intField = root.field( "int", f -> f.asInteger() ).toReference();
1019-
1020-
stringNotProjectableField = root.field( "stringNotProjectable", f -> f.asString()
1021-
.analyzer( DefaultAnalysisDefinitions.ANALYZER_STANDARD_ENGLISH.name )
1022-
).toReference();
1023-
}
1024-
}
1025-
1026-
private static class NestedIndexBinding {
1027-
1028-
NestedIndexBinding(IndexSchemaElement root) {
1029-
createObjects( "", root, 2, true );
1030-
}
1031-
1032-
private void createObjects(String prefix, IndexSchemaElement element, int level, boolean addNested) {
1033-
IndexSchemaObjectField objectDefault = element.objectField( prefix + "objectDefault" );
1034-
1035-
createString( "string", objectDefault );
1036-
if ( addNested ) {
1037-
createObjects( "level" + ( level ), objectDefault, level + 1, false );
1038-
}
1039-
objectDefault.toReference();
1040-
1041-
IndexSchemaObjectField objectNested = element.objectField( prefix + "objectNested", ObjectStructure.NESTED );
1042-
createString( "string", objectNested );
1043-
if ( addNested ) {
1044-
createObjects( "level" + ( level ), objectNested, level + 1, false );
1045-
}
1046-
objectNested.toReference();
1047-
1048-
IndexSchemaObjectField objectFlattened =
1049-
element.objectField( prefix + "objectFlattened", ObjectStructure.FLATTENED );
1050-
createString( "string", objectFlattened );
1051-
if ( addNested ) {
1052-
createObjects( "level" + ( level ), objectFlattened, level + 1, false );
1053-
}
1054-
objectFlattened.toReference();
1055-
}
1056-
1057-
private IndexFieldReference<String> createString(String name, IndexSchemaObjectField objectField) {
1058-
return objectField.field( name, f -> f.asString()
1059-
.highlightable( Arrays.asList( Highlightable.UNIFIED, Highlightable.PLAIN ) )
1060-
.analyzer( DefaultAnalysisDefinitions.ANALYZER_STANDARD_ENGLISH.name )
1061-
).toReference();
1062866
}
1063867
}
1064868

1065869
private static class NotMatchingTypeIndexBinding {
1066870
final IndexFieldReference<Integer> stringField;
1067-
final IndexObjectFieldReference nested;
1068-
final IndexFieldReference<LocalDate> objectFlattenedString;
1069871

1070872
NotMatchingTypeIndexBinding(IndexSchemaElement root) {
1071873
stringField = root.field( "string", f -> f.asInteger() ).toReference();
1072-
1073-
IndexSchemaObjectField objectField = root.objectField( "objectFlattened" );
1074-
nested = objectField.toReference();
1075-
1076-
objectFlattenedString = objectField.field( "string", f -> f.asLocalDate()
1077-
.projectable( Projectable.YES )
1078-
).toReference();
1079874
}
1080875
}
1081876
}

0 commit comments

Comments
 (0)