/* * Copyright 2000-2013 Enonic AS * http://www.enonic.com/license */ package com.enonic.cms.core.search; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.elasticsearch.ElasticSearchException; import org.elasticsearch.ElasticSearchTimeoutException; import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest; import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; import org.elasticsearch.action.admin.indices.delete.DeleteIndexResponse; import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest; import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsResponse; import org.elasticsearch.action.admin.indices.flush.FlushRequest; import org.elasticsearch.action.admin.indices.flush.FlushResponse; import org.elasticsearch.action.admin.indices.mapping.delete.DeleteMappingRequest; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; import org.elasticsearch.action.admin.indices.optimize.OptimizeRequest; import org.elasticsearch.action.admin.indices.optimize.OptimizeResponse; import org.elasticsearch.action.admin.indices.settings.UpdateSettingsRequestBuilder; import org.elasticsearch.action.admin.indices.settings.UpdateSettingsResponse; import org.elasticsearch.action.admin.indices.status.IndexStatus; import org.elasticsearch.action.admin.indices.status.IndicesStatusRequestBuilder; import org.elasticsearch.action.admin.indices.status.IndicesStatusResponse; import org.elasticsearch.action.count.CountRequestBuilder; import org.elasticsearch.action.count.CountResponse; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchType; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.action.support.replication.ReplicationType; import org.elasticsearch.client.Client; import org.elasticsearch.client.Requests; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.get.GetField; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.enonic.cms.core.content.ContentKey; import com.enonic.cms.core.search.builder.ContentIndexData; /** * Created by IntelliJ IDEA. * User: rmh * Date: 2/23/12 * Time: 10:26 AM */ @Service public class ElasticSearchIndexServiceImpl implements ElasticSearchIndexService { private static final SearchType DEFAULT_SEARCH_TYPE = SearchType.QUERY_THEN_FETCH; private static final int MAX_NUM_SEGMENTS = 1; private static final boolean WAIT_FOR_MERGE = true; public static final TimeValue INDEX_REQUEST_TIMEOUT_SECONDS = TimeValue.timeValueSeconds( 60 ); public static final TimeValue DELETE_FROM_INDEX_TIMEOUT_SECONDS = TimeValue.timeValueSeconds( 60 ); public static final String ROUND_ROBIN_SEARCH_PREFERENCE = "_round-robin"; public String searchPreference = "_local"; public String searchTimeout = "5s"; public ReplicationType indexReplicationType = ReplicationType.DEFAULT; private int statusTimeout; public static final TimeValue CLUSTER_NOWAIT_TIMEOUT = TimeValue.timeValueSeconds( 1 ); private IndexSettingBuilder indexSettingBuilder; private ContentIndexRequestCreator contentIndexRequestCreator; private Client client; private final static Logger LOG = LoggerFactory.getLogger( ElasticSearchIndexServiceImpl.class ); public Client getClient() { return client; } @Override public Map<String, String> getIndexSettings( final String indexName ) { final ClusterStateRequest clusterStateRequest = Requests.clusterStateRequest().filterRoutingTable( true ).filterNodes( true ).filteredIndices( indexName ); clusterStateRequest.listenerThreaded( false ); final ClusterStateResponse clusterStateResponse = client.admin().cluster().state( clusterStateRequest ).actionGet(); final MetaData metaData = clusterStateResponse.getState().metaData(); final Map<String, String> indexSettings = Maps.newHashMap(); for ( IndexMetaData indexMetaData : metaData ) { final Settings thisSettings = indexMetaData.settings(); for ( Map.Entry<String, String> entry : thisSettings.getAsMap().entrySet() ) { indexSettings.put( entry.getKey(), entry.getValue() ); } } return indexSettings; } @Override public Map<String, String> getClusterSettings() { final Map<String, String> clusterSettings = Maps.newHashMap(); ClusterStateRequest clusterStateRequest = Requests.clusterStateRequest().listenerThreaded( false ).filterRoutingTable( true ).filterNodes( true ); final ClusterStateResponse clusterStateResponse = client.admin().cluster().state( clusterStateRequest ).actionGet(); for ( Map.Entry<String, String> entry : clusterStateResponse.getState().metaData().persistentSettings().getAsMap().entrySet() ) { clusterSettings.put( entry.getKey() + " (P)", entry.getValue() ); } for ( Map.Entry<String, String> entry : clusterStateResponse.getState().metaData().transientSettings().getAsMap().entrySet() ) { clusterSettings.put( entry.getKey() + " (T)", entry.getValue() ); } return clusterSettings; } @Override public ClusterStateResponse getClusterState() { final ClusterStateRequest clusterStateRequest = Requests.clusterStateRequest(); clusterStateRequest.listenerThreaded( false ); clusterStateRequest.filterRoutingTable( true ); clusterStateRequest.filterMetaData( true ); clusterStateRequest.filterBlocks( true ); return client.admin().cluster().state( clusterStateRequest ).actionGet(); } @Override public NodeInfo getLocalNodeInfo() { final NodesInfoResponse nodeInfos = doGetNodesInfo( new String[]{"_local"} ); return nodeInfos.getAt( 0 ); } @Override public NodesInfoResponse getNodesInfo( final String[] nodeIds ) { return doGetNodesInfo( nodeIds ); } private NodesInfoResponse doGetNodesInfo( final String[] nodeIds ) { final NodesInfoRequest nodesInfoRequest = new NodesInfoRequest( nodeIds ); return client.admin().cluster().nodesInfo( nodesInfoRequest ).actionGet(); } @Override public void updateIndexSetting( final String indexName, final String setting, final String value ) { ClusterStateRequest clusterStateRequest = Requests.clusterStateRequest().filterRoutingTable( true ).filterNodes( true ).filteredIndices( indexName ); clusterStateRequest.listenerThreaded( false ); final ClusterStateResponse clusterStateResponse = client.admin().cluster().state( clusterStateRequest ).actionGet(); Map<String, Object> settingsMap = Maps.newHashMap(); settingsMap.put( setting, value ); final UpdateSettingsResponse updateSettingsResponse = new UpdateSettingsRequestBuilder( this.client.admin().indices(), indexName ).setSettings( settingsMap ).execute().actionGet(); return; } @Override public void updateClusterSettings( final String setting, final String value ) { Map<String, Object> settingsMap = Maps.newHashMap(); settingsMap.put( setting, value ); ClusterUpdateSettingsRequest request = new ClusterUpdateSettingsRequest(); request.transientSettings( settingsMap ); this.client.admin().cluster().updateSettings( request ); return; } @Override public void createIndex( final String indexName ) { LOG.debug( "creating index: " + indexName ); CreateIndexRequest createIndexRequest = new CreateIndexRequest( indexName ); createIndexRequest.settings( indexSettingBuilder.buildIndexSettings() ); try { client.admin().indices().create( createIndexRequest ).actionGet(); } catch ( ElasticSearchException e ) { throw new IndexException( "Failed to create index:" + indexName, e ); } LOG.info( "Created index: " + indexName ); } @Override public void putMapping( final String indexName, final String indexType, final String mapping ) { PutMappingRequest mappingRequest = new PutMappingRequest( indexName ).type( indexType ).source( mapping ); try { this.client.admin().indices().putMapping( mappingRequest ).actionGet(); } catch ( ElasticSearchException e ) { throw new IndexException( "Failed to apply mapping to index: " + indexName, e ); } LOG.info( "Mapping for index " + indexName + ", index-type: " + indexType + " deleted" ); } @Override public void deleteMapping( final String indexName, final IndexType indexType ) { DeleteMappingRequest deleteMappingRequest = new DeleteMappingRequest( indexName ).type( indexType.toString() ); this.client.admin().indices().deleteMapping( deleteMappingRequest ).actionGet(); LOG.info( "Mapping for index " + indexName + ", index-type: " + indexType + " deleted" ); } @Override public boolean delete( final String indexName, final IndexType indexType, final ContentKey contentKey ) { DeleteRequest deleteRequest = new DeleteRequest( indexName, indexType.toString(), contentKey.toString() ); final DeleteResponse deleteResponse; try { deleteResponse = this.client.delete( deleteRequest ).actionGet( DELETE_FROM_INDEX_TIMEOUT_SECONDS ); } catch ( ElasticSearchException e ) { throw new IndexException( "Failed to delete content with key: " + contentKey, e ); } return !deleteResponse.isNotFound(); } @Override public void index( final String indexName, final ContentIndexData contentIndexData ) { Set<IndexRequest> indexRequests = contentIndexRequestCreator.createIndexRequests( indexName, contentIndexData ); for ( IndexRequest indexRequest : indexRequests ) { final IndexResponse indexResponse = doIndex( indexRequest ); LOG.trace( "Content indexed with id: " + indexResponse.getId() ); } } public void index( final IndexRequest request ) { doIndex( request ); } private IndexResponse doIndex( IndexRequest indexRequest ) { try { return this.client.index( indexRequest ).actionGet( INDEX_REQUEST_TIMEOUT_SECONDS ); } catch ( ElasticSearchException e ) { throw new IndexException( "Failed to index content with id: " + indexRequest.id(), e ); } } @Override public boolean get( final String indexName, final IndexType indexType, final ContentKey contentKey ) { final GetRequest getRequest = new GetRequest( indexName, indexType.toString(), contentKey.toString() ); final GetResponse getResponse; try { getResponse = this.client.get( getRequest ).actionGet(); } catch ( ElasticSearchException e ) { throw new IndexException( "Failed to get contentKey with id " + contentKey.toString(), e ); } return getResponse.isExists(); } public long count( final String indexName, final String indexType, final SearchSourceBuilder sourceBuilder ) { final SearchRequest searchRequest = Requests.searchRequest( indexName ). types( indexType ). searchType( DEFAULT_SEARCH_TYPE ). source( sourceBuilder ); setSearchPreference( searchRequest ); final SearchResponse searchResponse = doSearchRequest( searchRequest ); parseSearchResultFailures( searchResponse ); return searchResponse.getHits().getTotalHits(); } private void setSearchPreference( final SearchRequest searchRequest ) { if ( !ROUND_ROBIN_SEARCH_PREFERENCE.equals( this.searchPreference ) ) { searchRequest.preference( this.searchPreference ); } } public long count( final String indexName, final String indexType ) { final CountRequestBuilder countRequestBuilder = new CountRequestBuilder( this.client ); countRequestBuilder.setIndices( indexName ); countRequestBuilder.setTypes( indexType ); final CountResponse countResponse = this.client.count( countRequestBuilder.request() ).actionGet(); return countResponse.getCount(); } @Override public void optimize( final String indexName ) { OptimizeRequest optimizeRequest = new OptimizeRequest( indexName ).maxNumSegments( MAX_NUM_SEGMENTS ).waitForMerge( WAIT_FOR_MERGE ); long start = System.currentTimeMillis(); final OptimizeResponse optimizeResponse = this.client.admin().indices().optimize( optimizeRequest ).actionGet(); long finished = System.currentTimeMillis(); LOG.debug( "Optimized index for " + optimizeResponse.getSuccessfulShards() + " shards in " + ( finished - start ) + " ms" ); } @Override public void deleteIndex( final String indexName ) { final DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest( indexName ); final DeleteIndexResponse deleteIndexResponse = this.client.admin().indices().delete( deleteIndexRequest ).actionGet(); if ( !deleteIndexResponse.isAcknowledged() ) { LOG.warn( "Index " + indexName + " not deleted" ); } else { LOG.debug( "Index " + indexName + " deleted" ); } } @Override public SearchResponse search( final String indexName, final String indexType, final SearchSourceBuilder sourceBuilder ) { final SearchRequest searchRequest = Requests.searchRequest( indexName ). types( indexType ). searchType( DEFAULT_SEARCH_TYPE ). source( sourceBuilder ); setSearchPreference( searchRequest ); final SearchResponse searchResponse = doSearchRequest( searchRequest ); parseSearchResultFailures( searchResponse ); return searchResponse; } @Override public SearchResponse search( final String indexName, final String indexType, final String sourceBuilder ) { SearchRequest searchRequest = new SearchRequest( indexName ). types( indexType ). source( sourceBuilder ); setSearchPreference( searchRequest ); return doSearchRequest( searchRequest ); } @Override public Map<String, GetField> search( final String indexName, final IndexType indexType, final ContentKey contentKey ) { final GetRequest getRequest = new GetRequest( indexName, indexType.toString(), contentKey.toString() ); final GetResponse getResponse = this.client.get( getRequest ).actionGet(); final Map<String, Object> fieldValues = getResponse.getSource(); final Map<String, GetField> fields = new HashMap<String, GetField>(); if ( fieldValues != null ) { for ( String key : fieldValues.keySet() ) { final Object value = fieldValues.get( key ); if ( value instanceof List ) { fields.put( key, new GetField( key, (List) value ) ); } else { fields.put( key, new GetField( key, Lists.newArrayList( value ) ) ); } } } return fields; } private SearchResponse doSearchRequest( final SearchRequest searchRequest ) { final SearchResponse searchResponse; try { searchResponse = this.client.search( searchRequest ).actionGet( this.searchTimeout ); } catch ( ElasticSearchTimeoutException e ) { throw new IndexException( "Search timed out, configured timeout = " + this.searchTimeout, e ); } catch ( ElasticSearchException e ) { throw new IndexException( "Search failed", e ); } return searchResponse; } private void parseSearchResultFailures( final SearchResponse res ) { if ( res.getFailedShards() > 0 ) { final ShardSearchFailure[] shardFailures = res.getShardFailures(); StringBuilder reasonBuilder = new StringBuilder(); for ( ShardSearchFailure failure : shardFailures ) { final String reason = failure.reason(); LOG.error( "Status: " + failure.status() + " - Search failed on shard: " + reason ); reasonBuilder.append( reason ); } throw new IndexException( "Search failed: " + reasonBuilder.toString() ); } } @Override public void flush( final String indexName ) { final FlushRequest flushRequest = Requests.flushRequest( indexName ); final FlushResponse flushResponse = client.admin().indices().flush( flushRequest ).actionGet(); LOG.debug( "Flush request executed with " + flushResponse.getSuccessfulShards() + " successfull shards" ); } @Override public boolean indexExists( final String indexName ) { final IndicesExistsResponse exists = this.client.admin().indices().exists( new IndicesExistsRequest( indexName ) ).actionGet(); return exists.isExists(); } @Override public IndexStatus getIndexStatus( final String indexName ) { final IndicesStatusResponse indicesStatusResponse = new IndicesStatusRequestBuilder( this.client.admin().indices() ). setIndices( indexName ). execute().actionGet(); return indicesStatusResponse.getIndex( indexName ); } @Override public ClusterHealthResponse getClusterHealth( final String indexName, final boolean waitForYellow ) { ClusterHealthRequest request = new ClusterHealthRequest( indexName ); if ( waitForYellow ) { request.waitForYellowStatus().timeout( TimeValue.timeValueSeconds( statusTimeout ) ); } else { request.timeout( CLUSTER_NOWAIT_TIMEOUT ); } final ClusterHealthResponse clusterHealthResponse = this.client.admin().cluster().health( request ).actionGet(); if ( clusterHealthResponse.isTimedOut() ) { LOG.warn( "ElasticSearch cluster health timed out" ); } else { LOG.trace( "ElasticSearch cluster health: Status " + clusterHealthResponse.getStatus().name() + "; " + clusterHealthResponse.getNumberOfNodes() + " nodes; " + clusterHealthResponse.getActiveShards() + " active shards." ); } return clusterHealthResponse; } @Autowired public void setIndexSettingBuilder( final IndexSettingBuilder indexSettingBuilder ) { this.indexSettingBuilder = indexSettingBuilder; } @Autowired public void setClient( Client client ) { this.client = client; } @Autowired public void setContentIndexRequestCreator( ContentIndexRequestCreator contentIndexRequestCreator ) { this.contentIndexRequestCreator = contentIndexRequestCreator; } @Value("${cms.index.statusTimeout}") public void setStatusTimeout( final int statusTimeout ) { this.statusTimeout = statusTimeout; LOG.info( "Setting searchTimeout to " + this.statusTimeout ); } @Value("${cms.index.search.preference}") public void setSearchPreference( final String searchPreference ) { this.searchPreference = searchPreference; LOG.info( "Setting search preference to " + this.searchPreference ); } @Value("${cms.index.search.timeout}") public void setSearchTimeout( final String searchTimeout ) { this.searchTimeout = searchTimeout; LOG.info( "Setting searchTimeout to " + this.searchTimeout ); } @Value("${cms.index.create.replication}") public void setIndexReplicationType( final String indexReplicationType ) { try { this.indexReplicationType = ReplicationType.fromString( indexReplicationType ); LOG.info( "Setting indexReplicationType to " + this.indexReplicationType.name() ); } catch ( Exception e ) { LOG.error( "Could not set replication mode : " + indexReplicationType + ", using default" ); this.indexReplicationType = ReplicationType.DEFAULT; } } }