// 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.solr;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import org.janusgraph.core.Cardinality;
import org.janusgraph.graphdb.internal.Order;
import org.janusgraph.core.JanusGraphElement;
import org.janusgraph.core.attribute.*;
import org.janusgraph.core.schema.Mapping;
import org.janusgraph.diskstorage.*;
import org.janusgraph.diskstorage.configuration.ConfigNamespace;
import org.janusgraph.diskstorage.configuration.ConfigOption;
import org.janusgraph.diskstorage.configuration.Configuration;
import org.janusgraph.diskstorage.indexing.*;
import org.janusgraph.diskstorage.solr.transform.GeoToWktConverter;
import org.janusgraph.diskstorage.util.DefaultTransaction;
import org.janusgraph.graphdb.configuration.PreInitializeConfigOptions;
import org.janusgraph.graphdb.database.serialize.AttributeUtil;
import org.janusgraph.graphdb.query.JanusGraphPredicate;
import org.janusgraph.graphdb.query.condition.*;
import org.janusgraph.graphdb.types.ParameterType;
import org.apache.commons.lang.StringUtils;
import org.apache.http.client.HttpClient;
import org.apache.solr.client.solrj.*;
import org.apache.solr.client.solrj.impl.*;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.client.solrj.response.CollectionAdminResponse;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.zookeeper.KeeperException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.*;
import java.util.AbstractMap.SimpleEntry;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.*;
/**
* @author Jared Holmberg (jholmberg@bericotechnoLogies.com), Pavel Yaskevich (pavel@thinkaurelius.com)
*/
@PreInitializeConfigOptions
public class SolrIndex implements IndexProvider {
private static final Logger logger = LoggerFactory.getLogger(SolrIndex.class);
private static final String DEFAULT_ID_FIELD = "id";
private enum Mode {
HTTP, CLOUD;
public static Mode parse(String mode) {
for (Mode m : Mode.values()) {
if (m.toString().equalsIgnoreCase(mode)) return m;
}
throw new IllegalArgumentException("Unrecognized mode: "+mode);
}
}
public static final ConfigNamespace SOLR_NS =
new ConfigNamespace(INDEX_NS, "solr", "Solr index configuration");
public static final ConfigOption<String> SOLR_MODE = new ConfigOption<String>(SOLR_NS,"mode",
"The operation mode for Solr which is either via HTTP (`http`) or using SolrCloud (`cloud`)",
ConfigOption.Type.GLOBAL_OFFLINE, "cloud");
public static final ConfigOption<Boolean> DYNAMIC_FIELDS = new ConfigOption<Boolean>(SOLR_NS,"dyn-fields",
"Whether to use dynamic fields (which appends the data type to the field name). If dynamic fields is disabled" +
"the user must map field names and define them explicitly in the schema.",
ConfigOption.Type.GLOBAL_OFFLINE, true);
public static final ConfigOption<String[]> KEY_FIELD_NAMES = new ConfigOption<String[]>(SOLR_NS,"key-field-names",
"Field name that uniquely identifies each document in Solr. Must be specified as a list of `collection=field`.",
ConfigOption.Type.GLOBAL, String[].class);
public static final ConfigOption<String> TTL_FIELD = new ConfigOption<String>(SOLR_NS,"ttl_field",
"Name of the TTL field for Solr collections.",
ConfigOption.Type.GLOBAL_OFFLINE, "ttl");
/** SolrCloud Configuration */
public static final ConfigOption<String> ZOOKEEPER_URL = new ConfigOption<String>(SOLR_NS,"zookeeper-url",
"URL of the Zookeeper instance coordinating the SolrCloud cluster",
ConfigOption.Type.MASKABLE, "localhost:2181");
public static final ConfigOption<Integer> NUM_SHARDS = new ConfigOption<Integer>(SOLR_NS,"num-shards",
"Number of shards for a collection. This applies when creating a new collection which is only supported under the SolrCloud operation mode.",
ConfigOption.Type.GLOBAL_OFFLINE, 1);
public static final ConfigOption<Integer> MAX_SHARDS_PER_NODE = new ConfigOption<Integer>(SOLR_NS,"max-shards-per-node",
"Maximum number of shards per node. This applies when creating a new collection which is only supported under the SolrCloud operation mode.",
ConfigOption.Type.GLOBAL_OFFLINE, 1);
public static final ConfigOption<Integer> REPLICATION_FACTOR = new ConfigOption<Integer>(SOLR_NS,"replication-factor",
"Replication factor for a collection. This applies when creating a new collection which is only supported under the SolrCloud operation mode.",
ConfigOption.Type.GLOBAL_OFFLINE, 1);
public static final ConfigOption<String> SOLR_DEFAULT_CONFIG = new ConfigOption<String>(SOLR_NS,"configset",
"If specified, the same solr configSet can be resued for each new Collection that is created in SolrCloud.",
ConfigOption.Type.MASKABLE, String.class);
/** HTTP Configuration */
public static final ConfigOption<String[]> HTTP_URLS = new ConfigOption<String[]>(SOLR_NS,"http-urls",
"List of URLs to use to connect to Solr Servers (LBHttpSolrClient is used), don't add core or collection name to the URL.",
ConfigOption.Type.MASKABLE, new String[] { "http://localhost:8983/solr" });
public static final ConfigOption<Integer> HTTP_CONNECTION_TIMEOUT = new ConfigOption<Integer>(SOLR_NS,"http-connection-timeout",
"Solr HTTP connection timeout.",
ConfigOption.Type.MASKABLE, 5000);
public static final ConfigOption<Boolean> HTTP_ALLOW_COMPRESSION = new ConfigOption<Boolean>(SOLR_NS,"http-compression",
"Enable/disable compression on the HTTP connections made to Solr.",
ConfigOption.Type.MASKABLE, false);
public static final ConfigOption<Integer> HTTP_MAX_CONNECTIONS_PER_HOST = new ConfigOption<Integer>(SOLR_NS,"http-max-per-host",
"Maximum number of HTTP connections per Solr host.",
ConfigOption.Type.MASKABLE, 20);
public static final ConfigOption<Integer> HTTP_GLOBAL_MAX_CONNECTIONS = new ConfigOption<Integer>(SOLR_NS,"http-max",
"Maximum number of HTTP connections in total to all Solr servers.",
ConfigOption.Type.MASKABLE, 100);
public static final ConfigOption<Boolean> WAIT_SEARCHER = new ConfigOption<Boolean>(SOLR_NS, "wait-searcher",
"When mutating - wait for the index to reflect new mutations before returning. This can have a negative impact on performance.",
ConfigOption.Type.LOCAL, false);
private static final IndexFeatures SOLR_FEATURES = new IndexFeatures.Builder().supportsDocumentTTL()
.setDefaultStringMapping(Mapping.TEXT).supportedStringMappings(Mapping.TEXT, Mapping.STRING).supportsCardinality(Cardinality.SINGLE).build();
private static Map<Geo, String> SPATIAL_PREDICATES = spatialPredicates();
private final SolrClient solrClient;
private final Configuration configuration;
private final Mode mode;
private final boolean dynFields;
private final Map<String, String> keyFieldIds;
private final String ttlField;
private final int maxResults;
private final boolean waitSearcher;
public SolrIndex(final Configuration config) throws BackendException {
Preconditions.checkArgument(config!=null);
configuration = config;
mode = Mode.parse(config.get(SOLR_MODE));
dynFields = config.get(DYNAMIC_FIELDS);
keyFieldIds = parseKeyFieldsForCollections(config);
maxResults = config.get(INDEX_MAX_RESULT_SET_SIZE);
ttlField = config.get(TTL_FIELD);
waitSearcher = config.get(WAIT_SEARCHER);
if (mode==Mode.CLOUD) {
String zookeeperUrl = config.get(SolrIndex.ZOOKEEPER_URL);
CloudSolrClient cloudServer = new CloudSolrClient(zookeeperUrl, true);
cloudServer.connect();
solrClient = cloudServer;
} else if (mode==Mode.HTTP) {
HttpClient clientParams = HttpClientUtil.createClient(new ModifiableSolrParams() {{
add(HttpClientUtil.PROP_ALLOW_COMPRESSION, config.get(HTTP_ALLOW_COMPRESSION).toString());
add(HttpClientUtil.PROP_CONNECTION_TIMEOUT, config.get(HTTP_CONNECTION_TIMEOUT).toString());
add(HttpClientUtil.PROP_MAX_CONNECTIONS_PER_HOST, config.get(HTTP_MAX_CONNECTIONS_PER_HOST).toString());
add(HttpClientUtil.PROP_MAX_CONNECTIONS, config.get(HTTP_GLOBAL_MAX_CONNECTIONS).toString());
}});
solrClient = new LBHttpSolrClient(clientParams, config.get(HTTP_URLS));
} else {
throw new IllegalArgumentException("Unsupported Solr operation mode: " + mode);
}
}
private Map<String, String> parseKeyFieldsForCollections(Configuration config) throws BackendException {
Map<String, String> keyFieldNames = new HashMap<String, String>();
String[] collectionFieldStatements = config.has(KEY_FIELD_NAMES)?config.get(KEY_FIELD_NAMES):new String[0];
for (String collectionFieldStatement : collectionFieldStatements) {
String[] parts = collectionFieldStatement.trim().split("=");
if (parts.length != 2) {
throw new PermanentBackendException("Unable to parse the collection name / key field name pair. It should be of the format collection=field");
}
String collectionName = parts[0];
String keyFieldName = parts[1];
keyFieldNames.put(collectionName, keyFieldName);
}
return keyFieldNames;
}
private String getKeyFieldId(String collection) {
String field = keyFieldIds.get(collection);
if (field==null) field = DEFAULT_ID_FIELD;
return field;
}
/**
* Unlike the ElasticSearch Index, which is schema free, Solr requires a schema to
* support searching. This means that you will need to modify the solr schema with the
* appropriate field definitions in order to work properly. If you have a running instance
* of Solr and you modify its schema with new fields, don't forget to re-index!
* @param store Index store
* @param key New key to register
* @param information Datatype to register for the key
* @param tx enclosing transaction
* @throws org.janusgraph.diskstorage.BackendException
*/
@Override
public void register(String store, String key, KeyInformation information, BaseTransaction tx) throws BackendException {
if (mode==Mode.CLOUD) {
CloudSolrClient client = (CloudSolrClient) solrClient;
try {
createCollectionIfNotExists(client, configuration, store);
} catch (IOException e) {
throw new PermanentBackendException(e);
} catch (SolrServerException e) {
throw new PermanentBackendException(e);
} catch (InterruptedException e) {
throw new PermanentBackendException(e);
} catch (KeeperException e) {
throw new PermanentBackendException(e);
}
}
//Since all data types must be defined in the schema.xml, pre-registering a type does not work
}
@Override
public void mutate(Map<String, Map<String, IndexMutation>> mutations, KeyInformation.IndexRetriever informations, BaseTransaction tx) throws BackendException {
logger.debug("Mutating SOLR");
try {
for (Map.Entry<String, Map<String, IndexMutation>> stores : mutations.entrySet()) {
String collectionName = stores.getKey();
String keyIdField = getKeyFieldId(collectionName);
List<String> deleteIds = new ArrayList<String>();
Collection<SolrInputDocument> changes = new ArrayList<SolrInputDocument>();
for (Map.Entry<String, IndexMutation> entry : stores.getValue().entrySet()) {
String docId = entry.getKey();
IndexMutation mutation = entry.getValue();
Preconditions.checkArgument(!(mutation.isNew() && mutation.isDeleted()));
Preconditions.checkArgument(!mutation.isNew() || !mutation.hasDeletions());
Preconditions.checkArgument(!mutation.isDeleted() || !mutation.hasAdditions());
//Handle any deletions
if (mutation.hasDeletions()) {
if (mutation.isDeleted()) {
logger.trace("Deleting entire document {}", docId);
deleteIds.add(docId);
} else {
HashSet<IndexEntry> fieldDeletions = Sets.newHashSet(mutation.getDeletions());
if (mutation.hasAdditions()) {
for (IndexEntry indexEntry : mutation.getAdditions()) {
fieldDeletions.remove(indexEntry);
}
}
deleteIndividualFieldsFromIndex(collectionName, keyIdField, docId, fieldDeletions);
}
}
if (mutation.hasAdditions()) {
int ttl = mutation.determineTTL();
SolrInputDocument doc = new SolrInputDocument();
doc.setField(keyIdField, docId);
boolean isNewDoc = mutation.isNew();
if (isNewDoc)
logger.trace("Adding new document {}", docId);
for (IndexEntry e : mutation.getAdditions()) {
final Object fieldValue = convertValue(e.value);
doc.setField(e.field, isNewDoc
? fieldValue : new HashMap<String, Object>(1) {{
put("set", fieldValue);
}});
}
if (ttl>0) {
Preconditions.checkArgument(isNewDoc,"Solr only supports TTL on new documents [%s]",docId);
doc.setField(ttlField, String.format("+%dSECONDS", ttl));
}
changes.add(doc);
}
}
commitDeletes(collectionName, deleteIds);
commitDocumentChanges(collectionName, changes);
}
} catch (IllegalArgumentException e) {
throw new PermanentBackendException("Unable to complete query on Solr.", e);
} catch (Exception e) {
throw storageException(e);
}
}
private Object convertValue(Object value) throws BackendException {
if (value instanceof Geoshape) {
return GeoToWktConverter.convertToWktString((Geoshape) value);
}
if (value instanceof UUID) {
return value.toString();
}
if(value instanceof Instant) {
if(Math.floorMod(((Instant) value).getNano(), 1000000) != 0) {
throw new IllegalArgumentException("Solr indexes do not support nanoseconds");
}
return new Date(((Instant) value).toEpochMilli());
}
return value;
}
@Override
public void restore(Map<String, Map<String, List<IndexEntry>>> documents, KeyInformation.IndexRetriever informations, BaseTransaction tx) throws BackendException {
try {
for (Map.Entry<String, Map<String, List<IndexEntry>>> stores : documents.entrySet()) {
final String collectionName = stores.getKey();
List<String> deleteIds = new ArrayList<String>();
List<SolrInputDocument> newDocuments = new ArrayList<SolrInputDocument>();
for (Map.Entry<String, List<IndexEntry>> entry : stores.getValue().entrySet()) {
final String docID = entry.getKey();
final List<IndexEntry> content = entry.getValue();
if (content == null || content.isEmpty()) {
if (logger.isTraceEnabled())
logger.trace("Deleting document [{}]", docID);
deleteIds.add(docID);
continue;
}
newDocuments.add(new SolrInputDocument() {{
setField(getKeyFieldId(collectionName), docID);
for (IndexEntry addition : content) {
Object fieldValue = addition.value;
setField(addition.field, convertValue(fieldValue));
}
}});
}
commitDeletes(collectionName, deleteIds);
commitDocumentChanges(collectionName, newDocuments);
}
} catch (Exception e) {
throw new TemporaryBackendException("Could not restore Solr index", e);
}
}
private void deleteIndividualFieldsFromIndex(String collectionName, String keyIdField, String docId, HashSet<IndexEntry> fieldDeletions) throws SolrServerException, IOException {
if (fieldDeletions.isEmpty()) return;
Map<String, String> fieldDeletes = new HashMap<String, String>(1) {{ put("set", null); }};
SolrInputDocument doc = new SolrInputDocument();
doc.addField(keyIdField, docId);
StringBuilder sb = new StringBuilder();
for (IndexEntry fieldToDelete : fieldDeletions) {
doc.addField(fieldToDelete.field, fieldDeletes);
sb.append(fieldToDelete).append(",");
}
if (logger.isTraceEnabled())
logger.trace("Deleting individual fields [{}] for document {}", sb.toString(), docId);
UpdateRequest singleDocument = newUpdateRequest();
singleDocument.add(doc);
solrClient.request(singleDocument, collectionName);
}
private void commitDocumentChanges(String collectionName, Collection<SolrInputDocument> documents) throws SolrServerException, IOException {
if (documents.size() == 0) return;
try {
solrClient.request(newUpdateRequest().add(documents), collectionName);
} catch (HttpSolrClient.RemoteSolrException rse) {
logger.error("Unable to save documents to Solr as one of the shape objects stored were not compatible with Solr.", rse);
logger.error("Details in failed document batch: ");
for (SolrInputDocument d : documents) {
Collection<String> fieldNames = d.getFieldNames();
for (String name : fieldNames) {
logger.error(name + ":" + d.getFieldValue(name).toString());
}
}
throw rse;
}
}
private void commitDeletes(String collectionName, List<String> deleteIds) throws SolrServerException, IOException {
if (deleteIds.size() == 0) return;
solrClient.request(newUpdateRequest().deleteById(deleteIds), collectionName);
}
@Override
public List<String> query(IndexQuery query, KeyInformation.IndexRetriever informations, BaseTransaction tx) throws BackendException {
List<String> result;
String collection = query.getStore();
String keyIdField = getKeyFieldId(collection);
SolrQuery solrQuery = new SolrQuery("*:*");
String queryFilter = buildQueryFilter(query.getCondition(), informations.get(collection));
solrQuery.addFilterQuery(queryFilter);
if (!query.getOrder().isEmpty()) {
List<IndexQuery.OrderEntry> orders = query.getOrder();
for (IndexQuery.OrderEntry order1 : orders) {
String item = order1.getKey();
SolrQuery.ORDER order = order1.getOrder() == Order.ASC ? SolrQuery.ORDER.asc : SolrQuery.ORDER.desc;
solrQuery.addSort(new SolrQuery.SortClause(item, order));
}
}
solrQuery.setStart(0);
if (query.hasLimit()) {
solrQuery.setRows(query.getLimit());
} else {
solrQuery.setRows(maxResults);
}
try {
QueryResponse response = solrClient.query(collection, solrQuery);
if (logger.isDebugEnabled())
logger.debug("Executed query [{}] in {} ms", query.getCondition(), response.getElapsedTime());
int totalHits = response.getResults().size();
if (!query.hasLimit() && totalHits >= maxResults)
logger.warn("Query result set truncated to first [{}] elements for query: {}", maxResults, query);
result = new ArrayList<String>(totalHits);
for (SolrDocument hit : response.getResults()) {
result.add(hit.getFieldValue(keyIdField).toString());
}
} catch (IOException e) {
logger.error("Query did not complete : ", e);
throw new PermanentBackendException(e);
} catch (SolrServerException e) {
logger.error("Unable to query Solr index.", e);
throw new PermanentBackendException(e);
}
return result;
}
@Override
public Iterable<RawQuery.Result<String>> query(RawQuery query, KeyInformation.IndexRetriever informations, BaseTransaction tx) throws BackendException {
List<RawQuery.Result<String>> result;
String collection = query.getStore();
String keyIdField = getKeyFieldId(collection);
SolrQuery solrQuery = new SolrQuery(query.getQuery())
.addField(keyIdField)
.setIncludeScore(true)
.setStart(query.getOffset())
.setRows(query.hasLimit() ? query.getLimit() : maxResults);
try {
QueryResponse response = solrClient.query(collection, solrQuery);
if (logger.isDebugEnabled())
logger.debug("Executed query [{}] in {} ms", query.getQuery(), response.getElapsedTime());
int totalHits = response.getResults().size();
if (!query.hasLimit() && totalHits >= maxResults) {
logger.warn("Query result set truncated to first [{}] elements for query: {}", maxResults, query);
}
result = new ArrayList<RawQuery.Result<String>>(totalHits);
for (SolrDocument hit : response.getResults()) {
double score = Double.parseDouble(hit.getFieldValue("score").toString());
result.add(new RawQuery.Result<String>(hit.getFieldValue(keyIdField).toString(), score));
}
} catch (IOException e) {
logger.error("Query did not complete : ", e);
throw new PermanentBackendException(e);
} catch (SolrServerException e) {
logger.error("Unable to query Solr index.", e);
throw new PermanentBackendException(e);
}
return result;
}
private static String escapeValue(Object value) {
return ClientUtils.escapeQueryChars(value.toString());
}
public String buildQueryFilter(Condition<JanusGraphElement> condition, KeyInformation.StoreRetriever informations) {
if (condition instanceof PredicateCondition) {
PredicateCondition<String, JanusGraphElement> atom = (PredicateCondition<String, JanusGraphElement>) condition;
Object value = atom.getValue();
String key = atom.getKey();
JanusGraphPredicate janusgraphPredicate = atom.getPredicate();
if (value instanceof Number) {
String queryValue = escapeValue(value);
Preconditions.checkArgument(janusgraphPredicate instanceof Cmp, "Relation not supported on numeric types: " + janusgraphPredicate);
Cmp numRel = (Cmp) janusgraphPredicate;
switch (numRel) {
case EQUAL:
return (key + ":" + queryValue);
case NOT_EQUAL:
return ("-" + key + ":" + queryValue);
case LESS_THAN:
//use right curly to mean up to but not including value
return (key + ":[* TO " + queryValue + "}");
case LESS_THAN_EQUAL:
return (key + ":[* TO " + queryValue + "]");
case GREATER_THAN:
//use left curly to mean greater than but not including value
return (key + ":{" + queryValue + " TO *]");
case GREATER_THAN_EQUAL:
return (key + ":[" + queryValue + " TO *]");
default: throw new IllegalArgumentException("Unexpected relation: " + numRel);
}
} else if (value instanceof String) {
Mapping map = getStringMapping(informations.get(key));
assert map==Mapping.TEXT || map==Mapping.STRING;
if (map==Mapping.TEXT && !janusgraphPredicate.toString().startsWith("CONTAINS"))
throw new IllegalArgumentException("Text mapped string values only support CONTAINS queries and not: " + janusgraphPredicate);
if (map==Mapping.STRING && janusgraphPredicate.toString().startsWith("CONTAINS"))
throw new IllegalArgumentException("String mapped string values do not support CONTAINS queries: " + janusgraphPredicate);
//Special case
if (janusgraphPredicate == Text.CONTAINS) {
//e.g. - if terms tomorrow and world were supplied, and fq=text:(tomorrow world)
//sample data set would return 2 documents: one where text = Tomorrow is the World,
//and the second where text = Hello World. Hence, we are decomposing the query string
//and building an AND query explicitly because we need AND semantics
value = ((String) value).toLowerCase();
List<String> terms = Text.tokenize((String) value);
if (terms.isEmpty()) {
return "";
} else if (terms.size() == 1) {
return (key + ":(" + escapeValue(terms.get(0)) + ")");
} else {
And<JanusGraphElement> andTerms = new And<JanusGraphElement>();
for (String term : terms) {
andTerms.add(new PredicateCondition<String, JanusGraphElement>(key, janusgraphPredicate, term));
}
return buildQueryFilter(andTerms, informations);
}
}
if (janusgraphPredicate == Text.PREFIX || janusgraphPredicate == Text.CONTAINS_PREFIX) {
return (key + ":" + escapeValue(value) + "*");
} else if (janusgraphPredicate == Text.REGEX || janusgraphPredicate == Text.CONTAINS_REGEX) {
return (key + ":/" + value + "/");
} else if (janusgraphPredicate == Cmp.EQUAL) {
return (key + ":\"" + escapeValue(value) + "\"");
} else if (janusgraphPredicate == Cmp.NOT_EQUAL) {
return ("-" + key + ":\"" + escapeValue(value) + "\"");
} else if (janusgraphPredicate == Text.FUZZY || janusgraphPredicate == Text.CONTAINS_FUZZY) {
return (key + ":"+escapeValue(value)+"~");
} else {
throw new IllegalArgumentException("Relation is not supported for string value: " + janusgraphPredicate);
}
} else if (value instanceof Geoshape) {
Mapping map = Mapping.getMapping(informations.get(key));
Preconditions.checkArgument(janusgraphPredicate instanceof Geo && janusgraphPredicate != Geo.DISJOINT, "Relation not supported on geo types: " + janusgraphPredicate);
Preconditions.checkArgument(map == Mapping.PREFIX_TREE || janusgraphPredicate == Geo.WITHIN || janusgraphPredicate == Geo.INTERSECT, "Relation not supported on geopoint types: " + janusgraphPredicate);
Geoshape geo = (Geoshape)value;
if (geo.getType() == Geoshape.Type.CIRCLE && (janusgraphPredicate == Geo.INTERSECT || map == Mapping.DEFAULT)) {
Geoshape.Point center = geo.getPoint();
return ("{!geofilt sfield=" + key +
" pt=" + center.getLatitude() + "," + center.getLongitude() +
" d=" + geo.getRadius() + "} distErrPct=0"); //distance in kilometers
} else if (geo.getType() == Geoshape.Type.BOX && (janusgraphPredicate == Geo.INTERSECT || map == Mapping.DEFAULT)) {
Geoshape.Point southwest = geo.getPoint(0);
Geoshape.Point northeast = geo.getPoint(1);
return (key + ":[" + southwest.getLatitude() + "," + southwest.getLongitude() +
" TO " + northeast.getLatitude() + "," + northeast.getLongitude() + "]");
} else if (map == Mapping.PREFIX_TREE) {
StringBuilder builder = new StringBuilder(key + ":\"");
builder.append(SPATIAL_PREDICATES.get((Geo) janusgraphPredicate) + "(");
builder.append(geo + ")\" distErrPct=0");
return builder.toString();
} else {
throw new IllegalArgumentException("Unsupported or invalid search shape type: " + geo.getType());
}
} else if (value instanceof Date || value instanceof Instant) {
String s = value.toString();
String queryValue = escapeValue(value instanceof Date ? toIsoDate((Date) value) : value.toString());
Preconditions.checkArgument(janusgraphPredicate instanceof Cmp, "Relation not supported on date types: " + janusgraphPredicate);
Cmp numRel = (Cmp) janusgraphPredicate;
switch (numRel) {
case EQUAL:
return (key + ":" + queryValue);
case NOT_EQUAL:
return ("-" + key + ":" + queryValue);
case LESS_THAN:
//use right curly to mean up to but not including value
return (key + ":[* TO " + queryValue + "}");
case LESS_THAN_EQUAL:
return (key + ":[* TO " + queryValue + "]");
case GREATER_THAN:
//use left curly to mean greater than but not including value
return (key + ":{" + queryValue + " TO *]");
case GREATER_THAN_EQUAL:
return (key + ":[" + queryValue + " TO *]");
default: throw new IllegalArgumentException("Unexpected relation: " + numRel);
}
} else if (value instanceof Boolean) {
Cmp numRel = (Cmp) janusgraphPredicate;
String queryValue = escapeValue(value);
switch (numRel) {
case EQUAL:
return (key + ":" + queryValue);
case NOT_EQUAL:
return ("-" + key + ":" + queryValue);
default:
throw new IllegalArgumentException("Boolean types only support EQUAL or NOT_EQUAL");
}
} else if (value instanceof UUID) {
if (janusgraphPredicate == Cmp.EQUAL) {
return (key + ":\"" + escapeValue(value) + "\"");
} else if (janusgraphPredicate == Cmp.NOT_EQUAL) {
return ("-" + key + ":\"" + escapeValue(value) + "\"");
} else {
throw new IllegalArgumentException("Relation is not supported for uuid value: " + janusgraphPredicate);
}
} else throw new IllegalArgumentException("Unsupported type: " + value);
} else if (condition instanceof Not) {
String sub = buildQueryFilter(((Not)condition).getChild(),informations);
if (StringUtils.isNotBlank(sub)) return "-("+sub+")";
else return "";
} else if (condition instanceof And) {
int numChildren = ((And) condition).size();
StringBuilder sb = new StringBuilder();
for (Condition<JanusGraphElement> c : condition.getChildren()) {
String sub = buildQueryFilter(c, informations);
if (StringUtils.isBlank(sub))
continue;
// we don't have to add "+" which means AND iff
// a. it's a NOT query,
// b. expression is a single statement in the AND.
if (!sub.startsWith("-") && numChildren > 1)
sb.append("+");
sb.append(sub).append(" ");
}
return sb.toString();
} else if (condition instanceof Or) {
StringBuilder sb = new StringBuilder();
int element=0;
for (Condition<JanusGraphElement> c : condition.getChildren()) {
String sub = buildQueryFilter(c,informations);
if (StringUtils.isBlank(sub)) continue;
if (element==0) sb.append("(");
else sb.append(" OR ");
sb.append(sub);
element++;
}
if (element>0) sb.append(")");
return sb.toString();
} else {
throw new IllegalArgumentException("Invalid condition: " + condition);
}
}
private String toIsoDate(Date value) {
TimeZone tz = TimeZone.getTimeZone("UTC");
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
df.setTimeZone(tz);
return df.format(value);
}
/**
* Solr handles all transactions on the server-side. That means all
* commit, optimize, or rollback applies since the last commit/optimize/rollback.
* Solr documentation recommends best way to update Solr is in one process to avoid
* race conditions.
*
* @return New Transaction Handle
* @throws org.janusgraph.diskstorage.BackendException
*/
@Override
public BaseTransactionConfigurable beginTransaction(BaseTransactionConfig config) throws BackendException {
return new DefaultTransaction(config);
}
@Override
public void close() throws BackendException {
logger.trace("Shutting down connection to Solr", solrClient);
try {
solrClient.close();
} catch (IOException e) {
throw new TemporaryBackendException(e);
}
}
@Override
public void clearStorage() throws BackendException {
try {
if (mode!=Mode.CLOUD) throw new UnsupportedOperationException("Operation only supported for SolrCloud");
logger.debug("Clearing storage from Solr: {}", solrClient);
ZkStateReader zkStateReader = ((CloudSolrClient) solrClient).getZkStateReader();
zkStateReader.updateClusterState(true);
ClusterState clusterState = zkStateReader.getClusterState();
for (String collection : clusterState.getCollections()) {
logger.debug("Clearing collection [{}] in Solr",collection);
UpdateRequest deleteAll = newUpdateRequest();
deleteAll.deleteByQuery("*:*");
solrClient.request(deleteAll, collection);
}
} catch (SolrServerException e) {
logger.error("Unable to clear storage from index due to server error on Solr.", e);
throw new PermanentBackendException(e);
} catch (IOException e) {
logger.error("Unable to clear storage from index due to low-level I/O error.", e);
throw new PermanentBackendException(e);
} catch (Exception e) {
logger.error("Unable to clear storage from index due to general error.", e);
throw new PermanentBackendException(e);
}
}
@Override
public boolean supports(KeyInformation information, JanusGraphPredicate janusgraphPredicate) {
Class<?> dataType = information.getDataType();
Mapping mapping = Mapping.getMapping(information);
if (mapping!=Mapping.DEFAULT && !AttributeUtil.isString(dataType) &&
!(mapping==Mapping.PREFIX_TREE && AttributeUtil.isGeo(dataType))) return false;
if(information.getCardinality() != Cardinality.SINGLE) {
return false;
}
if (Number.class.isAssignableFrom(dataType)) {
return janusgraphPredicate instanceof Cmp;
} else if (dataType == Geoshape.class) {
switch(mapping) {
case DEFAULT:
return janusgraphPredicate == Geo.WITHIN || janusgraphPredicate == Geo.INTERSECT;
case PREFIX_TREE:
return janusgraphPredicate == Geo.INTERSECT || janusgraphPredicate == Geo.WITHIN || janusgraphPredicate == Geo.CONTAINS;
}
} 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 == Cmp.EQUAL || janusgraphPredicate==Cmp.NOT_EQUAL || janusgraphPredicate==Text.REGEX || janusgraphPredicate==Text.PREFIX || janusgraphPredicate == Text.FUZZY;
// case TEXTSTRING:
// return (janusgraphPredicate instanceof Text) || janusgraphPredicate == Cmp.EQUAL || janusgraphPredicate==Cmp.NOT_EQUAL;
}
} else if (dataType == Date.class || dataType == Instant.class) {
if (janusgraphPredicate instanceof Cmp) return true;
} 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) {
if(information.getCardinality() != Cardinality.SINGLE) {
return false;
}
Class<?> dataType = information.getDataType();
Mapping mapping = Mapping.getMapping(information);
if (Number.class.isAssignableFrom(dataType) || dataType == Date.class || dataType == Instant.class || dataType == Boolean.class || dataType == UUID.class) {
if (mapping==Mapping.DEFAULT) return true;
} else if (AttributeUtil.isString(dataType)) {
if (mapping==Mapping.DEFAULT || mapping==Mapping.TEXT || mapping==Mapping.STRING) return true;
} else if (AttributeUtil.isGeo(dataType)) {
if (mapping==Mapping.DEFAULT || mapping==Mapping.PREFIX_TREE) return true;
}
return false;
}
@Override
public String mapKey2Field(String key, KeyInformation keyInfo) {
Preconditions.checkArgument(!StringUtils.containsAny(key, new char[]{' '}),"Invalid key name provided: %s",key);
if (!dynFields) return key;
if (ParameterType.MAPPED_NAME.hasParameter(keyInfo.getParameters())) return key;
String postfix;
Class datatype = keyInfo.getDataType();
if (AttributeUtil.isString(datatype)) {
Mapping map = getStringMapping(keyInfo);
switch (map) {
case TEXT: postfix = "_t"; break;
case STRING: postfix = "_s"; break;
default: throw new IllegalArgumentException("Unsupported string mapping: " + map);
}
} else if (AttributeUtil.isWholeNumber(datatype)) {
if (datatype.equals(Long.class)) postfix = "_l";
else postfix = "_i";
} else if (AttributeUtil.isDecimal(datatype)) {
if (datatype.equals(Float.class)) postfix = "_f";
else postfix = "_d";
} else if (datatype.equals(Geoshape.class)) {
postfix = "_g";
} else if (datatype.equals(Date.class) || datatype.equals(Instant.class)) {
postfix = "_dt";
} else if (datatype.equals(Boolean.class)) {
postfix = "_b";
} else if (datatype.equals(UUID.class)) {
postfix = "_uuid";
} else throw new IllegalArgumentException("Unsupported data type ["+datatype+"] for field: " + key);
return key+postfix;
}
@Override
public IndexFeatures getFeatures() {
return SOLR_FEATURES;
}
/*
################# UTILITY METHODS #######################
*/
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 Map<Geo, String> spatialPredicates() {
return Collections.unmodifiableMap(Stream.of(
new SimpleEntry<>(Geo.WITHIN, "IsWithin"),
new SimpleEntry<>(Geo.CONTAINS, "Contains"),
new SimpleEntry<>(Geo.INTERSECT, "Intersects"),
new SimpleEntry<>(Geo.DISJOINT, "IsDisjointTo"))
.collect(Collectors.toMap((e) -> e.getKey(), (e) -> e.getValue())));
}
private UpdateRequest newUpdateRequest() {
UpdateRequest req = new UpdateRequest();
if(waitSearcher) {
req.setAction(UpdateRequest.ACTION.COMMIT, true, true);
}
return req;
}
private BackendException storageException(Exception solrException) {
return new TemporaryBackendException("Unable to complete query on Solr.", solrException);
}
private static void createCollectionIfNotExists(CloudSolrClient client, Configuration config, String collection)
throws IOException, SolrServerException, KeeperException, InterruptedException {
if (!checkIfCollectionExists(client, collection)) {
Integer numShards = config.get(NUM_SHARDS);
Integer maxShardsPerNode = config.get(MAX_SHARDS_PER_NODE);
Integer replicationFactor = config.get(REPLICATION_FACTOR);
// Ideally this property used so a new configset is not uploaded for every single
// index (collection) created in solr.
// if a generic configSet is not set, make the configset name the same as the collection.
// This was the default behavior before a default configSet could be specified
String genericConfigSet = config.has(SOLR_DEFAULT_CONFIG) ? config.get(SOLR_DEFAULT_CONFIG):collection;
CollectionAdminRequest.Create createRequest = new CollectionAdminRequest.Create();
createRequest.setConfigName(genericConfigSet);
createRequest.setCollectionName(collection);
createRequest.setNumShards(numShards);
createRequest.setMaxShardsPerNode(maxShardsPerNode);
createRequest.setReplicationFactor(replicationFactor);
CollectionAdminResponse createResponse = createRequest.process(client);
if (createResponse.isSuccess()) {
logger.trace("Collection {} successfully created.", collection);
} else {
throw new SolrServerException(Joiner.on("\n").join(createResponse.getErrorMessages()));
}
}
waitForRecoveriesToFinish(client, collection);
}
/**
* Checks if the collection has already been created in Solr.
*/
private static boolean checkIfCollectionExists(CloudSolrClient server, String collection) throws KeeperException, InterruptedException {
ZkStateReader zkStateReader = server.getZkStateReader();
zkStateReader.updateClusterState(true);
ClusterState clusterState = zkStateReader.getClusterState();
return clusterState.getCollectionOrNull(collection) != null;
}
/**
* Wait for all the collection shards to be ready.
*/
private static void waitForRecoveriesToFinish(CloudSolrClient server, String collection) throws KeeperException, InterruptedException {
ZkStateReader zkStateReader = server.getZkStateReader();
try {
boolean cont = true;
while (cont) {
boolean sawLiveRecovering = false;
zkStateReader.updateClusterState(true);
ClusterState clusterState = zkStateReader.getClusterState();
Map<String, Slice> slices = clusterState.getSlicesMap(collection);
Preconditions.checkNotNull("Could not find collection:" + collection, slices);
// change paths for Replica.State per Solr refactoring
// remove SYNC state per: http://tinyurl.com/pag6rwt
for (Map.Entry<String, Slice> entry : slices.entrySet()) {
Map<String, Replica> shards = entry.getValue().getReplicasMap();
for (Map.Entry<String, Replica> shard : shards.entrySet()) {
String state = shard.getValue().getStr(ZkStateReader.STATE_PROP);
if ((state.equals(Replica.State.RECOVERING) || state.equals(Replica.State.DOWN))
&& clusterState.liveNodesContain(shard.getValue().getStr(
ZkStateReader.NODE_NAME_PROP))) {
sawLiveRecovering = true;
}
}
}
if (!sawLiveRecovering) {
cont = false;
} else {
Thread.sleep(1000);
}
}
} finally {
logger.info("Exiting solr wait");
}
}
}