/*
* (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* Tiry
* bdelbosc
*/
package org.nuxeo.elasticsearch.core;
import static org.nuxeo.elasticsearch.ElasticSearchConstants.DOC_TYPE;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchScrollRequestBuilder;
import org.elasticsearch.action.search.SearchType;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import org.elasticsearch.search.aggregations.bucket.filter.InternalFilter;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.api.IterableQueryResult;
import org.nuxeo.ecm.core.api.SortInfo;
import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
import org.nuxeo.ecm.platform.query.api.Aggregate;
import org.nuxeo.ecm.platform.query.api.Bucket;
import org.nuxeo.elasticsearch.aggregate.AggregateEsBase;
import org.nuxeo.elasticsearch.api.ElasticSearchService;
import org.nuxeo.elasticsearch.api.EsResult;
import org.nuxeo.elasticsearch.api.EsScrollResult;
import org.nuxeo.elasticsearch.fetcher.Fetcher;
import org.nuxeo.elasticsearch.query.NxQueryBuilder;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.metrics.MetricsService;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
import com.codahale.metrics.Timer.Context;
/**
* @since 6.0
*/
public class ElasticSearchServiceImpl implements ElasticSearchService {
private static final Log log = LogFactory.getLog(ElasticSearchServiceImpl.class);
private static final java.lang.String LOG_MIN_DURATION_FETCH_KEY = "org.nuxeo.elasticsearch.core.log_min_duration_fetch_ms";
private static final long LOG_MIN_DURATION_FETCH_NS = Long.parseLong(
Framework.getProperty(LOG_MIN_DURATION_FETCH_KEY, "200")) * 1000000;
// Metrics
protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
protected final Timer searchTimer;
protected final Timer scrollTimer;
protected final Timer fetchTimer;
private final ElasticSearchAdminImpl esa;
public ElasticSearchServiceImpl(ElasticSearchAdminImpl esa) {
this.esa = esa;
searchTimer = registry.timer(MetricRegistry.name("nuxeo", "elasticsearch", "service", "search"));
scrollTimer = registry.timer(MetricRegistry.name("nuxeo", "elasticsearch", "service", "scroll"));
fetchTimer = registry.timer(MetricRegistry.name("nuxeo", "elasticsearch", "service", "fetch"));
}
@Deprecated
@Override
public DocumentModelList query(CoreSession session, String nxql, int limit, int offset, SortInfo... sortInfos) {
NxQueryBuilder query = new NxQueryBuilder(session).nxql(nxql).limit(limit).offset(offset).addSort(sortInfos);
return query(query);
}
@Deprecated
@Override
public DocumentModelList query(CoreSession session, QueryBuilder queryBuilder, int limit, int offset,
SortInfo... sortInfos) {
NxQueryBuilder query = new NxQueryBuilder(session).esQuery(queryBuilder)
.limit(limit)
.offset(offset)
.addSort(sortInfos);
return query(query);
}
@Override
public DocumentModelList query(NxQueryBuilder queryBuilder) {
return queryAndAggregate(queryBuilder).getDocuments();
}
@Override
public EsResult queryAndAggregate(NxQueryBuilder queryBuilder) {
SearchResponse response = search(queryBuilder);
List<Aggregate<Bucket>> aggs = getAggregates(queryBuilder, response);
if (queryBuilder.returnsDocuments()) {
DocumentModelListImpl docs = getDocumentModels(queryBuilder, response);
return new EsResult(docs, aggs, response);
} else if (queryBuilder.returnsRows()) {
IterableQueryResult rows = getRows(queryBuilder, response);
return new EsResult(rows, aggs, response);
}
return new EsResult(response);
}
@Override
public EsScrollResult scroll(NxQueryBuilder queryBuilder, long keepAlive) {
return scroll(queryBuilder, SearchType.DFS_QUERY_THEN_FETCH, keepAlive);
}
protected EsScrollResult scroll(NxQueryBuilder queryBuilder, SearchType searchType, long keepAlive) {
SearchResponse response = searchScroll(queryBuilder, searchType, keepAlive);
return getScrollResults(queryBuilder, response, response.getScrollId(), keepAlive);
}
@Override
public EsScrollResult scroll(EsScrollResult scrollResult) {
SearchResponse response = nextScroll(scrollResult.getScrollId(), scrollResult.getKeepAlive());
return getScrollResults(scrollResult.getQueryBuilder(), response, response.getScrollId(),
scrollResult.getKeepAlive());
}
@Override
public void clearScroll(EsScrollResult scrollResult) {
clearScroll(scrollResult.getScrollId());
}
protected void clearScroll(String scrollId) {
if (log.isDebugEnabled()) {
log.debug(String.format(
"Clear scroll : curl -XDELETE 'http://localhost:9200/_search/scroll' -d '{\"scroll_id\" : [\"%s\"]}'",
scrollId));
}
esa.getClient().prepareClearScroll().addScrollId(scrollId).execute().actionGet();
}
protected EsScrollResult getScrollResults(NxQueryBuilder queryBuilder, SearchResponse response, String scrollId,
long keepAlive) {
if (queryBuilder.returnsDocuments()) {
DocumentModelListImpl docs = getDocumentModels(queryBuilder, response);
return new EsScrollResult(docs, response, queryBuilder, scrollId, keepAlive);
} else if (queryBuilder.returnsRows()) {
IterableQueryResult rows = getRows(queryBuilder, response);
return new EsScrollResult(rows, response, queryBuilder, scrollId, keepAlive);
}
return new EsScrollResult(response, queryBuilder, scrollId, keepAlive);
}
protected DocumentModelListImpl getDocumentModels(NxQueryBuilder queryBuilder, SearchResponse response) {
DocumentModelListImpl ret;
long totalSize = response.getHits().getTotalHits();
if (!queryBuilder.returnsDocuments() || response.getHits().getHits().length == 0) {
ret = new DocumentModelListImpl(0);
ret.setTotalSize(totalSize);
return ret;
}
try (Context stopWatch = fetchTimer.time()) {
Fetcher fetcher = queryBuilder.getFetcher(response, esa.getRepositoryMap());
ret = fetcher.fetchDocuments();
logMinDurationFetch(stopWatch.stop(), totalSize);
}
ret.setTotalSize(totalSize);
return ret;
}
private void logMinDurationFetch(long duration, long totalSize) {
if (log.isDebugEnabled() && (duration > LOG_MIN_DURATION_FETCH_NS)) {
String msg = String.format("Slow fetch duration_ms:\t%.2f\treturning:\t%d documents", duration / 1000000.0,
totalSize);
if (log.isTraceEnabled()) {
log.trace(msg, new Throwable("Slow fetch document stack trace"));
} else {
log.debug(msg);
}
}
}
protected List<Aggregate<Bucket>> getAggregates(NxQueryBuilder queryBuilder, SearchResponse response) {
for (AggregateEsBase<? extends Bucket> agg : queryBuilder.getAggregates()) {
InternalFilter filter = response.getAggregations().get(NxQueryBuilder.getAggregateFilterId(agg));
if (filter == null) {
continue;
}
MultiBucketsAggregation mba = filter.getAggregations().get(agg.getId());
if (mba == null) {
continue;
}
agg.parseEsBuckets(mba.getBuckets());
}
@SuppressWarnings("unchecked")
List<Aggregate<Bucket>> ret = (List<Aggregate<Bucket>>) (List<?>) queryBuilder.getAggregates();
return ret;
}
private IterableQueryResult getRows(NxQueryBuilder queryBuilder, SearchResponse response) {
return new EsResultSetImpl(response, queryBuilder.getSelectFieldsAndTypes());
}
protected SearchResponse search(NxQueryBuilder query) {
try (Context ignored = searchTimer.time()){
SearchType searchType = SearchType.DFS_QUERY_THEN_FETCH;
SearchRequestBuilder request = buildEsSearchRequest(query, searchType);
logSearchRequest(request, query, searchType);
SearchResponse response = request.execute().actionGet();
logSearchResponse(response);
return response;
}
}
protected SearchResponse searchScroll(NxQueryBuilder query, SearchType searchType, long keepAlive) {
try (Context ignored = searchTimer.time()){
SearchRequestBuilder request = buildEsSearchScrollRequest(query, searchType, keepAlive);
logSearchRequest(request, query, searchType, keepAlive);
SearchResponse response = request.execute().actionGet();
logSearchResponse(response);
return response;
}
}
protected SearchResponse nextScroll(String scrollId, long keepAlive) {
try (Context ignored = scrollTimer.time()) {
SearchScrollRequestBuilder request = buildEsScrollRequest(scrollId, keepAlive);
logScrollRequest(scrollId, keepAlive);
SearchResponse response = request.execute().actionGet();
logSearchResponse(response);
return response;
}
}
protected SearchRequestBuilder buildEsSearchRequest(NxQueryBuilder query, SearchType searchType) {
SearchRequestBuilder request = esa.getClient()
.prepareSearch(esa.getSearchIndexes(query.getSearchRepositories()))
.setTypes(DOC_TYPE)
.setSearchType(searchType);
query.updateRequest(request);
if (query.isFetchFromElasticsearch()) {
// fetch the _source without the binaryfulltext field
request.setFetchSource(esa.getIncludeSourceFields(), esa.getExcludeSourceFields());
}
return request;
}
protected SearchRequestBuilder buildEsSearchScrollRequest(NxQueryBuilder query, SearchType searchType,
long keepAlive) {
return buildEsSearchRequest(query, searchType).setScroll(new TimeValue(keepAlive)).setSize(query.getLimit());
}
protected SearchScrollRequestBuilder buildEsScrollRequest(String scrollId, long keepAlive) {
return esa.getClient().prepareSearchScroll(scrollId).setScroll(new TimeValue(keepAlive));
}
protected void logSearchResponse(SearchResponse response) {
if (log.isDebugEnabled()) {
log.debug("Response: " + response.toString());
}
}
protected void logSearchRequest(SearchRequestBuilder request, NxQueryBuilder query, SearchType searchType) {
logSearchRequest(request, query, searchType, null);
}
protected void logSearchRequest(SearchRequestBuilder request, NxQueryBuilder query, SearchType searchType,
Long keepAlive) {
if (log.isDebugEnabled()) {
String scroll = keepAlive != null ? "&scroll=" + keepAlive : "";
log.debug(String.format(
"Search query: curl -XGET 'http://localhost:9200/%s/%s/_search?pretty&search_type=%s%s' -d '%s'",
getSearchIndexesAsString(query), DOC_TYPE, searchType.toString().toLowerCase(), scroll,
request.toString()));
}
}
protected void logScrollRequest(String scrollId, long keepAlive) {
if (log.isDebugEnabled()) {
log.debug(String.format(
"Scroll search: curl -XGET 'http://localhost:9200/_search/scroll?pretty' -d '{\"scroll\" : \"%d\", \"scroll_id\" : \"%s\"}'",
keepAlive, scrollId));
}
}
protected String getSearchIndexesAsString(NxQueryBuilder query) {
return StringUtils.join(esa.getSearchIndexes(query.getSearchRepositories()), ',');
}
}