/* * 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.impl; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.Set; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.search.similarities.Similarity; import org.hibernate.search.backend.BackendFactory; import org.hibernate.search.backend.FlushLuceneWork; import org.hibernate.search.backend.IndexingMonitor; import org.hibernate.search.backend.LuceneWork; import org.hibernate.search.backend.OptimizeLuceneWork; import org.hibernate.search.cfg.Environment; import org.hibernate.search.elasticsearch.cfg.ElasticsearchEnvironment; import org.hibernate.search.elasticsearch.cfg.ElasticsearchIndexStatus; import org.hibernate.search.elasticsearch.cfg.IndexSchemaManagementStrategy; import org.hibernate.search.elasticsearch.client.impl.URLEncodedString; import org.hibernate.search.elasticsearch.logging.impl.Log; import org.hibernate.search.elasticsearch.processor.impl.ElasticsearchWorkProcessor; import org.hibernate.search.elasticsearch.schema.impl.ElasticsearchSchemaCreator; import org.hibernate.search.elasticsearch.schema.impl.ElasticsearchSchemaDropper; import org.hibernate.search.elasticsearch.schema.impl.ElasticsearchSchemaMigrator; import org.hibernate.search.elasticsearch.schema.impl.ElasticsearchSchemaTranslator; import org.hibernate.search.elasticsearch.schema.impl.ElasticsearchSchemaValidator; import org.hibernate.search.elasticsearch.schema.impl.ExecutionOptions; import org.hibernate.search.elasticsearch.schema.impl.model.DynamicType; import org.hibernate.search.elasticsearch.schema.impl.model.IndexMetadata; import org.hibernate.search.elasticsearch.spi.ElasticsearchIndexManagerType; import org.hibernate.search.elasticsearch.work.impl.ElasticsearchWork; import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator; import org.hibernate.search.engine.service.spi.ServiceManager; import org.hibernate.search.engine.spi.EntityIndexBinding; import org.hibernate.search.exception.AssertionFailure; import org.hibernate.search.indexes.serialization.spi.LuceneWorkSerializer; import org.hibernate.search.indexes.spi.IndexManager; import org.hibernate.search.indexes.spi.IndexManagerType; import org.hibernate.search.indexes.spi.IndexNameNormalizer; import org.hibernate.search.indexes.spi.ReaderProvider; import org.hibernate.search.spi.WorkerBuildContext; import org.hibernate.search.util.StringHelper; import org.hibernate.search.util.configuration.impl.ConfigurationParseHelper; import org.hibernate.search.util.logging.impl.LoggerFactory; /** * An {@link IndexManager} applying indexing work to an Elasticsearch server. * * @author Gunnar Morling */ public class ElasticsearchIndexManager implements IndexManager, IndexNameNormalizer { static final Log LOG = LoggerFactory.make( Log.class ); /** * The index name for Hibernate Search, which is actually * the index <em>manager</em> name. * <p> * Following the behavior of Lucene index managers, this * name will reflect any annotation-based index name override * ({@code @Indexed(index = "override")}) but will ignore * configuration-based override * ({@code hibernate.search.my.package.MyClass.indexName = foo}), * which is only reflected in {@link #actualIndexName}. */ private String indexName; /** * The index name for the Elasticsearch module, i.e. the * actual name of the underlying Elasticsearch index. */ private URLEncodedString actualIndexName; private boolean refreshAfterWrite; private boolean sync; private IndexSchemaManagementStrategy schemaManagementStrategy; private ExecutionOptions schemaManagementExecutionOptions; private Similarity similarity; private ExtendedSearchIntegrator searchIntegrator; private final Set<Class<?>> containedEntityTypes = new HashSet<>(); private boolean indexInitialized = false; private boolean indexCreatedByHibernateSearch = false; private final Set<Class<?>> initializedContainedEntityTypes = new HashSet<>(); private ServiceManager serviceManager; private ElasticsearchService elasticsearchService; private ElasticsearchIndexWorkVisitor visitor; private ElasticsearchWorkProcessor workProcessor; // Lifecycle @Override public void initialize(String indexName, Properties properties, Similarity similarity, WorkerBuildContext context) { this.serviceManager = context.getServiceManager(); this.indexName = indexName; this.schemaManagementStrategy = getIndexManagementStrategy( properties ); final ElasticsearchIndexStatus requiredIndexStatus = getRequiredIndexStatus( properties ); final int indexManagementWaitTimeout = getIndexManagementWaitTimeout( properties ); final boolean multitenancyEnabled = context.isMultitenancyEnabled(); final DynamicType dynamicMapping = getDynamicMapping( properties ); this.schemaManagementExecutionOptions = new ExecutionOptions() { @Override public ElasticsearchIndexStatus getRequiredIndexStatus() { return requiredIndexStatus; } @Override public int getIndexManagementTimeoutInMs() { return indexManagementWaitTimeout; } @Override public boolean isMultitenancyEnabled() { return multitenancyEnabled; } @Override public DynamicType getDynamicMapping() { return dynamicMapping; } }; String overriddenIndexName = getOverriddenIndexName( indexName, properties ); this.actualIndexName = ElasticsearchIndexNameNormalizer.getElasticsearchIndexName( overriddenIndexName ); this.refreshAfterWrite = getRefreshAfterWrite( properties ); this.sync = BackendFactory.isConfiguredAsSync( properties ); this.similarity = similarity; this.elasticsearchService = serviceManager.requestService( ElasticsearchService.class ); this.visitor = new ElasticsearchIndexWorkVisitor( this.actualIndexName, this.refreshAfterWrite, context.getUninitializedSearchIntegrator(), elasticsearchService.getWorkFactory() ); this.workProcessor = elasticsearchService.getWorkProcessor(); } /** * @return the ElasticsearchService used by this index manager. */ public ElasticsearchService getElasticsearchService() { return elasticsearchService; } private static String getOverriddenIndexName(String indexName, Properties properties) { String name = properties.getProperty( Environment.INDEX_NAME_PROP_NAME ); return name != null ? name : indexName; } private static IndexSchemaManagementStrategy getIndexManagementStrategy(Properties properties) { String propertyValue = properties.getProperty( ElasticsearchEnvironment.INDEX_SCHEMA_MANAGEMENT_STRATEGY ); if ( StringHelper.isNotEmpty( propertyValue ) ) { return IndexSchemaManagementStrategy.interpretPropertyValue( propertyValue ); } else { return ElasticsearchEnvironment.Defaults.INDEX_SCHEMA_MANAGEMENT_STRATEGY; } } private static int getIndexManagementWaitTimeout(Properties properties) { int timeout = ConfigurationParseHelper.getIntValue( properties, ElasticsearchEnvironment.INDEX_MANAGEMENT_WAIT_TIMEOUT, ElasticsearchEnvironment.Defaults.INDEX_MANAGEMENT_WAIT_TIMEOUT ); if ( timeout < 0 ) { throw LOG.negativeTimeoutValue( timeout ); } return timeout; } private static DynamicType getDynamicMapping(Properties properties) { String status = ConfigurationParseHelper.getString( properties, ElasticsearchEnvironment.DYNAMIC_MAPPING, ElasticsearchEnvironment.Defaults.DYNAMIC_MAPPING.name() ); return DynamicType.valueOf( status.toUpperCase( Locale.ROOT ) ); } private static ElasticsearchIndexStatus getRequiredIndexStatus(Properties properties) { String status = ConfigurationParseHelper.getString( properties, ElasticsearchEnvironment.REQUIRED_INDEX_STATUS, null ); if ( status == null ) { return ElasticsearchEnvironment.Defaults.REQUIRED_INDEX_STATUS; } else { return ElasticsearchIndexStatus.fromString( status ); } } private static boolean getRefreshAfterWrite(Properties properties) { return ConfigurationParseHelper.getBooleanValue( properties, ElasticsearchEnvironment.REFRESH_AFTER_WRITE, ElasticsearchEnvironment.Defaults.REFRESH_AFTER_WRITE ); } @Override public void destroy() { if ( schemaManagementStrategy == IndexSchemaManagementStrategy.DROP_AND_CREATE_AND_DROP ) { elasticsearchService.getSchemaDropper().dropIfExisting( actualIndexName, schemaManagementExecutionOptions ); } workProcessor = null; visitor = null; elasticsearchService = null; serviceManager.releaseService( ElasticsearchService.class ); schemaManagementExecutionOptions = null; serviceManager = null; } @Override public void setSearchFactory(ExtendedSearchIntegrator boundSearchIntegrator) { this.searchIntegrator = boundSearchIntegrator; initializeIndex(); } private void initializeIndex() { if ( !indexInitialized ) { /* * The value of this variable is only used for the "CREATE" schema management * strategy, but we store it in any case, just to be consistent. */ indexCreatedByHibernateSearch = initializeIndex( containedEntityTypes ); indexInitialized = true; initializedContainedEntityTypes.addAll( containedEntityTypes ); } else { Set<Class<?>> notYetInitializedContainedEntityTypes = new HashSet<>( containedEntityTypes ); notYetInitializedContainedEntityTypes.removeAll( initializedContainedEntityTypes ); if ( notYetInitializedContainedEntityTypes.isEmpty() ) { return; // Nothing to do } reinitializeIndex( notYetInitializedContainedEntityTypes ); initializedContainedEntityTypes.addAll( notYetInitializedContainedEntityTypes ); } } /** * Called only the first time we must initialize the index. * * @param entityTypesToInitialize The entity types whose mapping will be added to the index * (if it's part of the schema management strategy). * @return {@code true} if the index had to be created, {@code false} otherwise. */ private boolean initializeIndex(Set<Class<?>> entityTypesToInitialize) { if ( schemaManagementStrategy == IndexSchemaManagementStrategy.NONE ) { return false; } boolean createdIndex; ElasticsearchSchemaCreator schemaCreator = elasticsearchService.getSchemaCreator(); IndexMetadata indexMetadata = createIndexMetadata( entityTypesToInitialize ); switch ( schemaManagementStrategy ) { case CREATE: createdIndex = schemaCreator.createIndexIfAbsent( indexMetadata, schemaManagementExecutionOptions ); if ( createdIndex ) { schemaCreator.createMappings( indexMetadata, schemaManagementExecutionOptions ); } break; case DROP_AND_CREATE: case DROP_AND_CREATE_AND_DROP: ElasticsearchSchemaDropper schemaDropper = elasticsearchService.getSchemaDropper(); schemaDropper.dropIfExisting( actualIndexName, schemaManagementExecutionOptions ); schemaCreator.createIndex( indexMetadata, schemaManagementExecutionOptions ); schemaCreator.createMappings( indexMetadata, schemaManagementExecutionOptions ); createdIndex = true; break; case UPDATE: createdIndex = schemaCreator.createIndexIfAbsent( indexMetadata, schemaManagementExecutionOptions ); if ( createdIndex ) { schemaCreator.createMappings( indexMetadata, schemaManagementExecutionOptions ); } else { ElasticsearchSchemaMigrator schemaMigrator = elasticsearchService.getSchemaMigrator(); schemaMigrator.migrate( indexMetadata, schemaManagementExecutionOptions ); } break; case VALIDATE: ElasticsearchSchemaValidator schemaValidator = elasticsearchService.getSchemaValidator(); schemaCreator.checkIndexExists( actualIndexName, schemaManagementExecutionOptions ); schemaValidator.validate( indexMetadata, schemaManagementExecutionOptions ); createdIndex = false; break; default: throw new AssertionFailure( "Unexpected schema management strategy: " + schemaManagementStrategy ); } return createdIndex; } /** * Called for any initialization following the {@link #initialize(String, Properties, Similarity, WorkerBuildContext) first one} * (upon subsequent search factory changes). * * <p>This method only may add new mappings to the existing index (depending on the strategy), but will never * create or drop the index (since it's supposed to have been created by Hibernate Search already, if necessary). * * @param indexCreatedByHibernateSearch If the index was created by Hibernate Search in {@link #initializeIndex(Set)}. * @param entityTypesToInitialize The entity types whose mapping will be added to the index * (if it's part of the schema management strategy). */ private void reinitializeIndex(Set<Class<?>> entityTypesToInitialize) { if ( schemaManagementStrategy == IndexSchemaManagementStrategy.NONE ) { return; } ElasticsearchSchemaCreator schemaCreator = elasticsearchService.getSchemaCreator(); IndexMetadata indexMetadata = createIndexMetadata( entityTypesToInitialize ); switch ( schemaManagementStrategy ) { case CREATE: if ( indexCreatedByHibernateSearch ) { // Don't alter a pre-existing index schemaCreator.createMappings( indexMetadata, schemaManagementExecutionOptions ); } break; case DROP_AND_CREATE: case DROP_AND_CREATE_AND_DROP: schemaCreator.createMappings( indexMetadata, schemaManagementExecutionOptions ); break; case UPDATE: ElasticsearchSchemaMigrator schemaMigrator = elasticsearchService.getSchemaMigrator(); schemaMigrator.migrate( indexMetadata, schemaManagementExecutionOptions ); break; case VALIDATE: ElasticsearchSchemaValidator schemaValidator = elasticsearchService.getSchemaValidator(); schemaValidator.validate( indexMetadata, schemaManagementExecutionOptions ); break; default: throw new AssertionFailure( "Unexpected schema management strategy: " + schemaManagementStrategy ); } } private IndexMetadata createIndexMetadata(Collection<Class<?>> classes) { List<EntityIndexBinding> descriptors = new ArrayList<>(); for ( Class<?> entityType : classes ) { EntityIndexBinding descriptor = searchIntegrator.getIndexBinding( entityType ); descriptors.add( descriptor ); } ElasticsearchSchemaTranslator schemaTranslator = elasticsearchService.getSchemaTranslator(); return schemaTranslator.translate( actualIndexName, descriptors, schemaManagementExecutionOptions ); } @Override public void addContainedEntity(Class<?> entity) { containedEntityTypes.add( entity ); } // Getters @Override public String getIndexName() { return indexName; } @Override public ReaderProvider getReaderProvider() { throw LOG.indexManagerReaderProviderUnsupported(); } @Override public Set<Class<?>> getContainedTypes() { return containedEntityTypes; } @Override public Similarity getSimilarity() { return similarity; } @Override public Analyzer getAnalyzer(String name) { return searchIntegrator.getAnalyzer( name ); } @Override public LuceneWorkSerializer getSerializer() { return null; } @Override public void flushAndReleaseResources() { ElasticsearchWork<?> flushWork = elasticsearchService.getWorkFactory().flush() .index( actualIndexName ) .build(); workProcessor.awaitAsyncProcessingCompletion(); workProcessor.executeSyncSafe( Collections.singletonList( flushWork ) ); } @Override public String getActualIndexName() { return actualIndexName.original; } public boolean needsRefreshAfterWrite() { return refreshAfterWrite; } // Runtime ops @Override public void performOperations(List<LuceneWork> workList, IndexingMonitor monitor) { List<ElasticsearchWork<?>> elasticsearchWorks = new ArrayList<>( workList.size() ); for ( LuceneWork luceneWork : workList ) { elasticsearchWorks.add( luceneWork.acceptIndexWorkVisitor( visitor, monitor ) ); } if ( sync ) { workProcessor.executeSyncSafe( elasticsearchWorks ); } else { for ( ElasticsearchWork<?> work : elasticsearchWorks ) { workProcessor.executeAsync( work ); } } } @Override public void performStreamOperation(LuceneWork singleOperation, IndexingMonitor monitor, boolean forceAsync) { ElasticsearchWork<?> elasticsearchWork = singleOperation.acceptIndexWorkVisitor( visitor, monitor ); if ( singleOperation instanceof FlushLuceneWork ) { workProcessor.awaitAsyncProcessingCompletion(); executeWork( elasticsearchWork, true ); } else { executeWork( elasticsearchWork, false ); } } private void executeWork(ElasticsearchWork<?> elasticsearchWork, boolean sync) { if ( elasticsearchWork != null ) { if ( sync ) { workProcessor.executeSyncSafe( Collections.<ElasticsearchWork<?>>singletonList( elasticsearchWork ) ); } else { workProcessor.executeAsync( elasticsearchWork ); } } } @Override public void awaitAsyncProcessingCompletion() { workProcessor.awaitAsyncProcessingCompletion(); } @Override public void optimize() { performStreamOperation( OptimizeLuceneWork.INSTANCE, null, false ); } @Override public String toString() { return "ElasticsearchIndexManager [actualIndexName=" + actualIndexName + "]"; } @Override public IndexManagerType getIndexManagerType() { return ElasticsearchIndexManagerType.INSTANCE; } }