Skip to content

Commit 029e686

Browse files
yrodiereDavideD
authored andcommitted
HSEARCh-2508 Initialize the Elasticsearch embedded mappings lazily so as to allow the use of container bridges
Such bridges are enabled when using @field and @IndexedEmbedded on the same property. They expect the source property to be an array, Iterable or Map. When enabled, those bridges will unwrap the source property value and pass each element to the actual field bridge. For instance each integer in a List<Integer> will be passed to NumericFieldBridge.INT_FIELD_BRIDGE.set(...) in turns. This is very different from the originally intended use of @IndexedEmbedded, and in particular there is no sub-property to speak of as long as the elements in the container are not mapped themselves. Thus, if we refrain from adding the embedded mapping as long as there are no sub-properties, we allow the feature to work despite the inability for Elasticsearch to have a property mapped to both the 'object' and another, concrete datatype. See HSEARCH-2448 in particular.
1 parent 44874a2 commit 029e686

File tree

3 files changed

+82
-23
lines changed

3 files changed

+82
-23
lines changed

elasticsearch/src/main/java/org/hibernate/search/elasticsearch/schema/impl/DefaultElasticsearchSchemaTranslator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ private void addMappings(ElasticsearchMappingBuilder mappingBuilder) {
9797

9898
// Recurse into embedded types
9999
for ( EmbeddedTypeMetadata embeddedTypeMetadata : typeMetadata.getEmbeddedTypeMetadata() ) {
100-
ElasticsearchMappingBuilder embeddedContext = mappingBuilder.createEmbedded( embeddedTypeMetadata );
100+
ElasticsearchMappingBuilder embeddedContext = new ElasticsearchMappingBuilder( mappingBuilder, embeddedTypeMetadata );
101101
addMappings( embeddedContext );
102102
}
103103
}

elasticsearch/src/main/java/org/hibernate/search/elasticsearch/schema/impl/ElasticsearchMappingBuilder.java

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,32 +40,52 @@ final class ElasticsearchMappingBuilder {
4040
private final TypeMetadata typeMetadata;
4141

4242
private final PathComponentExtractor pathComponentExtractor;
43-
private final TypeMapping elasticsearchMapping;
43+
private TypeMapping elasticsearchMapping;
4444

45+
/**
46+
* Create a root mapping builder.
47+
* @param binding The Hibernate Search binding to be translated in an Elasticsearch mapping
48+
* @param elasticsearchMapping The Elasticsearch mapping on which properties should be added
49+
*/
4550
public ElasticsearchMappingBuilder(EntityIndexBinding binding, TypeMapping elasticsearchMapping) {
46-
this( null, binding, binding.getDocumentBuilder().getTypeMetadata(), elasticsearchMapping, new PathComponentExtractor() );
47-
}
48-
49-
private ElasticsearchMappingBuilder(ElasticsearchMappingBuilder parent, EntityIndexBinding binding, TypeMetadata typeMetadata,
50-
TypeMapping elasticsearchMapping, PathComponentExtractor pathComponentExtractor) {
5151
this.binding = binding;
52-
this.parent = parent;
52+
this.parent = null;
53+
this.typeMetadata = binding.getDocumentBuilder().getTypeMetadata();
54+
this.pathComponentExtractor = new PathComponentExtractor();
5355
this.elasticsearchMapping = elasticsearchMapping;
54-
this.typeMetadata = typeMetadata;
55-
this.pathComponentExtractor = pathComponentExtractor;
5656
}
5757

58-
public ElasticsearchMappingBuilder createEmbedded(EmbeddedTypeMetadata embeddedTypeMetadata) {
59-
PathComponentExtractor newExtractor = pathComponentExtractor.clone();
60-
newExtractor.append( embeddedTypeMetadata.getEmbeddedFieldPrefix() );
61-
62-
TypeMapping newElasticsearchMapping = getOrCreateParents( newExtractor );
63-
64-
return new ElasticsearchMappingBuilder( this, binding, embeddedTypeMetadata, newElasticsearchMapping, newExtractor );
58+
/**
59+
* Create an embedded mapping builder.
60+
* @param parent The builder for the mapping on which the new mapping will be added as a property
61+
* @param embeddedTypeMetadata The Hibernate Search metadata to be translated in an Elasticsearch mapping
62+
*/
63+
public ElasticsearchMappingBuilder(ElasticsearchMappingBuilder parent, EmbeddedTypeMetadata embeddedTypeMetadata) {
64+
this.binding = parent.binding;
65+
this.parent = parent;
66+
this.typeMetadata = embeddedTypeMetadata;
67+
this.pathComponentExtractor = parent.clonePathExtractor();
68+
this.pathComponentExtractor.append( embeddedTypeMetadata.getEmbeddedFieldPrefix() );
69+
this.elasticsearchMapping = null; // Will be lazily initialized
6570
}
6671

72+
6773
private TypeMapping getOrCreateParents(PathComponentExtractor extractor) {
68-
TypeMapping currentElasticsearchMapping = elasticsearchMapping;
74+
/*
75+
* Lazily add the mapping to its parent, because we'd been asked to do so.
76+
* This lazy initialization allows users to use
77+
* @IndexedEmbedded(prefix = "foo") in conjunction with @Field(name = "foo") on
78+
* the same property, provided the @IndexedEmbedded will not add any sub-property.
79+
*
80+
* This might seem weird (and arguably is), but this is how users tell Hibernate
81+
* Search to unwrap a multi-valued property (array, List, Map, ...) to pass each
82+
* value to the field bridge instead of simply passing the container itself.
83+
*/
84+
if ( this.elasticsearchMapping == null ) {
85+
this.elasticsearchMapping = parent.getOrCreateParents( pathComponentExtractor );
86+
}
87+
88+
TypeMapping currentElasticsearchMapping = this.elasticsearchMapping;
6989
String newPathComponent = extractor.next( ConsumptionLimit.SECOND_BUT_LAST );
7090
while ( newPathComponent != null ) {
7191
/*
@@ -112,14 +132,22 @@ private static PropertyMapping getPropertyRelative(TypeMapping parent, String na
112132
}
113133

114134
public boolean hasPropertyAbsolute(String absolutePath) {
135+
return getPropertyAbsolute( absolutePath ) != null;
136+
}
137+
138+
public TypeMapping getPropertyAbsolute(String absolutePath) {
115139
/*
116140
* Handle cases where the field name contains dots (and therefore requires
117-
* creating containing properties).
141+
* handling parent properties along the path).
118142
*/
119143
PathComponentExtractor newExtractor = createPathExtractorForAbsolutePath( absolutePath );
120-
TypeMapping parent = getOrCreateParents( newExtractor );
121-
String propertyName = newExtractor.next( ConsumptionLimit.LAST );
122-
return getPropertyRelative( parent, propertyName ) != null;
144+
TypeMapping currentMapping = elasticsearchMapping;
145+
String pathComponent = newExtractor.next( ConsumptionLimit.LAST );
146+
while ( currentMapping != null && pathComponent != null ) {
147+
currentMapping = getPropertyRelative( currentMapping, pathComponent );
148+
pathComponent = newExtractor.next( ConsumptionLimit.LAST );
149+
}
150+
return currentMapping;
123151
}
124152

125153
public void setPropertyAbsolute(String absolutePath, PropertyMapping propertyMapping) {
@@ -148,9 +176,20 @@ public void setPropertyAbsolute(String absolutePath, PropertyMapping propertyMap
148176
parent.addProperty( propertyName, propertyMapping );
149177
}
150178

179+
private PathComponentExtractor clonePathExtractor() {
180+
PathComponentExtractor newExtractor = pathComponentExtractor.clone();
181+
/*
182+
* Ignore the part of the path that hasn't been consumed yet due to the lazy initialization
183+
* not having been performed yet.
184+
* See getOrCreateParents().
185+
*/
186+
newExtractor.flushTo( ConsumptionLimit.SECOND_BUT_LAST );
187+
return newExtractor;
188+
}
189+
151190
private PathComponentExtractor createPathExtractorForAbsolutePath(String absolutePath) {
152191
try {
153-
PathComponentExtractor newExtractor = this.pathComponentExtractor.clone();
192+
PathComponentExtractor newExtractor = clonePathExtractor();
154193
newExtractor.appendRelativePart( absolutePath );
155194
return newExtractor;
156195
}

elasticsearch/src/main/java/org/hibernate/search/elasticsearch/util/impl/PathComponentExtractor.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,26 @@ else if ( ConsumptionLimit.LAST.equals( consumeLimit ) && currentIndexInPath < p
9191
}
9292
}
9393

94+
/**
95+
* Consume all components in the current path up to the given limit.
96+
*
97+
*<p>This is equivalent to calling {@code next(consumeLimit)} repeatedly until {@code null} is returned.
98+
*
99+
* @param consumeLimit The consumption limit, i.e. the definition of the last component
100+
* to consume in the path.
101+
*/
102+
public void flushTo(ConsumptionLimit consumeLimit) {
103+
if ( ConsumptionLimit.LAST.equals( consumeLimit ) ) {
104+
currentIndexInPath = path.length();
105+
}
106+
else {
107+
int nextSeparatorIndex = path.lastIndexOf( PATH_COMPONENT_SEPARATOR );
108+
if ( nextSeparatorIndex >= 0 ) {
109+
currentIndexInPath = nextSeparatorIndex + 1;
110+
}
111+
}
112+
}
113+
94114
public void reset() {
95115
path.delete( 0, path.length() );
96116
currentIndexInPath = 0;

0 commit comments

Comments
 (0)