/*
* Copyright 2014, Tuplejump Inc.
*
* 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 com.tuplejump.stargate.cassandra;
import com.tuplejump.stargate.RowIndex;
import com.tuplejump.stargate.Utils;
import com.tuplejump.stargate.lucene.IndexEntryCollector;
import com.tuplejump.stargate.lucene.LuceneUtils;
import com.tuplejump.stargate.lucene.Options;
import com.tuplejump.stargate.lucene.SearcherCallback;
import com.tuplejump.stargate.lucene.query.Search;
import com.tuplejump.stargate.lucene.query.function.Function;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.composites.CellName;
import org.apache.cassandra.db.composites.Composite;
import org.apache.cassandra.db.composites.Composites;
import org.apache.cassandra.db.filter.ExtendedFilter;
import org.apache.cassandra.db.index.SecondaryIndexManager;
import org.apache.cassandra.db.index.SecondaryIndexSearcher;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.lucene.search.Query;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.*;
/**
* User: satya
* <p>
* A searcher which can be used with a SGIndex
* Includes features to make lucene queries etc.
*/
public class SearchSupport extends SecondaryIndexSearcher {
public static final Logger logger = LoggerFactory.getLogger(SearchSupport.class);
protected RowIndex currentIndex;
protected TableMapper tableMapper;
protected Options options;
protected Set<String> fieldNames;
public SearchSupport(SecondaryIndexManager indexManager, RowIndex currentIndex, Set<ByteBuffer> columns, Options options) {
super(indexManager, columns);
this.options = options;
this.currentIndex = currentIndex;
this.fieldNames = options.fieldTypes.keySet();
this.tableMapper = currentIndex.getTableMapper();
}
protected Search getQuery(IndexExpression predicate) throws Exception {
return Search.fromJson(getQueryString(predicate));
}
protected Search getQuery(String queryString) throws Exception {
return Search.fromJson(queryString);
}
protected String getQueryString(IndexExpression predicate) throws Exception {
ColumnDefinition cd = baseCfs.metadata.getColumnDefinition(predicate.column);
String predicateValue = cd.type.getString(predicate.value);
if (logger.isDebugEnabled()) {
String columnName = cd.name.toString();
logger.debug("Index Searcher - query - predicate value [" + predicateValue + "] column name [" + columnName + "]");
logger.debug("Column name is {}", columnName);
}
return predicateValue;
}
@Override
public List<Row> search(ExtendedFilter mainFilter) {
List<IndexExpression> clause = mainFilter.getClause();
if (logger.isDebugEnabled())
logger.debug("All IndexExprs {}", clause);
try {
String queryString = getQueryString(matchThisIndex(clause));
Search search = getQuery(queryString);
return getRows(mainFilter, search, queryString);
} catch (Exception e) {
logger.error("Exception occurred while querying", e);
if (tableMapper.isMetaColumn) {
ByteBuffer errorMsg = UTF8Type.instance.decompose("{\"error\":\"" + StringEscapeUtils.escapeEcmaScript(e.getMessage()) + "\"}");
Row row = tableMapper.getRowWithMetaColumn(errorMsg);
if (row != null) {
return Collections.singletonList(row);
}
}
return Collections.EMPTY_LIST;
}
}
protected List<Row> getRows(final ExtendedFilter filter, final Search search, final String queryString) {
final SearchSupport searchSupport = this;
AbstractBounds<RowPosition> keyRange = filter.dataRange.keyRange();
final Range<Token> filterRange = new Range<>(keyRange.left.getToken(), keyRange.right.getToken());
final boolean isSingleToken = filterRange.left.equals(filterRange.right);
final boolean isFullRange = isSingleToken && baseCfs.partitioner.getMinimumToken().equals(filterRange.left);
final boolean shouldSaveToCache = isPagingQuery(filter.dataRange);
final boolean shouldRetrieveFromCache = shouldSaveToCache && !isFirstPage((DataRange.Paging) filter.dataRange);
SearcherCallback<List<Row>> sc = new SearcherCallback<List<Row>>() {
@Override
public List<Row> doWithSearcher(org.apache.lucene.search.IndexSearcher searcher) throws Exception {
Utils.SimpleTimer timer = Utils.getStartedTimer(logger);
List<Row> results;
if (search == null) {
results = new ArrayList<>();
} else {
Utils.SimpleTimer timer2 = Utils.getStartedTimer(SearchSupport.logger);
Function function = search.function();
Query query = LuceneUtils.getQueryUpdatedWithPKCondition(search.query(options), getPartitionKeyString(filter));
int resultsLimit = searcher.getIndexReader().maxDoc();
if (resultsLimit == 0) {
resultsLimit = 1;
}
function.init(options);
IndexEntryCollector collector = null;
if (shouldRetrieveFromCache) {
collector = currentIndex.collectorMap.get(queryString);
}
if (collector == null) {
collector = new IndexEntryCollector(tableMapper, search, options, resultsLimit);
searcher.search(query, collector);
if (shouldSaveToCache) {
currentIndex.collectorMap.put(queryString, collector);
}
if (logger.isInfoEnabled()) {
logger.info("Adding collector to cache");
}
} else if (logger.isInfoEnabled()){
logger.info("Found collector in cache");
}
timer2.endLogTime("Lucene search for [" + collector.getTotalHits() + "] results ");
if (SearchSupport.logger.isDebugEnabled()) {
SearchSupport.logger.debug(String.format("Search results [%s]", collector.getTotalHits()));
}
ResultMapper iter = new ResultMapper(tableMapper, searchSupport, filter, collector, function.shouldTryScoring() && search.isShowScore());
Utils.SimpleTimer timer3 = Utils.getStartedTimer(SearchSupport.logger);
results = function.process(iter, baseCfs, currentIndex);
timer3.endLogTime("Aggregation [" + results.size() + "] results");
}
timer.endLogTime("Search with results [" + results.size() + "] ");
return results;
}
@Override
public Range<Token> filterRange() {
return filterRange;
}
@Override
public boolean isSingleToken() {
return isSingleToken;
}
@Override
public boolean isFullRange() {
return isFullRange;
}
};
return currentIndex.search(sc);
}
protected IndexExpression matchThisIndex(List<IndexExpression> clause) {
for (IndexExpression expression : clause) {
ColumnDefinition cfDef = baseCfs.metadata.getColumnDefinition(expression.column);
String colName = cfDef.name.toString();
//we only support Equal - Operators should be a part of the lucene query
if (fieldNames.contains(colName) && expression.operator == Operator.EQ) {
return expression;
} else if (colName.equalsIgnoreCase(tableMapper.primaryColumnName())) {
return expression;
}
}
return null;
}
protected String getPartitionKeyString(ExtendedFilter mainFilter) {
AbstractBounds<RowPosition> keyRange = mainFilter.dataRange.keyRange();
if (keyRange != null && keyRange.left != null && keyRange.left instanceof DecoratedKey) {
DecoratedKey left = (DecoratedKey) keyRange.left;
DecoratedKey right = (DecoratedKey) keyRange.right;
if (left.equals(right)) {
return tableMapper.primaryKeyAbstractType.getString(left.getKey());
}
}
return null;
}
private boolean isPagingQuery(DataRange dataRange) {
return (dataRange instanceof DataRange.Paging);
}
private boolean isFirstPage(DataRange.Paging pageRange) {
try {
Composite start = (Composite) getPrivateProperty(pageRange, "firstPartitionColumnStart");
Composite finish = (Composite) getPrivateProperty(pageRange, "lastPartitionColumnFinish");
return (start == finish) && (start == Composites.EMPTY);
} catch (NoSuchFieldException e) {
//do nothing;
} catch (IllegalAccessException e) {
//do nothing
}
return false;
}
private Object getPrivateProperty(Object instance, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field field = instance.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(instance);
}
public boolean deleteIfNotLatest(DecoratedKey decoratedKey, long timestamp, String pkString, ColumnFamily cf) throws IOException {
if (deleteRowIfNotLatest(decoratedKey, cf)) return true;
Cell lastColumn = null;
for (CellName colKey : cf.getColumnNames()) {
String name = colKey.cql3ColumnName(tableMapper.cfMetaData).toString();
com.tuplejump.stargate.lucene.Properties option = options.fields.get(name);
//if option was not found then the column is not indexed
if (option != null) {
lastColumn = cf.getColumn(colKey);
}
}
if (lastColumn != null && lastColumn.timestamp() > timestamp) {
currentIndex.delete(decoratedKey, pkString, timestamp);
return true;
}
return false;
}
public boolean deleteRowIfNotLatest(DecoratedKey decoratedKey, ColumnFamily cf) {
if (!cf.getColumnNames().iterator().hasNext()) {//no columns available
currentIndex.deleteByKey(decoratedKey);
return true;
}
return false;
}
@Override
protected IndexExpression highestSelectivityPredicate(List<IndexExpression> clause, boolean includeInTrace) {
return matchThisIndex(clause);
}
@Override
public boolean canHandleIndexClause(List<IndexExpression> clause) {
return matchThisIndex(clause) != null;
}
public Options getOptions() {
return options;
}
}