Skip to content

Commit

Permalink
HSEARCh-2508 Initialize the Elasticsearch embedded mappings lazily so…
Browse files Browse the repository at this point in the history
… 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.
  • Loading branch information
yrodiere authored and Sanne committed Dec 19, 2016
1 parent 60fc623 commit 2dd5a03
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 23 deletions.
Expand Up @@ -97,7 +97,7 @@ private void addMappings(ElasticsearchMappingBuilder mappingBuilder) {

// Recurse into embedded types
for ( EmbeddedTypeMetadata embeddedTypeMetadata : typeMetadata.getEmbeddedTypeMetadata() ) {
ElasticsearchMappingBuilder embeddedContext = mappingBuilder.createEmbedded( embeddedTypeMetadata );
ElasticsearchMappingBuilder embeddedContext = new ElasticsearchMappingBuilder( mappingBuilder, embeddedTypeMetadata );
addMappings( embeddedContext );
}
}
Expand Down
Expand Up @@ -40,32 +40,52 @@ final class ElasticsearchMappingBuilder {
private final TypeMetadata typeMetadata;

private final PathComponentExtractor pathComponentExtractor;
private final TypeMapping elasticsearchMapping;
private TypeMapping elasticsearchMapping;

/**
* Create a root mapping builder.
* @param binding The Hibernate Search binding to be translated in an Elasticsearch mapping
* @param elasticsearchMapping The Elasticsearch mapping on which properties should be added
*/
public ElasticsearchMappingBuilder(EntityIndexBinding binding, TypeMapping elasticsearchMapping) {
this( null, binding, binding.getDocumentBuilder().getTypeMetadata(), elasticsearchMapping, new PathComponentExtractor() );
}

private ElasticsearchMappingBuilder(ElasticsearchMappingBuilder parent, EntityIndexBinding binding, TypeMetadata typeMetadata,
TypeMapping elasticsearchMapping, PathComponentExtractor pathComponentExtractor) {
this.binding = binding;
this.parent = parent;
this.parent = null;
this.typeMetadata = binding.getDocumentBuilder().getTypeMetadata();
this.pathComponentExtractor = new PathComponentExtractor();
this.elasticsearchMapping = elasticsearchMapping;
this.typeMetadata = typeMetadata;
this.pathComponentExtractor = pathComponentExtractor;
}

public ElasticsearchMappingBuilder createEmbedded(EmbeddedTypeMetadata embeddedTypeMetadata) {
PathComponentExtractor newExtractor = pathComponentExtractor.clone();
newExtractor.append( embeddedTypeMetadata.getEmbeddedFieldPrefix() );

TypeMapping newElasticsearchMapping = getOrCreateParents( newExtractor );

return new ElasticsearchMappingBuilder( this, binding, embeddedTypeMetadata, newElasticsearchMapping, newExtractor );
/**
* Create an embedded mapping builder.
* @param parent The builder for the mapping on which the new mapping will be added as a property
* @param embeddedTypeMetadata The Hibernate Search metadata to be translated in an Elasticsearch mapping
*/
public ElasticsearchMappingBuilder(ElasticsearchMappingBuilder parent, EmbeddedTypeMetadata embeddedTypeMetadata) {
this.binding = parent.binding;
this.parent = parent;
this.typeMetadata = embeddedTypeMetadata;
this.pathComponentExtractor = parent.clonePathExtractor();
this.pathComponentExtractor.append( embeddedTypeMetadata.getEmbeddedFieldPrefix() );
this.elasticsearchMapping = null; // Will be lazily initialized
}


private TypeMapping getOrCreateParents(PathComponentExtractor extractor) {
TypeMapping currentElasticsearchMapping = elasticsearchMapping;
/*
* Lazily add the mapping to its parent, because we'd been asked to do so.
* This lazy initialization allows users to use
* @IndexedEmbedded(prefix = "foo") in conjunction with @Field(name = "foo") on
* the same property, provided the @IndexedEmbedded will not add any sub-property.
*
* This might seem weird (and arguably is), but this is how users tell Hibernate
* Search to unwrap a multi-valued property (array, List, Map, ...) to pass each
* value to the field bridge instead of simply passing the container itself.
*/
if ( this.elasticsearchMapping == null ) {
this.elasticsearchMapping = parent.getOrCreateParents( pathComponentExtractor );
}

TypeMapping currentElasticsearchMapping = this.elasticsearchMapping;
String newPathComponent = extractor.next( ConsumptionLimit.SECOND_BUT_LAST );
while ( newPathComponent != null ) {
/*
Expand Down Expand Up @@ -112,14 +132,22 @@ private static PropertyMapping getPropertyRelative(TypeMapping parent, String na
}

public boolean hasPropertyAbsolute(String absolutePath) {
return getPropertyAbsolute( absolutePath ) != null;
}

public TypeMapping getPropertyAbsolute(String absolutePath) {
/*
* Handle cases where the field name contains dots (and therefore requires
* creating containing properties).
* handling parent properties along the path).
*/
PathComponentExtractor newExtractor = createPathExtractorForAbsolutePath( absolutePath );
TypeMapping parent = getOrCreateParents( newExtractor );
String propertyName = newExtractor.next( ConsumptionLimit.LAST );
return getPropertyRelative( parent, propertyName ) != null;
TypeMapping currentMapping = elasticsearchMapping;
String pathComponent = newExtractor.next( ConsumptionLimit.LAST );
while ( currentMapping != null && pathComponent != null ) {
currentMapping = getPropertyRelative( currentMapping, pathComponent );
pathComponent = newExtractor.next( ConsumptionLimit.LAST );
}
return currentMapping;
}

public void setPropertyAbsolute(String absolutePath, PropertyMapping propertyMapping) {
Expand Down Expand Up @@ -148,9 +176,20 @@ public void setPropertyAbsolute(String absolutePath, PropertyMapping propertyMap
parent.addProperty( propertyName, propertyMapping );
}

private PathComponentExtractor clonePathExtractor() {
PathComponentExtractor newExtractor = pathComponentExtractor.clone();
/*
* Ignore the part of the path that hasn't been consumed yet due to the lazy initialization
* not having been performed yet.
* See getOrCreateParents().
*/
newExtractor.flushTo( ConsumptionLimit.SECOND_BUT_LAST );
return newExtractor;
}

private PathComponentExtractor createPathExtractorForAbsolutePath(String absolutePath) {
try {
PathComponentExtractor newExtractor = this.pathComponentExtractor.clone();
PathComponentExtractor newExtractor = clonePathExtractor();
newExtractor.appendRelativePart( absolutePath );
return newExtractor;
}
Expand Down
Expand Up @@ -91,6 +91,26 @@ else if ( ConsumptionLimit.LAST.equals( consumeLimit ) && currentIndexInPath < p
}
}

/**
* Consume all components in the current path up to the given limit.
*
*<p>This is equivalent to calling {@code next(consumeLimit)} repeatedly until {@code null} is returned.
*
* @param consumeLimit The consumption limit, i.e. the definition of the last component
* to consume in the path.
*/
public void flushTo(ConsumptionLimit consumeLimit) {
if ( ConsumptionLimit.LAST.equals( consumeLimit ) ) {
currentIndexInPath = path.length();
}
else {
int nextSeparatorIndex = path.lastIndexOf( PATH_COMPONENT_SEPARATOR );
if ( nextSeparatorIndex >= 0 ) {
currentIndexInPath = nextSeparatorIndex + 1;
}
}
}

public void reset() {
path.delete( 0, path.length() );
currentIndexInPath = 0;
Expand Down

0 comments on commit 2dd5a03

Please sign in to comment.