All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.vertexium.elasticsearch5.Elasticsearch5SearchIndex Maven / Gradle / Ivy

There is a newer version: 4.10.0
Show newest version
package org.vertexium.elasticsearch5;

import com.carrotsearch.hppc.cursors.ObjectCursor;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import io.netty.util.internal.ConcurrentSet;
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.NodesInfoResponse;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse;
import org.elasticsearch.action.bulk.BulkItemResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateRequestBuilder;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.cluster.metadata.MappingMetaData;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.collect.ImmutableOpenMap;
import org.elasticsearch.common.geo.builders.CircleBuilder;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.PluginInfo;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import org.vertexium.Edge;
import org.vertexium.*;
import org.vertexium.elasticsearch5.bulk.BulkItem;
import org.vertexium.elasticsearch5.bulk.BulkUpdateService;
import org.vertexium.elasticsearch5.bulk.BulkUpdateServiceConfiguration;
import org.vertexium.elasticsearch5.utils.ElasticsearchRequestUtils;
import org.vertexium.metric.VertexiumMetricRegistry;
import org.vertexium.mutation.*;
import org.vertexium.property.StreamingPropertyValue;
import org.vertexium.query.*;
import org.vertexium.search.SearchIndex;
import org.vertexium.search.SearchIndexWithVertexPropertyCountByValue;
import org.vertexium.type.*;
import org.vertexium.util.*;

import java.io.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static org.elasticsearch.common.geo.builders.ShapeBuilder.*;
import static org.vertexium.elasticsearch5.ElasticsearchPropertyNameInfo.PROPERTY_NAME_PATTERN;
import static org.vertexium.elasticsearch5.utils.SearchResponseUtils.checkForFailures;
import static org.vertexium.util.IterableUtils.toList;
import static org.vertexium.util.Preconditions.checkNotNull;
import static org.vertexium.util.StreamUtils.stream;

public class Elasticsearch5SearchIndex implements SearchIndex, SearchIndexWithVertexPropertyCountByValue {
    private static final VertexiumLogger LOGGER = VertexiumLoggerFactory.getLogger(Elasticsearch5SearchIndex.class);
    protected static final VertexiumLogger MUTATION_LOGGER = VertexiumLoggerFactory.getMutationLogger(SearchIndex.class);
    public static final String ELEMENT_ID_FIELD_NAME = "__elementId";
    public static final String ELEMENT_TYPE_FIELD_NAME = "__elementType";
    public static final String VISIBILITY_FIELD_NAME = "__visibility";
    public static final String HIDDEN_VERTEX_FIELD_NAME = "__hidden";
    public static final String HIDDEN_PROPERTY_FIELD_NAME = "__hidden_property";
    public static final String OUT_VERTEX_ID_FIELD_NAME = "__outVertexId";
    public static final String IN_VERTEX_ID_FIELD_NAME = "__inVertexId";
    public static final String EDGE_LABEL_FIELD_NAME = "__edgeLabel";
    public static final String EXTENDED_DATA_TABLE_NAME_FIELD_NAME = "__extendedDataTableName";
    public static final String EXTENDED_DATA_TABLE_ROW_ID_FIELD_NAME = "__extendedDataRowId";
    public static final String EXTENDED_DATA_TABLE_COLUMN_VISIBILITIES_FIELD_NAME = "__extendedDataColumnVisibilities";
    public static final String ADDITIONAL_VISIBILITY_FIELD_NAME = "__additionalVisibility";
    public static final String ADDITIONAL_VISIBILITY_METADATA_PREFIX = "elasticsearch_additionalVisibility_";
    public static final String EXACT_MATCH_FIELD_NAME = "exact";
    public static final String EXACT_MATCH_PROPERTY_NAME_SUFFIX = "." + EXACT_MATCH_FIELD_NAME;
    public static final String GEO_PROPERTY_NAME_SUFFIX = "_g";
    public static final String GEO_POINT_PROPERTY_NAME_SUFFIX = "_gp"; // Used for geo hash aggregation of geo points
    public static final String LOWERCASER_NORMALIZER_NAME = "visallo_lowercaser";
    public static final int EXACT_MATCH_IGNORE_ABOVE_LIMIT = 10000;
    public static final String FIELDNAME_DOT_REPLACEMENT = "-_-";
    private static final int MAX_RETRIES = 10;
    private final Client client;
    private final ElasticsearchSearchIndexConfiguration config;
    private final Graph graph;
    private Map indexInfos;
    private Set additionalVisibilitiesCache = new ConcurrentSet<>();
    private final ReadWriteLock indexInfosLock = new ReentrantReadWriteLock();
    private int indexInfosLastSize = -1; // Used to prevent creating a index name array each time
    private String[] indexNamesAsArray;
    private IndexSelectionStrategy indexSelectionStrategy;
    private boolean allFieldEnabled;
    public static final String AGGREGATION_HAS_NOT_SUFFIX = "HAS_NOT_FILTER";
    public static final Pattern AGGREGATION_NAME_PATTERN = Pattern.compile("(.*?)_([0-9a-f]+|" + AGGREGATION_HAS_NOT_SUFFIX + ")");
    private final PropertyNameVisibilitiesStore propertyNameVisibilitiesStore;
    private final BulkUpdateService bulkUpdateService;
    private final String geoShapePrecision;
    private final String geoShapeErrorPct;
    private boolean serverPluginInstalled;
    private final IdStrategy idStrategy = new IdStrategy();
    private final IndexRefreshTracker indexRefreshTracker;
    private Integer logRequestSizeLimit;
    private final Elasticsearch5ExceptionHandler exceptionHandler;
    private final boolean refreshIndexOnFlush;

    public Elasticsearch5SearchIndex(Graph graph, GraphConfiguration config) {
        this.graph = graph;
        this.indexRefreshTracker = new IndexRefreshTracker(graph.getMetricsRegistry());
        this.config = new ElasticsearchSearchIndexConfiguration(graph, config);
        this.indexSelectionStrategy = this.config.getIndexSelectionStrategy();
        this.allFieldEnabled = this.config.isAllFieldEnabled(false);
        this.propertyNameVisibilitiesStore = this.config.createPropertyNameVisibilitiesStore(graph);
        this.client = createClient(this.config);
        this.serverPluginInstalled = checkPluginInstalled(this.client);
        this.geoShapePrecision = this.config.getGeoShapePrecision();
        this.geoShapeErrorPct = this.config.getGeoShapeErrorPct();
        this.logRequestSizeLimit = this.config.getLogRequestSizeLimit();
        this.exceptionHandler = this.config.getExceptionHandler(graph);
        this.refreshIndexOnFlush = this.config.getRefreshIndexOnFlush();
        BulkUpdateServiceConfiguration bulkUpdateServiceConfiguration = new BulkUpdateServiceConfiguration()
            .setPoolSize(this.config.getBulkPoolSize())
            .setBacklogSize(this.config.getBulkBacklogSize())
            .setBulkRequestTimeout(this.config.getBulkRequestTimeout())
            .setMaxBatchSize(this.config.getBulkMaxBatchSize())
            .setMaxBatchSizeInBytes(this.config.getBulkMaxBatchSizeInBytes())
            .setBatchWindowTime(this.config.getBulkBatchWindowTime())
            .setMaxFailCount(this.config.getBulkMaxFailCount());
        this.bulkUpdateService = new BulkUpdateService(this, indexRefreshTracker, bulkUpdateServiceConfiguration);

        storePainlessScript("deleteFieldsFromDocumentScript", "remove-fields-from-document.painless");
        storePainlessScript("updateFieldsOnDocumentScript", "update-fields-on-document.painless");
    }

    private void storePainlessScript(String scriptId, String scriptSourceName) {
        try (
            InputStream scriptSource = getClass().getResourceAsStream(scriptSourceName);
            InputStream helperSource = getClass().getResourceAsStream("helper-functions.painless")
        ) {
            String source = IOUtils.toString(helperSource) + " " + IOUtils.toString(scriptSource);
            source = source.replaceAll("\\r?\\n", " ").replaceAll("\"", "\\\\\"");
            client.admin().cluster().preparePutStoredScript()
                .setId(scriptId)
                .setContent(new BytesArray("{\"script\": {\"lang\": \"painless\", \"source\": \"" + source + "\"}}"), XContentType.JSON)
                .get();
        } catch (Exception ex) {
            throw new VertexiumException("Could not load painless script: " + scriptId, ex);
        }
    }

    public PropertyNameVisibilitiesStore getPropertyNameVisibilitiesStore() {
        return propertyNameVisibilitiesStore;
    }

    protected Client createClient(ElasticsearchSearchIndexConfiguration config) {
        return createTransportClient(config);
    }

    private static TransportClient createTransportClient(ElasticsearchSearchIndexConfiguration config) {
        Settings settings = tryReadSettingsFromFile(config);
        if (settings == null) {
            Settings.Builder settingsBuilder = Settings.builder();
            if (config.getClusterName() != null) {
                settingsBuilder.put("cluster.name", config.getClusterName());
            }
            for (Map.Entry esSetting : config.getEsSettings().entrySet()) {
                settingsBuilder.put(esSetting.getKey(), esSetting.getValue());
            }
            settings = settingsBuilder.build();
        }
        Collection> plugins = loadTransportClientPlugins(config);
        TransportClient transportClient = new PreBuiltTransportClient(settings, plugins);
        for (String esLocation : config.getEsLocations()) {
            String[] locationSocket = esLocation.split(":");
            String hostname;
            int port;
            if (locationSocket.length == 2) {
                hostname = locationSocket[0];
                port = Integer.parseInt(locationSocket[1]);
            } else if (locationSocket.length == 1) {
                hostname = locationSocket[0];
                port = config.getPort();
            } else {
                throw new VertexiumException("Invalid elastic search location: " + esLocation);
            }
            InetAddress host;
            try {
                host = InetAddress.getByName(hostname);
            } catch (UnknownHostException ex) {
                throw new VertexiumException("Could not resolve host: " + hostname, ex);
            }
            transportClient.addTransportAddress(new InetSocketTransportAddress(host, port));
        }
        return transportClient;
    }

    @SuppressWarnings("unchecked")
    private static Collection> loadTransportClientPlugins(ElasticsearchSearchIndexConfiguration config) {
        return config.getEsPluginClassNames().stream()
            .map(pluginClassName -> {
                try {
                    return (Class) Class.forName(pluginClassName);
                } catch (ClassNotFoundException ex) {
                    throw new VertexiumException("Could not load transport client plugin: " + pluginClassName, ex);
                }
            })
            .collect(Collectors.toList());
    }

    private static Settings tryReadSettingsFromFile(ElasticsearchSearchIndexConfiguration config) {
        File esConfigFile = config.getEsConfigFile();
        if (esConfigFile == null) {
            return null;
        }
        if (!esConfigFile.exists()) {
            throw new VertexiumException(esConfigFile.getAbsolutePath() + " does not exist");
        }
        try (FileInputStream fileIn = new FileInputStream(esConfigFile)) {
            return Settings.builder().loadFromStream(esConfigFile.getAbsolutePath(), fileIn).build();
        } catch (IOException e) {
            throw new VertexiumException("Could not read ES config file: " + esConfigFile.getAbsolutePath(), e);
        }
    }

    private boolean checkPluginInstalled(Client client) {
        if (config.isForceDisableVertexiumPlugin()) {
            LOGGER.info("Forcing the vertexium plugin off. Running without the server side Vertexium plugin will disable some features.");
            return false;
        }

        NodesInfoResponse nodesInfoResponse = client.admin().cluster().prepareNodesInfo().setPlugins(true).get();
        for (NodeInfo nodeInfo : nodesInfoResponse.getNodes()) {
            for (PluginInfo pluginInfo : nodeInfo.getPlugins().getPluginInfos()) {
                if ("vertexium".equals(pluginInfo.getName())) {
                    return true;
                }
            }
        }
        if (config.isErrorOnMissingVertexiumPlugin()) {
            throw new VertexiumException("Vertexium plugin cannot be found");
        }
        LOGGER.warn("Running without the server side Vertexium plugin will disable some features.");
        return false;
    }

    protected final boolean isAllFieldEnabled() {
        return allFieldEnabled;
    }

    public Set getIndexNamesFromElasticsearch() {
        return client.admin().indices().prepareStats().execute().actionGet().getIndices().keySet();
    }

    @Override
    public void clearCache() {
        indexInfosLock.writeLock().lock();
        try {
            this.indexInfos = null;
            this.indexInfosLastSize = -1;
        } finally {
            indexInfosLock.writeLock().unlock();
        }
    }

    private Map getIndexInfos() {
        indexInfosLock.readLock().lock();
        try {
            if (this.indexInfos != null) {
                return new HashMap<>(this.indexInfos);
            }
        } finally {
            indexInfosLock.readLock().unlock();
        }
        return loadIndexInfos();
    }

    private Map loadIndexInfos() {
        indexInfosLock.writeLock().lock();
        try {
            this.indexInfos = new HashMap<>();

            Set indices = getIndexNamesFromElasticsearch();
            for (String indexName : indices) {
                if (!indexSelectionStrategy.isIncluded(this, indexName)) {
                    LOGGER.debug("skipping index %s, not in indicesToQuery", indexName);
                    continue;
                }

                LOGGER.debug("loading index info for %s", indexName);
                IndexInfo indexInfo = createIndexInfo(indexName);
                loadExistingMappingIntoIndexInfo(graph, indexInfo, indexName);
                indexInfo.setElementTypeDefined(indexInfo.isPropertyDefined(ELEMENT_TYPE_FIELD_NAME));
                addPropertyNameVisibility(graph, indexInfo, ELEMENT_ID_FIELD_NAME, null);
                addPropertyNameVisibility(graph, indexInfo, ELEMENT_TYPE_FIELD_NAME, null);
                addPropertyNameVisibility(graph, indexInfo, VISIBILITY_FIELD_NAME, null);
                addPropertyNameVisibility(graph, indexInfo, OUT_VERTEX_ID_FIELD_NAME, null);
                addPropertyNameVisibility(graph, indexInfo, IN_VERTEX_ID_FIELD_NAME, null);
                addPropertyNameVisibility(graph, indexInfo, EDGE_LABEL_FIELD_NAME, null);
                addPropertyNameVisibility(graph, indexInfo, ADDITIONAL_VISIBILITY_FIELD_NAME, null);
                indexInfos.put(indexName, indexInfo);
            }
            return new HashMap<>(this.indexInfos);
        } finally {
            indexInfosLock.writeLock().unlock();
        }
    }

    private void loadExistingMappingIntoIndexInfo(Graph graph, IndexInfo indexInfo, String indexName) {
        indexRefreshTracker.refresh(client, indexName);
        GetMappingsResponse mapping = client.admin().indices().prepareGetMappings(indexName).get();
        for (ObjectCursor mappingIndexName : mapping.getMappings().keys()) {
            ImmutableOpenMap typeMappings = mapping.getMappings().get(mappingIndexName.value);
            for (ObjectCursor typeName : typeMappings.keys()) {
                MappingMetaData typeMapping = typeMappings.get(typeName.value);
                Map> properties = getPropertiesFromTypeMapping(typeMapping);
                if (properties == null) {
                    continue;
                }

                for (Map.Entry> propertyEntry : properties.entrySet()) {
                    String rawPropertyName = propertyEntry.getKey().replace(FIELDNAME_DOT_REPLACEMENT, ".");
                    loadExistingPropertyMappingIntoIndexInfo(graph, indexInfo, rawPropertyName);
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private Map> getPropertiesFromTypeMapping(MappingMetaData typeMapping) {
        return (Map>) typeMapping.getSourceAsMap().get("properties");
    }

    private void loadExistingPropertyMappingIntoIndexInfo(Graph graph, IndexInfo indexInfo, String rawPropertyName) {
        ElasticsearchPropertyNameInfo p = ElasticsearchPropertyNameInfo.parse(graph, propertyNameVisibilitiesStore, rawPropertyName);
        if (p == null) {
            return;
        }
        addPropertyNameVisibility(graph, indexInfo, rawPropertyName, p.getPropertyVisibility());
    }

    @Override
    public void addElement(
        Graph graph,
        Element element,
        Set additionalVisibilities,
        Set additionalVisibilitiesToDelete,
        Authorizations authorizations
    ) {
        addElement(
            graph,
            element,
            additionalVisibilities,
            additionalVisibilitiesToDelete,
            true,
            authorizations
        );
    }

    void addElement(
        Graph graph,
        Element element,
        Set additionalVisibilities,
        Set additionalVisibilitiesToDelete,
        boolean waitUntilFlushed,
        Authorizations authorizations
    ) {
        if (MUTATION_LOGGER.isTraceEnabled()) {
            MUTATION_LOGGER.trace("addElement: %s", element.getId());
        }

        if (!getConfig().isIndexEdges() && element instanceof Edge) {
            return;
        }

        if (waitUntilFlushed) {
            bulkUpdateService.flushUntilElementIdIsComplete(element.getId());
        }

        UpdateRequestBuilder updateRequestBuilder = prepareUpdate(
            graph,
            element,
            additionalVisibilities,
            additionalVisibilitiesToDelete
        );
        addActionRequestBuilderForFlush(element, updateRequestBuilder.request());

        if (getConfig().isAutoFlush()) {
            flush(graph);
        }
    }

    @Override
    public  void updateElement(
        Graph graph,
        ExistingElementMutation elementMutation,
        Authorizations authorizations
    ) {
        TElement element = elementMutation.getElement();

        if (MUTATION_LOGGER.isTraceEnabled()) {
            MUTATION_LOGGER.trace("updateElement: %s", elementMutation.getId());
        }

        if (!getConfig().isIndexEdges() && elementMutation.getElementType() == ElementType.EDGE) {
            return;
        }

        bulkUpdateService.flushUntilElementIdIsComplete(elementMutation.getId());

        UpdateRequestBuilder updateRequestBuilder = prepareUpdateForMutation(graph, elementMutation);

        if (updateRequestBuilder != null) {
            addMutationPropertiesToIndex(graph, elementMutation);
            addActionRequestBuilderForFlush(element, updateRequestBuilder.request());

            if (elementMutation.getNewElementVisibility() != null && element.getFetchHints().isIncludeExtendedDataTableNames()) {
                ImmutableSet extendedDataTableNames = element.getExtendedDataTableNames();
                if (extendedDataTableNames != null && !extendedDataTableNames.isEmpty()) {
                    extendedDataTableNames.forEach(tableName ->
                        alterExtendedDataElementTypeVisibility(
                            graph,
                            elementMutation,
                            element.getExtendedData(tableName),
                            elementMutation.getOldElementVisibility(),
                            elementMutation.getNewElementVisibility()
                        ));
                }
            }

            if (getConfig().isAutoFlush()) {
                flush(graph);
            }
        }
    }

    private  IndexInfo addMutationPropertiesToIndex(Graph graph, ExistingElementMutation mutation) {
        TElement element = mutation.getElement();
        IndexInfo indexInfo = addPropertiesToIndex(graph, element, mutation.getProperties());
        mutation.getAlterPropertyVisibilities().stream()
            .filter(p -> p.getExistingVisibility() != null && !p.getExistingVisibility().equals(p.getVisibility()))
            .forEach(p -> {
                PropertyDefinition propertyDefinition = getPropertyDefinition(graph, p.getName());
                if (propertyDefinition != null) {
                    try {
                        addPropertyDefinitionToIndex(graph, indexInfo, p.getName(), p.getVisibility(), propertyDefinition);
                    } catch (Exception e) {
                        throw new VertexiumException("Unable to add property to index: " + p, e);
                    }
                }
            });
        if (mutation.getNewElementVisibility() != null) {
            try {
                String newFieldName = addVisibilityToPropertyName(graph, ELEMENT_TYPE_FIELD_NAME, mutation.getNewElementVisibility());
                addPropertyToIndex(graph, indexInfo, newFieldName, element.getVisibility(), String.class, false, false, false);
            } catch (Exception e) {
                throw new VertexiumException("Unable to add new element type visibility to index", e);
            }
        }
        return indexInfo;
    }

    private  UpdateRequestBuilder prepareUpdateForMutation(
        Graph graph,
        ExistingElementMutation mutation
    ) {
        TElement element = mutation.getElement();

        Map fieldVisibilityChanges = getFieldVisibilityChanges(graph, mutation);
        List fieldsToRemove = getFieldsToRemove(graph, mutation);
        Map fieldsToSet = getFieldsToSet(graph, mutation);
        Set additionalVisibilities = getAdditionalVisibilities(mutation);
        Set additionalVisibilitiesToDelete = getAdditionalVisibilitiesToDelete(mutation);
        ensureAdditionalVisibilitiesDefined(additionalVisibilities);

        String documentId = getIdStrategy().createElementDocId(element);
        String indexName = getIndexName(element);
        IndexInfo indexInfo = ensureIndexCreatedAndInitialized(indexName);
        return prepareUpdateFieldsOnDocument(
            indexInfo.getIndexName(),
            documentId,
            fieldsToSet,
            fieldsToRemove,
            fieldVisibilityChanges,
            additionalVisibilities,
            additionalVisibilitiesToDelete
        );
    }

    private  Map getFieldsToSet(
        Graph graph,
        ExistingElementMutation mutation
    ) {
        TElement element = mutation.getElement();

        Map fieldsToSet = new HashMap<>();

        mutation.getProperties().forEach(p ->
            addExistingValuesToFieldMap(graph, element, p.getName(), p.getVisibility(), fieldsToSet));
        mutation.getPropertyDeletes().forEach(p ->
            addExistingValuesToFieldMap(graph, element, p.getName(), p.getVisibility(), fieldsToSet));
        mutation.getPropertySoftDeletes().forEach(p ->
            addExistingValuesToFieldMap(graph, element, p.getName(), p.getVisibility(), fieldsToSet));
        mutation.getMarkPropertyHiddenMutations().forEach(p -> {
            String hiddenVisibilityPropertyName = addVisibilityToPropertyName(graph, HIDDEN_PROPERTY_FIELD_NAME, p.getVisibility());
            String indexName = getIndexName(mutation);
            if (!isPropertyInIndex(graph, HIDDEN_PROPERTY_FIELD_NAME, p.getVisibility())) {
                IndexInfo indexInfo = ensureIndexCreatedAndInitialized(indexName);
                addPropertyToIndex(graph, indexInfo, hiddenVisibilityPropertyName, p.getVisibility(), Boolean.class, false, false, false);
            }
            fieldsToSet.put(hiddenVisibilityPropertyName, true);
        });

        return fieldsToSet;
    }

    private  Set getAdditionalVisibilities(ElementMutation mutation) {
        Set results = new HashSet<>();
        for (AdditionalVisibilityAddMutation additionalVisibility : mutation.getAdditionalVisibilities()) {
            results.add(additionalVisibility.getAdditionalVisibility());
        }
        return results;
    }

    private  Set getAdditionalVisibilitiesToDelete(ElementMutation mutation) {
        Set results = new HashSet<>();
        for (AdditionalVisibilityDeleteMutation additionalVisibilityDelete : mutation.getAdditionalVisibilityDeletes()) {
            results.add(additionalVisibilityDelete.getAdditionalVisibility());
        }
        return results;
    }

    private  List getFieldsToRemove(Graph graph, ElementMutation mutation) {
        List fieldsToRemove = new ArrayList<>();
        mutation.getPropertyDeletes().forEach(p -> fieldsToRemove.addAll(getFieldsToRemove(graph, p.getName(), p.getVisibility())));
        mutation.getPropertySoftDeletes().forEach(p -> fieldsToRemove.addAll(getFieldsToRemove(graph, p.getName(), p.getVisibility())));
        mutation.getMarkPropertyVisibleMutations().forEach(p -> {
            String hiddenVisibilityPropertyName = addVisibilityToPropertyName(graph, HIDDEN_PROPERTY_FIELD_NAME, p.getVisibility());
            if (isPropertyInIndex(graph, HIDDEN_PROPERTY_FIELD_NAME, p.getVisibility())) {
                fieldsToRemove.add(hiddenVisibilityPropertyName);
            }
        });
        return fieldsToRemove;
    }

    private List getFieldsToRemove(Graph graph, String name, Visibility visibility) {
        List fieldsToRemove = new ArrayList<>();
        String propertyName = addVisibilityToPropertyName(graph, name, visibility);
        fieldsToRemove.add(propertyName);

        PropertyDefinition propertyDefinition = getPropertyDefinition(graph, name);
        if (GeoShape.class.isAssignableFrom(propertyDefinition.getDataType())) {
            fieldsToRemove.add(propertyName + GEO_PROPERTY_NAME_SUFFIX);

            if (GeoPoint.class.isAssignableFrom(propertyDefinition.getDataType())) {
                fieldsToRemove.add(propertyName + GEO_POINT_PROPERTY_NAME_SUFFIX);
            }
        }
        return fieldsToRemove;
    }

    private  Map getFieldVisibilityChanges(Graph graph, ExistingElementMutation mutation) {
        Map fieldVisibilityChanges = new HashMap<>();

        mutation.getAlterPropertyVisibilities().stream()
            .filter(p -> p.getExistingVisibility() != null && !p.getExistingVisibility().equals(p.getVisibility()))
            .forEach(p -> {
                String oldFieldName = addVisibilityToPropertyName(graph, p.getName(), p.getExistingVisibility());
                String newFieldName = addVisibilityToPropertyName(graph, p.getName(), p.getVisibility());
                fieldVisibilityChanges.put(oldFieldName, newFieldName);

                PropertyDefinition propertyDefinition = getPropertyDefinition(graph, p.getName());
                if (GeoShape.class.isAssignableFrom(propertyDefinition.getDataType())) {
                    fieldVisibilityChanges.put(oldFieldName + GEO_PROPERTY_NAME_SUFFIX, newFieldName + GEO_PROPERTY_NAME_SUFFIX);

                    if (GeoPoint.class.isAssignableFrom(propertyDefinition.getDataType())) {
                        fieldVisibilityChanges.put(oldFieldName + GEO_POINT_PROPERTY_NAME_SUFFIX, newFieldName + GEO_POINT_PROPERTY_NAME_SUFFIX);
                    }
                }
            });

        if (mutation.getNewElementVisibility() != null) {
            String oldFieldName = addVisibilityToPropertyName(graph, ELEMENT_TYPE_FIELD_NAME, mutation.getOldElementVisibility());
            String newFieldName = addVisibilityToPropertyName(graph, ELEMENT_TYPE_FIELD_NAME, mutation.getNewElementVisibility());
            fieldVisibilityChanges.put(oldFieldName, newFieldName);
        }
        return fieldVisibilityChanges;
    }

    private void addExistingValuesToFieldMap(Graph graph, Element element, String propertyName, Visibility propertyVisibility, Map fieldsToSet) {
        Iterable properties = stream(element.getProperties(propertyName))
            .filter(p -> p.getVisibility().equals(propertyVisibility))
            .collect(Collectors.toList());
        Map remainingProperties = getPropertiesAsFields(graph, properties);
        for (Map.Entry remainingPropertyEntry : remainingProperties.entrySet()) {
            String remainingField = remainingPropertyEntry.getKey();
            Object remainingValue = remainingPropertyEntry.getValue();
            if (remainingValue instanceof List) {
                for (Object v : ((List) remainingValue)) {
                    addPropertyValueToPropertiesMap(fieldsToSet, remainingField, v);
                }
            } else {
                addPropertyValueToPropertiesMap(fieldsToSet, remainingField, remainingValue);
            }
        }
    }

    private UpdateRequestBuilder prepareUpdate(
        Graph graph,
        Element element,
        Set additionalVisibilities,
        Set additionalVisibilitiesToDelete
    ) {
        try {
            IndexInfo indexInfo = addPropertiesToIndex(graph, element, element.getProperties());
            if (additionalVisibilities != null) {
                ensureAdditionalVisibilitiesDefined(additionalVisibilities);
            }
            XContentBuilder source = buildJsonContentFromElement(graph, element);
            if (MUTATION_LOGGER.isTraceEnabled()) {
                MUTATION_LOGGER.trace("addElement json: %s: %s", element.getId(), source.string());
            }

            List additionalVisibilitiesParam = additionalVisibilities == null
                ? Collections.emptyList()
                : toList(additionalVisibilities);
            List additionalVisibilitiesToDeleteParam = additionalVisibilitiesToDelete == null
                ? Collections.emptyList()
                : toList(additionalVisibilitiesToDelete);

            Map fieldsToSet = getPropertiesAsFields(graph, element.getProperties());
            if (element instanceof Edge) {
                Edge edge = (Edge) element;
                fieldsToSet.put(IN_VERTEX_ID_FIELD_NAME, edge.getVertexId(Direction.IN));
                fieldsToSet.put(OUT_VERTEX_ID_FIELD_NAME, edge.getVertexId(Direction.OUT));
                fieldsToSet.put(EDGE_LABEL_FIELD_NAME, edge.getLabel());
            }

            for (Property property : element.getProperties()) {
                for (Visibility hiddenVisibility : property.getHiddenVisibilities()) {
                    String hiddenVisibilityPropertyName = addVisibilityToPropertyName(graph, HIDDEN_PROPERTY_FIELD_NAME, hiddenVisibility);
                    if (!isPropertyInIndex(graph, HIDDEN_PROPERTY_FIELD_NAME, hiddenVisibility)) {
                        addPropertyToIndex(graph, indexInfo, hiddenVisibilityPropertyName, hiddenVisibility, Boolean.class, false, false, false);
                    }
                    fieldsToSet.put(hiddenVisibilityPropertyName, true);
                }
            }
            for (Visibility hiddenVisibility : element.getHiddenVisibilities()) {
                String hiddenVisibilityPropertyName = addVisibilityToPropertyName(graph, HIDDEN_VERTEX_FIELD_NAME, hiddenVisibility);
                if (!isPropertyInIndex(graph, HIDDEN_VERTEX_FIELD_NAME, hiddenVisibility)) {
                    addPropertyToIndex(graph, indexInfo, hiddenVisibilityPropertyName, hiddenVisibility, Boolean.class, false, false, false);
                }
                fieldsToSet.put(hiddenVisibilityPropertyName, true);
            }

            fieldsToSet = fieldsToSet == null ? Collections.emptyMap() : fieldsToSet.entrySet().stream()
                .collect(Collectors.toMap(e -> replaceFieldnameDots(e.getKey()), Map.Entry::getValue));

            return getClient()
                .prepareUpdate(indexInfo.getIndexName(), getIdStrategy().getType(), getIdStrategy().createElementDocId(element))
                .setScriptedUpsert(true)
                .setUpsert(source)
                .setScript(new Script(
                    ScriptType.STORED,
                    "painless",
                    "updateFieldsOnDocumentScript",
                    ImmutableMap.of(
                        "fieldsToSet", fieldsToSet,
                        "fieldsToRemove", Collections.emptyList(),
                        "fieldsToRename", Collections.emptyMap(),
                        "additionalVisibilities", additionalVisibilitiesParam,
                        "additionalVisibilitiesToDelete", additionalVisibilitiesToDeleteParam
                    )))
                .setRetryOnConflict(MAX_RETRIES);
        } catch (IOException e) {
            throw new VertexiumException("Could not add element", e);
        }
    }

    private void addActionRequestBuilderForFlush(
        ElementLocation elementLocation,
        UpdateRequest updateRequest
    ) {
        addActionRequestBuilderForFlush(
            elementLocation,
            null,
            null,
            updateRequest
        );
    }

    private void addActionRequestBuilderForFlush(
        ElementLocation elementLocation,
        String extendedDataTableName,
        String rowId,
        UpdateRequest updateRequest
    ) {
        logRequestSize(elementLocation.getId(), updateRequest);
        bulkUpdateService.addUpdate(elementLocation, extendedDataTableName, rowId, updateRequest);
    }

    @Override
    public void addElementExtendedData(
        Graph graph,
        ElementLocation elementLocation,
        Iterable extendedData,
        Iterable additionalExtendedDataVisibilities,
        Iterable additionalExtendedDataVisibilityDeletes,
        Authorizations authorizations
    ) {
        Map> byTableThenRowId = ExtendedDataMutationUtils.getByTableThenRowId(
            extendedData,
            null,
            additionalExtendedDataVisibilities,
            additionalExtendedDataVisibilityDeletes
        );

        for (Map.Entry> byTableThenRowIdEntry : byTableThenRowId.entrySet()) {
            String tableName = byTableThenRowIdEntry.getKey();
            Map byRow = byTableThenRowIdEntry.getValue();
            for (Map.Entry byRowEntry : byRow.entrySet()) {
                String rowId = byRowEntry.getKey();
                ExtendedDataMutationUtils.Mutations mutations = byRowEntry.getValue();
                addElementExtendedData(
                    graph,
                    elementLocation,
                    tableName,
                    rowId,
                    mutations.getExtendedData(),
                    mutations.getAdditionalExtendedDataVisibilities(),
                    mutations.getAdditionalExtendedDataVisibilityDeletes()
                );
            }
        }
    }

    @Override
    public void deleteExtendedData(Graph graph, ExtendedDataRowId rowId, Authorizations authorizations) {
        String indexName = getExtendedDataIndexName(rowId);
        String docId = getIdStrategy().createExtendedDataDocId(rowId);
        bulkUpdateService.addDelete(
            ElementId.create(rowId.getElementType(), rowId.getElementId()),
            docId,
            getClient().prepareDelete(indexName, getIdStrategy().getType(), docId).request()
        );
    }

    @Override
    public void deleteExtendedData(
        Graph graph,
        ElementLocation elementLocation,
        String tableName,
        String row,
        String columnName,
        String key,
        Visibility visibility,
        Authorizations authorizations
    ) {
        String extendedDataDocId = getIdStrategy().createExtendedDataDocId(elementLocation, tableName, row);
        String fieldName = addVisibilityToPropertyName(graph, columnName, visibility);
        String indexName = getExtendedDataIndexName(elementLocation, tableName, row);
        removeFieldsFromDocument(
            graph,
            indexName,
            elementLocation,
            extendedDataDocId,
            Lists.newArrayList(fieldName, fieldName + "_e")
        );
    }

    private void addElementExtendedData(
        Graph graph,
        ElementLocation elementLocation,
        String tableName,
        String rowId,
        Iterable extendedData,
        Iterable additionalExtendedDataVisibilities,
        Iterable additionalExtendedDataVisibilityDeletes
    ) {
        if (MUTATION_LOGGER.isTraceEnabled()) {
            MUTATION_LOGGER.trace("addElementExtendedData: %s:%s:%s", elementLocation.getId(), tableName, rowId);
        }

        UpdateRequestBuilder updateRequestBuilder = prepareUpdate(
            graph,
            elementLocation,
            tableName,
            rowId,
            extendedData,
            additionalExtendedDataVisibilities,
            additionalExtendedDataVisibilityDeletes
        );
        addActionRequestBuilderForFlush(elementLocation, tableName, rowId, updateRequestBuilder.request());

        if (getConfig().isAutoFlush()) {
            flush(graph);
        }
    }

    public  void alterExtendedDataElementTypeVisibility(
        Graph graph,
        ElementMutation elementMutation,
        Iterable rows,
        Visibility oldVisibility,
        Visibility newVisibility
    ) {
        for (ExtendedDataRow row : rows) {
            String tableName = (String) row.getPropertyValue(ExtendedDataRow.TABLE_NAME);
            String rowId = (String) row.getPropertyValue(ExtendedDataRow.ROW_ID);
            String extendedDataDocId = getIdStrategy().createExtendedDataDocId(elementMutation, tableName, rowId);

            List columns = stream(row.getProperties())
                .map(property -> new ExtendedDataMutation(
                    tableName,
                    rowId,
                    property.getName(),
                    property.getKey(),
                    property.getValue(),
                    property.getTimestamp(),
                    property.getVisibility()
                )).collect(Collectors.toList());

            IndexInfo indexInfo = addExtendedDataColumnsToIndex(graph, elementMutation, tableName, rowId, columns);

            String oldElementTypeVisibilityPropertyName = addVisibilityToPropertyName(graph, ELEMENT_TYPE_FIELD_NAME, oldVisibility);
            String newElementTypeVisibilityPropertyName = addVisibilityToPropertyName(graph, ELEMENT_TYPE_FIELD_NAME, newVisibility);
            Map fieldsToRename = Collections.singletonMap(oldElementTypeVisibilityPropertyName, newElementTypeVisibilityPropertyName);

            UpdateRequest updateRequest = getClient()
                .prepareUpdate(indexInfo.getIndexName(), getIdStrategy().getType(), extendedDataDocId)
                .setScript(new Script(
                    ScriptType.STORED,
                    "painless",
                    "updateFieldsOnDocumentScript",
                    ImmutableMap.of(
                        "fieldsToSet", Collections.emptyMap(),
                        "fieldsToRemove", Collections.emptyList(),
                        "fieldsToRename", fieldsToRename,
                        "additionalVisibilities", Collections.emptyList(),
                        "additionalVisibilitiesToDelete", Collections.emptyList()
                    )
                ))
                .setRetryOnConflict(MAX_RETRIES)
                .request();
            bulkUpdateService.addUpdate(elementMutation, row.getId().getTableName(), row.getId().getRowId(), updateRequest);
        }
    }

    @Override
    public void addExtendedData(
        Graph graph,
        ElementLocation elementLocation,
        Iterable extendedDatas,
        Authorizations authorizations
    ) {
        Map>> rowsByElementTypeAndId = mapExtendedDatasByElementTypeByElementId(extendedDatas);
        rowsByElementTypeAndId.forEach((elementType, elements) -> {
            elements.forEach((elementId, rows) -> {
                rows.forEach(row -> {
                    String tableName = (String) row.getPropertyValue(ExtendedDataRow.TABLE_NAME);
                    String rowId = (String) row.getPropertyValue(ExtendedDataRow.ROW_ID);
                    List columns = stream(row.getProperties())
                        .map(property -> new ExtendedDataMutation(
                            tableName,
                            rowId,
                            property.getName(),
                            property.getKey(),
                            property.getValue(),
                            property.getTimestamp(),
                            property.getVisibility()
                        )).collect(Collectors.toList());
                    UpdateRequest updateRequest = prepareUpdate(
                        graph,
                        elementLocation,
                        tableName,
                        rowId,
                        columns,
                        Collections.emptyList(),
                        Collections.emptyList()
                    ).request();
                    bulkUpdateService.addUpdate(elementLocation, tableName, rowId, updateRequest);
                });
            });
        });
    }

    private UpdateRequestBuilder prepareUpdate(
        Graph graph,
        ElementLocation elementLocation,
        String tableName,
        String rowId,
        Iterable extendedData,
        Iterable additionalExtendedDataVisibilities,
        Iterable additionalExtendedDataVisibilityDeletes
    ) {
        try {
            IndexInfo indexInfo = addExtendedDataColumnsToIndex(graph, elementLocation, tableName, rowId, extendedData);
            String extendedDataDocId = getIdStrategy().createExtendedDataDocId(elementLocation, tableName, rowId);

            Map fieldsToSet =
                getExtendedDataColumnsAsFields(graph, extendedData).entrySet().stream()
                    .collect(Collectors.toMap(e -> replaceFieldnameDots(e.getKey()), Map.Entry::getValue));

            XContentBuilder source = buildJsonContentForExtendedDataUpsert(graph, elementLocation, tableName, rowId);
            if (MUTATION_LOGGER.isTraceEnabled()) {
                String fieldsDebug = Joiner.on(", ").withKeyValueSeparator(": ").join(fieldsToSet);
                MUTATION_LOGGER.trace(
                    "addElementExtendedData json: %s:%s:%s: %s {%s}",
                    elementLocation.getId(),
                    tableName,
                    rowId,
                    source.string(),
                    fieldsDebug
                );
            }

            List additionalVisibilities = additionalExtendedDataVisibilities == null
                ? Collections.emptyList()
                : stream(additionalExtendedDataVisibilities).map(AdditionalExtendedDataVisibilityAddMutation::getAdditionalVisibility).collect(Collectors.toList());
            List additionalVisibilitiesToDelete = additionalExtendedDataVisibilityDeletes == null
                ? Collections.emptyList()
                : stream(additionalExtendedDataVisibilityDeletes).map(AdditionalExtendedDataVisibilityDeleteMutation::getAdditionalVisibility).collect(Collectors.toList());
            ensureAdditionalVisibilitiesDefined(additionalVisibilities);

            return getClient()
                .prepareUpdate(indexInfo.getIndexName(), getIdStrategy().getType(), extendedDataDocId)
                .setScriptedUpsert(true)
                .setUpsert(source)
                .setScript(new Script(
                    ScriptType.STORED,
                    "painless",
                    "updateFieldsOnDocumentScript",
                    ImmutableMap.of(
                        "fieldsToSet", fieldsToSet,
                        "fieldsToRemove", Collections.emptyList(),
                        "fieldsToRename", Collections.emptyMap(),
                        "additionalVisibilities", additionalVisibilities,
                        "additionalVisibilitiesToDelete", additionalVisibilitiesToDelete
                    )))
                .setRetryOnConflict(MAX_RETRIES);
        } catch (IOException e) {
            throw new VertexiumException("Could not add element extended data", e);
        }
    }

    private XContentBuilder buildJsonContentForExtendedDataUpsert(
        Graph graph,
        ElementLocation elementLocation,
        String tableName,
        String rowId
    ) throws IOException {
        XContentBuilder jsonBuilder;
        jsonBuilder = XContentFactory.jsonBuilder().startObject();

        String elementTypeString = ElasticsearchDocumentType.getExtendedDataDocumentTypeFromElementType(
            elementLocation.getElementType()
        ).getKey();
        jsonBuilder.field(ELEMENT_ID_FIELD_NAME, elementLocation.getId());
        jsonBuilder.field(ELEMENT_TYPE_FIELD_NAME, elementTypeString);
        String elementTypeVisibilityPropertyName = addElementTypeVisibilityPropertyToExtendedDataIndex(
            graph,
            elementLocation,
            tableName,
            rowId
        );
        jsonBuilder.field(elementTypeVisibilityPropertyName, elementTypeString);
        jsonBuilder.field(EXTENDED_DATA_TABLE_NAME_FIELD_NAME, tableName);
        jsonBuilder.field(EXTENDED_DATA_TABLE_ROW_ID_FIELD_NAME, rowId);
        if (elementLocation.getElementType() == ElementType.EDGE) {
            if (!(elementLocation instanceof EdgeElementLocation)) {
                throw new VertexiumException(String.format(
                    "element location (%s) has type edge but does not implement %s",
                    elementLocation.getClass().getName(),
                    EdgeElementLocation.class.getName()
                ));
            }
            EdgeElementLocation edgeElementLocation = (EdgeElementLocation) elementLocation;
            jsonBuilder.field(IN_VERTEX_ID_FIELD_NAME, edgeElementLocation.getVertexId(Direction.IN));
            jsonBuilder.field(OUT_VERTEX_ID_FIELD_NAME, edgeElementLocation.getVertexId(Direction.OUT));
            jsonBuilder.field(EDGE_LABEL_FIELD_NAME, edgeElementLocation.getLabel());
        }

        jsonBuilder.endObject();

        return jsonBuilder;
    }

    private Map getExtendedDataColumnsAsFields(Graph graph, Iterable columns) {
        Map fieldsMap = new HashMap<>();
        List streamingColumns = new ArrayList<>();
        for (ExtendedDataMutation column : columns) {
            if (column.getValue() != null && shouldIgnoreType(column.getValue().getClass())) {
                continue;
            }

            if (column.getValue() instanceof StreamingPropertyValue) {
                StreamingPropertyValue spv = (StreamingPropertyValue) column.getValue();
                if (isStreamingPropertyValueIndexable(graph, column.getColumnName(), spv)) {
                    streamingColumns.add(column);
                }
            } else {
                addExtendedDataColumnToFieldMap(graph, column, column.getValue(), fieldsMap);
            }
        }
        addStreamingExtendedDataColumnsValuesToMap(graph, streamingColumns, fieldsMap);
        return fieldsMap;
    }

    private void addStreamingExtendedDataColumnsValuesToMap(Graph graph, List columns, Map fieldsMap) {
        List streamingPropertyValues = columns.stream()
            .map((column) -> {
                if (!(column.getValue() instanceof StreamingPropertyValue)) {
                    throw new VertexiumException("column with a value that is not a StreamingPropertyValue passed to addStreamingPropertyValuesToFieldMap");
                }
                return (StreamingPropertyValue) column.getValue();
            })
            .collect(Collectors.toList());

        List inputStreams = graph.getStreamingPropertyValueInputStreams(streamingPropertyValues);
        for (int i = 0; i < columns.size(); i++) {
            try {
                String propertyValue = IOUtils.toString(inputStreams.get(i));
                addExtendedDataColumnToFieldMap(graph, columns.get(i), new StreamingPropertyString(propertyValue), fieldsMap);
            } catch (IOException ex) {
                throw new VertexiumException("could not convert streaming property to string", ex);
            }
        }
    }

    private void addExtendedDataColumnToFieldMap(Graph graph, ExtendedDataMutation column, Object value, Map fieldsMap) {
        String propertyName = addVisibilityToExtendedDataColumnName(graph, column);
        addValuesToFieldMap(graph, fieldsMap, propertyName, value);
    }

    private Map>> mapExtendedDatasByElementTypeByElementId(Iterable extendedData) {
        Map>> rowsByElementTypeByElementId = new HashMap<>();
        extendedData.forEach(row -> {
            ExtendedDataRowId rowId = row.getId();
            Map> elementTypeData = rowsByElementTypeByElementId.computeIfAbsent(rowId.getElementType(), key -> new HashMap<>());
            List elementExtendedData = elementTypeData.computeIfAbsent(rowId.getElementId(), key -> new ArrayList<>());
            elementExtendedData.add(row);
        });
        return rowsByElementTypeByElementId;
    }

    @Override
    public  void alterElementVisibility(
        Graph graph,
        ExistingElementMutation elementMutation,
        Visibility oldVisibility,
        Visibility newVisibility,
        Authorizations authorizations
    ) {
        // Remove old element field name
        String oldFieldName = addVisibilityToPropertyName(graph, ELEMENT_TYPE_FIELD_NAME, oldVisibility);
        removeFieldsFromDocument(graph, elementMutation, oldFieldName);

        addElement(graph, elementMutation.getElement(), null, null, authorizations);
    }

    private XContentBuilder buildJsonContentFromElement(Graph graph, Element element) throws IOException {
        XContentBuilder jsonBuilder;
        jsonBuilder = XContentFactory.jsonBuilder()
            .startObject();

        String elementTypeVisibilityPropertyName = addElementTypeVisibilityPropertyToIndex(graph, element);

        jsonBuilder.field(ELEMENT_ID_FIELD_NAME, element.getId());
        jsonBuilder.field(ELEMENT_TYPE_FIELD_NAME, getElementTypeValueFromElement(element));
        if (element instanceof Vertex) {
            jsonBuilder.field(elementTypeVisibilityPropertyName, ElasticsearchDocumentType.VERTEX.getKey());
        } else if (element instanceof Edge) {
            Edge edge = (Edge) element;
            jsonBuilder.field(elementTypeVisibilityPropertyName, ElasticsearchDocumentType.EDGE.getKey());
            jsonBuilder.field(IN_VERTEX_ID_FIELD_NAME, edge.getVertexId(Direction.IN));
            jsonBuilder.field(OUT_VERTEX_ID_FIELD_NAME, edge.getVertexId(Direction.OUT));
            jsonBuilder.field(EDGE_LABEL_FIELD_NAME, edge.getLabel());
        } else {
            throw new VertexiumException("Unexpected element type " + element.getClass().getName());
        }

        jsonBuilder.endObject();
        return jsonBuilder;
    }

    @Override
    public void markElementHidden(Graph graph, Element element, Visibility visibility, Authorizations authorizations) {
        try {
            String hiddenVisibilityPropertyName = addVisibilityToPropertyName(graph, HIDDEN_VERTEX_FIELD_NAME, visibility);
            String indexName = getIndexName(element);
            if (!isPropertyInIndex(graph, HIDDEN_VERTEX_FIELD_NAME, visibility)) {
                IndexInfo indexInfo = ensureIndexCreatedAndInitialized(indexName);
                addPropertyToIndex(graph, indexInfo, hiddenVisibilityPropertyName, visibility, Boolean.class, false, false, false);
            }

            XContentBuilder jsonBuilder = XContentFactory.jsonBuilder().startObject();
            jsonBuilder.field(hiddenVisibilityPropertyName, true);
            jsonBuilder.endObject();

            UpdateRequest updateRequest = getClient()
                .prepareUpdate(indexName, getIdStrategy().getType(), getIdStrategy().createElementDocId(element))
                .setDoc(jsonBuilder)
                .setRetryOnConflict(MAX_RETRIES)
                .request();
            bulkUpdateService.addUpdate(element, updateRequest);
        } catch (IOException e) {
            throw new VertexiumException("Could not mark element hidden", e);
        }
    }

    @Override
    public void markElementVisible(
        Graph graph,
        ElementLocation elementLocation,
        Visibility visibility,
        Authorizations authorizations
    ) {
        String hiddenVisibilityPropertyName = addVisibilityToPropertyName(graph, HIDDEN_VERTEX_FIELD_NAME, visibility);
        if (isPropertyInIndex(graph, HIDDEN_VERTEX_FIELD_NAME, visibility)) {
            removeFieldsFromDocument(graph, elementLocation, hiddenVisibilityPropertyName);
        }
    }

    @Override
    public void markPropertyHidden(
        Graph graph,
        ElementLocation elementLocation,
        Property property,
        Visibility visibility,
        Authorizations authorizations
    ) {
        try {
            String hiddenVisibilityPropertyName = addVisibilityToPropertyName(graph, HIDDEN_PROPERTY_FIELD_NAME, visibility);
            String indexName = getIndexName(elementLocation);
            if (!isPropertyInIndex(graph, HIDDEN_PROPERTY_FIELD_NAME, visibility)) {
                IndexInfo indexInfo = ensureIndexCreatedAndInitialized(indexName);
                addPropertyToIndex(graph, indexInfo, hiddenVisibilityPropertyName, visibility, Boolean.class, false, false, false);
            }

            XContentBuilder jsonBuilder = XContentFactory.jsonBuilder().startObject();
            jsonBuilder.field(hiddenVisibilityPropertyName, true);
            jsonBuilder.endObject();

            UpdateRequest updateRequest = getClient()
                .prepareUpdate(indexName, getIdStrategy().getType(), getIdStrategy().createElementDocId(elementLocation))
                .setDoc(jsonBuilder)
                .setRetryOnConflict(MAX_RETRIES)
                .request();
            bulkUpdateService.addUpdate(elementLocation, updateRequest);
        } catch (IOException e) {
            throw new VertexiumException("Could not mark element hidden", e);
        }
    }

    @Override
    public void markPropertyVisible(
        Graph graph,
        ElementLocation elementLocation,
        Property property,
        Visibility visibility,
        Authorizations authorizations
    ) {
        String hiddenVisibilityPropertyName = addVisibilityToPropertyName(graph, HIDDEN_PROPERTY_FIELD_NAME, visibility);
        if (isPropertyInIndex(graph, HIDDEN_PROPERTY_FIELD_NAME, visibility)) {
            removeFieldsFromDocument(graph, elementLocation, hiddenVisibilityPropertyName);
        }
    }

    private String getElementTypeValueFromElement(Element element) {
        if (element instanceof Vertex) {
            return ElasticsearchDocumentType.VERTEX.getKey();
        }
        if (element instanceof Edge) {
            return ElasticsearchDocumentType.EDGE.getKey();
        }
        throw new VertexiumException("Unhandled element type: " + element.getClass().getName());
    }

    protected Object convertValueForIndexing(Object obj) {
        if (obj == null) {
            return null;
        }
        if (obj instanceof BigDecimal) {
            return ((BigDecimal) obj).doubleValue();
        }
        if (obj instanceof BigInteger) {
            return ((BigInteger) obj).intValue();
        }
        return obj;
    }

    private String addElementTypeVisibilityPropertyToExtendedDataIndex(
        Graph graph,
        ElementLocation elementLocation,
        String tableName,
        String rowId
    ) {
        String elementTypeVisibilityPropertyName = addVisibilityToPropertyName(
            graph,
            ELEMENT_TYPE_FIELD_NAME,
            elementLocation.getVisibility()
        );
        String indexName = getExtendedDataIndexName(elementLocation, tableName, rowId);
        IndexInfo indexInfo = ensureIndexCreatedAndInitialized(indexName);
        addPropertyToIndex(
            graph,
            indexInfo,
            elementTypeVisibilityPropertyName,
            elementLocation.getVisibility(),
            String.class,
            false,
            false,
            false
        );
        return elementTypeVisibilityPropertyName;
    }

    private String addElementTypeVisibilityPropertyToIndex(Graph graph, Element element) {
        String elementTypeVisibilityPropertyName = addVisibilityToPropertyName(
            graph,
            ELEMENT_TYPE_FIELD_NAME,
            element.getVisibility()
        );
        String indexName = getIndexName(element);
        IndexInfo indexInfo = ensureIndexCreatedAndInitialized(indexName);
        addPropertyToIndex(
            graph,
            indexInfo,
            elementTypeVisibilityPropertyName,
            element.getVisibility(),
            String.class,
            false,
            false,
            false
        );
        return elementTypeVisibilityPropertyName;
    }

    private Map getPropertiesAsFields(Graph graph, Iterable properties) {
        Map fieldsMap = new HashMap<>();
        List streamingProperties = new ArrayList<>();
        for (Property property : properties) {
            if (property.getValue() != null && shouldIgnoreType(property.getValue().getClass())) {
                continue;
            }

            if (property.getValue() instanceof StreamingPropertyValue) {
                StreamingPropertyValue spv = (StreamingPropertyValue) property.getValue();
                if (isStreamingPropertyValueIndexable(graph, property.getName(), spv)) {
                    streamingProperties.add(property);
                }
            } else {
                addPropertyToFieldMap(graph, property, property.getValue(), fieldsMap);
            }
        }
        addStreamingPropertyValuesToFieldMap(graph, streamingProperties, fieldsMap);
        return fieldsMap;
    }

    private void addPropertyToFieldMap(Graph graph, Property property, Object propertyValue, Map propertiesMap) {
        String propertyName = addVisibilityToPropertyName(graph, property);
        addValuesToFieldMap(graph, propertiesMap, propertyName, propertyValue);
    }

    private void addValuesToFieldMap(Graph graph, Map propertiesMap, String propertyName, Object propertyValue) {
        PropertyDefinition propertyDefinition = getPropertyDefinition(graph, propertyName);
        if (propertyValue instanceof GeoShape) {
            convertGeoShape(propertiesMap, propertyName, (GeoShape) propertyValue);
            if (propertyValue instanceof GeoPoint) {
                GeoPoint geoPoint = (GeoPoint) propertyValue;
                Map coordinates = new HashMap<>();
                coordinates.put("lat", geoPoint.getLatitude());
                coordinates.put("lon", geoPoint.getLongitude());
                addPropertyValueToPropertiesMap(propertiesMap, propertyName + GEO_POINT_PROPERTY_NAME_SUFFIX, coordinates);
            }
            return;
        } else if (propertyValue instanceof StreamingPropertyString) {
            propertyValue = ((StreamingPropertyString) propertyValue).getPropertyValue();
        } else if (propertyValue instanceof String) {
            if (propertyDefinition == null ||
                propertyDefinition.getTextIndexHints().contains(TextIndexHint.FULL_TEXT) ||
                propertyDefinition.getTextIndexHints().contains(TextIndexHint.EXACT_MATCH) ||
                propertyDefinition.isSortable()) {
                addPropertyValueToPropertiesMap(propertiesMap, propertyName, propertyValue);
            }
            return;
        }

        if (propertyValue instanceof DateOnly) {
            propertyValue = ((DateOnly) propertyValue).getDate();
        } else if (propertyValue instanceof IpV4Address) {
            propertyValue = propertyValue.toString();
        }

        addPropertyValueToPropertiesMap(propertiesMap, propertyName, propertyValue);
    }

    private boolean isStreamingPropertyValueIndexable(Graph graph, String propertyName, StreamingPropertyValue streamingPropertyValue) {
        if (!streamingPropertyValue.isSearchIndex()) {
            return false;
        }

        PropertyDefinition propertyDefinition = getPropertyDefinition(graph, propertyName);
        if (propertyDefinition != null && !propertyDefinition.getTextIndexHints().contains(TextIndexHint.FULL_TEXT)) {
            return false;
        }

        Class valueType = streamingPropertyValue.getValueType();
        if (valueType == String.class) {
            return true;
        } else {
            throw new VertexiumException("Unhandled StreamingPropertyValue type: " + valueType.getName());
        }
    }

    private void addStreamingPropertyValuesToFieldMap(Graph graph, List properties, Map propertiesMap) {
        List streamingPropertyValues = properties.stream()
            .map((property) -> {
                if (!(property.getValue() instanceof StreamingPropertyValue)) {
                    throw new VertexiumException("property with a value that is not a StreamingPropertyValue passed to addStreamingPropertyValuesToFieldMap");
                }
                return (StreamingPropertyValue) property.getValue();
            })
            .collect(Collectors.toList());
        if (streamingPropertyValues.size() > 0 && graph instanceof GraphWithSearchIndex) {
            ((GraphWithSearchIndex) graph).flushGraph();
        }

        List inputStreams = graph.getStreamingPropertyValueInputStreams(streamingPropertyValues);
        for (int i = 0; i < properties.size(); i++) {
            try {
                String propertyValue = IOUtils.toString(inputStreams.get(i));
                addPropertyToFieldMap(graph, properties.get(i), new StreamingPropertyString(propertyValue), propertiesMap);
            } catch (IOException ex) {
                throw new VertexiumException("could not convert streaming property to string", ex);
            }
        }
    }

    public boolean isServerPluginInstalled() {
        return serverPluginInstalled;
    }

    public void handleBulkFailure(BulkItem bulkItem, BulkItemResponse bulkItemResponse, AtomicBoolean retry) throws Exception {
        if (exceptionHandler == null) {
            LOGGER.error("bulk failure: %s: %s", bulkItem, bulkItemResponse.getFailureMessage());
            return;
        }
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("handleBulkFailure: (bulkItem: %s, bulkItemResponse: %s)", bulkItem, bulkItemResponse);
        }
        exceptionHandler.handleBulkFailure(graph, this, bulkItem, bulkItemResponse, retry);
    }

    public boolean supportsExactMatchSearch(PropertyDefinition propertyDefinition) {
        return propertyDefinition.getTextIndexHints().contains(TextIndexHint.EXACT_MATCH) || propertyDefinition.isSortable();
    }

    public VertexiumMetricRegistry getMetricsRegistry() {
        return graph.getMetricsRegistry();
    }

    private static class StreamingPropertyString {
        private final String propertyValue;

        public StreamingPropertyString(String propertyValue) {
            this.propertyValue = propertyValue;
        }

        public String getPropertyValue() {
            return propertyValue;
        }
    }

    protected String addVisibilityToPropertyName(Graph graph, Property property) {
        String propertyName = property.getName();
        Visibility propertyVisibility = property.getVisibility();
        return addVisibilityToPropertyName(graph, propertyName, propertyVisibility);
    }

    protected String addVisibilityToExtendedDataColumnName(Graph graph, ExtendedDataMutation extendedDataMutation) {
        String columnName = extendedDataMutation.getColumnName();
        Visibility propertyVisibility = extendedDataMutation.getVisibility();
        return addVisibilityToPropertyName(graph, columnName, propertyVisibility);
    }

    String addVisibilityToPropertyName(Graph graph, String propertyName, Visibility propertyVisibility) {
        String visibilityHash = getVisibilityHash(graph, propertyName, propertyVisibility);
        return propertyName + "_" + visibilityHash;
    }

    protected String removeVisibilityFromPropertyName(String string) {
        Matcher m = PROPERTY_NAME_PATTERN.matcher(string);
        if (m.matches()) {
            string = m.group(1);
        }
        return string;
    }

    private String removeVisibilityFromPropertyNameWithTypeSuffix(String string) {
        Matcher m = PROPERTY_NAME_PATTERN.matcher(string);
        if (m.matches()) {
            if (m.groupCount() >= 4 && m.group(4) != null) {
                string = m.group(1) + m.group(4);
            } else {
                string = m.group(1);
            }
        }
        return string;
    }

    public String getPropertyVisibilityHashFromPropertyName(String propertyName) {
        Matcher m = PROPERTY_NAME_PATTERN.matcher(propertyName);
        if (m.matches()) {
            return m.group(3);
        }
        throw new VertexiumException("Could not match property name: " + propertyName);
    }

    public String getAggregationName(String name) {
        Matcher m = AGGREGATION_NAME_PATTERN.matcher(name);
        if (m.matches()) {
            return m.group(1);
        }
        throw new VertexiumException("Could not get aggregation name from: " + name);
    }

    public String replaceFieldnameDots(String fieldName) {
        return fieldName.replace(".", FIELDNAME_DOT_REPLACEMENT);
    }

    public String[] getAllMatchingPropertyNames(Graph graph, String propertyName, Authorizations authorizations) {
        if (Element.ID_PROPERTY_NAME.equals(propertyName)
            || Edge.LABEL_PROPERTY_NAME.equals(propertyName)
            || Edge.OUT_VERTEX_ID_PROPERTY_NAME.equals(propertyName)
            || Edge.IN_VERTEX_ID_PROPERTY_NAME.equals(propertyName)
            || Edge.IN_OR_OUT_VERTEX_ID_PROPERTY_NAME.equals(propertyName)
            || ExtendedDataRow.ELEMENT_ID.equals(propertyName)
            || ExtendedDataRow.ELEMENT_TYPE.equals(propertyName)
            || ExtendedDataRow.ROW_ID.equals(propertyName)
            || ExtendedDataRow.TABLE_NAME.equals(propertyName)) {
            return new String[]{propertyName};
        }
        Collection hashes = this.propertyNameVisibilitiesStore.getHashes(graph, propertyName, authorizations);
        return Arrays.stream(addHashesToPropertyName(propertyName, hashes))
            .filter(matchedPropertyName -> isPropertyInIndex(graph, matchedPropertyName))
            .toArray(String[]::new);
    }

    public String[] addHashesToPropertyName(String propertyName, Collection hashes) {
        if (hashes.size() == 0) {
            return new String[0];
        }
        String[] results = new String[hashes.size()];
        int i = 0;
        for (String hash : hashes) {
            results[i++] = propertyName + "_" + hash;
        }
        return results;
    }

    public Collection getQueryableExtendedDataVisibilities(Graph graph, Authorizations authorizations) {
        return propertyNameVisibilitiesStore.getHashes(graph, authorizations);
    }

    public Collection getQueryableElementTypeVisibilityPropertyNames(Graph graph, Authorizations authorizations) {
        Set propertyNames = new HashSet<>();
        for (String hash : propertyNameVisibilitiesStore.getHashes(graph, ELEMENT_TYPE_FIELD_NAME, authorizations)) {
            propertyNames.add(ELEMENT_TYPE_FIELD_NAME + "_" + hash);
        }
        if (propertyNames.size() == 0) {
            throw new VertexiumNoMatchingPropertiesException("No queryable " + ELEMENT_TYPE_FIELD_NAME + " for authorizations " + authorizations);
        }
        return propertyNames;
    }

    public Collection getQueryablePropertyNames(Graph graph, Authorizations authorizations) {
        Set propertyNames = new HashSet<>();
        for (PropertyDefinition propertyDefinition : graph.getPropertyDefinitions()) {
            List queryableTypeSuffixes = getQueryableTypeSuffixes(propertyDefinition);
            if (queryableTypeSuffixes.size() == 0) {
                continue;
            }
            String propertyNameNoVisibility = removeVisibilityFromPropertyName(propertyDefinition.getPropertyName()); // could have visibility
            if (isReservedFieldName(propertyNameNoVisibility)) {
                continue;
            }
            for (String hash : propertyNameVisibilitiesStore.getHashes(graph, propertyNameNoVisibility, authorizations)) {
                for (String typeSuffix : queryableTypeSuffixes) {
                    propertyNames.add(propertyNameNoVisibility + "_" + hash + typeSuffix);
                }
            }
        }
        return propertyNames;
    }

    private static List getQueryableTypeSuffixes(PropertyDefinition propertyDefinition) {
        List typeSuffixes = new ArrayList<>();
        if (propertyDefinition.getDataType() == String.class) {
            if (propertyDefinition.getTextIndexHints().contains(TextIndexHint.EXACT_MATCH)) {
                typeSuffixes.add(EXACT_MATCH_PROPERTY_NAME_SUFFIX);
            }
            if (propertyDefinition.getTextIndexHints().contains(TextIndexHint.FULL_TEXT)) {
                typeSuffixes.add("");
            }
        } else if (GeoShape.class.isAssignableFrom(propertyDefinition.getDataType())) {
            typeSuffixes.add("");
        }
        return typeSuffixes;
    }

    protected static boolean isReservedFieldName(String fieldName) {
        return fieldName.startsWith("__");
    }

    private String getVisibilityHash(Graph graph, String propertyName, Visibility visibility) {
        return this.propertyNameVisibilitiesStore.getHash(graph, propertyName, visibility);
    }

    @Override
    public void deleteElement(Graph graph, ElementId elementId, Authorizations authorizations) {
        String indexName = getIndexName(elementId);
        String docId = getIdStrategy().createElementDocId(elementId);
        if (MUTATION_LOGGER.isTraceEnabled()) {
            LOGGER.trace("deleting document %s (docId: %s)", elementId.getId(), docId);
        }
        DeleteRequest deleteRequest = getClient()
            .prepareDelete(indexName, getIdStrategy().getType(), docId)
            .request();
        bulkUpdateService.addDelete(elementId, docId, deleteRequest);
    }

    @Override
    public SearchIndexSecurityGranularity getSearchIndexSecurityGranularity() {
        return SearchIndexSecurityGranularity.PROPERTY;
    }

    @Override
    public GraphQuery queryGraph(Graph graph, String queryString, Authorizations authorizations) {
        return new ElasticsearchSearchGraphQuery(
            getClient(),
            graph,
            queryString,
            new ElasticsearchSearchQueryBase.Options()
                .setIndexSelectionStrategy(getIndexSelectionStrategy())
                .setPageSize(getConfig().getQueryPageSize())
                .setPagingLimit(getConfig().getPagingLimit())
                .setScrollKeepAlive(getConfig().getScrollKeepAlive())
                .setTermAggregationShardSize(getConfig().getTermAggregationShardSize())
                .setMaxQueryStringTerms(getConfig().getMaxQueryStringTerms()),
            authorizations
        );
    }

    @Override
    public VertexQuery queryVertex(Graph graph, Vertex vertex, String queryString, Authorizations authorizations) {
        return new ElasticsearchSearchVertexQuery(
            getClient(),
            graph,
            vertex,
            queryString,
            new ElasticsearchSearchVertexQuery.Options()
                .setIndexSelectionStrategy(getIndexSelectionStrategy())
                .setPageSize(getConfig().getQueryPageSize())
                .setPagingLimit(getConfig().getPagingLimit())
                .setScrollKeepAlive(getConfig().getScrollKeepAlive())
                .setTermAggregationShardSize(getConfig().getTermAggregationShardSize())
                .setMaxQueryStringTerms(getConfig().getMaxQueryStringTerms()),
            authorizations
        );
    }

    @Override
    public Query queryExtendedData(Graph graph, Element element, String tableName, String queryString, Authorizations authorizations) {
        return new ElasticsearchSearchExtendedDataQuery(
            getClient(),
            graph,
            element.getId(),
            tableName,
            queryString,
            new ElasticsearchSearchExtendedDataQuery.Options()
                .setIndexSelectionStrategy(getIndexSelectionStrategy())
                .setPageSize(getConfig().getQueryPageSize())
                .setPagingLimit(getConfig().getPagingLimit())
                .setScrollKeepAlive(getConfig().getScrollKeepAlive())
                .setTermAggregationShardSize(getConfig().getTermAggregationShardSize())
                .setMaxQueryStringTerms(getConfig().getMaxQueryStringTerms()),
            authorizations
        );
    }

    @Override
    public SimilarToGraphQuery querySimilarTo(Graph graph, String[] similarToFields, String similarToText, Authorizations authorizations) {
        return new ElasticsearchSearchGraphQuery(
            getClient(),
            graph,
            similarToFields,
            similarToText,
            new ElasticsearchSearchQueryBase.Options()
                .setIndexSelectionStrategy(getIndexSelectionStrategy())
                .setPageSize(getConfig().getQueryPageSize())
                .setPagingLimit(getConfig().getPagingLimit())
                .setScrollKeepAlive(getConfig().getScrollKeepAlive())
                .setTermAggregationShardSize(getConfig().getTermAggregationShardSize())
                .setMaxQueryStringTerms(getConfig().getMaxQueryStringTerms()),
            authorizations
        );
    }

    @Override
    public boolean isFieldLevelSecuritySupported() {
        return true;
    }

    @Override
    public boolean isDeleteElementSupported() {
        return true;
    }

    protected void addPropertyDefinitionToIndex(
        Graph graph,
        IndexInfo indexInfo,
        String propertyName,
        Visibility propertyVisibility,
        PropertyDefinition propertyDefinition
    ) {
        String propertyNameWithVisibility = addVisibilityToPropertyName(graph, propertyName, propertyVisibility);

        if (propertyDefinition.getDataType() == String.class) {
            boolean exact = propertyDefinition.getTextIndexHints().contains(TextIndexHint.EXACT_MATCH);
            boolean analyzed = propertyDefinition.getTextIndexHints().contains(TextIndexHint.FULL_TEXT);
            boolean sortable = propertyDefinition.isSortable();
            if (analyzed || exact || sortable) {
                addPropertyToIndex(graph, indexInfo, propertyNameWithVisibility, propertyVisibility, String.class, analyzed, exact, sortable);
            }
            return;
        }

        if (GeoShape.class.isAssignableFrom(propertyDefinition.getDataType())) {
            addPropertyToIndex(graph, indexInfo, propertyNameWithVisibility + GEO_PROPERTY_NAME_SUFFIX, propertyVisibility, propertyDefinition.getDataType(), true, false, false);
            addPropertyToIndex(graph, indexInfo, propertyNameWithVisibility, propertyVisibility, String.class, true, true, false);
            if (propertyDefinition.getDataType() == GeoPoint.class) {
                addPropertyToIndex(graph, indexInfo, propertyNameWithVisibility + GEO_POINT_PROPERTY_NAME_SUFFIX, propertyVisibility, propertyDefinition.getDataType(), true, true, false);
            }
            return;
        }

        addPropertyToIndex(graph, indexInfo, propertyNameWithVisibility, propertyVisibility, propertyDefinition.getDataType(), true, false, false);
    }

    protected PropertyDefinition getPropertyDefinition(Graph graph, String propertyName) {
        propertyName = removeVisibilityFromPropertyNameWithTypeSuffix(propertyName);
        return graph.getPropertyDefinition(propertyName);
    }

    public void addPropertyToIndex(
        Graph graph,
        IndexInfo indexInfo,
        String propertyName,
        Object propertyValue,
        Visibility propertyVisibility
    ) {
        PropertyDefinition propertyDefinition = getPropertyDefinition(graph, propertyName);
        if (propertyDefinition != null) {
            addPropertyDefinitionToIndex(graph, indexInfo, propertyName, propertyVisibility, propertyDefinition);
        } else {
            addPropertyToIndexInner(graph, indexInfo, propertyName, propertyValue, propertyVisibility);
        }

        propertyDefinition = getPropertyDefinition(graph, propertyName + EXACT_MATCH_PROPERTY_NAME_SUFFIX);
        if (propertyDefinition != null) {
            addPropertyDefinitionToIndex(graph, indexInfo, propertyName, propertyVisibility, propertyDefinition);
        }

        if (propertyValue instanceof GeoShape) {
            propertyDefinition = getPropertyDefinition(graph, propertyName + GEO_PROPERTY_NAME_SUFFIX);
            if (propertyDefinition != null) {
                addPropertyDefinitionToIndex(graph, indexInfo, propertyName, propertyVisibility, propertyDefinition);
            }
        }
    }

    private void addPropertyToIndexInner(
        Graph graph,
        IndexInfo indexInfo,
        String propertyName,
        Object propertyValue,
        Visibility propertyVisibility
    ) {
        String propertyNameWithVisibility = addVisibilityToPropertyName(graph, propertyName, propertyVisibility);

        if (indexInfo.isPropertyDefined(propertyNameWithVisibility, propertyVisibility)) {
            return;
        }

        Class dataType;
        if (propertyValue instanceof StreamingPropertyValue) {
            StreamingPropertyValue streamingPropertyValue = (StreamingPropertyValue) propertyValue;
            if (!streamingPropertyValue.isSearchIndex()) {
                return;
            }
            dataType = streamingPropertyValue.getValueType();
            addPropertyToIndex(graph, indexInfo, propertyNameWithVisibility, propertyVisibility, dataType, true, false, false);
        } else if (propertyValue instanceof String) {
            dataType = String.class;
            addPropertyToIndex(graph, indexInfo, propertyNameWithVisibility, propertyVisibility, dataType, true, true, false);
        } else if (propertyValue instanceof GeoShape) {
            addPropertyToIndex(graph, indexInfo, propertyNameWithVisibility + GEO_PROPERTY_NAME_SUFFIX, propertyVisibility, propertyValue.getClass(), true, false, false);
            addPropertyToIndex(graph, indexInfo, propertyNameWithVisibility, propertyVisibility, String.class, true, true, false);
            if (propertyValue instanceof GeoPoint) {
                addPropertyToIndex(graph, indexInfo, propertyNameWithVisibility + GEO_POINT_PROPERTY_NAME_SUFFIX, propertyVisibility, propertyValue.getClass(), true, true, false);
            }
        } else {
            checkNotNull(propertyValue, "property value cannot be null for property: " + propertyNameWithVisibility);
            dataType = propertyValue.getClass();
            addPropertyToIndex(graph, indexInfo, propertyNameWithVisibility, propertyVisibility, dataType, true, false, false);
        }
    }

    protected void addPropertyToIndex(
        Graph graph,
        IndexInfo indexInfo,
        String propertyName,
        Visibility propertyVisibility,
        Class dataType,
        boolean analyzed,
        boolean exact,
        boolean sortable
    ) {
        if (indexInfo.isPropertyDefined(propertyName, propertyVisibility)) {
            return;
        }

        if (shouldIgnoreType(dataType)) {
            return;
        }

        this.indexInfosLock.writeLock().lock();
        try {
            XContentBuilder mapping = XContentFactory.jsonBuilder()
                .startObject()
                .startObject(getIdStrategy().getType())
                .startObject("properties")
                .startObject(replaceFieldnameDots(propertyName));

            addTypeToMapping(mapping, propertyName, dataType, analyzed, exact, sortable);

            mapping
                .endObject()
                .endObject()
                .endObject()
                .endObject();
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("addPropertyToIndex: %s: %s", dataType.getName(), mapping.string());
            }

            getClient()
                .admin()
                .indices()
                .preparePutMapping(indexInfo.getIndexName())
                .setType(getIdStrategy().getType())
                .setSource(mapping)
                .execute()
                .actionGet();

            addPropertyNameVisibility(graph, indexInfo, propertyName, propertyVisibility);
            updateMetadata(graph, indexInfo);
        } catch (IOException ex) {
            throw new VertexiumException(
                String.format(
                    "Could not add property to index (index: %s, propertyName: %s)",
                    indexInfo.getIndexName(),
                    propertyName
                ),
                ex
            );
        } finally {
            this.indexInfosLock.writeLock().unlock();
        }
    }

    @SuppressWarnings("unchecked")
    private void updateMetadata(Graph graph, IndexInfo indexInfo) {
        try {
            indexRefreshTracker.refresh(client, indexInfo.getIndexName());
            XContentBuilder mapping = XContentFactory.jsonBuilder()
                .startObject()
                .startObject(getIdStrategy().getType());
            GetMappingsResponse existingMapping = getClient()
                .admin()
                .indices()
                .prepareGetMappings(indexInfo.getIndexName())
                .execute()
                .actionGet();

            Map existingElementData = existingMapping.mappings()
                .get(indexInfo.getIndexName())
                .get(getIdStrategy().getType())
                .getSourceAsMap();

            mapping = mapping.startObject("_meta")
                .startObject("vertexium");
            Map properties = (Map) existingElementData.get("properties");
            for (String propertyName : properties.keySet()) {
                ElasticsearchPropertyNameInfo p = ElasticsearchPropertyNameInfo.parse(graph, propertyNameVisibilitiesStore, propertyName);
                if (p == null || p.getPropertyVisibility() == null) {
                    continue;
                }
                mapping.field(replaceFieldnameDots(propertyName), p.getPropertyVisibility());
            }
            mapping.endObject()
                .endObject()
                .endObject()
                .endObject();
            getClient()
                .admin()
                .indices()
                .preparePutMapping(indexInfo.getIndexName())
                .setType(getIdStrategy().getType())
                .setSource(mapping)
                .execute()
                .actionGet();
        } catch (IOException ex) {
            throw new VertexiumException("Could not update mapping", ex);
        }
    }

    protected void addPropertyNameVisibility(Graph graph, IndexInfo indexInfo, String propertyName, Visibility propertyVisibility) {
        String propertyNameNoVisibility = removeVisibilityFromPropertyName(propertyName);
        if (propertyVisibility != null) {
            this.propertyNameVisibilitiesStore.getHash(graph, propertyNameNoVisibility, propertyVisibility);
        }
        indexInfo.addPropertyNameVisibility(propertyNameNoVisibility, propertyVisibility);
        indexInfo.addPropertyNameVisibility(propertyName, propertyVisibility);
    }

    @Override
    @Deprecated
    public Map getVertexPropertyCountByValue(Graph graph, String propertyName, Authorizations authorizations) {
        if (shouldRefreshIndexOnQuery()) {
            indexRefreshTracker.refresh(client);
        }

        TermQueryBuilder elementTypeFilterBuilder = new TermQueryBuilder(ELEMENT_TYPE_FIELD_NAME, ElasticsearchDocumentType.VERTEX.getKey());
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery()
            .must(QueryBuilders.matchAllQuery())
            .filter(elementTypeFilterBuilder);
        SearchRequestBuilder q = getClient().prepareSearch(getIndexNamesAsArray(graph))
            .setQuery(queryBuilder)
            .setSize(0);

        for (String p : getAllMatchingPropertyNames(graph, propertyName, authorizations)) {
            String countAggName = "count-" + p;
            PropertyDefinition propertyDefinition = getPropertyDefinition(graph, p);
            p = replaceFieldnameDots(p);
            if (propertyDefinition != null && propertyDefinition.getTextIndexHints().contains(TextIndexHint.EXACT_MATCH)) {
                p = p + EXACT_MATCH_PROPERTY_NAME_SUFFIX;
            }

            TermsAggregationBuilder countAgg = AggregationBuilders
                .terms(countAggName)
                .field(p)
                .size(500000);
            q = q.addAggregation(countAgg);
        }

        if (ElasticsearchSearchQueryBase.QUERY_LOGGER.isTraceEnabled()) {
            ElasticsearchSearchQueryBase.QUERY_LOGGER.trace("query: %s", q);
        }
        SearchResponse response = checkForFailures(getClient().search(q.request()).actionGet());
        Map results = new HashMap<>();
        for (Aggregation agg : response.getAggregations().asList()) {
            Terms propertyCountResults = (Terms) agg;
            for (Terms.Bucket propertyCountResult : propertyCountResults.getBuckets()) {
                String mapKey = ((String) propertyCountResult.getKey()).toLowerCase();
                Long previousValue = results.get(mapKey);
                if (previousValue == null) {
                    previousValue = 0L;
                }
                results.put(mapKey, previousValue + propertyCountResult.getDocCount());
            }
        }
        return results;
    }

    public IndexInfo ensureIndexCreatedAndInitialized(String indexName) {
        Map indexInfos = getIndexInfos();
        IndexInfo indexInfo = indexInfos.get(indexName);
        if (indexInfo != null && indexInfo.isElementTypeDefined()) {
            return indexInfo;
        }
        return initializeIndex(indexInfo, indexName);
    }

    private IndexInfo initializeIndex(String indexName) {
        return initializeIndex(null, indexName);
    }

    private IndexInfo initializeIndex(IndexInfo indexInfo, String indexName) {
        indexInfosLock.writeLock().lock();
        try {
            if (indexInfo == null) {
                if (!client.admin().indices().prepareExists(indexName).execute().actionGet().isExists()) {
                    try {
                        createIndex(indexName);
                    } catch (Exception e) {
                        throw new VertexiumException("Could not create index: " + indexName, e);
                    }
                }

                indexInfo = createIndexInfo(indexName);

                if (indexInfos == null) {
                    loadIndexInfos();
                }
                indexInfos.put(indexName, indexInfo);
            }

            ensureMappingsCreated(indexInfo);

            return indexInfo;
        } finally {
            indexInfosLock.writeLock().unlock();
        }
    }


    protected IndexInfo createIndexInfo(String indexName) {
        return new IndexInfo(indexName);
    }

    protected void ensureMappingsCreated(IndexInfo indexInfo) {
        if (!indexInfo.isElementTypeDefined()) {
            try {
                XContentBuilder mappingBuilder = XContentFactory.jsonBuilder()
                    .startObject()
                    .startObject("_source").field("enabled", true).endObject()
                    .startObject("_all").field("enabled", isAllFieldEnabled()).endObject()
                    .startObject("properties");
                createIndexAddFieldsToElementType(mappingBuilder);
                XContentBuilder mapping = mappingBuilder.endObject()
                    .endObject();

                client.admin().indices().preparePutMapping(indexInfo.getIndexName())
                    .setType(getIdStrategy().getType())
                    .setSource(mapping)
                    .execute()
                    .actionGet();
                indexInfo.setElementTypeDefined(true);

                updateMetadata(graph, indexInfo);
            } catch (Throwable e) {
                throw new VertexiumException("Could not add mappings to index: " + indexInfo.getIndexName(), e);
            }
        }
    }

    protected void createIndexAddFieldsToElementType(XContentBuilder builder) throws IOException {
        builder
            .startObject(ELEMENT_ID_FIELD_NAME).field("type", "keyword").field("store", "true").endObject()
            .startObject(ELEMENT_TYPE_FIELD_NAME).field("type", "keyword").field("store", "true").endObject()
            .startObject(EXTENDED_DATA_TABLE_ROW_ID_FIELD_NAME).field("type", "keyword").field("store", "true").endObject()
            .startObject(EXTENDED_DATA_TABLE_NAME_FIELD_NAME).field("type", "keyword").field("store", "true").endObject()
            .startObject(VISIBILITY_FIELD_NAME).field("type", "keyword").field("store", "true").endObject()
            .startObject(IN_VERTEX_ID_FIELD_NAME).field("type", "keyword").field("store", "true").endObject()
            .startObject(OUT_VERTEX_ID_FIELD_NAME).field("type", "keyword").field("store", "true").endObject()
            .startObject(EDGE_LABEL_FIELD_NAME).field("type", "keyword").field("store", "true").endObject()
            .startObject(ADDITIONAL_VISIBILITY_FIELD_NAME).field("type", "keyword").field("store", "true").endObject()
        ;
    }

    @Override
    public void deleteProperty(Graph graph, Element element, PropertyDescriptor property, Authorizations authorizations) {
        deleteProperties(graph, element, Collections.singletonList(property), authorizations);
    }

    @Override
    public void deleteProperties(Graph graph, Element element, Collection propertyList, Authorizations authorizations) {
        List fieldsToRemove = new ArrayList<>();
        Map fieldsToSet = new HashMap<>();
        propertyList.forEach(p -> {
            fieldsToRemove.addAll(getFieldsToRemove(graph, p.getName(), p.getVisibility()));
            addExistingValuesToFieldMap(graph, element, p.getName(), p.getVisibility(), fieldsToSet);
        });

        String documentId = getIdStrategy().createElementDocId(element);
        String indexName = getIndexName(element);
        IndexInfo indexInfo = ensureIndexCreatedAndInitialized(indexName);
        UpdateRequestBuilder updateRequestBuilder = prepareUpdateFieldsOnDocument(
            indexInfo.getIndexName(),
            documentId,
            fieldsToSet,
            fieldsToRemove,
            null,
            null,
            null
        );
        if (updateRequestBuilder != null) {
            addActionRequestBuilderForFlush(element, updateRequestBuilder.request());

            if (getConfig().isAutoFlush()) {
                flush(graph);
            }
        }
    }

    @Override
    public void addElements(Graph graph, Iterable elements, Authorizations authorizations) {
        for (Element element : elements) {
            addElement(graph, element, null, null, authorizations);
        }
    }

    private void logRequestSize(String elementId, UpdateRequest request) {
        if (logRequestSizeLimit == null) {
            return;
        }
        int sizeInBytes = ElasticsearchRequestUtils.getSize(request);
        if (sizeInBytes > logRequestSizeLimit) {
            LOGGER.warn("Large document detected (id: %s). Size in bytes: %d", elementId, sizeInBytes);
        }
    }

    @Override
    public MultiVertexQuery queryGraph(Graph graph, String[] vertexIds, String queryString, Authorizations authorizations) {
        return new ElasticsearchSearchMultiVertexQuery(
            getClient(),
            graph,
            vertexIds,
            queryString,
            new ElasticsearchSearchQueryBase.Options()
                .setIndexSelectionStrategy(getIndexSelectionStrategy())
                .setPageSize(getConfig().getQueryPageSize())
                .setPagingLimit(getConfig().getPagingLimit())
                .setScrollKeepAlive(getConfig().getScrollKeepAlive())
                .setTermAggregationShardSize(getConfig().getTermAggregationShardSize())
                .setMaxQueryStringTerms(getConfig().getMaxQueryStringTerms()),
            authorizations
        );

    }

    @Override
    public boolean isQuerySimilarToTextSupported() {
        return true;
    }

    @Override
    public void flush(Graph graph) {
        bulkUpdateService.flush();
        if (shouldRefreshIndexOnFlush()) {
            indexRefreshTracker.refresh(client);
        }
    }

    private void removeFieldsFromDocument(Graph graph, ElementLocation elementLocation, String field) {
        removeFieldsFromDocument(graph, elementLocation, Lists.newArrayList(field));
    }

    private void removeFieldsFromDocument(Graph graph, ElementLocation elementLocation, Collection fields) {
        String indexName = getIndexName(elementLocation);
        String documentId = getIdStrategy().createElementDocId(elementLocation);
        removeFieldsFromDocument(graph, indexName, elementLocation, documentId, fields);
    }

    private void removeFieldsFromDocument(
        Graph graph,
        String indexName,
        ElementLocation elementLocation,
        String documentId,
        Collection fields
    ) {
        if (fields == null || fields.isEmpty()) {
            return;
        }

        UpdateRequestBuilder updateRequestBuilder = prepareRemoveFieldsFromDocument(indexName, documentId, fields);
        addActionRequestBuilderForFlush(elementLocation, updateRequestBuilder.request());

        if (getConfig().isAutoFlush()) {
            flush(graph);
        }
    }

    private UpdateRequestBuilder prepareRemoveFieldsFromDocument(String indexName, String documentId, Collection fields) {
        List fieldNames = fields.stream().map(this::replaceFieldnameDots).collect(Collectors.toList());
        if (fieldNames.isEmpty()) {
            return null;
        }

        return getClient().prepareUpdate()
            .setIndex(indexName)
            .setId(documentId)
            .setType(getIdStrategy().getType())
            .setScript(new Script(
                ScriptType.STORED,
                "painless",
                "deleteFieldsFromDocumentScript",
                ImmutableMap.of("fieldNames", fieldNames)
            ))
            .setRetryOnConflict(MAX_RETRIES);
    }

    private UpdateRequestBuilder prepareUpdateFieldsOnDocument(
        String indexName,
        String documentId,
        Map fieldsToSet,
        Collection fieldsToRemove,
        Map fieldsToRename,
        Set additionalVisibilities,
        Set additionalVisibilitiesToDelete
    ) {
        if ((fieldsToSet == null || fieldsToSet.isEmpty()) &&
            (fieldsToRemove == null || fieldsToRemove.isEmpty()) &&
            (fieldsToRename == null || fieldsToRename.isEmpty()) &&
            (additionalVisibilities == null || additionalVisibilities.isEmpty()) &&
            (additionalVisibilitiesToDelete == null || additionalVisibilitiesToDelete.isEmpty())) {
            return null;
        }

        fieldsToSet = fieldsToSet == null ? Collections.emptyMap() : fieldsToSet.entrySet().stream()
            .collect(Collectors.toMap(e -> replaceFieldnameDots(e.getKey()), Map.Entry::getValue));
        fieldsToRemove = fieldsToRemove == null ? Collections.emptyList() : fieldsToRemove.stream().map(this::replaceFieldnameDots).collect(Collectors.toList());
        fieldsToRename = fieldsToRename == null ? Collections.emptyMap() : fieldsToRename.entrySet().stream()
            .collect(Collectors.toMap(e -> replaceFieldnameDots(e.getKey()), e -> replaceFieldnameDots(e.getValue())));
        List additionalVisibilitiesParam = additionalVisibilities == null ? Collections.emptyList() : new ArrayList<>(additionalVisibilities);
        List additionalVisibilitiesToDeleteParam = additionalVisibilitiesToDelete == null ? Collections.emptyList() : new ArrayList<>(additionalVisibilitiesToDelete);
        ensureAdditionalVisibilitiesDefined(additionalVisibilitiesParam);

        return getClient().prepareUpdate()
            .setIndex(indexName)
            .setId(documentId)
            .setType(getIdStrategy().getType())
            .setScript(new Script(
                ScriptType.STORED,
                "painless",
                "updateFieldsOnDocumentScript",
                ImmutableMap.of(
                    "fieldsToSet", fieldsToSet,
                    "fieldsToRemove", fieldsToRemove,
                    "fieldsToRename", fieldsToRename,
                    "additionalVisibilities", additionalVisibilitiesParam,
                    "additionalVisibilitiesToDelete", additionalVisibilitiesToDeleteParam
                )
            ))
            .setRetryOnConflict(MAX_RETRIES);
    }

    protected String[] getIndexNamesAsArray(Graph graph) {
        Map indexInfos = getIndexInfos();
        if (indexInfos.size() == indexInfosLastSize) {
            return indexNamesAsArray;
        }
        synchronized (this) {
            Set keys = indexInfos.keySet();
            indexNamesAsArray = keys.toArray(new String[0]);
            indexInfosLastSize = indexInfos.size();
            return indexNamesAsArray;
        }
    }

    @Override
    public void shutdown() {
        bulkUpdateService.shutdown();
        client.close();

        if (propertyNameVisibilitiesStore instanceof Closeable) {
            try {
                ((Closeable) propertyNameVisibilitiesStore).close();
            } catch (IOException e) {
                Throwables.propagate(e);
            }
        }
    }

    @SuppressWarnings("unused")
    protected String[] getIndexNames(PropertyDefinition propertyDefinition) {
        return indexSelectionStrategy.getIndexNames(this, propertyDefinition);
    }

    protected String getIndexName(ElementId elementId) {
        return indexSelectionStrategy.getIndexName(this, elementId);
    }

    protected String getExtendedDataIndexName(
        ElementLocation elementLocation,
        String tableName,
        String rowId
    ) {
        return indexSelectionStrategy.getExtendedDataIndexName(this, elementLocation, tableName, rowId);
    }

    protected String getExtendedDataIndexName(ExtendedDataRowId rowId) {
        return indexSelectionStrategy.getExtendedDataIndexName(this, rowId);
    }

    protected String[] getIndicesToQuery() {
        return indexSelectionStrategy.getIndicesToQuery(this);
    }

    @Override
    public boolean isFieldBoostSupported() {
        return false;
    }

    private IndexInfo addExtendedDataColumnsToIndex(
        Graph graph,
        ElementLocation elementLocation,
        String tableName,
        String rowId,
        Iterable columns
    ) {
        String indexName = getExtendedDataIndexName(elementLocation, tableName, rowId);
        IndexInfo indexInfo = ensureIndexCreatedAndInitialized(indexName);
        for (ExtendedDataMutation column : columns) {
            addPropertyToIndex(graph, indexInfo, column.getColumnName(), column.getValue(), column.getVisibility());
        }
        return indexInfo;
    }

    public IndexInfo addPropertiesToIndex(Graph graph, Element element, Iterable properties) {
        String indexName = getIndexName(element);
        IndexInfo indexInfo = ensureIndexCreatedAndInitialized(indexName);
        for (Property property : properties) {
            addPropertyToIndex(graph, indexInfo, property.getName(), property.getValue(), property.getVisibility());
        }
        return indexInfo;
    }

    protected boolean shouldIgnoreType(Class dataType) {
        return dataType == byte[].class;
    }

    protected void addTypeToMapping(XContentBuilder mapping, String propertyName, Class dataType, boolean analyzed, boolean exact, boolean sortable) throws IOException {
        if (dataType == String.class) {
            LOGGER.debug("Registering 'string' type for %s", propertyName);
            if (analyzed || exact || sortable) {
                mapping.field("type", "text");
                if (!analyzed) {
                    mapping.field("index", "false");
                }
                if (exact || sortable) {
                    mapping.startObject("fields");
                    mapping.startObject(EXACT_MATCH_FIELD_NAME)
                        .field("type", "keyword")
                        .field("ignore_above", EXACT_MATCH_IGNORE_ABOVE_LIMIT)
                        .field("normalizer", LOWERCASER_NORMALIZER_NAME)
                        .endObject();
                    mapping.endObject();
                }
            } else {
                mapping.field("type", "keyword");
                mapping.field("ignore_above", EXACT_MATCH_IGNORE_ABOVE_LIMIT);
                mapping.field("normalizer", LOWERCASER_NORMALIZER_NAME);
            }
        } else if (dataType == IpV4Address.class) {
            LOGGER.debug("Registering 'ip' type for %s", propertyName);
            mapping.field("type", "ip");
        } else if (dataType == Float.class || dataType == Float.TYPE) {
            LOGGER.debug("Registering 'float' type for %s", propertyName);
            mapping.field("type", "float");
        } else if (dataType == Double.class || dataType == Double.TYPE) {
            LOGGER.debug("Registering 'double' type for %s", propertyName);
            mapping.field("type", "double");
        } else if (dataType == Byte.class || dataType == Byte.TYPE) {
            LOGGER.debug("Registering 'byte' type for %s", propertyName);
            mapping.field("type", "byte");
        } else if (dataType == Short.class || dataType == Short.TYPE) {
            LOGGER.debug("Registering 'short' type for %s", propertyName);
            mapping.field("type", "short");
        } else if (dataType == Integer.class || dataType == Integer.TYPE) {
            LOGGER.debug("Registering 'integer' type for %s", propertyName);
            mapping.field("type", "integer");
        } else if (dataType == Long.class || dataType == Long.TYPE) {
            LOGGER.debug("Registering 'long' type for %s", propertyName);
            mapping.field("type", "long");
        } else if (dataType == Date.class || dataType == DateOnly.class) {
            LOGGER.debug("Registering 'date' type for %s", propertyName);
            mapping.field("type", "date");
        } else if (dataType == Boolean.class || dataType == Boolean.TYPE) {
            LOGGER.debug("Registering 'boolean' type for %s", propertyName);
            mapping.field("type", "boolean");
        } else if (dataType == GeoPoint.class && exact) {
            // ES5 doesn't support geo hash aggregations for shapes, so if this is a point marked for EXACT_MATCH
            // define it as a geo_point instead of a geo_shape. Points end up with 3 fields in the index for this
            // reason. This one for aggregating as well as the "_g" and description fields that all geoshapes get.
            LOGGER.debug("Registering 'geo_point' type for %s", propertyName);
            mapping.field("type", "geo_point");
        } else if (GeoShape.class.isAssignableFrom(dataType)) {
            LOGGER.debug("Registering 'geo_shape' type for %s", propertyName);
            mapping.field("type", "geo_shape");
            if (dataType == GeoPoint.class) {
                mapping.field("points_only", "true");
            }
            mapping.field("tree", "quadtree");
            mapping.field("precision", geoShapePrecision);
            mapping.field("distance_error_pct", geoShapeErrorPct);
        } else if (Number.class.isAssignableFrom(dataType)) {
            LOGGER.debug("Registering 'double' type for %s", propertyName);
            mapping.field("type", "double");
        } else {
            throw new VertexiumException("Unexpected value type for property \"" + propertyName + "\": " + dataType.getName());
        }
    }

    @Override
    public synchronized void truncate(Graph graph) {
        LOGGER.warn("Truncate of Elasticsearch is not possible, dropping the indices and recreating instead.");
        drop(graph);
    }

    @Override
    public void drop(Graph graph) {
        this.indexInfosLock.writeLock().lock();
        try {
            if (this.indexInfos == null) {
                loadIndexInfos();
            }
            Set indexInfosSet = new HashSet<>(this.indexInfos.keySet());
            for (String indexName : indexInfosSet) {
                try {
                    DeleteIndexRequest deleteRequest = new DeleteIndexRequest(indexName);
                    getClient().admin().indices().delete(deleteRequest).actionGet();
                } catch (Exception ex) {
                    throw new VertexiumException("Could not delete index " + indexName, ex);
                }
                this.indexInfos.remove(indexName);
                initializeIndex(indexName);
            }
        } finally {
            this.indexInfosLock.writeLock().unlock();
        }
    }

    @SuppressWarnings("unchecked")
    protected void addPropertyValueToPropertiesMap(Map propertiesMap, String propertyName, Object propertyValue) {
        Object existingValue = propertiesMap.get(propertyName);
        Object valueForIndex = convertValueForIndexing(propertyValue);
        if (existingValue == null) {
            propertiesMap.put(propertyName, valueForIndex);
            return;
        }

        if (existingValue instanceof List) {
            try {
                ((List) existingValue).add(valueForIndex);
            } catch (Exception ex) {
                LOGGER.error("could not add to existing list, this could cause performance issues", ex);
                ArrayList newList = new ArrayList<>((List) existingValue);
                newList.add(valueForIndex);
                propertiesMap.put(propertyName, newList);
            }
            return;
        }

        List list = new ArrayList();
        list.add(existingValue);
        list.add(valueForIndex);
        propertiesMap.put(propertyName, list);
    }

    protected void convertGeoShape(Map propertiesMap, String propertyNameWithVisibility, GeoShape geoShape) {
        try {
            geoShape.validate();
        } catch (VertexiumInvalidShapeException ve) {
            LOGGER.warn("Attempting to repair invalid GeoShape for property " + propertyNameWithVisibility, ve);
            geoShape = GeoUtils.repair(geoShape);
        }

        Map propertyValueMap;
        if (geoShape instanceof GeoPoint) {
            propertyValueMap = convertGeoPoint((GeoPoint) geoShape);
        } else if (geoShape instanceof GeoCircle) {
            propertyValueMap = convertGeoCircle((GeoCircle) geoShape);
        } else if (geoShape instanceof GeoLine) {
            propertyValueMap = convertGeoLine((GeoLine) geoShape);
        } else if (geoShape instanceof GeoPolygon) {
            propertyValueMap = convertGeoPolygon((GeoPolygon) geoShape);
        } else if (geoShape instanceof GeoCollection) {
            propertyValueMap = convertGeoCollection((GeoCollection) geoShape);
        } else if (geoShape instanceof GeoRect) {
            propertyValueMap = convertGeoRect((GeoRect) geoShape);
        } else {
            throw new VertexiumException("Unexpected GeoShape value of type: " + geoShape.getClass().getName());
        }

        addPropertyValueToPropertiesMap(propertiesMap, propertyNameWithVisibility + GEO_PROPERTY_NAME_SUFFIX, propertyValueMap);

        if (geoShape.getDescription() != null) {
            addPropertyValueToPropertiesMap(propertiesMap, propertyNameWithVisibility, geoShape.getDescription());
        }
    }

    protected Map convertGeoPoint(GeoPoint geoPoint) {
        Map propertyValueMap = new HashMap<>();
        propertyValueMap.put(FIELD_TYPE, "point");
        propertyValueMap.put(FIELD_COORDINATES, Arrays.asList(geoPoint.getLongitude(), geoPoint.getLatitude()));
        return propertyValueMap;
    }

    protected Map convertGeoCircle(GeoCircle geoCircle) {
        Map propertyValueMap = new HashMap<>();
        propertyValueMap.put(FIELD_TYPE, "circle");
        List coordinates = new ArrayList<>();
        coordinates.add(geoCircle.getLongitude());
        coordinates.add(geoCircle.getLatitude());
        propertyValueMap.put(FIELD_COORDINATES, coordinates);
        propertyValueMap.put(CircleBuilder.FIELD_RADIUS, geoCircle.getRadius() + "km");
        return propertyValueMap;
    }

    protected Map convertGeoRect(GeoRect geoRect) {
        Map propertyValueMap = new HashMap<>();
        propertyValueMap.put(FIELD_TYPE, "envelope");
        List> coordinates = new ArrayList<>();
        coordinates.add(Arrays.asList(geoRect.getNorthWest().getLongitude(), geoRect.getNorthWest().getLatitude()));
        coordinates.add(Arrays.asList(geoRect.getSouthEast().getLongitude(), geoRect.getSouthEast().getLatitude()));
        propertyValueMap.put(FIELD_COORDINATES, coordinates);
        return propertyValueMap;
    }

    protected Map convertGeoLine(GeoLine geoLine) {
        Map propertyValueMap = new HashMap<>();
        propertyValueMap.put(FIELD_TYPE, "linestring");
        List> coordinates = new ArrayList<>();
        geoLine.getGeoPoints().forEach(geoPoint -> coordinates.add(Arrays.asList(geoPoint.getLongitude(), geoPoint.getLatitude())));
        propertyValueMap.put(FIELD_COORDINATES, coordinates);
        return propertyValueMap;
    }

    protected Map convertGeoPolygon(GeoPolygon geoPolygon) {
        Map propertyValueMap = new HashMap<>();
        propertyValueMap.put(FIELD_TYPE, "polygon");
        List>> coordinates = new ArrayList<>();
        coordinates.add(geoPolygon.getOuterBoundary().stream()
            .map(geoPoint -> Arrays.asList(geoPoint.getLongitude(), geoPoint.getLatitude()))
            .collect(Collectors.toList()));
        geoPolygon.getHoles().forEach(holeBoundary ->
            coordinates.add(holeBoundary.stream()
                .map(geoPoint -> Arrays.asList(geoPoint.getLongitude(), geoPoint.getLatitude()))
                .collect(Collectors.toList())));
        propertyValueMap.put(FIELD_COORDINATES, coordinates);
        return propertyValueMap;
    }

    protected Map convertGeoCollection(GeoCollection geoCollection) {
        Map propertyValueMap = new HashMap<>();
        propertyValueMap.put(FIELD_TYPE, "geometrycollection");

        List> geometries = new ArrayList<>();
        geoCollection.getGeoShapes().forEach(geoShape -> {
            if (geoShape instanceof GeoPoint) {
                geometries.add(convertGeoPoint((GeoPoint) geoShape));
            } else if (geoShape instanceof GeoCircle) {
                geometries.add(convertGeoCircle((GeoCircle) geoShape));
            } else if (geoShape instanceof GeoLine) {
                geometries.add(convertGeoLine((GeoLine) geoShape));
            } else if (geoShape instanceof GeoPolygon) {
                geometries.add(convertGeoPolygon((GeoPolygon) geoShape));
            } else {
                throw new VertexiumException("Unsupported GeoShape value in GeoCollection of type: " + geoShape.getClass().getName());
            }
        });
        propertyValueMap.put(FIELD_GEOMETRIES, geometries);

        return propertyValueMap;
    }

    public IndexSelectionStrategy getIndexSelectionStrategy() {
        return indexSelectionStrategy;
    }

    @SuppressWarnings("unused")
    protected void createIndex(String indexName) throws IOException {
        CreateIndexResponse createResponse = client.admin().indices().prepareCreate(indexName)
            .setSettings(XContentFactory.jsonBuilder()
                .startObject()
                .startObject("analysis")
                .startObject("normalizer")
                .startObject(LOWERCASER_NORMALIZER_NAME)
                .field("type", "custom")
                .array("filter", "lowercase")
                .endObject()
                .endObject()
                .endObject()
                .field("number_of_shards", getConfig().getNumberOfShards())
                .field("number_of_replicas", getConfig().getNumberOfReplicas())
                .field("index.mapping.total_fields.limit", getConfig().getIndexMappingTotalFieldsLimit())
                .field("refresh_interval", getConfig().getIndexRefreshInterval())
                .endObject()
            )
            .execute().actionGet();

        ClusterHealthResponse health = client.admin().cluster().prepareHealth(indexName)
            .setWaitForGreenStatus()
            .execute().actionGet();
        LOGGER.debug("Index status: %s", health.toString());
        if (health.isTimedOut()) {
            LOGGER.warn("timed out waiting for yellow/green index status, for index: %s", indexName);
        }
    }

    public Client getClient() {
        return client;
    }

    public ElasticsearchSearchIndexConfiguration getConfig() {
        return config;
    }

    public IdStrategy getIdStrategy() {
        return idStrategy;
    }

    public IndexRefreshTracker getIndexRefreshTracker() {
        return indexRefreshTracker;
    }

    private void ensureAdditionalVisibilitiesDefined(Iterable additionalVisibilities) {
        for (String additionalVisibility : additionalVisibilities) {
            if (!additionalVisibilitiesCache.contains(additionalVisibility)) {
                String key = ADDITIONAL_VISIBILITY_METADATA_PREFIX + additionalVisibility;
                if (graph.getMetadata(key) == null) {
                    graph.setMetadata(key, additionalVisibility);
                }
                additionalVisibilitiesCache.add(additionalVisibility);
            }
        }
    }

    public QueryBuilder getAdditionalVisibilitiesFilter(Authorizations authorizations) {
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        for (GraphMetadataEntry metadata : graph.getMetadataWithPrefix(ADDITIONAL_VISIBILITY_METADATA_PREFIX)) {
            String visibilityString = (String) metadata.getValue();
            if (!authorizations.canRead(new Visibility(visibilityString))) {
                boolQuery.mustNot(QueryBuilders.termQuery(ADDITIONAL_VISIBILITY_FIELD_NAME, visibilityString));
            }
        }
        return boolQuery;
    }

    public boolean isPropertyInIndex(Graph graph, String propertyName, Visibility visibility) {
        Map indexInfos = getIndexInfos();
        for (Map.Entry entry : indexInfos.entrySet()) {
            if (entry.getValue().isPropertyDefined(propertyName, visibility)) {
                return true;
            }
        }
        return false;
    }

    public boolean isPropertyInIndex(Graph graph, String propertyName) {
        Map indexInfos = getIndexInfos();
        for (Map.Entry entry : indexInfos.entrySet()) {
            if (entry.getValue().isPropertyDefined(propertyName)) {
                return true;
            }
        }
        return false;
    }

    public String[] getPropertyNames(Graph graph, String propertyName, Authorizations authorizations) {
        String[] allMatchingPropertyNames = getAllMatchingPropertyNames(graph, propertyName, authorizations);
        return Arrays.stream(allMatchingPropertyNames)
            .map(this::replaceFieldnameDots)
            .collect(Collectors.toList())
            .toArray(new String[allMatchingPropertyNames.length]);
    }

    boolean shouldRefreshIndexOnQuery() {
        return !shouldRefreshIndexOnFlush();
    }

    boolean shouldRefreshIndexOnFlush() {
        return refreshIndexOnFlush;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy