Skip to content

Commit

Permalink
HSEARCH-2387 Add Elasticsearch dynamic mapping type tests
Browse files Browse the repository at this point in the history
  • Loading branch information
DavideD authored and Sanne committed Dec 19, 2016
1 parent f034ef8 commit 58fb49e
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 0 deletions.
@@ -0,0 +1,133 @@
/*
* 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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.elasticsearch.test.bridge;

import static org.fest.assertions.Assertions.assertThat;

import java.util.ArrayList;
import java.util.List;

import org.apache.lucene.search.Query;
import org.hibernate.search.backend.spi.Work;
import org.hibernate.search.backend.spi.WorkType;
import org.hibernate.search.cfg.Environment;
import org.hibernate.search.elasticsearch.cfg.ElasticsearchEnvironment;
import org.hibernate.search.elasticsearch.schema.impl.model.DynamicType;
import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator;
import org.hibernate.search.exception.ErrorContext;
import org.hibernate.search.exception.ErrorHandler;
import org.hibernate.search.exception.SearchException;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.hibernate.search.query.engine.spi.EntityInfo;
import org.hibernate.search.testsupport.junit.SearchFactoryHolder;
import org.hibernate.search.testsupport.setup.TransactionContextForTest;
import org.junit.Rule;
import org.junit.Test;

/**
* Checks that there is a way to create fields in an Elasticsearch indexes having dynamic attributes even when the
* global option is set to strict.
*
* @author Davide D'Alto
*/
public class DynamicMappingIT {

private static final String DYNAMIC_MAPPING = "hibernate.search." + ElasticsearchDynamicIndexedValueHolder.INDEX_NAME + "."
+ ElasticsearchEnvironment.DYNAMIC_MAPPING;

@Rule
public SearchFactoryHolder sfHolder = new SearchFactoryHolder( ElasticsearchDynamicIndexedValueHolder.class )
.withProperty( Environment.ERROR_HANDLER, TestExceptionHandler.class.getName() )
.withProperty( DYNAMIC_MAPPING, DynamicType.STRICT.name() );

@Test
public void testIndexingWithDynamicField() {
// Store some not defined data:
ElasticsearchDynamicIndexedValueHolder holder = new ElasticsearchDynamicIndexedValueHolder( "1" )
.dynamicProperty( "age", "227" )
.dynamicProperty( "name", "Thorin" )
.dynamicProperty( "surname", "Oakenshield" )
.dynamicProperty( "race", "dwarf" );

index( holder );
List<EntityInfo> fieldValue = searchField( "dynamicField.name" );

assertThat( fieldValue ).hasSize( 1 );
assertThat( fieldValue.get( 0 ).getProjection()[0] ).isEqualTo( "Thorin" );
}

@Test
public void testIndexingWithStrictField() {
// This property should be mapped with dynamic: strict, this means that we cannot change the value adding new
// properties and therefore an error should occurs
ElasticsearchDynamicIndexedValueHolder holder = new ElasticsearchDynamicIndexedValueHolder( "2" )
.strictProperty( "age", "64" )
.strictProperty( "name", "Gimli" )
.strictProperty( "race", "dwarf" );

index( holder );

TestExceptionHandler errorHandler = getErrorHandler();

assertThat( errorHandler.getHandleInvocations() ).hasSize( 1 );

ErrorContext errorContext = errorHandler.getHandleInvocations().get( 0 );
Throwable throwable = errorContext.getThrowable();

assertThat( throwable ).isInstanceOf( SearchException.class );
assertThat( throwable.getMessage() ).startsWith( "HSEARCH400007" );
assertThat( throwable.getMessage() ).contains( "strict_dynamic_mapping_exception" );
assertThat( throwable.getMessage() ).contains( "strictField" );
}

private TestExceptionHandler getErrorHandler() {
ExtendedSearchIntegrator searchFactory = sfHolder.getSearchFactory();
ErrorHandler errorHandler = searchFactory.getErrorHandler();
assertThat( errorHandler ).isInstanceOf( TestExceptionHandler.class );
return (TestExceptionHandler) errorHandler;
}

private void index(ElasticsearchDynamicIndexedValueHolder holder) {
ExtendedSearchIntegrator searchFactory = sfHolder.getSearchFactory();
Work work = new Work( holder, holder.id, WorkType.ADD, false );
TransactionContextForTest tc = new TransactionContextForTest();
searchFactory.getWorker().performWork( work, tc );
tc.end();
}

private List<EntityInfo> searchField(String field) {
ExtendedSearchIntegrator searchFactory = sfHolder.getSearchFactory();
QueryBuilder guestQueryBuilder = searchFactory.buildQueryBuilder().forEntity( ElasticsearchDynamicIndexedValueHolder.class ).get();
Query queryAllGuests = guestQueryBuilder.all().createQuery();

return searchFactory.createHSQuery( queryAllGuests, ElasticsearchDynamicIndexedValueHolder.class )
.projection( field )
.queryEntityInfos();
}

public static class TestExceptionHandler implements ErrorHandler {

private List<ErrorContext> handleInvocations = new ArrayList<>();

@Override
public void handle(ErrorContext context) {
handleInvocations.add( context );
}

@Override
public void handleException(String errorMsg, Throwable exception) {
}

public List<ErrorContext> getHandleInvocations() {
return handleInvocations;
}

public void reset() {
handleInvocations.clear();
}
}
}
@@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.elasticsearch.test.bridge;

import java.util.HashMap;
import java.util.Map;

import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.FieldBridge;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Norms;
import org.hibernate.search.annotations.Parameter;
import org.hibernate.search.annotations.Store;

/**
* @author Davide D'Alto
*/
@Indexed(index = ElasticsearchDynamicIndexedValueHolder.INDEX_NAME)
public class ElasticsearchDynamicIndexedValueHolder {

public static final String INDEX_NAME = "elasticsearchDynamicIndex";

@DocumentId
public final String id;

@Field(norms = Norms.NO, store = Store.YES, analyze = Analyze.YES, name = "dynamicField")
@FieldBridge(
impl = MapAsInnerObjectFieldBridge.class,
params = @Parameter(name = MapAsInnerObjectFieldBridge.DYNAMIC, value = "true")
)
private Map<String, String> dynamicFields = new HashMap<String, String>();

@Field(norms = Norms.NO, store = Store.YES, analyze = Analyze.YES, name = "strictField")
@FieldBridge(impl = MapAsInnerObjectFieldBridge.class)
private Map<String, String> strictFields = new HashMap<String, String>();

public ElasticsearchDynamicIndexedValueHolder(String id) {
this.id = id;
}

public ElasticsearchDynamicIndexedValueHolder dynamicProperty(String key, String value) {
dynamicFields.put( key, value );
return this;
}

/*
* Add fields to a property that does not enable dynamic mapping: it will cause an exception at some point
*/
public ElasticsearchDynamicIndexedValueHolder strictProperty(String key, String value) {
strictFields.put( key, value );
return this;
}
}
@@ -0,0 +1,64 @@
/*
* 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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.elasticsearch.test.bridge;

import java.util.Locale;
import java.util.Map;

import org.apache.lucene.document.Document;
import org.hibernate.search.bridge.LuceneOptions;
import org.hibernate.search.bridge.MetadataProvidingFieldBridge;
import org.hibernate.search.bridge.ParameterizedBridge;
import org.hibernate.search.bridge.spi.FieldMetadataBuilder;
import org.hibernate.search.bridge.spi.FieldMetadataCreationContext;
import org.hibernate.search.bridge.spi.FieldType;
import org.hibernate.search.elasticsearch.bridge.builtin.impl.Elasticsearch;
import org.hibernate.search.elasticsearch.cfg.DynamicType;

/**
* @author Davide D'Alto
*/
public class MapAsInnerObjectFieldBridge implements ParameterizedBridge, MetadataProvidingFieldBridge {

public static final String DYNAMIC = "dynamicMapping";

public DynamicType dynamicType = null;

@Override
public void set(String name, Object value, Document document, LuceneOptions luceneOptions) {
if ( !( value instanceof Map ) ) {
throw new IllegalArgumentException( "This field can only be applied on a Map type field" );
}
else {
Map<Object, Object> userValue = (Map) value;
for ( Map.Entry<Object, Object> e : userValue.entrySet() ) {
setField( name, String.valueOf( e.getKey() ), String.valueOf( e.getValue() ), document, luceneOptions );
}
}
}

private void setField(String fieldPrefix, String key, String value, Document document, LuceneOptions luceneOptions) {
luceneOptions.addFieldToDocument( fieldPrefix + "." + key, value, document );
}

@Override
public void configureFieldMetadata(String name, FieldMetadataBuilder builder) {
FieldMetadataCreationContext field = builder.field( name, FieldType.OBJECT );
if ( dynamicType != null ) {
field.mappedOn( Elasticsearch.class )
.dynamic( dynamicType );
}
}

@Override
public void setParameterValues(Map<String, String> parameters) {
String dynamicTypeAsString = parameters.get( DYNAMIC );
if ( dynamicTypeAsString != null ) {
dynamicType = DynamicType.valueOf( dynamicTypeAsString.toUpperCase( Locale.ROOT ) );
}
}
}

0 comments on commit 58fb49e

Please sign in to comment.