/**
* Copyright 2011 Molindo GmbH
*
* 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 at.molindo.elastic.compass;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.compass.core.Property;
import org.compass.core.Property.TermVector;
import org.compass.core.Resource;
import org.compass.core.engine.SearchEngineException;
import org.compass.core.engine.SearchEngineHits;
import org.compass.core.mapping.Mapping;
import org.compass.core.mapping.ResourceMapping;
import org.compass.core.mapping.ResourcePropertyMapping;
import org.compass.core.spi.ResourceKey;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.admin.indices.analyze.AnalyzeResponse.AnalyzeToken;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetField;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.action.bulk.BulkRequestBuilder;
import org.elasticsearch.client.action.count.CountRequestBuilder;
import org.elasticsearch.client.action.delete.DeleteRequestBuilder;
import org.elasticsearch.client.action.index.IndexRequestBuilder;
import org.elasticsearch.client.action.search.SearchRequestBuilder;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.index.query.xcontent.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHitField;
import org.elasticsearch.search.facet.FacetBuilders;
import org.elasticsearch.search.facet.terms.TermsFacet;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import at.molindo.elastic.query.SortField;
import at.molindo.elastic.query.SortField.SortType;
import at.molindo.elastic.term.TermFreqVector;
import at.molindo.elastic.term.TermPositionVector;
import at.molindo.utils.collections.ArrayUtils;
public class ElasticClient {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory
.getLogger(ElasticClient.class);
// FIXME no paging yet!!!
private static final int DEFAULT_FETCH_SIZE = 100;
private final ElasticSearchEngineFactory _searchEngineFactory;
private final ElasticIndex _index;
private final String _indexName;
private final Client _client;
private final Map<String, String[]> _typeFields;
public ElasticClient(ElasticSearchEngineFactory searchEngineFactory, ElasticIndex index, Client client) {
if (searchEngineFactory == null) {
throw new NullPointerException("searchEngineFactory");
}
if (index == null) {
throw new NullPointerException("index");
}
if (client == null) {
throw new NullPointerException("client");
}
_searchEngineFactory = searchEngineFactory;
_index = index;
_indexName = index.getAlias(); // always use alias
_client = client;
_typeFields = new HashMap<String, String[]>();
for (String type : _index.getTypes()) {
Set<String> fields = _index.getFieldMapping(type).keySet();
_typeFields.put(type, fields.toArray(new String[fields.size()]));
}
}
public void create(final ElasticResource resource) {
try {
IndexRequestBuilder index = _client
.prepareIndex(_indexName, resource.getAlias(), resource.getId())
.setSource(toXContentBuilder(resource));
if (_index.getSettings().isAsyncWrite()) {
index.execute(new ActionListener<IndexResponse>() {
@Override
public void onResponse(IndexResponse response) {
if (log.isTraceEnabled()) {
log.trace("created id " + resource.getAlias() + "#" + response.getId());
}
}
@Override
public void onFailure(Throwable e) {
log.warn("failed to create " + resource.getAlias() + "#" + resource.getId(), e);
}
});
} else {
index.setOperationThreaded(false).execute().actionGet();
}
} catch (IOException e) {
throw new SearchEngineException("failed to create resource", e);
}
}
public void update(final ElasticResource resource) {
try {
IndexRequestBuilder index = _client
.prepareIndex(_indexName, resource.getAlias(), resource.getId())
.setSource(toXContentBuilder(resource));
if (_index.getSettings().isAsyncWrite()) {
index.execute(new ActionListener<IndexResponse>() {
@Override
public void onResponse(IndexResponse response) {
if (log.isTraceEnabled()) {
log.trace("updated id " + resource.getAlias() + "#" + response.getId());
}
}
@Override
public void onFailure(Throwable e) {
log.warn("failed to update " + resource.getAlias() + "#" + resource.getId(), e);
}
});
} else {
index.setOperationThreaded(false).execute().actionGet();
}
} catch (IOException e) {
throw new SearchEngineException("failed to create resource", e);
}
}
public Resource[] get(ResourceKey key) {
Property[] ids = key.getIds();
if (ids == null || ids.length == 0) {
return ElasticResource.NO_RESOURCES;
}
Resource[] resources = new Resource[ids.length];
if (ids.length > 1) {
// FIXME ids.length > 1 si a composite id
throw new NotImplementedException("composite ids not implemented");
// Map<String, Integer> idStrings = new HashMap<String, Integer>();
// for (int i = 0; i < ids.length; i++) {
// idStrings.put(ids[i].getStringValue(), i);
// }
//
// IdsQuery idsQuery = new IdsQuery(key.getAlias()).addIds(idStrings.keySet()
// .toArray(new String[idStrings.size()]));
// ElasticSearchEngineQuery query = new ElasticSearchEngineQuery(_searchEngineFactory, idsQuery)
// .setAlias(key.getAlias());
//
// SearchEngineHits hits = find(query);
// for (int i = 0; i < hits.getLength(); i++) {
// Resource r = hits.getResource(i);
// resources[idStrings.get(r.getId())] = r;
// }
} else {
String[] fields = _typeFields.get(key.getAlias());
if (fields == null) {
throw new SearchEngineException("unknown alias " + key.getAlias());
}
GetResponse response = _client
.prepareGet(_indexName, key.getAlias(), ids[0].getStringValue())
.setFields(fields).execute().actionGet();
if (response.exists()) {
ElasticResource resource = new ElasticResource(response.getType(), _searchEngineFactory);
ResourceMapping mapping = _searchEngineFactory.getMapping()
.getRootMappingByAlias(key.getAlias());
for (Map.Entry<String, GetField> e : response.getFields().entrySet()) {
resource.addProperties(toProperties(mapping, e.getKey(), e.getValue()
.getValues()));
}
resources[0] = resource;
}
}
return resources;
}
public void delete(final ResourceKey key) {
Property[] ids = key.getIds();
if (ArrayUtils.empty(ids)) {
return;
} else if (ids.length == 1) {
// simple delete
DeleteRequestBuilder delete = _client.prepareDelete(_indexName, key.getAlias(), ids[0]
.getStringValue());
if (_index.getSettings().isAsyncWrite()) {
delete.execute(new ActionListener<DeleteResponse>() {
@Override
public void onResponse(DeleteResponse response) {
if (log.isTraceEnabled()) {
log.trace("deleted id " + key.getIds()[0] + " of type "
+ key.getAlias());
}
}
@Override
public void onFailure(Throwable e) {
log.warn("failed to delete id " + key.getIds()[0] + " of type "
+ key.getAlias(), e);
}
});
} else {
delete.setOperationThreaded(false).execute().actionGet();
}
} else {
// bulk delete
BulkRequestBuilder bulk = _client.prepareBulk();
for (Property id : ids) {
bulk.add(_client.prepareDelete(_indexName, key.getAlias(), id.getStringValue())
.setOperationThreaded(_index.getSettings().isAsyncWrite()));
}
if (_index.getSettings().isAsyncWrite()) {
bulk.execute(new ActionListener<BulkResponse>() {
@Override
public void onResponse(BulkResponse response) {
if (log.isTraceEnabled()) {
log.trace("deleted ids " + Arrays.toString(key.getIds()) + " of type "
+ key.getAlias());
}
}
@Override
public void onFailure(Throwable e) {
log.warn("failed to delete ids " + Arrays.toString(key.getIds())
+ " of type " + key.getAlias(), e);
}
});
} else {
bulk.execute().actionGet();
}
}
}
protected XContentBuilder toXContentBuilder(ElasticResource resource) throws IOException {
XContentBuilder builder = jsonBuilder().startObject();
for (Property property : resource.getProperties()) {
builder.field(property.getName(), property.getStringValue());
}
return builder.endObject();
}
public SearchEngineHits find(ElasticSearchEngineQuery query) throws SearchEngineException {
SearchRequestBuilder search = _client.prepareSearch(_indexName).setQuery(query.getBuilder());
String[] aliases = toAliases(query);
search.setTypes(aliases);
// fields
String[] fields;
if (aliases.length == 1) {
String alias = aliases[0];
fields = _typeFields.get(alias);
if (fields == null) {
throw new SearchEngineException("unknown alias: '" + alias + "'");
}
} else {
HashSet<String> fieldSet = new HashSet<String>();
for (String alias : aliases) {
String[] aliasFields = _typeFields.get(alias);
if (aliasFields == null) {
throw new SearchEngineException("unknown alias: '" + alias + "'");
}
fieldSet.addAll(Arrays.asList(aliasFields));
}
fields = fieldSet.toArray(new String[fieldSet.size()]);
}
search.addFields(fields);
for (SortField sort : query.getSorts()) {
SortBuilder builder;
switch (sort.getType()) {
case FIELD:
builder = SortBuilders.fieldSort(sort.getProperty());
break;
case DOC:
builder = SortBuilders.fieldSort("_id");
break;
case SCORE:
builder = SortBuilders.scoreSort();
break;
case DISTANCE:
builder = SortBuilders.geoDistanceSort(sort.getProperty());
default:
throw new SearchEngineException("unknown SortType " + sort.getType());
}
builder.order(toOrder(sort.getType(), sort.isReverse()));
search.addSort(builder);
}
search.setFrom(0);
search.setSize(DEFAULT_FETCH_SIZE);
return new ElasticSearchEngineHits(this, search.execute().actionGet().hits());
}
private SortOrder toOrder(SortType sortType, boolean reverse) {
if (sortType == SortType.SCORE) {
// default ist DESC
return reverse ? SortOrder.ASC : SortOrder.DESC;
} else {
return reverse ? SortOrder.DESC : SortOrder.ASC;
}
}
public long count(ElasticSearchEngineQuery query) {
CountRequestBuilder search = _client.prepareCount(_indexName).setQuery(query.getBuilder());
search.setTypes(toAliases(query));
return search.execute().actionGet().count();
}
public void delete(ElasticSearchEngineQuery query) {
_client.prepareDeleteByQuery(_indexName).setQuery(query.getQuery().getBuilder())
.setTypes(toAliases(query)).execute();
}
private String[] toAliases(ElasticSearchEngineQuery query) {
String[] aliases = query.getAliases();
if (ArrayUtils.empty(aliases)) {
aliases = _typeFields.keySet().toArray(new String[_typeFields.size()]);
}
return aliases;
}
public ElasticResource toResource(SearchHit hit) {
String alias = hit.getType();
ResourceMapping mapping = _searchEngineFactory.getMapping().getRootMappingByAlias(alias);
ElasticResource resource = new ElasticResource(hit.getType(), _searchEngineFactory);
for (Map.Entry<String, SearchHitField> e : hit.getFields().entrySet()) {
resource.addProperties(toProperties(mapping, e.getKey(), e.getValue().getValues()));
}
return resource;
}
private Property[] toProperties(ResourceMapping mapping, String name, Collection<?> values) {
Mapping m = _index.getFieldMapping(mapping.getAlias()).get(name);
if (m == null) {
throw new SearchEngineException("No resource property mapping is defined for alias ["
+ mapping.getAlias() + "] and resource property [" + name + "]");
}
Property[] properties = new ElasticProperty[values.size()];
int i = 0;
for (Object value : values) {
if (value != null && value instanceof String == false) {
// compass expects all strings
value = value.toString();
}
Property property;
if (m instanceof ResourcePropertyMapping) {
ResourcePropertyMapping propertyMapping = (ResourcePropertyMapping) m;
property = _searchEngineFactory.getResourceFactory()
.createProperty((String) value, propertyMapping);
property.setBoost(propertyMapping.getBoost());
} else {
// col size / class
property = _searchEngineFactory
.getResourceFactory()
.createProperty(name, (String) value, Property.Store.YES, Property.Index.NOT_ANALYZED);
}
properties[i++] = property;
}
return properties;
}
public String[] findPropertyValues(String propertyName) {
// TODO use facetted search?
throw new NotImplementedException();
}
public void verifyIndex() {
_index.verifyIndex();
}
public void deleteIndex() {
// TODO lock
_index.deleteIndex();
}
public void refresh() {
_client.admin().indices().prepareRefresh(_index.getAlias()).execute().actionGet();
}
public List<AnalyzeToken> analyze(String analyzer, String text) {
return _client.admin().indices().prepareAnalyze(_index.getAlias(), text)
.setAnalyzer(analyzer).execute().actionGet().getTokens();
}
public TermFreqVector getTermFreqVector(String alias, String docId, String field) {
Property.TermVector tf = null;
if (ElasticEnvironment.Mapping.ALL_FIELD.equals(field)) {
tf = _searchEngineFactory.getMapping().getRootMappingByAlias(alias).getAllMapping()
.getTermVector();
} else {
Mapping mapping = _index.getFieldMapping(alias).get(field);
if (mapping instanceof ResourcePropertyMapping) {
return TermFreqVector.NULL;
}
ResourcePropertyMapping resource = (ResourcePropertyMapping) mapping;
tf = resource.getTermVector();
}
if (tf == null || tf == TermVector.NO) {
return TermFreqVector.NULL;
}
// @formatter:off
SearchResponse resp = _client.prepareSearch(_indexName)
.addField(field)
.setQuery(QueryBuilders.idsQuery(alias).addIds(docId))
.addFacet(FacetBuilders.termsFacet(field).field(field))
.execute().actionGet();
// @formatter:on
if (resp.getHits().getTotalHits() == 0) {
return TermFreqVector.NULL;
}
TermsFacet facet = (TermsFacet) resp.getFacets().getFacets().get(field);
if (facet == null) {
return TermFreqVector.NULL;
}
switch (tf) {
case YES:
return new TermFreqVector(facet);
case WITH_OFFSETS:
return new TermPositionVector(facet, false, true);
case WITH_POSITIONS:
return new TermPositionVector(facet, true, false);
case WITH_POSITIONS_OFFSETS:
return new TermPositionVector(facet, true, true);
default: throw new SearchEngineException("unexpected TermVector value " + tf);
}
}
}