org.janusgraph.diskstorage.es.ElasticSearchIndex Maven / Gradle / Ivy
// Copyright 2017 JanusGraph Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.janusgraph.diskstorage.es;
import static org.janusgraph.diskstorage.es.ElasticSearchConstants.ES_DOC_KEY;
import static org.janusgraph.diskstorage.es.ElasticSearchConstants.ES_GEO_COORDS_KEY;
import static org.janusgraph.diskstorage.es.ElasticSearchConstants.ES_TYPE_KEY;
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.INDEX_MAX_RESULT_SET_SIZE;
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.INDEX_NAME;
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.INDEX_NS;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterators;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;
import org.janusgraph.diskstorage.es.compat.ES6Compat;
import org.janusgraph.diskstorage.es.rest.util.HttpAuthTypes;
import org.locationtech.spatial4j.shape.Rectangle;
import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;
import org.apache.tinkerpop.shaded.jackson.databind.ObjectWriter;
import org.apache.tinkerpop.shaded.jackson.databind.SerializationFeature;
import org.janusgraph.core.Cardinality;
import org.janusgraph.core.JanusGraphException;
import org.janusgraph.core.attribute.Cmp;
import org.janusgraph.core.attribute.Geo;
import org.janusgraph.core.attribute.Geoshape;
import org.janusgraph.core.attribute.Text;
import org.janusgraph.core.schema.Mapping;
import org.janusgraph.core.schema.Parameter;
import org.janusgraph.diskstorage.BackendException;
import org.janusgraph.diskstorage.BaseTransaction;
import org.janusgraph.diskstorage.BaseTransactionConfig;
import org.janusgraph.diskstorage.BaseTransactionConfigurable;
import org.janusgraph.diskstorage.PermanentBackendException;
import org.janusgraph.diskstorage.TemporaryBackendException;
import org.janusgraph.diskstorage.configuration.ConfigNamespace;
import org.janusgraph.diskstorage.configuration.ConfigOption;
import org.janusgraph.diskstorage.configuration.Configuration;
import org.janusgraph.diskstorage.es.IndexMappings.IndexMapping;
import org.janusgraph.diskstorage.es.compat.AbstractESCompat;
import org.janusgraph.diskstorage.es.compat.ES1Compat;
import org.janusgraph.diskstorage.es.compat.ES2Compat;
import org.janusgraph.diskstorage.es.compat.ES5Compat;
import org.janusgraph.diskstorage.indexing.IndexEntry;
import org.janusgraph.diskstorage.indexing.IndexFeatures;
import org.janusgraph.diskstorage.indexing.IndexMutation;
import org.janusgraph.diskstorage.indexing.IndexProvider;
import org.janusgraph.diskstorage.indexing.IndexQuery;
import org.janusgraph.diskstorage.indexing.KeyInformation;
import org.janusgraph.diskstorage.indexing.RawQuery;
import org.janusgraph.diskstorage.util.DefaultTransaction;
import org.janusgraph.graphdb.configuration.PreInitializeConfigOptions;
import static org.janusgraph.diskstorage.configuration.ConfigOption.disallowEmpty;
import org.janusgraph.graphdb.database.serialize.AttributeUtil;
import org.janusgraph.graphdb.query.JanusGraphPredicate;
import org.janusgraph.graphdb.query.condition.And;
import org.janusgraph.graphdb.query.condition.Condition;
import org.janusgraph.graphdb.query.condition.Not;
import org.janusgraph.graphdb.query.condition.Or;
import org.janusgraph.graphdb.query.condition.PredicateCondition;
import org.janusgraph.graphdb.types.ParameterType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
/**
* @author Matthias Broecheler ([email protected])
*/
@PreInitializeConfigOptions
public class ElasticSearchIndex implements IndexProvider {
private static final Logger log = LoggerFactory.getLogger(ElasticSearchIndex.class);
private static final String STRING_MAPPING_SUFFIX = "__STRING";
public static final ConfigNamespace ELASTICSEARCH_NS =
new ConfigNamespace(INDEX_NS, "elasticsearch", "Elasticsearch index configuration");
public static final ConfigOption INTERFACE =
new ConfigOption<>(ELASTICSEARCH_NS, "interface",
"Interface for connecting to Elasticsearch. " +
"TRANSPORT_CLIENT and NODE were previously supported, but now are required to migrate to REST_CLIENT. " +
"See the JanusGraph upgrade instructions for more details.",
ConfigOption.Type.MASKABLE, String.class, ElasticSearchSetup.REST_CLIENT.toString(),
disallowEmpty(String.class));
public static final ConfigOption HEALTH_REQUEST_TIMEOUT =
new ConfigOption<>(ELASTICSEARCH_NS, "health-request-timeout",
"When JanusGraph initializes its ES backend, JanusGraph waits up to this duration for the " +
"ES cluster health to reach at least yellow status. " +
"This string should be formatted as a natural number followed by the lowercase letter " +
"\"s\", e.g. 3s or 60s.", ConfigOption.Type.MASKABLE, "30s");
public static final ConfigOption MAX_RETRY_TIMEOUT =
new ConfigOption<>(ELASTICSEARCH_NS, "max-retry-timeout",
"Sets the maximum timeout (in milliseconds) to honour in case of multiple retries of the same request " +
"sent using the ElasticSearch Rest Client by JanusGraph.", ConfigOption.Type.MASKABLE, Integer.class);
public static final ConfigOption BULK_REFRESH =
new ConfigOption<>(ELASTICSEARCH_NS, "bulk-refresh",
"Elasticsearch bulk API refresh setting used to control when changes made by this request are made " +
"visible to search", ConfigOption.Type.MASKABLE, "false");
public static final ConfigNamespace ES_CREATE_NS =
new ConfigNamespace(ELASTICSEARCH_NS, "create", "Settings related to index creation");
public static final ConfigOption CREATE_SLEEP =
new ConfigOption<>(ES_CREATE_NS, "sleep",
"How long to sleep, in milliseconds, between the successful completion of a (blocking) index " +
"creation request and the first use of that index. This only applies when creating an index in ES, " +
"which typically only happens the first time JanusGraph is started on top of ES. If the index JanusGraph is " +
"configured to use already exists, then this setting has no effect.", ConfigOption.Type.MASKABLE, 200L);
public static final ConfigNamespace ES_CREATE_EXTRAS_NS =
new ConfigNamespace(ES_CREATE_NS, "ext", "Overrides for arbitrary settings applied at index creation", true);
public static final ConfigOption USE_EXTERNAL_MAPPINGS =
new ConfigOption<>(ES_CREATE_NS, "use-external-mappings",
"Whether JanusGraph should make use of an external mapping when registering an index.", ConfigOption.Type.MASKABLE, false);
public static final ConfigOption ALLOW_MAPPING_UPDATE =
new ConfigOption<>(ES_CREATE_NS, "allow-mapping-update",
"Whether JanusGraph should allow a mapping update when registering an index. " +
"Only applicable when " + USE_EXTERNAL_MAPPINGS.getName() + " is true.", ConfigOption.Type.MASKABLE, false);
public static final ConfigOption USE_ALL_FIELD =
new ConfigOption<>(ELASTICSEARCH_NS, "use-all-field",
"Whether JanusGraph should add an \"all\" field mapping. When enabled field mappings will " +
"include a \"copy_to\" parameter referencing the \"all\" field. This is supported since Elasticsearch 6.x " +
" and is required when using wildcard fields starting in Elasticsearch 6.x.", ConfigOption.Type.GLOBAL_OFFLINE, true);
public static final ConfigOption USE_DEPRECATED_MULTITYPE_INDEX =
new ConfigOption<>(ELASTICSEARCH_NS, "use-deprecated-multitype-index",
"Whether JanusGraph should group these indices into a single Elasticsearch index " +
"(requires Elasticsearch 5.x or earlier).", ConfigOption.Type.GLOBAL_OFFLINE, false);
public static final ConfigOption ES_SCROLL_KEEP_ALIVE =
new ConfigOption<>(ELASTICSEARCH_NS, "scroll-keep-alive",
"How long (in seconds) elasticsearch should keep alive the scroll context.", ConfigOption.Type.GLOBAL_OFFLINE, 60);
public static final ConfigNamespace ES_INGEST_PIPELINES =
new ConfigNamespace(ELASTICSEARCH_NS, "ingest-pipeline", "Ingest pipeline applicable to a store of an index.");
public static final ConfigNamespace SSL_NS =
new ConfigNamespace(ELASTICSEARCH_NS, "ssl", "Elasticsearch SSL configuration");
public static final ConfigOption SSL_ENABLED =
new ConfigOption<>(SSL_NS, "enabled",
"Controls use of the SSL connection to Elasticsearch.", ConfigOption.Type.LOCAL, false);
public static final ConfigOption SSL_DISABLE_HOSTNAME_VERIFICATION =
new ConfigOption<>(SSL_NS, "disable-hostname-verification",
"Disables the SSL hostname verification if set to true. Hostname verification is enabled by default.",
ConfigOption.Type.LOCAL, false);
public static final ConfigOption SSL_ALLOW_SELF_SIGNED_CERTIFICATES =
new ConfigOption<>(SSL_NS, "allow-self-signed-certificates",
"Controls the accepting of the self-signed SSL certificates.",
ConfigOption.Type.LOCAL, false);
public static final ConfigNamespace SSL_TRUSTSTORE_NS =
new ConfigNamespace(SSL_NS, "truststore", "Configuration options for SSL Truststore.");
public static final ConfigOption SSL_TRUSTSTORE_LOCATION =
new ConfigOption<>(SSL_TRUSTSTORE_NS, "location",
"Marks the location of the SSL Truststore.", ConfigOption.Type.LOCAL, "");
public static final ConfigOption SSL_TRUSTSTORE_PASSWORD =
new ConfigOption<>(SSL_TRUSTSTORE_NS, "password",
"The password to access SSL Truststore.", ConfigOption.Type.LOCAL, "", Objects::nonNull);
public static final ConfigNamespace SSL_KEYSTORE_NS =
new ConfigNamespace(SSL_NS, "keystore", "Configuration options for SSL Keystore.");
public static final ConfigOption SSL_KEYSTORE_LOCATION =
new ConfigOption<>(SSL_KEYSTORE_NS, "location",
"Marks the location of the SSL Keystore.", ConfigOption.Type.LOCAL, "");
public static final ConfigOption SSL_KEYSTORE_PASSWORD =
new ConfigOption<>(SSL_KEYSTORE_NS, "storepassword",
"The password to access SSL Keystore.", ConfigOption.Type.LOCAL, "", Objects::nonNull);
public static final ConfigOption SSL_KEY_PASSWORD =
new ConfigOption<>(SSL_KEYSTORE_NS, "keypassword",
"The password to access the key in the SSL Keystore. If the option is not present, the value of \"storepassword\" is used.",
ConfigOption.Type.LOCAL, "", Objects::nonNull);
public static final ConfigNamespace ES_HTTP_NS =
new ConfigNamespace(ELASTICSEARCH_NS, "http", "Configuration options for HTTP(S) transport.");
public static final ConfigNamespace ES_HTTP_AUTH_NS =
new ConfigNamespace(ES_HTTP_NS, "auth", "Configuration options for HTTP(S) authentication.");
public static final ConfigOption ES_HTTP_AUTH_TYPE =
new ConfigOption<>(ES_HTTP_AUTH_NS, "type",
"Authentication type to be used for HTTP(S) access.", ConfigOption.Type.LOCAL, HttpAuthTypes.NONE.toString());
public static final ConfigNamespace ES_HTTP_AUTH_BASIC_NS =
new ConfigNamespace(ES_HTTP_AUTH_NS, "basic", "Configuration options for HTTP(S) Basic authentication.");
public static final ConfigOption ES_HTTP_AUTH_USERNAME =
new ConfigOption<>(ES_HTTP_AUTH_BASIC_NS, "username",
"Username for HTTP(S) authentication.", ConfigOption.Type.LOCAL, "");
public static final ConfigOption ES_HTTP_AUTH_PASSWORD =
new ConfigOption<>(ES_HTTP_AUTH_BASIC_NS, "password",
"Password for HTTP(S) authentication.", ConfigOption.Type.LOCAL, "");
public static final ConfigOption ES_HTTP_AUTH_REALM = new ConfigOption<>(ES_HTTP_AUTH_BASIC_NS,
"realm", "Realm value for HTTP(S) authentication. If empty, any realm is accepted.",
ConfigOption.Type.LOCAL, "");
public static final ConfigNamespace ES_HTTP_AUTH_CUSTOM_NS =
new ConfigNamespace(ES_HTTP_AUTH_NS, "custom", "Configuration options for custom HTTP(S) authenticator.");
public static final ConfigOption ES_HTTP_AUTHENTICATOR_CLASS = new ConfigOption<>(
ES_HTTP_AUTH_CUSTOM_NS, "authenticator-class", "Authenticator fully qualified class name.",
ConfigOption.Type.LOCAL, "");
public static final ConfigOption ES_HTTP_AUTHENTICATOR_ARGS = new ConfigOption<>(
ES_HTTP_AUTH_CUSTOM_NS, "authenticator-args", "Comma-separated custom authenticator constructor arguments.",
ConfigOption.Type.LOCAL, new String[0]);
public static final int HOST_PORT_DEFAULT = 9200;
/**
* Default tree_levels used when creating geo_shape mappings.
*/
public static final int DEFAULT_GEO_MAX_LEVELS = 20;
/**
* Default distance_error_pct used when creating geo_shape mappings.
*/
public static final double DEFAULT_GEO_DIST_ERROR_PCT = 0.025;
private static final ObjectWriter mapWriter;
static {
final ObjectMapper mapper = new ObjectMapper();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapWriter = mapper.writerWithView(Map.class);
}
private static final Parameter[] NULL_PARAMETERS = null;
private final AbstractESCompat compat;
private final ElasticSearchClient client;
private final String indexName;
private final int batchSize;
private final boolean useExternalMappings;
private final boolean allowMappingUpdate;
private final Map indexSetting;
private final long createSleep;
private final boolean useAllField;
private final boolean useMultitypeIndex;
private final Map ingestPipelines;
public ElasticSearchIndex(Configuration config) throws BackendException {
indexName = config.get(INDEX_NAME);
useAllField = config.get(USE_ALL_FIELD);
useExternalMappings = config.get(USE_EXTERNAL_MAPPINGS);
allowMappingUpdate = config.get(ALLOW_MAPPING_UPDATE);
createSleep = config.get(CREATE_SLEEP);
ingestPipelines = config.getSubset(ES_INGEST_PIPELINES);
final ElasticSearchSetup.Connection c = interfaceConfiguration(config);
client = c.getClient();
batchSize = config.get(INDEX_MAX_RESULT_SET_SIZE);
log.debug("Configured ES query nb result by query to {}", batchSize);
switch (client.getMajorVersion()) {
case ONE:
compat = new ES1Compat();
Preconditions.checkArgument(ingestPipelines.isEmpty(),
"Ingest pipelines are not supported by Elasticsearch 1.x.");
break;
case TWO:
compat = new ES2Compat();
Preconditions.checkArgument(ingestPipelines.isEmpty(),
"Ingest pipelines are not supported by Elasticsearch 2.x.");
break;
case FIVE:
compat = new ES5Compat();
break;
case SIX:
compat = new ES6Compat();
break;
default:
throw new PermanentBackendException("Unsupported Elasticsearch version: " + client.getMajorVersion());
}
try {
client.clusterHealthRequest(config.get(HEALTH_REQUEST_TIMEOUT));
} catch (final IOException e) {
throw new PermanentBackendException(e.getMessage(), e);
}
if (!config.has(USE_DEPRECATED_MULTITYPE_INDEX) && client.isIndex(indexName)) {
// upgrade scenario where multitype index was the default behavior
useMultitypeIndex = true;
} else {
useMultitypeIndex = config.get(USE_DEPRECATED_MULTITYPE_INDEX);
Preconditions.checkArgument(!useMultitypeIndex || !client.isAlias(indexName),
"The key '" + USE_DEPRECATED_MULTITYPE_INDEX
+ "' cannot be true when existing index is split.");
Preconditions.checkArgument(useMultitypeIndex || !client.isIndex(indexName),
"The key '" + USE_DEPRECATED_MULTITYPE_INDEX
+ "' cannot be false when existing index contains multiple types.");
}
indexSetting = new HashMap<>();
ElasticSearchSetup.applySettingsFromJanusGraphConf(indexSetting, config);
indexSetting.put("index.max_result_window", Integer.MAX_VALUE);
}
/**
* If ES already contains this instance's target index, then do nothing.
* Otherwise, create the index, then wait {@link #CREATE_SLEEP}.
*
* The {@code client} field must point to a live, connected client.
* The {@code indexName} field must be non-null and point to the name
* of the index to check for existence or create.
*
* @param index index name
* @throws IOException if the index status could not be checked or index could not be created
*/
private void checkForOrCreateIndex(String index) throws IOException {
Preconditions.checkState(null != client);
Preconditions.checkNotNull(index);
// Create index if it does not useExternalMappings and if it does not already exist
if (!useExternalMappings && !client.indexExists(index)) {
client.createIndex(index, indexSetting);
try {
log.debug("Sleeping {} ms after {} index creation returned from actionGet()", createSleep, index);
Thread.sleep(createSleep);
} catch (final InterruptedException e) {
throw new JanusGraphException("Interrupted while waiting for index to settle in", e);
}
}
Preconditions.checkState(client.indexExists(index), "Could not create index: %s",index);
if (!useMultitypeIndex) {
client.addAlias(indexName, index);
}
}
/**
* Configure ElasticSearchIndex's ES client. See{@link org.janusgraph.diskstorage.es.ElasticSearchSetup} for more
* information.
*
* @param config a config passed to ElasticSearchIndex's constructor
* @return a client object open and ready for use
*/
private ElasticSearchSetup.Connection interfaceConfiguration(Configuration config) {
final ElasticSearchSetup clientMode = ConfigOption.getEnumValue(config.get(INTERFACE), ElasticSearchSetup.class);
try {
return clientMode.connect(config);
} catch (final IOException e) {
throw new JanusGraphException(e);
}
}
private BackendException convert(Exception esException) {
if (esException instanceof InterruptedException) {
return new TemporaryBackendException("Interrupted while waiting for response", esException);
} else {
return new PermanentBackendException("Unknown exception while executing index operation", esException);
}
}
private static String getDualMappingName(String key) {
return key + STRING_MAPPING_SUFFIX;
}
private String getIndexStoreName(String store) {
return useMultitypeIndex ? indexName : indexName + "_" + store.toLowerCase();
}
@Override
public void register(String store, String key, KeyInformation information,
BaseTransaction tx) throws BackendException {
final Class> dataType = information.getDataType();
final Mapping map = Mapping.getMapping(information);
Preconditions.checkArgument(map==Mapping.DEFAULT || AttributeUtil.isString(dataType) ||
(map==Mapping.PREFIX_TREE && AttributeUtil.isGeo(dataType)),
"Specified illegal mapping [%s] for data type [%s]",map,dataType);
final String indexStoreName = getIndexStoreName(store);
if (useExternalMappings) {
try {
//We check if the externalMapping have the property 'key'
final IndexMapping mappings = client.getMapping(indexStoreName, store);
if (mappings == null || (!mappings.isDynamic() && !mappings.getProperties().containsKey(key))) {
//Error if it is not dynamic and have not the property 'key'
throw new PermanentBackendException("The external mapping for index '"+ indexStoreName + "' and type '" + store + "' do not have property '" + key + "'");
} else if (allowMappingUpdate && mappings.isDynamic()) {
//If it is dynamic, we push the unknown property 'key'
this.pushMapping(store, key, information);
}
} catch (final IOException e) {
throw new PermanentBackendException(e);
}
} else {
try {
checkForOrCreateIndex(indexStoreName);
} catch (final IOException e) {
throw new PermanentBackendException(e);
}
this.pushMapping(store, key, information);
}
}
/**
* Push mapping to ElasticSearch
* @param store the type in the index
* @param key the name of the property in the index
* @param information information of the key
*/
private void pushMapping(String store, String key,
KeyInformation information) throws AssertionError, PermanentBackendException, BackendException {
final Class> dataType = information.getDataType();
Mapping map = Mapping.getMapping(information);
final Map properties = new HashMap<>();
if (AttributeUtil.isString(dataType)) {
if (map==Mapping.DEFAULT) map=Mapping.TEXT;
log.debug("Registering string type for {} with mapping {}", key, map);
final String stringAnalyzer
= ParameterType.STRING_ANALYZER.findParameter(information.getParameters(), null);
final String textAnalyzer = ParameterType.TEXT_ANALYZER.findParameter(information.getParameters(), null);
// use keyword type for string mappings unless custom string analyzer is provided
final Map stringMapping
= stringAnalyzer == null ? compat.createKeywordMapping() : compat.createTextMapping(stringAnalyzer);
switch (map) {
case STRING:
properties.put(key, stringMapping);
break;
case TEXT:
properties.put(key, compat.createTextMapping(textAnalyzer));
break;
case TEXTSTRING:
properties.put(key, compat.createTextMapping(textAnalyzer));
properties.put(getDualMappingName(key), stringMapping);
break;
default: throw new AssertionError("Unexpected mapping: "+map);
}
} else if (dataType == Float.class) {
log.debug("Registering float type for {}", key);
properties.put(key, ImmutableMap.of(ES_TYPE_KEY, "float"));
} else if (dataType == Double.class) {
log.debug("Registering double type for {}", key);
properties.put(key, ImmutableMap.of(ES_TYPE_KEY, "double"));
} else if (dataType == Byte.class) {
log.debug("Registering byte type for {}", key);
properties.put(key, ImmutableMap.of(ES_TYPE_KEY, "byte"));
} else if (dataType == Short.class) {
log.debug("Registering short type for {}", key);
properties.put(key, ImmutableMap.of(ES_TYPE_KEY, "short"));
} else if (dataType == Integer.class) {
log.debug("Registering integer type for {}", key);
properties.put(key, ImmutableMap.of(ES_TYPE_KEY, "integer"));
} else if (dataType == Long.class) {
log.debug("Registering long type for {}", key);
properties.put(key, ImmutableMap.of(ES_TYPE_KEY, "long"));
} else if (dataType == Boolean.class) {
log.debug("Registering boolean type for {}", key);
properties.put(key, ImmutableMap.of(ES_TYPE_KEY, "boolean"));
} else if (dataType == Geoshape.class) {
switch (map) {
case PREFIX_TREE:
final int maxLevels = ParameterType.INDEX_GEO_MAX_LEVELS.findParameter(information.getParameters(),
DEFAULT_GEO_MAX_LEVELS);
final double distErrorPct
= ParameterType.INDEX_GEO_DIST_ERROR_PCT.findParameter(information.getParameters(),
DEFAULT_GEO_DIST_ERROR_PCT);
log.debug("Registering geo_shape type for {} with tree_levels={} and distance_error_pct={}", key,
maxLevels, distErrorPct);
properties.put(key, ImmutableMap.of(ES_TYPE_KEY, "geo_shape",
"tree", "quadtree",
"tree_levels", maxLevels,
"distance_error_pct", distErrorPct));
break;
default:
log.debug("Registering geo_point type for {}", key);
properties.put(key, ImmutableMap.of(ES_TYPE_KEY, "geo_point"));
}
} else if (dataType == Date.class || dataType == Instant.class) {
log.debug("Registering date type for {}", key);
properties.put(key, ImmutableMap.of(ES_TYPE_KEY, "date"));
} else if (dataType == UUID.class) {
log.debug("Registering uuid type for {}", key);
properties.put(key, compat.createKeywordMapping());
}
if (useAllField && client.getMajorVersion().getValue() >= 6) {
// add custom all field mapping if it doesn't exist
properties.put(ElasticSearchConstants.CUSTOM_ALL_FIELD, compat.createTextMapping(null));
// add copy_to for custom all field mapping
if (properties.containsKey(key) && dataType != Geoshape.class) {
final Map mapping = new HashMap<>(((Map) properties.get(key)));
mapping.put("copy_to", ElasticSearchConstants.CUSTOM_ALL_FIELD);
properties.put(key, mapping);
}
}
final List customParameters = ParameterType.getCustomParameters(information.getParameters());
if (properties.containsKey(key) && !customParameters.isEmpty()) {
final Map mapping = new HashMap<>(((Map) properties.get(key)));
customParameters.forEach(p -> mapping.put(p.key(), p.value()));
properties.put(key, mapping);
}
final Map mapping = ImmutableMap.of("properties", properties);
try {
client.createMapping(getIndexStoreName(store), store, mapping);
} catch (final Exception e) {
throw convert(e);
}
}
private static Mapping getStringMapping(KeyInformation information) {
assert AttributeUtil.isString(information.getDataType());
Mapping map = Mapping.getMapping(information);
if (map==Mapping.DEFAULT) map = Mapping.TEXT;
return map;
}
private static boolean hasDualStringMapping(KeyInformation information) {
return AttributeUtil.isString(information.getDataType()) && getStringMapping(information)==Mapping.TEXTSTRING;
}
public Map getNewDocument(final List additions,
KeyInformation.StoreRetriever information) throws BackendException {
// JSON writes duplicate fields one after another, which forces us
// at this stage to make de-duplication on the IndexEntry list. We don't want to pay the
// price map storage on the Mutation level because none of other backends need that.
final Multimap unique = LinkedListMultimap.create();
for (final IndexEntry e : additions) {
unique.put(e.field, e);
}
final Map doc = new HashMap<>();
for (final Map.Entry> add : unique.asMap().entrySet()) {
final KeyInformation keyInformation = information.get(add.getKey());
final Object value;
switch (keyInformation.getCardinality()) {
case SINGLE:
value = convertToEsType(Iterators.getLast(add.getValue().iterator()).value,
Mapping.getMapping(keyInformation));
break;
case SET:
case LIST:
value = add.getValue().stream()
.map(v -> convertToEsType(v.value, Mapping.getMapping(keyInformation)))
.filter(v -> {
Preconditions.checkArgument(!(v instanceof byte[]),
"Collections not supported for " + add.getKey());
return true;
}).toArray();
break;
default:
value = null;
break;
}
doc.put(add.getKey(), value);
if (hasDualStringMapping(information.get(add.getKey())) && keyInformation.getDataType() == String.class) {
doc.put(getDualMappingName(add.getKey()), value);
}
}
return doc;
}
private static Object convertToEsType(Object value, Mapping mapping) {
if (value instanceof Number) {
if (AttributeUtil.isWholeNumber((Number) value)) {
return ((Number) value).longValue();
} else { //double or float
return ((Number) value).doubleValue();
}
} else if (AttributeUtil.isString(value)) {
return value;
} else if (value instanceof Geoshape) {
return convertGeoshape((Geoshape) value, mapping);
} else if (value instanceof Date) {
return value;
} else if (value instanceof Instant) {
return Date.from((Instant) value);
} else if (value instanceof Boolean) {
return value;
} else if (value instanceof UUID) {
return value.toString();
} else throw new IllegalArgumentException("Unsupported type: " + value.getClass() + " (value: " + value + ")");
}
@SuppressWarnings("unchecked")
private static Object convertGeoshape(Geoshape geoshape, Mapping mapping) {
if (geoshape.getType() == Geoshape.Type.POINT && Mapping.PREFIX_TREE != mapping) {
final Geoshape.Point p = geoshape.getPoint();
return new double[]{p.getLongitude(), p.getLatitude()};
} else if (geoshape.getType() == Geoshape.Type.BOX) {
final Rectangle box = geoshape.getShape().getBoundingBox();
final Map map = new HashMap<>();
map.put("type", "envelope");
map.put("coordinates", new double[][] {{box.getMinX(),box.getMaxY()},{box.getMaxX(),box.getMinY()}});
return map;
} else if (geoshape.getType() == Geoshape.Type.CIRCLE) {
try {
final Map map = geoshape.toMap();
map.put("radius", map.get("radius") + ((Map) map.remove("properties")).get("radius_units"));
return map;
} catch (final IOException e) {
throw new IllegalArgumentException("Invalid geoshape: " + geoshape, e);
}
} else {
try {
return geoshape.toMap();
} catch (final IOException e) {
throw new IllegalArgumentException("Invalid geoshape: " + geoshape, e);
}
}
}
@Override
public void mutate(Map> mutations, KeyInformation.IndexRetriever information,
BaseTransaction tx) throws BackendException {
final List requests = new ArrayList<>();
try {
for (final Map.Entry> stores : mutations.entrySet()) {
final List requestByStore = new ArrayList<>();
final String storeName = stores.getKey();
final String indexStoreName = getIndexStoreName(storeName);
for (final Map.Entry entry : stores.getValue().entrySet()) {
final String documentId = entry.getKey();
final IndexMutation mutation = entry.getValue();
assert mutation.isConsolidated();
Preconditions.checkArgument(!(mutation.isNew() && mutation.isDeleted()));
Preconditions.checkArgument(!mutation.isNew() || !mutation.hasDeletions());
Preconditions.checkArgument(!mutation.isDeleted() || !mutation.hasAdditions());
//Deletions first
if (mutation.hasDeletions()) {
if (mutation.isDeleted()) {
log.trace("Deleting entire document {}", documentId);
requestByStore.add(ElasticSearchMutation.createDeleteRequest(indexStoreName, storeName,
documentId));
} else {
final String script = getDeletionScript(information, storeName, mutation);
final Map doc = compat.prepareScript(script).build();
requestByStore.add(ElasticSearchMutation.createUpdateRequest(indexStoreName, storeName,
documentId, doc));
log.trace("Adding script {}", script);
}
}
if (mutation.hasAdditions()) {
if (mutation.isNew()) { //Index
log.trace("Adding entire document {}", documentId);
final Map source = getNewDocument(mutation.getAdditions(),
information.get(storeName));
requestByStore.add(ElasticSearchMutation.createIndexRequest(indexStoreName, storeName,
documentId, source));
} else {
final Map upsert;
if (!mutation.hasDeletions()) {
upsert = getNewDocument(mutation.getAdditions(), information.get(storeName));
} else {
upsert = null;
}
final String inline = getAdditionScript(information, storeName, mutation);
if (!inline.isEmpty()) {
final ImmutableMap.Builder builder = compat.prepareScript(inline);
requestByStore.add(ElasticSearchMutation.createUpdateRequest(indexStoreName, storeName,
documentId, builder, upsert));
log.trace("Adding script {}", inline);
}
final Map doc = getAdditionDoc(information, storeName, mutation);
if (!doc.isEmpty()) {
final ImmutableMap.Builder builder = ImmutableMap.builder().put(ES_DOC_KEY, doc);
requestByStore.add(ElasticSearchMutation.createUpdateRequest(indexStoreName, storeName,
documentId, builder, upsert));
log.trace("Adding update {}", doc);
}
}
}
}
if (!requestByStore.isEmpty() && ingestPipelines.containsKey(storeName)) {
client.bulkRequest(requestByStore, String.valueOf(ingestPipelines.get(storeName)));
} else if (!requestByStore.isEmpty()) {
requests.addAll(requestByStore);
}
}
if (!requests.isEmpty()) {
client.bulkRequest(requests, null);
}
} catch (final Exception e) {
log.error("Failed to execute bulk Elasticsearch mutation", e);
throw convert(e);
}
}
private String getDeletionScript(KeyInformation.IndexRetriever information, String storeName,
IndexMutation mutation) throws PermanentBackendException {
final StringBuilder script = new StringBuilder();
final String INDEX_NAME = "index";
int i = 0;
for (final IndexEntry deletion : mutation.getDeletions()) {
final KeyInformation keyInformation = information.get(storeName).get(deletion.field);
switch (keyInformation.getCardinality()) {
case SINGLE:
script.append("ctx._source.remove(\"").append(deletion.field).append("\");");
if (hasDualStringMapping(information.get(storeName, deletion.field))) {
script.append("ctx._source.remove(\"").append(getDualMappingName(deletion.field)).append("\");");
}
break;
case SET:
case LIST:
final String jsValue = convertToJsType(deletion.value, compat.scriptLang(), Mapping.getMapping(keyInformation));
String index = INDEX_NAME + i++;
script.append("def ")
.append(index)
.append(" = ctx._source[\"")
.append(deletion.field)
.append("\"].indexOf(")
.append(jsValue)
.append("); ctx._source[\"")
.append(deletion.field)
.append("\"].remove(")
.append(index)
.append(");");
if (hasDualStringMapping(information.get(storeName, deletion.field))) {
index = INDEX_NAME + i++;
script.append("def ")
.append(index).append(" = ctx._source[\"")
.append(getDualMappingName(deletion.field))
.append("\"].indexOf(")
.append(jsValue)
.append("); ctx._source[\"")
.append(getDualMappingName(deletion.field))
.append("\"].remove(")
.append(index)
.append(");");
}
break;
}
}
return script.toString();
}
private String getAdditionScript(KeyInformation.IndexRetriever information, String storeName,
IndexMutation mutation) throws PermanentBackendException {
final StringBuilder script = new StringBuilder();
for (final IndexEntry e : mutation.getAdditions()) {
final KeyInformation keyInformation = information.get(storeName).get(e.field);
switch (keyInformation.getCardinality()) {
case SET:
case LIST:
script.append("if(ctx._source[\"").append(e.field).append("\"] == null) ctx._source[\"").append(e.field).append("\"] = [];");
script.append("ctx._source[\"").append(e.field).append("\"].add(").append(convertToJsType(e.value, compat.scriptLang(), Mapping.getMapping(keyInformation))).append(");");
if (hasDualStringMapping(keyInformation)) {
script.append("if(ctx._source[\"").append(getDualMappingName(e.field)).append("\"] == null) ctx._source[\"").append(getDualMappingName(e.field)).append("\"] = [];");
script.append("ctx._source[\"").append(getDualMappingName(e.field)).append("\"].add(").append(convertToJsType(e.value, compat.scriptLang(), Mapping.getMapping(keyInformation))).append(");");
}
break;
default:
break;
}
}
return script.toString();
}
private Map getAdditionDoc(KeyInformation.IndexRetriever information,
String store, IndexMutation mutation) throws PermanentBackendException {
final Map doc = new HashMap<>();
for (final IndexEntry e : mutation.getAdditions()) {
final KeyInformation keyInformation = information.get(store).get(e.field);
if (keyInformation.getCardinality() == Cardinality.SINGLE) {
doc.put(e.field, convertToEsType(e.value, Mapping.getMapping(keyInformation)));
if (hasDualStringMapping(keyInformation)) {
doc.put(getDualMappingName(e.field), convertToEsType(e.value, Mapping.getMapping(keyInformation)));
}
}
}
return doc;
}
private static String convertToJsType(Object value, String scriptLang,
Mapping mapping) throws PermanentBackendException {
final String esValue;
try {
esValue = mapWriter.writeValueAsString(convertToEsType(value, mapping));
} catch (final IOException e) {
throw new PermanentBackendException("Could not write json");
}
return scriptLang.equals("groovy") ? esValue.replace("$", "\\$") : esValue;
}
@Override
public void restore(Map>> documents, KeyInformation.IndexRetriever information,
BaseTransaction tx) throws BackendException {
final List requests = new ArrayList<>();
try {
for (final Map.Entry>> stores : documents.entrySet()) {
final List requestByStore = new ArrayList<>();
final String store = stores.getKey();
final String indexStoreName = getIndexStoreName(store);
for (final Map.Entry> entry : stores.getValue().entrySet()) {
final String docID = entry.getKey();
final List content = entry.getValue();
if (content == null || content.size() == 0) {
// delete
if (log.isTraceEnabled())
log.trace("Deleting entire document {}", docID);
requestByStore.add(ElasticSearchMutation.createDeleteRequest(indexStoreName, store, docID));
} else {
// Add
if (log.isTraceEnabled())
log.trace("Adding entire document {}", docID);
final Map source = getNewDocument(content, information.get(store));
requestByStore.add(ElasticSearchMutation.createIndexRequest(indexStoreName, store, docID,
source));
}
}
if (!requestByStore.isEmpty() && ingestPipelines.containsKey(store)) {
client.bulkRequest(requestByStore, String.valueOf(ingestPipelines.get(store)));
} else if (!requestByStore.isEmpty()) {
requests.addAll(requestByStore);
}
}
if (!requests.isEmpty())
client.bulkRequest(requests, null);
} catch (final Exception e) {
throw convert(e);
}
}
public Map getFilter(Condition> condition, KeyInformation.StoreRetriever information) {
if (condition instanceof PredicateCondition) {
final PredicateCondition atom = (PredicateCondition) condition;
Object value = atom.getValue();
final String key = atom.getKey();
final JanusGraphPredicate predicate = atom.getPredicate();
if (value instanceof Number) {
Preconditions.checkArgument(predicate instanceof Cmp,
"Relation not supported on numeric types: " + predicate);
final Cmp numRel = (Cmp) predicate;
switch (numRel) {
case EQUAL:
return compat.term(key, value);
case NOT_EQUAL:
return compat.boolMustNot(compat.term(key, value));
case LESS_THAN:
return compat.lt(key, value);
case LESS_THAN_EQUAL:
return compat.lte(key, value);
case GREATER_THAN:
return compat.gt(key, value);
case GREATER_THAN_EQUAL:
return compat.gte(key, value);
default:
throw new IllegalArgumentException("Unexpected relation: " + numRel);
}
} else if (value instanceof String) {
final Mapping mapping = getStringMapping(information.get(key));
final String fieldName;
if (mapping==Mapping.TEXT && !(Text.HAS_CONTAINS.contains(predicate) || predicate instanceof Cmp))
throw new IllegalArgumentException("Text mapped string values only support CONTAINS and Compare queries and not: " + predicate);
if (mapping==Mapping.STRING && Text.HAS_CONTAINS.contains(predicate))
throw new IllegalArgumentException("String mapped string values do not support CONTAINS queries: " + predicate);
if (mapping==Mapping.TEXTSTRING && !(Text.HAS_CONTAINS.contains(predicate) || predicate instanceof Cmp)) {
fieldName = getDualMappingName(key);
} else {
fieldName = key;
}
if (predicate == Text.CONTAINS || predicate == Cmp.EQUAL) {
return compat.match(key, value);
} else if (predicate == Text.CONTAINS_PREFIX) {
if (!ParameterType.TEXT_ANALYZER.hasParameter(information.get(key).getParameters()))
value = ((String) value).toLowerCase();
return compat.prefix(fieldName, value);
} else if (predicate == Text.CONTAINS_REGEX) {
if (!ParameterType.TEXT_ANALYZER.hasParameter(information.get(key).getParameters()))
value = ((String) value).toLowerCase();
return compat.regexp(fieldName, value);
} else if (predicate == Text.PREFIX) {
return compat.prefix(fieldName, value);
} else if (predicate == Text.REGEX) {
return compat.regexp(fieldName, value);
} else if (predicate == Cmp.NOT_EQUAL) {
return compat.boolMustNot(compat.match(fieldName, value));
} else if (predicate == Text.FUZZY || predicate == Text.CONTAINS_FUZZY) {
return compat.fuzzyMatch(fieldName, value);
} else if (predicate == Cmp.LESS_THAN) {
return compat.lt(fieldName, value);
} else if (predicate == Cmp.LESS_THAN_EQUAL) {
return compat.lte(fieldName, value);
} else if (predicate == Cmp.GREATER_THAN) {
return compat.gt(fieldName, value);
} else if (predicate == Cmp.GREATER_THAN_EQUAL) {
return compat.gte(fieldName, value);
} else
throw new IllegalArgumentException("Predicate is not supported for string value: " + predicate);
} else if (value instanceof Geoshape && Mapping.getMapping(information.get(key)) == Mapping.DEFAULT) {
// geopoint
final Geoshape shape = (Geoshape) value;
Preconditions.checkArgument(predicate instanceof Geo && predicate != Geo.CONTAINS,
"Relation not supported on geopoint types: " + predicate);
final Map query;
switch (shape.getType()) {
case CIRCLE:
final Geoshape.Point center = shape.getPoint();
query = compat.geoDistance(key, center.getLatitude(), center.getLongitude(), shape.getRadius());
break;
case BOX:
final Geoshape.Point southwest = shape.getPoint(0);
final Geoshape.Point northeast = shape.getPoint(1);
query = compat.geoBoundingBox(key, southwest.getLatitude(), southwest.getLongitude(),
northeast.getLatitude(), northeast.getLongitude());
break;
case POLYGON:
final List> points = IntStream.range(0, shape.size())
.mapToObj(i -> ImmutableList.of(shape.getPoint(i).getLongitude(),
shape.getPoint(i).getLatitude()))
.collect(Collectors.toList());
query = compat.geoPolygon(key, points);
break;
default:
throw new IllegalArgumentException("Unsupported or invalid search shape type for geopoint: "
+ shape.getType());
}
return predicate == Geo.DISJOINT ? compat.boolMustNot(query) : query;
} else if (value instanceof Geoshape) {
Preconditions.checkArgument(predicate instanceof Geo,
"Relation not supported on geoshape types: " + predicate);
final Geoshape shape = (Geoshape) value;
final Map geo;
switch (shape.getType()) {
case CIRCLE:
final Geoshape.Point center = shape.getPoint();
geo = ImmutableMap.of(ES_TYPE_KEY, "circle",
ES_GEO_COORDS_KEY, ImmutableList.of(center.getLongitude(), center.getLatitude()),
"radius", shape.getRadius() + "km");
break;
case BOX:
final Geoshape.Point southwest = shape.getPoint(0);
final Geoshape.Point northeast = shape.getPoint(1);
geo = ImmutableMap.of(ES_TYPE_KEY, "envelope",
ES_GEO_COORDS_KEY,
ImmutableList.of(
ImmutableList.of(southwest.getLongitude(), northeast.getLatitude()),
ImmutableList.of(northeast.getLongitude(), southwest.getLatitude())));
break;
case LINE:
final List lineCoords = IntStream.range(0, shape.size())
.mapToObj(i -> ImmutableList.of(shape.getPoint(i).getLongitude(),
shape.getPoint(i).getLatitude()))
.collect(Collectors.toList());
geo = ImmutableMap.of(ES_TYPE_KEY, "linestring", ES_GEO_COORDS_KEY, lineCoords);
break;
case POLYGON:
final List polyCoords = IntStream.range(0, shape.size())
.mapToObj(i -> ImmutableList.of(shape.getPoint(i).getLongitude(),
shape.getPoint(i).getLatitude()))
.collect(Collectors.toList());
geo = ImmutableMap.of(ES_TYPE_KEY, "polygon", ES_GEO_COORDS_KEY,
ImmutableList.of(polyCoords));
break;
case POINT:
geo = ImmutableMap.of(ES_TYPE_KEY, "point",
ES_GEO_COORDS_KEY, ImmutableList.of(shape.getPoint().getLongitude(),
shape.getPoint().getLatitude()));
break;
default:
throw new IllegalArgumentException("Unsupported or invalid search shape type: "
+ shape.getType());
}
return compat.geoShape(key, geo, (Geo) predicate);
} else if (value instanceof Date || value instanceof Instant) {
Preconditions.checkArgument(predicate instanceof Cmp,
"Relation not supported on date types: " + predicate);
final Cmp numRel = (Cmp) predicate;
if (value instanceof Instant) {
value = Date.from((Instant) value);
}
switch (numRel) {
case EQUAL:
return compat.term(key, value);
case NOT_EQUAL:
return compat.boolMustNot(compat.term(key, value));
case LESS_THAN:
return compat.lt(key, value);
case LESS_THAN_EQUAL:
return compat.lte(key, value);
case GREATER_THAN:
return compat.gt(key, value);
case GREATER_THAN_EQUAL:
return compat.gte(key, value);
default:
throw new IllegalArgumentException("Unexpected relation: " + numRel);
}
} else if (value instanceof Boolean) {
final Cmp numRel = (Cmp) predicate;
switch (numRel) {
case EQUAL:
return compat.term(key, value);
case NOT_EQUAL:
return compat.boolMustNot(compat.term(key, value));
default:
throw new IllegalArgumentException("Boolean types only support EQUAL or NOT_EQUAL");
}
} else if (value instanceof UUID) {
if (predicate == Cmp.EQUAL) {
return compat.term(key, value);
} else if (predicate == Cmp.NOT_EQUAL) {
return compat.boolMustNot(compat.term(key, value));
} else {
throw new IllegalArgumentException("Only equal or not equal is supported for UUIDs: "
+ predicate);
}
} else throw new IllegalArgumentException("Unsupported type: " + value);
} else if (condition instanceof Not) {
return compat.boolMustNot(getFilter(((Not) condition).getChild(),information));
} else if (condition instanceof And) {
final List queries = StreamSupport.stream(condition.getChildren().spliterator(), false)
.map(c -> getFilter(c,information)).collect(Collectors.toList());
return compat.boolMust(queries);
} else if (condition instanceof Or) {
final List queries = StreamSupport.stream(condition.getChildren().spliterator(), false)
.map(c -> getFilter(c,information)).collect(Collectors.toList());
return compat.boolShould(queries);
} else throw new IllegalArgumentException("Invalid condition: " + condition);
}
@Override
public Stream query(IndexQuery query, KeyInformation.IndexRetriever informations,
BaseTransaction tx) throws BackendException {
final ElasticSearchRequest sr = new ElasticSearchRequest();
final Map esQuery = getFilter(query.getCondition(), informations.get(query.getStore()));
sr.setQuery(compat.prepareQuery(esQuery));
if (!query.getOrder().isEmpty()) {
final List orders = query.getOrder();
for (final IndexQuery.OrderEntry orderEntry : orders) {
final String order = orderEntry.getOrder().name();
final KeyInformation information = informations.get(query.getStore()).get(orderEntry.getKey());
final Mapping mapping = Mapping.getMapping(information);
final Class> datatype = orderEntry.getDatatype();
sr.addSort(orderEntry.getKey(), order.toLowerCase(), convertToEsDataType(datatype, mapping));
}
}
sr.setFrom(0);
if (query.hasLimit()) {
sr.setSize(Math.min(query.getLimit(), batchSize));
} else {
sr.setSize(batchSize);
}
ElasticSearchResponse response;
try {
final String indexStoreName = getIndexStoreName(query.getStore());
final String indexType = useMultitypeIndex ? query.getStore() : null;
response = client.search(indexStoreName, indexType, compat.createRequestBody(sr, NULL_PARAMETERS),
sr.getSize() >= batchSize);
log.debug("First Executed query [{}] in {} ms", query.getCondition(), response.getTook());
final ElasticSearchScroll resultIterator = new ElasticSearchScroll(client, response, sr.getSize());
final Stream> toReturn
= StreamSupport.stream(Spliterators.spliteratorUnknownSize(resultIterator, Spliterator.ORDERED), false);
return (query.hasLimit() ? toReturn.limit(query.getLimit()) : toReturn).map(RawQuery.Result::getResult);
} catch (final IOException | UncheckedIOException e) {
throw new PermanentBackendException(e);
}
}
private String convertToEsDataType(Class> dataType, Mapping mapping) {
if(String.class.isAssignableFrom(dataType)) {
return "string";
}
else if (Integer.class.isAssignableFrom(dataType)) {
return "integer";
}
else if (Long.class.isAssignableFrom(dataType)) {
return "long";
}
else if (Float.class.isAssignableFrom(dataType)) {
return "float";
}
else if (Double.class.isAssignableFrom(dataType)) {
return "double";
}
else if (Boolean.class.isAssignableFrom(dataType)) {
return "boolean";
}
else if (Date.class.isAssignableFrom(dataType)) {
return "date";
}
else if (Instant.class.isAssignableFrom(dataType)) {
return "date";
}
else if (Geoshape.class.isAssignableFrom(dataType)) {
return mapping == Mapping.DEFAULT ? "geo_point" : "geo_shape";
}
return null;
}
private ElasticSearchResponse runCommonQuery(RawQuery query, BaseTransaction tx, int size,
boolean useScroll) throws BackendException{
final ElasticSearchRequest sr = new ElasticSearchRequest();
sr.setQuery(compat.queryString(query.getQuery()));
sr.setFrom(0);
sr.setSize(size);
try {
return client.search(getIndexStoreName(query.getStore()), useMultitypeIndex ? query.getStore() : null,
compat.createRequestBody(sr, query.getParameters()), useScroll);
} catch (final IOException | UncheckedIOException e) {
throw new PermanentBackendException(e);
}
}
@Override
public Stream> query(RawQuery query, KeyInformation.IndexRetriever information,
BaseTransaction tx) throws BackendException {
final int size = query.hasLimit() ? Math.min(query.getLimit() + query.getOffset(), batchSize) : batchSize;
final ElasticSearchResponse response = runCommonQuery(query, tx, size, size >= batchSize );
log.debug("First Executed query [{}] in {} ms", query.getQuery(), response.getTook());
final ElasticSearchScroll resultIterator = new ElasticSearchScroll(client, response, size);
final Stream> toReturn
= StreamSupport.stream(Spliterators.spliteratorUnknownSize(resultIterator, Spliterator.ORDERED),
false).skip(query.getOffset());
return query.hasLimit() ? toReturn.limit(query.getLimit()) : toReturn;
}
@Override
public Long totals(RawQuery query, KeyInformation.IndexRetriever information,
BaseTransaction tx) throws BackendException {
final int size = query.hasLimit() ? Math.min(query.getLimit() + query.getOffset(), batchSize) : batchSize;
final ElasticSearchResponse response = runCommonQuery(query, tx, size, false);
log.debug("Executed query [{}] in {} ms", query.getQuery(), response.getTook());
return response.getTotal();
}
@Override
public boolean supports(KeyInformation information, JanusGraphPredicate janusgraphPredicate) {
final Class> dataType = information.getDataType();
final Mapping mapping = Mapping.getMapping(information);
if (mapping!=Mapping.DEFAULT && !AttributeUtil.isString(dataType) &&
!(mapping==Mapping.PREFIX_TREE && AttributeUtil.isGeo(dataType))) return false;
if (Number.class.isAssignableFrom(dataType)) {
return janusgraphPredicate instanceof Cmp;
} else if (dataType == Geoshape.class) {
switch(mapping) {
case DEFAULT:
return janusgraphPredicate instanceof Geo && janusgraphPredicate != Geo.CONTAINS;
case PREFIX_TREE:
return janusgraphPredicate instanceof Geo;
}
} else if (AttributeUtil.isString(dataType)) {
switch(mapping) {
case DEFAULT:
case TEXT:
return janusgraphPredicate == Text.CONTAINS || janusgraphPredicate == Text.CONTAINS_PREFIX
|| janusgraphPredicate == Text.CONTAINS_REGEX || janusgraphPredicate == Text.CONTAINS_FUZZY;
case STRING:
return janusgraphPredicate instanceof Cmp || janusgraphPredicate==Text.REGEX
|| janusgraphPredicate==Text.PREFIX || janusgraphPredicate == Text.FUZZY;
case TEXTSTRING:
return janusgraphPredicate instanceof Text || janusgraphPredicate instanceof Cmp;
}
} else if (dataType == Date.class || dataType == Instant.class) {
return janusgraphPredicate instanceof Cmp;
} else if (dataType == Boolean.class) {
return janusgraphPredicate == Cmp.EQUAL || janusgraphPredicate == Cmp.NOT_EQUAL;
} else if (dataType == UUID.class) {
return janusgraphPredicate == Cmp.EQUAL || janusgraphPredicate==Cmp.NOT_EQUAL;
}
return false;
}
@Override
public boolean supports(KeyInformation information) {
final Class> dataType = information.getDataType();
final Mapping mapping = Mapping.getMapping(information);
if (Number.class.isAssignableFrom(dataType) || dataType == Date.class || dataType== Instant.class
|| dataType == Boolean.class || dataType == UUID.class) {
return mapping == Mapping.DEFAULT;
} else if (AttributeUtil.isString(dataType)) {
return mapping == Mapping.DEFAULT || mapping == Mapping.STRING
|| mapping == Mapping.TEXT || mapping == Mapping.TEXTSTRING;
} else if (AttributeUtil.isGeo(dataType)) {
return mapping == Mapping.DEFAULT || mapping == Mapping.PREFIX_TREE;
}
return false;
}
@Override
public String mapKey2Field(String key, KeyInformation information) {
IndexProvider.checkKeyValidity(key);
return key.replace(' ', IndexProvider.REPLACEMENT_CHAR);
}
@Override
public IndexFeatures getFeatures() {
return compat.getIndexFeatures();
}
@Override
public BaseTransactionConfigurable beginTransaction(BaseTransactionConfig config) throws BackendException {
return new DefaultTransaction(config);
}
@Override
public void close() throws BackendException {
try {
client.close();
} catch (final IOException e) {
throw new PermanentBackendException(e);
}
}
@Override
public void clearStorage() throws BackendException {
try {
client.deleteIndex(indexName);
} catch (final Exception e) {
throw new PermanentBackendException("Could not delete index " + indexName, e);
} finally {
close();
}
}
@Override
public boolean exists() throws BackendException {
try {
return client.indexExists(indexName);
} catch (final IOException e) {
throw new PermanentBackendException("Could not check if index " + indexName + " exists", e);
}
}
ElasticMajorVersion getVersion() {
return client.getMajorVersion();
}
}