/** * Copyright 2011 Molindo GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package at.molindo.elastic.compass; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import java.io.IOException; import java.sql.Date; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; import org.compass.core.Property.Index; import org.compass.core.Property.Store; import org.compass.core.Property.TermVector; import org.compass.core.converter.Converter; import org.compass.core.engine.SearchEngineException; import org.compass.core.mapping.AllMapping; import org.compass.core.mapping.BoostPropertyMapping; import org.compass.core.mapping.CompassMapping; import org.compass.core.mapping.ExcludeFromAll; import org.compass.core.mapping.Mapping; import org.compass.core.mapping.ResourceMapping; import org.compass.core.mapping.ResourcePropertyMapping; import org.compass.core.mapping.osem.AbstractCollectionMapping; import org.compass.core.mapping.osem.AbstractRefAliasMapping; import org.compass.core.mapping.osem.ClassMapping; import org.compass.core.mapping.support.AbstractResourceMapping; import org.elasticsearch.ElasticSearchException; import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; import org.elasticsearch.action.admin.indices.status.IndexStatus; import org.elasticsearch.action.admin.indices.status.IndicesStatusResponse; import org.elasticsearch.client.AdminClient; import org.elasticsearch.client.Client; import org.elasticsearch.client.IndicesAdminClient; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.indices.IndexMissingException; import at.molindo.utils.collections.CollectionUtils; import at.molindo.utils.collections.IteratorUtils; import at.molindo.utils.data.StringUtils; import at.molindo.utils.reflect.ClassUtils; /** * manages an index */ public class ElasticIndex { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory .getLogger(ElasticIndex.class); private final String _alias; private final Client _client; private final CompassMapping _mapping; private final ElasticSettings _settings; private String _index; private Map<String, Map<String, Mapping>> _aliasFields = new HashMap<String, Map<String, Mapping>>(); public ElasticIndex(ElasticSettings settings, Client client, CompassMapping mapping) { if (settings == null) { throw new NullPointerException("settings"); } _settings = settings; _alias = _settings.getAliasName(); if (client == null) { throw new NullPointerException("client"); } _client = client; if (mapping == null) { throw new NullPointerException("mapping"); } _mapping = mapping; // root mappings to alias list for (ResourceMapping rootMapping : mapping.getRootMappings()) { HashMap<String, Mapping> map = new HashMap<String, Mapping>(); addFields(rootMapping, map, new HashSet<ResourceMapping>()); if (log.isDebugEnabled()) { log.debug("alias '" + rootMapping.getAlias() + "' with fields " + map.keySet()); } _aliasFields.put(rootMapping.getAlias(), Collections.unmodifiableMap(map)); } } protected void addFields(ResourceMapping mapping, HashMap<String, Mapping> map, Set<ResourceMapping> added) { if (!added.add(mapping)) { return; } if (mapping.getUIDPath() != null) { map.put(mapping.getUIDPath(), mapping); } if (mapping instanceof ClassMapping) { ClassMapping clsMapping = (ClassMapping) mapping; if (clsMapping.getClazz().isEnum() && clsMapping.getEnumNamePath() != null) { map.put(clsMapping.getEnumNamePath().getPath(), clsMapping); } if (clsMapping.isPoly() && clsMapping.getClassPath() != null) { map.put(clsMapping.getClassPath().getPath(), clsMapping); } } if (mapping.getResourcePropertyMappings() != null) { for (ResourcePropertyMapping property : mapping.getResourcePropertyMappings()) { if (property.getPath() != null && property.getStore() != Store.NO) { String field = property.getPath().getPath(); map.put(field, property); } } } for (Mapping m : IteratorUtils.iterable(mapping.mappingsIt())) { if (m instanceof AbstractCollectionMapping) { AbstractCollectionMapping col = (AbstractCollectionMapping) m; if (col.getCollectionType() == AbstractCollectionMapping.CollectionType.UNKNOWN && col.getCollectionTypePath() != null) { map.put(col.getCollectionTypePath().getPath(), col); } if (col.getColSizePath() != null) { map.put(col.getColSizePath().getPath(), col); } m = col.getElementMapping(); } if (m instanceof AbstractRefAliasMapping) { AbstractRefAliasMapping comp = (AbstractRefAliasMapping) m; for (ClassMapping refCls : comp.getRefClassMappings()) { addFields(refCls, map, added); } } } } private void createIndex() { _index = generateIndexName(); // fix dangling aliases IndicesAdminClient indicesAdminClient = indicesAdminClient(); try { IndicesStatusResponse response = indicesAdminClient.prepareStatus(_alias).execute() .actionGet(); for (Map.Entry<String, IndexStatus> e : response.getIndices().entrySet()) { // check for unknown indexes try { indicesAdminClient.prepareStatus(e.getKey()).execute().actionGet(); // index exists - don't delete throw new SearchEngineException("can't crate index for alias '" + _alias + "' as it is mapped to '" + e.getKey() + "'"); } catch (IndexMissingException e1) { // alias unknown pointing to unknown index, delete alias indicesAdminClient.prepareAliases().removeAlias(e.getKey(), _alias); } } } catch (IndexMissingException e) { // alias unknown, that's what we want } indicesAdminClient.prepareCreate(getIndex()).execute().actionGet(); indicesAdminClient.prepareAliases().addAlias(getIndex(), _alias).execute().actionGet(); putMappings(); } public void putMappings() { for (ResourceMapping mapping : _mapping.getRootMappings()) { indicesAdminClient().preparePutMapping(getIndex()).setType(mapping.getAlias()) .setSource(toMappingSource((AbstractResourceMapping) mapping)).execute() .actionGet(); // if (!resp.acknowledged()) { // throw new SearchEngineException("failed to put mapping for type " // + mapping.getAlias()); // } } } public synchronized void deleteIndex() { String index = getIndex(false); if (index != null) { try { log.info("deleting alias '" + _alias + "' of index '" + index + "'"); IndicesAdminClient client = indicesAdminClient(); client.prepareAliases().removeAlias(index, _alias).execute().actionGet(); } catch (IndexMissingException e) { log.trace("alias " + _alias + " didn't exist, ignore"); } ClusterStateResponse state = adminClient().cluster().prepareState().execute() .actionGet(); IndexMetaData indexState = state.getState().getMetaData().getIndices().get(index); if (indexState != null) { if (indexState.getAliases().size() == 0) { log.info("deleting index '" + index + "' without aliases"); indicesAdminClient().prepareDelete(index).execute().actionGet(); } else { log.info("keeping index '" + index + "' with aliases " + indexState.getAliases()); } } } } public synchronized void verifyIndex() { IndicesAdminClient indicesAdminClient = indicesAdminClient(); log.info("verifying index with alias '" + _alias + "'"); try { IndicesStatusResponse response = indicesAdminClient.prepareStatus(_alias).execute() .actionGet(); Map<String, IndexStatus> indices = response.getIndices(); if (indices.size() > 1) { throw new SearchEngineException("alias name points to more than one index, was '" + _alias + "'"); } IndexStatus indexStatus = CollectionUtils.firstValue(indices); if (indexStatus == null) { // alias without index throw new IndexMissingException(null); } _index = indexStatus.getIndex(); if (getIndex().equals(_alias)) { throw new SearchEngineException("alias name must not point to index, was '" + _alias + "'"); } log.info("index '" + _alias + "' verified successfully"); // verify mappings putMappings(); } catch (IndexMissingException e) { // alias unknown, create new index createIndex(); } catch (ElasticSearchException e) { throw new SearchEngineException("failed to verify index '" + _alias + "'", e); } } private AdminClient adminClient() { return _client.admin(); } private IndicesAdminClient indicesAdminClient() { return adminClient().indices(); } // @formatter:off private XContentBuilder toMappingSource(AbstractResourceMapping mapping) { try { XContentBuilder builder = jsonBuilder().startObject(); // start alias builder.startObject(mapping.getAlias()); // start properties builder.startObject("properties"); for (Map.Entry<String, Mapping> e : getFieldMapping(mapping.getAlias()).entrySet()) { String field = e.getKey(); Mapping m = e.getValue(); // TODO should we really use string only? ElasticType type = toType(m); if (m instanceof ResourcePropertyMapping) { ResourcePropertyMapping property = (ResourcePropertyMapping) m; builder .startObject(field) .field("type", type.getName()) .field("index", index(property.getIndex())) .field("store", store(property.getStore())) .field("include_in_all", includeInAll(property.getExcludeFromAll())) .field("term_vector", termVector(property.getTermVector())) .field("boost", property.getBoost()); String analyzer = analyzer(mapping, property); if (!StringUtils.empty(analyzer)) { builder.field("analyzer", analyzer); } builder.endObject(); } else { // col size / class builder .startObject(field) .field("type", type.getName()) .field("index", index(Index.NO)) .field("store", store(Store.YES)) .field("include_in_all", includeInAll(ExcludeFromAll.YES)) .field("term_vector", termVector(TermVector.NO)) .endObject(); } } builder.endObject(); // end properties // all AllMapping allMapping = mapping.getAllMapping(); if (allMapping.isExcludeAlias()) { log.warn("excluding _type from _all not supported, type " + mapping.getAlias()); } builder .startObject("_all") .field("enabled", allMapping.isSupported() != Boolean.FALSE) .field("term_vector", termVector(allMapping.getTermVector())) .endObject(); // end all // boost BoostPropertyMapping boostMapping = mapping.getBoostPropertyMapping(); if (boostMapping != null) { builder .startObject("_boost") .field("name", boostMapping.getPath().getPath()) .field("null_value", boostMapping.getDefaultBoost()) .endObject(); } else { // TODO what about mapping.getBoost()? } // end boost // analyzer if (mapping.getAnalyzerController() != null) { builder .startObject("_analyzer") .field("path", mapping.getAnalyzerController().getPath().getPath()) .endObject(); } builder .startObject("_source") .field("enabled", _settings.isStoreSource()) .endObject(); builder.endObject(); // end alias return builder.endObject(); } catch (IOException e) { throw new SearchEngineException("failed to create mapping source", e); } } // @formatter:on private ElasticType toType(Mapping m) { if (m instanceof ResourcePropertyMapping) { ResourcePropertyMapping property = (ResourcePropertyMapping) m; if (property.getConverter() != null) { Class<?> converterClass = property.getConverter().getClass(); Class<?> type = ClassUtils.getTypeArgument(converterClass, Converter.class); if (type != null) { if (Number.class.isAssignableFrom(type)) { if (Integer.class.isAssignableFrom(type)) { return ElasticType.INTEGER; } else if (Long.class.isAssignableFrom(type)) { return ElasticType.LONG; } else if (Float.class.isAssignableFrom(type)) { return ElasticType.FLOAT; } else if (Double.class.isAssignableFrom(type)) { return ElasticType.DOUBLE; } } else if (Date.class.isAssignableFrom(type)) { return ElasticType.DATE; } else if (Boolean.class.isAssignableFrom(type)) { return ElasticType.BOOLEAN; } } } } return ElasticType.STRING; } @SuppressWarnings("deprecation") private String index(Index index) { switch (index) { case NOT_ANALYZED: case UN_TOKENIZED: return "not_analyzed"; case NO: return "no"; case ANALYZED: case TOKENIZED: return "analyzed"; default: throw new SearchEngineException("unknown index type: " + index); } } private String store(Store store) { switch (store) { case NO: return "no"; case YES: case COMPRESS: return "yes"; default: throw new SearchEngineException("unknown store type: " + store); } } private boolean includeInAll(ExcludeFromAll excludeFromAll) { switch (excludeFromAll) { case NO: case NO_ANALYZED: return true; case YES: return false; default: throw new SearchEngineException("unknown excludeFromAll type: " + excludeFromAll); } } private String termVector(TermVector termVector) { switch (termVector) { case NO: return "no"; case YES: return "yes"; case WITH_OFFSETS: return "with_offsets"; case WITH_POSITIONS: return "with_positions"; case WITH_POSITIONS_OFFSETS: return "with_positions_offsets"; default: throw new SearchEngineException("unknown termVector type: " + termVector); } } private String analyzer(AbstractResourceMapping mapping, ResourcePropertyMapping property) { if (!StringUtils.empty(property.getAnalyzer())) { return property.getAnalyzer(); } else if (!StringUtils.empty(mapping.getAnalyzer())) { return mapping.getAnalyzer(); } else { return null; } } private String generateIndexName() { return UUID.randomUUID().toString(); } public String getAlias() { return _alias; } private String getIndex() { return getIndex(true); } private String getIndex(boolean create) { if (_index == null && create) { verifyIndex(); } return _index; } public ElasticSettings getSettings() { return _settings; } public void addAlias(String alias) { String index = getIndex(); try { indicesAdminClient().prepareAliases().addAlias(index, alias).execute().actionGet(); } catch (ElasticSearchException e) { throw new SearchEngineException("failed to add alias '" + alias + "' to index '" + index + "'"); } } public Map<String, Mapping> getFieldMapping(String type) { Map<String, Mapping> fieldMapping = _aliasFields.get(type); if (fieldMapping == null) { throw new SearchEngineException("alias does not exist " + type); } return fieldMapping; } public String[] getTypes() { return _aliasFields.keySet().toArray(new String[_aliasFields.size()]); } }