/* * 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; import static org.hibernate.search.test.util.impl.ExceptionMatcherBuilder.isException; import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.persistence.Entity; import javax.persistence.Id; import org.hibernate.search.annotations.Analyze; import org.hibernate.search.annotations.Analyzer; import org.hibernate.search.annotations.AnalyzerDef; import org.hibernate.search.annotations.CharFilterDef; import org.hibernate.search.annotations.DocumentId; import org.hibernate.search.annotations.Facet; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Index; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.annotations.Norms; import org.hibernate.search.annotations.Parameter; import org.hibernate.search.annotations.Store; import org.hibernate.search.annotations.TokenFilterDef; import org.hibernate.search.annotations.TokenizerDef; import org.hibernate.search.elasticsearch.analyzer.ElasticsearchCharFilterFactory; import org.hibernate.search.elasticsearch.analyzer.ElasticsearchTokenFilterFactory; import org.hibernate.search.elasticsearch.analyzer.ElasticsearchTokenizerFactory; import org.hibernate.search.elasticsearch.cfg.ElasticsearchEnvironment; import org.hibernate.search.elasticsearch.cfg.IndexSchemaManagementStrategy; import org.hibernate.search.elasticsearch.impl.ElasticsearchIndexManager; import org.hibernate.search.elasticsearch.impl.ToElasticsearch; import org.hibernate.search.elasticsearch.schema.impl.ElasticsearchSchemaValidationException; import org.hibernate.search.elasticsearch.testutil.TestElasticsearchClient; import org.hibernate.search.elasticsearch.testutil.junit.SkipOnElasticsearch2; import org.hibernate.search.test.SearchInitializationTestBase; import org.hibernate.search.test.util.ImmutableTestConfiguration; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.rules.ExpectedException; /** * Tests for {@link ElasticsearchIndexManager}'s schema validation feature. * * @author Yoann Rodiere */ @Category(SkipOnElasticsearch2.class) public class Elasticsearch5SchemaValidationIT extends SearchInitializationTestBase { private static final String VALIDATION_FAILED_MESSAGE_ID = "HSEARCH400033"; @Rule public ExpectedException thrown = ExpectedException.none(); @Rule public TestElasticsearchClient elasticSearchClient = new TestElasticsearchClient(); @Override protected void init(Class<?>... annotatedClasses) { Map<String, Object> settings = new HashMap<>(); settings.put( "hibernate.search.default." + ElasticsearchEnvironment.INDEX_SCHEMA_MANAGEMENT_STRATEGY, IndexSchemaManagementStrategy.VALIDATE.getExternalName() ); init( new ImmutableTestConfiguration( settings, annotatedClasses ) ); } @Test public void success_simple() throws Exception { elasticSearchClient.index( SimpleDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleDateEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true" + "}," + "'myField': {" + "'type': 'date'," + "'index': true," + "'ignore_malformed': true" // Ignored during validation + "}," + "'NOTmyField': {" // Ignored during validation + "'type': 'date'," + "'index': true" + "}" + "}" + "}" ); elasticSearchClient.index( SimpleBooleanEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleBooleanEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true" + "}," + "'myField': {" + "'type': 'boolean'," + "'index': true" + "}," + "'NOTmyField': {" // Ignored during validation + "'type': 'boolean'," + "'index': true" + "}" + "}" + "}" ); elasticSearchClient.index( SimpleTextEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleTextEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true" + "}," + "'myField': {" + "'type': 'text'," + "'index': true," + "'analyzer': 'default'" + "}," + "'NOTmyField': {" // Ignored during validation + "'type': 'text'," + "'index': true" + "}" + "}" + "}" ); elasticSearchClient.index( FacetedDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( FacetedDateEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'store': true" + "}," + "'myField': {" + "'type': 'date'," + "'ignore_malformed': true," // Ignored during validation + "'fields': {" + "'myField" + ToElasticsearch.FACET_FIELD_SUFFIX + "': {" + "'type': 'date'" + "}" + "}" + "}," + "'NOTmyField': {" // Ignored during validation + "'type': 'date'," + "'index': 'false'" + "}" + "}" + "}" ); elasticSearchClient.index( AnalyzedEntity.class ).deleteAndCreate(); elasticSearchClient.index( AnalyzedEntity.class ).settings( "index.analysis" ).put( "{" + "'analyzer': {" + "'analyzerWithElasticsearchFactories': {" + "'char_filter': ['custom-pattern-replace']," + "'tokenizer': 'custom-edgeNGram'," + "'filter': ['custom-keep-types', 'custom-word-delimiter']" + "}" + "}," + "'char_filter': {" + "'custom-pattern-replace': {" + "'type': 'pattern_replace'," + "'pattern': '[^0-9]'," + "'replacement': '0'," + "'tags': 'CASE_INSENSITIVE|COMMENTS'" + "}" + "}," + "'tokenizer': {" + "'custom-edgeNGram': {" + "'type': 'edgeNGram'," /* * Strangely enough, even if you send properly typed numbers * to Elasticsearch, when you ask for the current settings it * will spit back strings instead of numbers... * Testing both a properly typed number and a number as string here. */ + "'min_gram': 1," + "'max_gram': '10'" + "}" + "}," + "'filter': {" + "'custom-keep-types': {" + "'type': 'keep_types'," /* * The order doesn't matter with this array. * Here we test that validation ignores order. */ + "'types': ['<DOUBLE>', '<NUM>']" + "}," + "'custom-word-delimiter': {" + "'type': 'word_delimiter'," + "'generate_word_parts': false" + "}" + "}" + "}" ); elasticSearchClient.type( AnalyzedEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': 'true'," + "'store': true" + "}," + "'myField': {" + "'type': 'text'," + "'analyzer': 'analyzerWithElasticsearchFactories'" + "}" + "}" + "}" ); init( SimpleDateEntity.class, SimpleBooleanEntity.class, SimpleTextEntity.class, FacetedDateEntity.class, AnalyzedEntity.class ); // If we get here, it means validation passed (no exception was thrown) } @Test public void mapping_missing() throws Exception { elasticSearchClient.index( SimpleDateEntity.class ).deleteAndCreate(); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "\n\tMissing type mapping" ) .build() ); init( SimpleDateEntity.class ); } @Test public void rootMapping_attribute_missing() throws Exception { elasticSearchClient.index( SimpleDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleDateEntity.class ).putMapping( "{" + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true" + "}," + "'myField': {" + "'type': 'date'," + "'index': true" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "\n\tInvalid value for attribute 'dynamic'. Expected 'STRICT', actual is 'null'" ) .build() ); init( SimpleDateEntity.class ); } @Test public void rootMapping_attribute_dynamic_invalid() throws Exception { elasticSearchClient.index( SimpleDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleDateEntity.class ).putMapping( "{" + "'dynamic': false," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true" + "}," + "'myField': {" + "'type': 'date'," + "'index': true" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "\n\tInvalid value for attribute 'dynamic'. Expected 'STRICT', actual is 'FALSE'" ) .build() ); init( SimpleDateEntity.class ); } @Test public void properties_missing() throws Exception { elasticSearchClient.index( SimpleDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleDateEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "property 'myField'" ) .withMessage( "\n\tMissing property mapping" ) .build() ); init( SimpleDateEntity.class ); } @Test public void property_missing() throws Exception { elasticSearchClient.index( SimpleDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleDateEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true" + "}," + "'NOTmyField': {" + "'type': 'date'," + "'index': true" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "property 'myField'" ) .withMessage( "\n\tMissing property mapping" ) .build() ); init( SimpleDateEntity.class ); } @Test public void property_attribute_missing() throws Exception { elasticSearchClient.index( SimpleDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleDateEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true" + "}," + "'myField': {" + "'type': 'object'" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "property 'myField'" ) .withMessage( "\n\tInvalid value for attribute 'type'. Expected 'DATE', actual is 'OBJECT'" ) .build() ); init( SimpleDateEntity.class ); } @Test public void property_attribute_invalid() throws Exception { elasticSearchClient.index( SimpleDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleDateEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true" + "}," + "'myField': {" + "'type': 'date'," + "'index': false" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "property 'myField'" ) .withMessage( "\n\tInvalid value for attribute 'index'. Expected 'TRUE', actual is 'FALSE'" ) .build() ); init( SimpleDateEntity.class ); } @Test public void property_analyzer_invalid() throws Exception { elasticSearchClient.index( SimpleTextEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleTextEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true" + "}," + "'myField': {" + "'type': 'text'," + "'index': true," + "'analyzer': 'keyword'" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "property 'myField'" ) .withMessage( "\n\tInvalid value for attribute 'analyzer'. Expected 'default', actual is 'keyword'" ) .build() ); init( SimpleTextEntity.class ); } @Test public void property_norms_invalid() throws Exception { elasticSearchClient.index( SimpleTextEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleTextEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true" + "}," + "'myField': {" + "'type': 'text'," + "'norms': false" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "property 'myField'" ) .withMessage( "\n\tInvalid value for attribute 'norms'. Expected 'TRUE', actual is 'FALSE'" ) .build() ); init( SimpleTextEntity.class ); } @Test public void property_format_invalidOutputFormat() throws Exception { elasticSearchClient.index( SimpleDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleDateEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true" + "}," + "'myField': {" + "'type': 'date'," + "'format': 'epoch_millis||yyyy'" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "property 'myField'" ) .withMessage( "\n\tThe output format (the first format in the 'format' attribute) is invalid. Expected 'strict_date_optional_time', actual is 'epoch_millis'" ) .build() ); init( SimpleDateEntity.class ); } @Test public void property_format_missingInputFormat() throws Exception { elasticSearchClient.index( SimpleDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleDateEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true" + "}," + "'myField': {" + "'type': 'date'," + "'format': 'strict_date_optional_time'" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "property 'myField'" ) .withMessage( "\n\tInvalid formats for attribute 'format'" ) .withMessage( "missing elements are '[epoch_millis]'" ) .build() ); init( SimpleDateEntity.class ); } @Test public void property_format_unexpectedInputFormat() throws Exception { elasticSearchClient.index( SimpleDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleDateEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true" + "}," + "'myField': {" + "'type': 'date'," + "'format': 'strict_date_optional_time||epoch_millis||yyyy'" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "property 'myField'" ) .withMessage( "\n\tInvalid formats for attribute 'format'" ) .withMessage( "unexpected elements are '[yyyy]'" ) .build() ); init( SimpleDateEntity.class ); } /** * Tests that mappings that are more powerful than requested will pass validation. */ @Test public void property_attribute_leniency() throws Exception { elasticSearchClient.index( SimpleLenientEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleLenientEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': true," + "'store': true," + "'norms': true" + "}," + "'myField': {" + "'type': 'long'," + "'index': true," + "'store': true" + "}," + "'myTextField': {" + "'type': 'text'," + "'index': true," + "'norms': true" + "}" + "}" + "}" ); init( SimpleLenientEntity.class ); } @Test public void multipleErrors_singleIndexManagers() throws Exception { elasticSearchClient.index( SimpleDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleDateEntity.class ).putMapping( "{" + "'dynamic': false," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'store': true" + "}," + "'myField': {" + "'type': 'keyword'" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "\nindex 'org.hibernate.search.elasticsearch.test.elasticsearch5schemavalidationit$simpledateentity'," + " mapping 'org.hibernate.search.elasticsearch.test.Elasticsearch5SchemaValidationIT$SimpleDateEntity':" + "\n\tInvalid value for attribute 'dynamic'. Expected 'STRICT', actual is 'FALSE'" + "\nindex 'org.hibernate.search.elasticsearch.test.elasticsearch5schemavalidationit$simpledateentity'" + ", mapping 'org.hibernate.search.elasticsearch.test.Elasticsearch5SchemaValidationIT$SimpleDateEntity'," + " property 'myField':" + "\n\tInvalid value for attribute 'type'. Expected 'DATE', actual is 'KEYWORD'" ) .build() ); init( SimpleDateEntity.class ); } @Test public void multipleErrors_multipleIndexManagers() throws Exception { elasticSearchClient.index( SimpleDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleDateEntity.class ).putMapping( "{" + "'dynamic': false," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'store': true" + "}," + "'myField': {" + "'type': 'keyword'" + "}" + "}" + "}" ); elasticSearchClient.index( SimpleBooleanEntity.class ).deleteAndCreate(); elasticSearchClient.type( SimpleBooleanEntity.class ).putMapping( "{" + "'dynamic': false," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'store': true" + "}," + "'myField': {" + "'type': 'boolean'" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMainOrSuppressed( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "\nindex 'org.hibernate.search.elasticsearch.test.elasticsearch5schemavalidationit$simplebooleanentity'," + " mapping 'org.hibernate.search.elasticsearch.test.Elasticsearch5SchemaValidationIT$SimpleBooleanEntity':" + "\n\tInvalid value for attribute 'dynamic'. Expected 'STRICT', actual is 'FALSE'" ) .build() ) .withMainOrSuppressed( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "\nindex 'org.hibernate.search.elasticsearch.test.elasticsearch5schemavalidationit$simpledateentity'," + " mapping 'org.hibernate.search.elasticsearch.test.Elasticsearch5SchemaValidationIT$SimpleDateEntity':" + "\n\tInvalid value for attribute 'dynamic'. Expected 'STRICT', actual is 'FALSE'" + "\nindex 'org.hibernate.search.elasticsearch.test.elasticsearch5schemavalidationit$simpledateentity'," + " mapping 'org.hibernate.search.elasticsearch.test.Elasticsearch5SchemaValidationIT$SimpleDateEntity'," + " property 'myField':" + "\n\tInvalid value for attribute 'type'. Expected 'DATE', actual is 'KEYWORD'" ) .build() ) .build() ); init( SimpleBooleanEntity.class, SimpleDateEntity.class ); } @Test public void property_fields_missing() throws Exception { elasticSearchClient.index( FacetedDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( FacetedDateEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'store': true" + "}," + "'myField': {" + "'type': 'date'" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "property 'myField', field 'myField" + ToElasticsearch.FACET_FIELD_SUFFIX + "'" ) .withMessage( "\n\tMissing field mapping" ) .build() ); init( FacetedDateEntity.class ); } @Test public void property_field_missing() throws Exception { elasticSearchClient.index( FacetedDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( FacetedDateEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'store': true" + "}," + "'myField': {" + "'type': 'date'," + "'fields': {" + "'NOTmyField" + ToElasticsearch.FACET_FIELD_SUFFIX + "': {" + "'type': 'date'" + "}" + "}" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "property 'myField', field 'myField" + ToElasticsearch.FACET_FIELD_SUFFIX + "'" ) .withMessage( "\n\tMissing field mapping" ) .build() ); init( FacetedDateEntity.class ); } @Test public void property_field_attribute_invalid() throws Exception { elasticSearchClient.index( FacetedDateEntity.class ).deleteAndCreate(); elasticSearchClient.type( FacetedDateEntity.class ).putMapping( "{" + "'dynamic': 'strict'," + "'properties': {" + "'id': {" + "'type': 'keyword'," + "'index': 'true'," + "'store': true" + "}," + "'myField': {" + "'type': 'date'," + "'index': 'false'," + "'fields': {" + "'myField" + ToElasticsearch.FACET_FIELD_SUFFIX + "': {" + "'type': 'date'," + "'index': 'false'" + "}" + "}" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "property 'myField', field 'myField" + ToElasticsearch.FACET_FIELD_SUFFIX + "'" ) .withMessage( "\n\tInvalid value for attribute 'index'. Expected 'TRUE', actual is 'FALSE'" ) .build() ); init( FacetedDateEntity.class ); } @Test public void analyzer_missing() throws Exception { elasticSearchClient.index( AnalyzedEntity.class ).deleteAndCreate(); elasticSearchClient.index( AnalyzedEntity.class ).settings( "index.analysis" ).put( "{" + "'char_filter': {" + "'custom-pattern-replace': {" + "'type': 'pattern_replace'," + "'pattern': '[^0-9]'," + "'replacement': '0'," + "'tags': 'CASE_INSENSITIVE|COMMENTS'" + "}" + "}," + "'tokenizer': {" + "'custom-edgeNGram': {" + "'type': 'edgeNGram'," + "'min_gram': 1," + "'max_gram': 10" + "}" + "}," + "'filter': {" + "'custom-keep-types': {" + "'type': 'keep_types'," + "'types': ['<NUM>', '<DOUBLE>']" + "}," + "'custom-word-delimiter': {" + "'type': 'word_delimiter'," + "'generate_word_parts': false" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "analyzer 'analyzerWithElasticsearchFactories':\n\tMissing analyzer" ) .build() ); init( AnalyzedEntity.class ); } @Test public void analyzer_charFilters_invalid() throws Exception { elasticSearchClient.index( AnalyzedEntity.class ).deleteAndCreate(); elasticSearchClient.index( AnalyzedEntity.class ).settings( "index.analysis" ).put( "{" + "'analyzer': {" + "'analyzerWithElasticsearchFactories': {" + "'char_filter': ['html_strip']," + "'tokenizer': 'custom-edgeNGram'," + "'filter': ['custom-keep-types', 'custom-word-delimiter']" + "}" + "}," + "'char_filter': {" + "'custom-pattern-replace': {" + "'type': 'pattern_replace'," + "'pattern': '[^0-9]'," + "'replacement': '0'," + "'tags': 'CASE_INSENSITIVE|COMMENTS'" + "}" + "}," + "'tokenizer': {" + "'custom-edgeNGram': {" + "'type': 'edgeNGram'," + "'min_gram': 1," + "'max_gram': 10" + "}" + "}," + "'filter': {" + "'custom-keep-types': {" + "'type': 'keep_types'," + "'types': ['<NUM>', '<DOUBLE>']" + "}," + "'custom-word-delimiter': {" + "'type': 'word_delimiter'," + "'generate_word_parts': false" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "analyzer 'analyzerWithElasticsearchFactories':\n" + "\tInvalid char filters. Expected '[custom-pattern-replace]'," + " actual is '[html_strip]'" ) .build() ); init( AnalyzedEntity.class ); } @Test public void analyzer_tokenizer_invalid() throws Exception { elasticSearchClient.index( AnalyzedEntity.class ).deleteAndCreate(); elasticSearchClient.index( AnalyzedEntity.class ).settings( "index.analysis" ).put( "{" + "'analyzer': {" + "'analyzerWithElasticsearchFactories': {" + "'char_filter': ['custom-pattern-replace']," + "'tokenizer': 'whitespace'," + "'filter': ['custom-keep-types', 'custom-word-delimiter']" + "}" + "}," + "'char_filter': {" + "'custom-pattern-replace': {" + "'type': 'pattern_replace'," + "'pattern': '[^0-9]'," + "'replacement': '0'," + "'tags': 'CASE_INSENSITIVE|COMMENTS'" + "}" + "}," + "'tokenizer': {" + "'custom-edgeNGram': {" + "'type': 'edgeNGram'," + "'min_gram': 1," + "'max_gram': 10" + "}" + "}," + "'filter': {" + "'custom-keep-types': {" + "'type': 'keep_types'," + "'types': ['<NUM>', '<DOUBLE>']" + "}," + "'custom-word-delimiter': {" + "'type': 'word_delimiter'," + "'generate_word_parts': false" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "analyzer 'analyzerWithElasticsearchFactories':\n" + "\tInvalid tokenizer. Expected 'custom-edgeNGram'," + " actual is 'whitespace'" ) .build() ); init( AnalyzedEntity.class ); } @Test public void analyzer_tokenFilters_invalid() throws Exception { elasticSearchClient.index( AnalyzedEntity.class ).deleteAndCreate(); elasticSearchClient.index( AnalyzedEntity.class ).settings( "index.analysis" ).put( "{" + "'analyzer': {" + "'analyzerWithElasticsearchFactories': {" + "'char_filter': ['custom-pattern-replace']," + "'tokenizer': 'custom-edgeNGram'," + "'filter': ['standard', 'custom-word-delimiter']" + "}" + "}," + "'char_filter': {" + "'custom-pattern-replace': {" + "'type': 'pattern_replace'," + "'pattern': '[^0-9]'," + "'replacement': '0'," + "'tags': 'CASE_INSENSITIVE|COMMENTS'" + "}" + "}," + "'tokenizer': {" + "'custom-edgeNGram': {" + "'type': 'edgeNGram'," + "'min_gram': 1," + "'max_gram': 10" + "}" + "}," + "'filter': {" + "'custom-keep-types': {" + "'type': 'keep_types'," + "'types': ['<NUM>', '<DOUBLE>']" + "}," + "'custom-word-delimiter': {" + "'type': 'word_delimiter'," + "'generate_word_parts': false" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "analyzer 'analyzerWithElasticsearchFactories':\n" + "\tInvalid token filters. Expected '[custom-keep-types, custom-word-delimiter]'," + " actual is '[standard, custom-word-delimiter]'" ) .build() ); init( AnalyzedEntity.class ); } @Test public void charFilter_missing() throws Exception { elasticSearchClient.index( AnalyzedEntity.class ).deleteAndCreate(); elasticSearchClient.index( AnalyzedEntity.class ).settings( "index.analysis" ).put( "{" + "'tokenizer': {" + "'custom-edgeNGram': {" + "'type': 'edgeNGram'," + "'min_gram': 1," + "'max_gram': 10" + "}" + "}," + "'filter': {" + "'custom-keep-types': {" + "'type': 'keep_types'," + "'types': ['<NUM>', '<DOUBLE>']" + "}," + "'custom-word-delimiter': {" + "'type': 'word_delimiter'," + "'generate_word_parts': false" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "char filter 'custom-pattern-replace':\n\tMissing char filter" ) .build() ); init( AnalyzedEntity.class ); } @Test public void tokenizer_missing() throws Exception { elasticSearchClient.index( AnalyzedEntity.class ).deleteAndCreate(); elasticSearchClient.index( AnalyzedEntity.class ).settings( "index.analysis" ).put( "{" + "'char_filter': {" + "'custom-pattern-replace': {" + "'type': 'pattern_replace'," + "'pattern': '[^0-9]'," + "'replacement': '0'," + "'tags': 'CASE_INSENSITIVE|COMMENTS'" + "}" + "}," + "'filter': {" + "'custom-keep-types': {" + "'type': 'keep_types'," + "'types': ['<NUM>', '<DOUBLE>']" + "}," + "'custom-word-delimiter': {" + "'type': 'word_delimiter'," + "'generate_word_parts': false" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "tokenizer 'custom-edgeNGram':\n\tMissing tokenizer" ) .build() ); init( AnalyzedEntity.class ); } @Test public void tokenFilter_missing() throws Exception { elasticSearchClient.index( AnalyzedEntity.class ).deleteAndCreate(); elasticSearchClient.index( AnalyzedEntity.class ).settings( "index.analysis" ).put( "{" + "'char_filter': {" + "'custom-pattern-replace': {" + "'type': 'pattern_replace'," + "'pattern': '[^0-9]'," + "'replacement': '0'," + "'tags': 'CASE_INSENSITIVE|COMMENTS'" + "}" + "}," + "'tokenizer': {" + "'custom-edgeNGram': {" + "'type': 'edgeNGram'," + "'min_gram': 1," + "'max_gram': 10" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "token filter 'custom-keep-types':\n\tMissing token filter" ) .build() ); init( AnalyzedEntity.class ); } @Test public void charFilter_type_invalid() throws Exception { elasticSearchClient.index( AnalyzedEntity.class ).deleteAndCreate(); elasticSearchClient.index( AnalyzedEntity.class ).settings( "index.analysis" ).put( "{" + "'analyzer': {" + "'analyzerWithElasticsearchFactories': {" + "'char_filter': ['custom-pattern-replace']," + "'tokenizer': 'custom-edgeNGram'," + "'filter': ['custom-keep-types', 'custom-word-delimiter']" + "}" + "}," + "'char_filter': {" + "'custom-pattern-replace': {" + "'type': 'html_strip'," + "'pattern': '[^0-9]'," + "'replacement': '0'," + "'tags': 'CASE_INSENSITIVE|COMMENTS'" + "}" + "}," + "'tokenizer': {" + "'custom-edgeNGram': {" + "'type': 'edgeNGram'," + "'min_gram': 1," + "'max_gram': 10" + "}" + "}," + "'filter': {" + "'custom-keep-types': {" + "'type': 'keep_types'," + "'types': ['<NUM>', '<DOUBLE>']" + "}," + "'custom-word-delimiter': {" + "'type': 'word_delimiter'," + "'generate_word_parts': false" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "char filter 'custom-pattern-replace':\n\tInvalid type." + " Expected 'pattern_replace', actual is 'html_strip'" ) .build() ); init( AnalyzedEntity.class ); } @Test public void charFilter_parameter_invalid() throws Exception { elasticSearchClient.index( AnalyzedEntity.class ).deleteAndCreate(); elasticSearchClient.index( AnalyzedEntity.class ).settings( "index.analysis" ).put( "{" + "'analyzer': {" + "'analyzerWithElasticsearchFactories': {" + "'char_filter': ['custom-pattern-replace']," + "'tokenizer': 'custom-edgeNGram'," + "'filter': ['custom-keep-types', 'custom-word-delimiter']" + "}" + "}," + "'char_filter': {" + "'custom-pattern-replace': {" + "'type': 'pattern_replace'," + "'pattern': '[^a-z]'," + "'replacement': '0'," + "'tags': 'CASE_INSENSITIVE|COMMENTS'" + "}" + "}," + "'tokenizer': {" + "'custom-edgeNGram': {" + "'type': 'edgeNGram'," + "'min_gram': 1," + "'max_gram': 10" + "}" + "}," + "'filter': {" + "'custom-keep-types': {" + "'type': 'keep_types'," + "'types': ['<NUM>', '<DOUBLE>']" + "}," + "'custom-word-delimiter': {" + "'type': 'word_delimiter'," + "'generate_word_parts': false" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "char filter 'custom-pattern-replace':\n" + "\tInvalid value for parameter 'pattern'. Expected '\"[^0-9]\"', actual is '\"[^a-z]\"'" ) .build() ); init( AnalyzedEntity.class ); } @Test public void charFilter_parameter_missing() throws Exception { elasticSearchClient.index( AnalyzedEntity.class ).deleteAndCreate(); elasticSearchClient.index( AnalyzedEntity.class ).settings( "index.analysis" ).put( "{" + "'analyzer': {" + "'analyzerWithElasticsearchFactories': {" + "'char_filter': ['custom-pattern-replace']," + "'tokenizer': 'custom-edgeNGram'," + "'filter': ['custom-keep-types', 'custom-word-delimiter']" + "}" + "}," + "'char_filter': {" + "'custom-pattern-replace': {" + "'type': 'pattern_replace'," + "'pattern': '[^0-9]'," + "'replacement': '0'" // Missing "tags" + "}" + "}," + "'tokenizer': {" + "'custom-edgeNGram': {" + "'type': 'edgeNGram'," + "'min_gram': 1," + "'max_gram': 10" + "}" + "}," + "'filter': {" + "'custom-keep-types': {" + "'type': 'keep_types'," + "'types': ['<NUM>', '<DOUBLE>']" + "}," + "'custom-word-delimiter': {" + "'type': 'word_delimiter'," + "'generate_word_parts': false" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "char filter 'custom-pattern-replace':\n" + "\tInvalid value for parameter 'tags'." + " Expected '\"CASE_INSENSITIVE|COMMENTS\"', actual is 'null'" ) .build() ); init( AnalyzedEntity.class ); } @Test public void tokenFilter_parameter_unexpected() throws Exception { elasticSearchClient.index( AnalyzedEntity.class ).deleteAndCreate(); elasticSearchClient.index( AnalyzedEntity.class ).settings( "index.analysis" ).put( "{" + "'analyzer': {" + "'analyzerWithElasticsearchFactories': {" + "'char_filter': ['custom-pattern-replace']," + "'tokenizer': 'custom-edgeNGram'," + "'filter': ['custom-keep-types', 'custom-word-delimiter']" + "}" + "}," + "'char_filter': {" + "'custom-pattern-replace': {" + "'type': 'pattern_replace'," + "'pattern': '[^0-9]'," + "'replacement': '0'," + "'tags': 'CASE_INSENSITIVE|COMMENTS'" + "}" + "}," + "'tokenizer': {" + "'custom-edgeNGram': {" + "'type': 'edgeNGram'," + "'min_gram': 1," + "'max_gram': 10" + "}" + "}," + "'filter': {" + "'custom-keep-types': {" + "'type': 'keep_types'," + "'types': ['<NUM>', '<DOUBLE>']" + "}," + "'custom-word-delimiter': {" + "'type': 'word_delimiter'," + "'generate_word_parts': false," + "'generate_number_parts': false" + "}" + "}" + "}" ); thrown.expect( isException( ElasticsearchSchemaValidationException.class ) .withMessage( VALIDATION_FAILED_MESSAGE_ID ) .withMessage( "filter 'custom-word-delimiter':\n" + "\tInvalid value for parameter 'generate_number_parts'." + " Expected 'null', actual is '\"false\"'" ) .build() ); init( AnalyzedEntity.class ); } @Indexed @Entity public static class SimpleBooleanEntity { @DocumentId @Id Long id; @Field Boolean myField; } @Indexed @Entity public static class SimpleDateEntity { @DocumentId @Id Long id; @Field Date myField; } @Indexed @Entity public static class SimpleTextEntity { @DocumentId @Id Long id; @Field String myField; } @Indexed @Entity public static class SimpleLenientEntity { @DocumentId @Id Long id; @Field(index = Index.NO, store = Store.NO) Long myField; @Field(norms = Norms.NO) String myTextField; } @Indexed @Entity public static class FacetedDateEntity { @DocumentId @Id Long id; @Field(index = Index.NO, analyze = Analyze.NO) @Facet Date myField; } @Indexed @Entity @AnalyzerDef( name = "analyzerWithElasticsearchFactories", charFilters = @CharFilterDef( name = "custom-pattern-replace", factory = ElasticsearchCharFilterFactory.class, params = { @Parameter(name = "type", value = "'pattern_replace'"), @Parameter(name = "pattern", value = "'[^0-9]'"), @Parameter(name = "replacement", value = "'0'"), @Parameter(name = "tags", value = "'CASE_INSENSITIVE|COMMENTS'") } ), tokenizer = @TokenizerDef( name = "custom-edgeNGram", factory = ElasticsearchTokenizerFactory.class, params = { @Parameter(name = "type", value = "edgeNGram"), @Parameter(name = "min_gram", value = "1"), /* * Use a string instead of an integer here on purpose. * We must test that, because we tend to use strings * instead of integers/floats/booleans/etc. when translating * from definitions referring to Lucene factories. */ @Parameter(name = "max_gram", value = "'10'") } ), filters = { @TokenFilterDef( name = "custom-keep-types", factory = ElasticsearchTokenFilterFactory.class, params = { @Parameter(name = "type", value = "'keep_types'"), @Parameter(name = "types", value = "['<NUM>','<DOUBLE>']") } ), @TokenFilterDef( name = "custom-word-delimiter", factory = ElasticsearchTokenFilterFactory.class, params = { @Parameter(name = "type", value = "'word_delimiter'"), @Parameter(name = "generate_word_parts", value = "false") } ) } ) public static class AnalyzedEntity { @DocumentId @Id Long id; @Field(analyzer = @Analyzer(definition = "analyzerWithElasticsearchFactories")) String myField; } }