com.erudika.para.server.search.es.ESUtils Maven / Gradle / Ivy
/*
* Copyright 2013-2022 Erudika. http://erudika.com
*
* 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.
*
* For issues and patches go to: https://github.com/erudika
*/
package com.erudika.para.server.search.es;
import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.Conflicts;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.HealthStatus;
import co.elastic.clients.elasticsearch._types.Refresh;
import co.elastic.clients.elasticsearch._types.SortOptions;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.Time;
import co.elastic.clients.elasticsearch._types.mapping.Property;
import co.elastic.clients.elasticsearch._types.mapping.TypeMapping;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.ChildScoreMode;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchAllQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
import co.elastic.clients.elasticsearch._types.query_dsl.QueryVariant;
import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery;
import co.elastic.clients.elasticsearch.core.BulkResponse;
import co.elastic.clients.elasticsearch.core.DeleteByQueryRequest;
import co.elastic.clients.elasticsearch.core.DeleteByQueryResponse;
import co.elastic.clients.elasticsearch.core.bulk.BulkOperation;
import co.elastic.clients.elasticsearch.core.search.TrackHits;
import co.elastic.clients.elasticsearch.indices.IndexSettings;
import co.elastic.clients.elasticsearch.indices.get_alias.IndexAliases;
import co.elastic.clients.elasticsearch.indices.update_aliases.Action;
import co.elastic.clients.elasticsearch.indices.update_aliases.AddAction;
import co.elastic.clients.elasticsearch.indices.update_aliases.RemoveAction;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import co.elastic.clients.util.ObjectBuilder;
import com.erudika.para.core.App;
import com.erudika.para.core.ParaObject;
import com.erudika.para.core.Sysprop;
import com.erudika.para.core.listeners.DestroyListener;
import com.erudika.para.core.persistence.DAO;
import com.erudika.para.core.utils.Config;
import com.erudika.para.core.utils.Pager;
import com.erudika.para.core.utils.Para;
import com.erudika.para.core.utils.ParaObjectUtils;
import com.erudika.para.core.utils.Utils;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.protocol.HttpContext;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.flexible.core.QueryNodeException;
import org.apache.lucene.queryparser.flexible.standard.StandardQueryParser;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.BoostQuery;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermRangeQuery;
import org.apache.lucene.search.WildcardQuery;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.auth.signer.Aws4Signer;
import software.amazon.awssdk.auth.signer.params.Aws4SignerParams;
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.regions.Region;
/**
*
* @author Alex Bogdanovski [[email protected]]
*/
public final class ESUtils {
private static final Logger logger = LoggerFactory.getLogger(ESUtils.class);
private static ElasticsearchClient restClient;
private static ElasticsearchAsyncClient restClientAsync;
private static final int MAX_QUERY_DEPTH = 10; // recursive depth for compound queries - bool, boost
private static final String DATE_FORMAT = "epoch_millis||epoch_second||yyyy-MM-dd HH:mm:ss||"
+ "yyyy-MM-dd||yyyy/MM/dd||yyyyMMdd||yyyy";
static final String PROPS_FIELD = "properties";
static final String PROPS_PREFIX = PROPS_FIELD + ".";
static final String PROPS_JSON = "_" + PROPS_FIELD;
static final String PROPS_REGEX = "(^|.*\\W)" + PROPS_FIELD + "[\\.\\:].+";
/**
* Switches between normal indexing and indexing with nested key/value objects for Sysprop.properties.
* When this is 'false' (normal mode), Para objects will be indexed without modification but this could lead to
* a field mapping explosion and crash the ES cluster.
*
* When set to 'true' (nested mode), Para objects will be indexed with all custom fields flattened to an array of
* key/value properties: properties: [{"k": "field", "v": "value"},...]. This is done for Sysprop objects with
* containing custom properties. This mode prevents an eventual field mapping explosion.
*/
static boolean nestedMode() {
return Para.getConfig().elasticsearchNestedModeEnabled();
}
/**
* @return true if asynchronous indexing/unindexing is enabled.
*/
static boolean asyncEnabled() {
return Para.getConfig().elasticsearchAsyncModeEnabled();
}
/**
* @return true if we want the bulk processor to flush immediately after each bulk request.
*/
static boolean flushImmediately() {
return Para.getConfig().elasticsearchBulkFlushEnabled();
}
/**
* A list of default mappings that are defined upon index creation.
* @return the default mapping properties map
*/
public static Map getDefaultMapping() {
Map props = new HashMap();
props.put("nstd", Property.of(p -> p.nested(n -> n.enabled(true))));
props.put("properties", Property.of(p -> {
if (nestedMode()) {
p.nested(n -> n.enabled(true));
} else {
p.object(n -> n.enabled(true));
}
return p;
}
));
props.put("latlng", Property.of(p -> p.geoPoint(n -> n.nullValue(v -> v.text("0,0")))));
props.put("_docid", Property.of(p -> p.long_(n -> n.index(false))));
props.put("updated", Property.of(p -> p.date(n -> n.format(DATE_FORMAT))));
props.put("timestamp", Property.of(p -> p.date(n -> n.format(DATE_FORMAT))));
props.put("tag", Property.of(p -> p.keyword(n -> n.index(true))));
props.put("id", Property.of(p -> p.keyword(n -> n.index(true))));
props.put("key", Property.of(p -> p.keyword(n -> n.index(true))));
props.put("name", Property.of(p -> p.keyword(n -> n.index(true))));
props.put("type", Property.of(p -> p.keyword(n -> n.index(true))));
props.put("tags", Property.of(p -> p.keyword(n -> n.index(true))));
props.put("token", Property.of(p -> p.keyword(n -> n.index(true))));
props.put("email", Property.of(p -> p.keyword(n -> n.index(true))));
props.put("appid", Property.of(p -> p.keyword(n -> n.index(true))));
props.put("groups", Property.of(p -> p.keyword(n -> n.index(true))));
props.put("password", Property.of(p -> p.keyword(n -> n.index(true))));
props.put("parentid", Property.of(p -> p.keyword(n -> n.index(true))));
props.put("creatorid", Property.of(p -> p.keyword(n -> n.index(true))));
props.put("identifier", Property.of(p -> p.keyword(n -> n.index(true))));
return props;
}
/**
* These fields are not indexed.
*/
private static final String[] IGNORED_FIELDS = new String[] {
"settings", // App
"datatypes", // App
"deviceState", // Thing
"deviceMetadata", // Thing
"resourcePermissions", // App
"validationConstraints" // App
};
private ESUtils() { }
/**
* Creates an instance of the high-level REST client that talks to Elasticsearch.
* @return a RestHighLevelClient instance
*/
public static ElasticsearchClient getRESTClient() {
if (restClient != null) {
return restClient;
}
String esScheme = Para.getConfig().elasticsearchRestClientScheme();
String esHost = Para.getConfig().elasticsearchRestClientHost();
int esPort = Para.getConfig().elasticsearchRestClientPort();
boolean signRequests = Para.getConfig().elasticsearchSignRequestsForAwsEnabled();
HttpHost host = new HttpHost(esHost, esPort, esScheme);
RestClientBuilder clientBuilder = RestClient.builder(host);
String esPrefix = Para.getConfig().elasticsearchRestClientContextPath();
if (StringUtils.isNotEmpty(esPrefix)) {
clientBuilder.setPathPrefix(esPrefix);
}
List configurationCallbacks = new ArrayList<>();
if (signRequests) {
configurationCallbacks.add(getAWSRequestSigningInterceptor(host.getSchemeName() + "://" + host.getHostName()));
}
configurationCallbacks.add(getAuthenticationCallback());
// register all customizations
clientBuilder.setHttpClientConfigCallback(httpClientBuilder -> {
configurationCallbacks.forEach(c -> c.customizeHttpClient(httpClientBuilder));
if (esHost.startsWith("localhost") || !Para.getConfig().inProduction()) {
httpClientBuilder.setSSLHostnameVerifier((hostname, session) -> true);
// httpClientBuilder.setSSLContext(SSLContextBuilder.create().);
}
return httpClientBuilder;
});
// Create the transport with a Jackson mapper
RestClientTransport transport = new RestClientTransport(clientBuilder.build(), new JacksonJsonpMapper());
restClient = new ElasticsearchClient(transport);
restClientAsync = new ElasticsearchAsyncClient(transport);
Para.addDestroyListener(new DestroyListener() {
public void onDestroy() {
shutdownClient();
}
});
if (!existsIndex(Para.getConfig().getRootAppIdentifier())) {
createIndex(Para.getConfig().getRootAppIdentifier());
}
return restClient;
}
/**
* Stops the client instance and releases resources.
*/
protected static void shutdownClient() {
if (restClient != null) {
try {
restClient._transport().close();
restClientAsync._transport().close();
} catch (IOException ex) {
logger.error(null, ex);
}
}
}
private static ElasticsearchAsyncClient getAsyncRESTClient() {
if (restClientAsync == null) {
getRESTClient();
}
return restClientAsync;
}
private static boolean createIndexWithoutAlias(String name, int shards, int replicas) {
if (StringUtils.isBlank(name) || StringUtils.containsWhitespace(name) || existsIndex(name)) {
return false;
}
try {
if (shards <= 0) {
shards = Para.getConfig().elasticsearchRootIndexShards();
}
if (replicas < 0) {
replicas = Para.getConfig().elasticsearchRootIndexReplicas();
}
final int numShards = shards;
final int numReplicas = replicas;
IndexSettings settings = IndexSettings.of(b -> {
b.numberOfShards(Integer.toString(numShards));
b.numberOfReplicas(Integer.toString(numReplicas));
b.autoExpandReplicas(Para.getConfig().elasticsearchAutoExpandReplicas());
return b;
});
// create index with default system mappings; ES allows only one type per index
getRESTClient().indices().create(b -> b.index(name).settings(settings).
mappings(TypeMapping.of(t -> t.properties(getDefaultMapping()))));
logger.info("Created a new index '{}' with {} shards, {} replicas.", name, shards, replicas);
} catch (Exception e) {
logger.warn(null, e);
return false;
}
return true;
}
/**
* Creates a new search index.
* @param appid the index name (alias)
* @return true if created
*/
public static boolean createIndex(String appid) {
return createIndex(appid, Para.getConfig().elasticsearchRootIndexShards(), Para.getConfig().elasticsearchRootIndexReplicas());
}
/**
* Creates a new search index.
* @param appid the index name (alias)
* @param shards number of shards
* @param replicas number of replicas
* @return true if created
*/
public static boolean createIndex(String appid, int shards, int replicas) {
if (StringUtils.isBlank(appid)) {
return false;
}
String indexName = appid.trim() + "_1";
boolean created = createIndexWithoutAlias(indexName, shards, replicas);
if (created) {
boolean withAliasRouting = App.isRoot(appid) && Para.getConfig().elasticsearchRootIndexSharingEnabled();
boolean aliased = addIndexAlias(indexName, appid, withAliasRouting);
if (!aliased) {
logger.info("Created ES index '{}' without an alias '{}'.", indexName, appid);
} else {
logger.info("Created ES index '{}' with alias '{}'.", indexName, appid);
}
}
return created;
}
/**
* Deletes an existing search index.
* @param appid the index name (alias)
* @return true if deleted
*/
public static boolean deleteIndex(String appid) {
if (StringUtils.isBlank(appid) || !existsIndex(appid)) {
return false;
}
try {
// wildcard deletion might fail if "action.destructive_requires_name" is "true"
String indexName = getIndexNameForAlias(appid.trim());
getRESTClient().indices().delete(b -> b.index(indexName));
logger.info("Deleted ES index '{}'.", indexName);
} catch (Exception e) {
logger.warn(null, e);
return false;
}
return true;
}
/**
* Checks if the index exists.
* @param appid the index name (alias)
* @return true if exists
*/
public static boolean existsIndex(String appid) {
if (StringUtils.isBlank(appid)) {
return false;
}
// don't assume false, might be distructive!
boolean exists;
try {
String indexName = appid.trim();
exists = getRESTClient().indices().exists(b -> b.index(indexName)).value();
} catch (Exception e) {
logger.warn(null, e);
exists = false;
}
return exists;
}
/**
* Rebuilds an index.
* Reads objects from the data store and indexes them in batches.
* Works on one DB table and index only.
* @param dao DAO for connecting to the DB - the primary data source
* @param app an app
* @param destinationIndex the new index where data will be reindexed to
* @param pager a Pager instance
* @return true if successful, false if index doesn't exist or failed.
*/
public static boolean rebuildIndex(DAO dao, App app, String destinationIndex, Pager... pager) {
Objects.requireNonNull(dao, "DAO object cannot be null!");
Objects.requireNonNull(app, "App object cannot be null!");
if (StringUtils.isBlank(app.getAppIdentifier())) {
return false;
}
try {
String indexName = app.getAppIdentifier().trim();
if (!existsIndex(indexName)) {
if (app.isSharingIndex()) {
// add alias pointing to the root index
addIndexAliasWithRouting(getIndexName(Para.getConfig().getRootAppIdentifier()), app.getAppIdentifier());
} else {
logger.info("Creating '{}' index because it doesn't exist.", indexName);
createIndex(indexName);
}
}
String oldName = getIndexNameForAlias(indexName);
String newName = indexName;
if (!app.isSharingIndex()) {
if (StringUtils.isBlank(destinationIndex)) {
newName = getNewIndexName(indexName, oldName);
createIndexWithoutAlias(newName, -1, -1); // use defaults
} else {
newName = destinationIndex;
}
}
List batch = new LinkedList<>();
Pager p = getPager(pager);
int batchSize = Para.getConfig().reindexBatchSize(p.getLimit());
long reindexedCount = 0;
List list;
final String newIndex = newName;
do {
list = dao.readPage(app.getAppIdentifier(), p); // use appid!
logger.debug("rebuildIndex(): Read {} objects from table {}.", list.size(), indexName);
for (final ParaObject obj : list) {
if (obj != null) {
// put objects from DB into the newly created index
batch.add(BulkOperation.of(b -> b.index(i -> i.
index(newIndex).id(obj.getId()).
document(getSourceFromParaObject(obj)))));
// index in batches of ${queueSize} objects
if (batch.size() >= batchSize) {
reindexedCount += batch.size();
executeRequests(batch);
logger.debug("rebuildIndex(): indexed {}", batch.size());
batch.clear();
}
}
}
} while (!list.isEmpty());
// anything left after loop? index that too
if (batch.size() > 0) {
reindexedCount += batch.size();
executeRequests(batch);
logger.debug("rebuildIndex(): indexed {}", batch.size());
}
if (!app.isSharingIndex()) {
// switch to alias NEW_INDEX -> ALIAS, OLD_INDEX -> DELETE old index
switchIndexToAlias(oldName, newName, indexName, true);
}
logger.info("rebuildIndex(): {} objects reindexed in '{}' [shared: {}].",
reindexedCount, indexName, app.isSharingIndex());
} catch (Exception e) {
logger.warn(null, e);
return false;
}
return true;
}
/**
* Executes a synchronous index refresh request. Also flushes
* @param appid the appid / index alias
* @throws IOException exception
*/
public static void refreshIndex(String appid) throws IOException {
if (!StringUtils.isBlank(appid)) {
// if (asyncEnabled()) {
// bulkProcessor(getRESTClient()).flush();
// }
getRESTClient().indices().refresh(b -> b.index(getIndexName(appid)));
}
}
/**
* Executes a delete_by_query request to ES and refreshes the index.
* @param appid the appid / index alias
* @param fb query
* @return number of unindexed documents.
*/
public static long deleteByQuery(String appid, QueryVariant fb) {
return deleteByQuery(appid, fb, asyncEnabled() ? (res) -> {
logger.debug("Unindexed {}", res.total());
if (!res.failures().isEmpty()) {
logger.error("Delete by query reqest failed for app '" + appid + "' - {}",
res.failures().iterator().next().cause().reason());
}
} : null);
}
/**
* Executes a delete_by_query request to ES and refreshes the index.
* @param appid the appid / index alias
* @param fb query
* @param cb callback
* @return number of unindexed documents.
*/
public static long deleteByQuery(String appid, QueryVariant fb, Consumer cb) {
int batchSize = 1000;
boolean isSharingIndex = !App.isRoot(appid) && StringUtils.startsWith(appid, " ");
String indexName = getIndexName(appid);
DeleteByQueryRequest.Builder deleteByQueryReq = new DeleteByQueryRequest.Builder();
deleteByQueryReq.index(indexName);
deleteByQueryReq.conflicts(Conflicts.Proceed);
deleteByQueryReq.query(fb._toQuery());
// deleteByQueryReq.BatchSize(batchSize);
deleteByQueryReq.slices(s -> s.value(1)); // parallelize operation?
deleteByQueryReq.scroll(Time.of(t -> t.time("10m")));
deleteByQueryReq.refresh(true);
if (isSharingIndex) {
deleteByQueryReq.routing(indexName);
}
if (cb != null) {
// getRESTClient().deleteByQueryAsync(deleteByQueryReq, RequestOptions.DEFAULT, cb);
getAsyncRESTClient().deleteByQuery(deleteByQueryReq.build()).thenAccept(cb);
} else {
DeleteByQueryResponse res;
try {
res = getRESTClient().deleteByQuery(deleteByQueryReq.build());
if (!res.failures().isEmpty()) {
logger.warn("Failures in deleteByQuery() - {}", res.failures().iterator().next().cause().reason());
}
return res.total();
} catch (IOException ex) {
logger.error(null, ex);
}
}
return 0L;
}
/**
* @param pager an array of optional Pagers
* @return the first {@link Pager} object in the array or a new Pager
*/
protected static Pager getPager(Pager[] pager) {
return (pager != null && pager.length > 0) ? pager[0] : new Pager();
}
/**
* The {@code pager.sortBy} can contain comma-separated sort fields. For example "name,timestamp".
* It can also contain sort orders for each field, for example: "name:asc,timestamp:desc".
* @param pager a {@link Pager} object
* @return a list of ES SortBuilder objects for sorting the results of a search request
*/
protected static List getSortFieldsFromPager(Pager pager) {
if (pager == null) {
pager = new Pager();
}
SortOrder defaultOrder = pager.isDesc() ? SortOrder.Desc : SortOrder.Asc;
if (pager.getSortby().contains(",")) {
String[] fields = pager.getSortby().split(",");
ArrayList sortFields = new ArrayList<>(fields.length);
for (String field : fields) {
SortOrder order;
String fieldName;
if (field.endsWith(":asc")) {
order = SortOrder.Asc;
fieldName = field.substring(0, field.indexOf(":asc")).trim();
} else if (field.endsWith(":desc")) {
order = SortOrder.Desc;
fieldName = field.substring(0, field.indexOf(":desc")).trim();
} else {
order = defaultOrder;
fieldName = field.trim();
}
if (nestedMode() && fieldName.startsWith(PROPS_PREFIX)) {
sortFields.add(getNestedFieldSort(fieldName, order));
} else {
sortFields.add(SortOptions.of(b -> b.field(f -> f.field(fieldName).order(order))));
}
}
return sortFields;
} else if (StringUtils.isBlank(pager.getSortby())) {
return Collections.singletonList(SortOptions.of(b -> b.score(s -> s.order(defaultOrder))));
} else {
String fieldName = pager.getSortby();
if (nestedMode() && fieldName.startsWith(PROPS_PREFIX)) {
return Collections.singletonList(getNestedFieldSort(fieldName, defaultOrder));
} else {
return Collections.singletonList(SortOptions.of(b -> b.field(f -> f.field(fieldName).order(defaultOrder))));
}
}
}
private static SortOptions getNestedFieldSort(String fieldName, SortOrder order) {
// nested sorting works only on numeric fields (sorting on properties.v requires fielddata enabled)
return SortOptions.of(b -> b.field(f -> f.field(PROPS_FIELD + ".vn").
order(order).
nested(n -> n.path(PROPS_FIELD).
filter(nf -> nf.term(t -> t.field(PROPS_FIELD + ".k").
value(fv -> fv.stringValue(StringUtils.removeStart(fieldName, PROPS_FIELD + "."))))))));
}
/**
* Adds a new alias to an existing index with routing and filtering by appid.
* @param indexName the index name
* @param aliasName the alias
* @return true if acknowledged
*/
public static boolean addIndexAliasWithRouting(String indexName, String aliasName) {
return addIndexAlias(indexName, aliasName, true);
}
/**
* Adds a new alias to an existing index.
* @param indexName the index name
* @param aliasName the alias
* @param withAliasRouting enables alias routing for index with filtering by appid
* @return true if acknowledged
*/
public static boolean addIndexAlias(String indexName, String aliasName, boolean withAliasRouting) {
if (StringUtils.isBlank(aliasName) || !existsIndex(indexName)) {
return false;
}
try {
String alias = aliasName.trim();
String index = getIndexNameWithWildcard(indexName.trim());
return getRESTClient().indices().updateAliases(b -> b.actions(a -> {
if (withAliasRouting) {
return a.add(aa -> aa.index(index).alias(alias).
searchRouting(alias).indexRouting(alias).
filter(QueryBuilders.term().
field(Config._APPID).
value(FieldValue.of(aliasName)).build()._toQuery())); // DO NOT trim filter query!
}
return a.add(aa -> aa.index(index).alias(alias));
})).acknowledged();
} catch (Exception e) {
logger.error(null, e);
return false;
}
}
/**
* Removes an alias from an index.
* @param indexName the index name
* @param aliasName the alias
* @return true if acknowledged
*/
public static boolean removeIndexAlias(String indexName, String aliasName) {
if (StringUtils.isBlank(aliasName) || !existsIndex(indexName)) {
return false;
}
String alias = aliasName.trim();
try {
String index = getIndexNameWithWildcard(indexName.trim());
return getRESTClient().indices().updateAliases(b -> b.
actions(a -> a.remove(r -> r.index(index).alias(alias)))).acknowledged();
} catch (Exception e) {
logger.warn("Failed to remove index alias '" + alias + "' for index " + indexName + ": {}", e.getMessage());
return false;
}
}
/**
* Checks if an index has a registered alias.
* @param indexName the index name
* @param aliasName the alias
* @return true if alias is set on index
*/
public static boolean existsIndexAlias(String indexName, String aliasName) {
if (StringUtils.isBlank(indexName) || StringUtils.isBlank(aliasName)) {
return false;
}
try {
String alias = aliasName.trim();
String index = getIndexNameWithWildcard(indexName.trim());
return getRESTClient().indices().existsAlias(b -> b.index(index).name(alias)).value();
} catch (Exception e) {
logger.error(null, e);
return false;
}
}
/**
* Replaces the index to which an alias points with another index.
* @param oldIndex the index name to be replaced
* @param newIndex the new index name to switch to
* @param alias the alias (unchanged)
* @param deleteOld if true will delete the old index completely
*/
public static void switchIndexToAlias(String oldIndex, String newIndex, String alias, boolean deleteOld) {
if (StringUtils.isBlank(oldIndex) || StringUtils.isBlank(newIndex) || StringUtils.isBlank(alias)) {
return;
}
try {
String aliaz = alias.trim();
String oldName = oldIndex.trim();
String newName = newIndex.trim();
logger.info("Switching index aliases {}->{}, deleting '{}': {}", aliaz, newIndex, oldIndex, deleteOld);
Action removeAction = RemoveAction.of(b -> b.index(oldName).alias(aliaz))._toAction();
Action addAction = AddAction.of(b -> b.index(newName).alias(aliaz))._toAction();
getRESTClient().indices().updateAliases(b -> b.actions(removeAction, addAction));
// delete the old index
if (deleteOld) {
deleteIndex(oldName);
}
} catch (Exception e) {
logger.error(null, e);
}
}
/**
* Returns the real index name for a given alias.
* @param appid the index name (alias)
* @return the real index name (not alias)
*/
public static String getIndexNameForAlias(String appid) {
if (StringUtils.isBlank(appid)) {
return appid;
}
try {
Map aliases = getRESTClient().indices().getAlias(b -> b.index(appid)).result();
if (!aliases.isEmpty()) {
return aliases.keySet().iterator().next();
}
} catch (Exception e) {
logger.error(null, e);
}
return appid;
}
/**
* @param appid the index name (alias)
* @param oldName old index name
* @return a new index name, e.g. "app_15698795757"
*/
static String getNewIndexName(String appid, String oldName) {
if (StringUtils.isBlank(appid)) {
return appid;
}
return (oldName.contains("_") ? oldName.substring(0, oldName.indexOf('_')) : appid) + "_" + Utils.timestamp();
}
/**
* Executes a batch of write requests.
* @param requests a list of index/delete requests,
*/
public static void executeRequests(List requests) {
if (requests == null || requests.isEmpty()) {
return;
}
try {
if (asyncEnabled()) {
getAsyncRESTClient().bulk(b -> b.operations(requests).
refresh(flushImmediately() ? Refresh.True : Refresh.False)).thenAccept(b -> {
if (b.errors()) {
b.items().stream().filter(i -> i.status() != 200).forEach(item -> {
//FUTURE: Increment counter metric for failed document indexing
logger.error("Failed to execute async {} operation for index '{}', document id '{}': {}",
item.operationType(), item.index(), item.id(), item.error().reason());
});
}
});
} else {
BulkResponse res = getRESTClient().bulk(b -> b.operations(requests).
refresh(flushImmediately() ? Refresh.True : Refresh.False));
if (res.errors()) {
res.items().stream().filter(i -> i.status() != 200).forEach(item -> {
//FUTURE: Increment counter metric for failed document indexing
logger.error("Failed to execute sync {} operation for index '{}', document id '{}': {}",
item.operationType(), item.index(), item.id(), item.error().reason());
});
handleFailedRequests();
}
}
} catch (Exception e) {
logger.error(null, e);
}
}
private static void handleFailedRequests() {
if (Para.getConfig().exceptionOnWriteErrorsEnabled()) {
throw new RuntimeException("Synchronous indexing operation failed!");
}
}
/**
* Check if cluster status is green or yellow.
* @return false if status is red
*/
public static boolean isClusterOK() {
try {
HealthStatus status = getRESTClient().cluster().health().status();
return !HealthStatus.Red.equals(status);
} catch (Exception e) {
logger.error(null, e);
}
return false;
}
/**
* Creates a term filter for a set of terms.
* @param terms some terms
* @param mustMatchAll if true all terms must match ('AND' operation)
* @return the filter
*/
static QueryVariant getTermsQuery(Map terms, boolean mustMatchAll) {
BoolQuery.Builder fb = QueryBuilders.bool();
int addedTerms = 0;
boolean noop = true;
QueryVariant bfb = null;
for (Map.Entry term : terms.entrySet()) {
Object val = term.getValue();
if (!StringUtils.isBlank(term.getKey()) && val != null && Utils.isBasicType(val.getClass())) {
String stringValue = val.toString();
if (StringUtils.isBlank(stringValue)) {
continue;
}
Matcher matcher = Pattern.compile(".*(<|>|<=|>=)$").matcher(term.getKey().trim());
if (matcher.matches()) {
bfb = range(matcher.group(1), term.getKey(), stringValue);
} else {
if (nestedMode()) {
bfb = (QueryVariant) term(new org.apache.lucene.search.
TermQuery(new Term(term.getKey(), stringValue))).build();
} else {
bfb = QueryBuilders.term().field(term.getKey()).value(v -> v.stringValue(stringValue)).build();
}
}
if (mustMatchAll) {
fb.must(bfb._toQuery());
} else {
fb.should(bfb._toQuery());
}
addedTerms++;
noop = false;
}
}
if (addedTerms == 1 && bfb != null) {
return bfb;
}
return noop ? null : fb.build();
}
/**
* Tries to parse a query string in order to check if it is valid.
* @param query a Lucene query string
* @return the query if valid, or '*' if invalid
*/
static String qs(String query) {
if (StringUtils.isBlank(query) || "*".equals(query.trim())) {
return "*";
}
query = query.trim();
if (query.length() > 1 && query.startsWith("*")) {
query = query.substring(1);
}
try {
StandardQueryParser parser = new StandardQueryParser();
parser.setAllowLeadingWildcard(false);
parser.parse(query, "");
} catch (Exception ex) {
logger.warn("Failed to parse query string '{}'.", query);
query = "*";
}
return query.trim();
}
static Query qsParsed(String query) {
if (StringUtils.isBlank(query) || "*".equals(query.trim())) {
return null;
}
try {
StandardQueryParser parser = new StandardQueryParser();
parser.setAllowLeadingWildcard(false);
return parser.parse(query, "");
} catch (Exception ex) {
logger.warn("Failed to parse query string '{}'.", query);
}
return null;
}
public static boolean isValidQueryString(String query) {
if (StringUtils.isBlank(query)) {
return false;
}
if ("*".equals(query.trim())) {
return true;
}
try {
StandardQueryParser parser = new StandardQueryParser();
parser.setAllowLeadingWildcard(false);
parser.parse(query, "");
return true;
} catch (QueryNodeException ex) {
return false;
}
}
/**
* Converts a {@link ParaObject} to a map of fields and values.
* @param po an object
* @return a map of keys and values
*/
@SuppressWarnings("unchecked")
public static Map getSourceFromParaObject(ParaObject po) {
if (po == null) {
return Collections.emptyMap();
}
Map data = ParaObjectUtils.getAnnotatedFields(po, null, false);
Map source = new HashMap<>(data.size() + 1);
source.putAll(data);
if (nestedMode() && po instanceof Sysprop) {
try {
Map props = (Map) data.get(PROPS_FIELD);
// flatten properites object to array of keys/values, to prevent field mapping explosion
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy