package org.elassandra.index.search;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.stream.Collectors;
import org.apache.lucene.index.IndexReaderContext;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.ReaderUtil;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.search.FilteredDocIdSetIterator;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.Weight;
import org.apache.lucene.search.join.BitSetProducer;
import org.apache.lucene.util.Accountable;
import org.apache.lucene.util.BitSet;
import org.apache.lucene.util.Bits;
import org.apache.lucene.util.RamUsageEstimator;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers;
/**
* A {@link BitSetProducer} that wraps a query and caches matching
* {@link BitSet}s per segment.
*/
public class TokenRangesBitsetProducer implements BitSetProducer, Accountable {
private static final ESLogger logger = Loggers.getLogger(TokenRangesBitsetProducer.class);
//memory usage of a simple term query
static final long QUERY_DEFAULT_RAM_BYTES_USED = 192;
static final long HASHTABLE_RAM_BYTES_PER_ENTRY =
2 * RamUsageEstimator.NUM_BYTES_OBJECT_REF // key + value
* 2; // hash tables need to be oversized to avoid collisions, assume 2x capacity
static class Value implements Accountable {
int tombestones;
BitSet bitset;
Value(int tombestones, BitSet bitset) {
this.tombestones = tombestones;
this.bitset = bitset;
}
@Override
public long ramBytesUsed() {
return HASHTABLE_RAM_BYTES_PER_ENTRY + (bitset==null ? 0 : bitset.ramBytesUsed());
}
@Override
public Collection<Accountable> getChildResources() {
return null;
}
}
private final TokenRangesBitsetFilterCache bitsetFilterCache;
private final Query query;
private final Map<Object,Value> leafCache;
/** Wraps another query's result and caches it into bitsets.
* @param query Query to cache results of
*/
public TokenRangesBitsetProducer(TokenRangesBitsetFilterCache bitsetFilterCache, Query query) {
this.bitsetFilterCache = bitsetFilterCache;
this.query = query;
this.leafCache = Collections.synchronizedMap(new WeakHashMap<Object,Value>());
//new ConcurrentReferenceHashMap<Object,Value>(10, 0.9f, 1, ReferenceType.WEAK);
this.bitsetFilterCache.listener.onCache(this.bitsetFilterCache.shardId, this);
}
/**
* Gets the contained query.
* @return the contained query.
*/
public Query getQuery() {
return query;
}
public void remove(Object coreCacheKey) {
if (logger.isTraceEnabled())
logger.trace("query={} coreCacheKey={} removed", query, coreCacheKey);
Value value = leafCache.remove(coreCacheKey);
if (value != null)
this.bitsetFilterCache.listener.onRemoval(this.bitsetFilterCache.shardId, value);
}
public void clear() {
for(Value value : leafCache.values())
this.bitsetFilterCache.listener.onRemoval(this.bitsetFilterCache.shardId, value);
leafCache.clear();
}
@Override
public BitSet getBitSet(LeafReaderContext context) throws IOException {
final LeafReader reader = context.reader();
final Object key = reader.getCoreCacheKey();
Value value = leafCache.get(key);
BitSet bitset = value == null ? null : value.bitset;
if (value == null || value.tombestones < reader.numDeletedDocs()) {
final IndexReaderContext topLevelContext = ReaderUtil.getTopLevelContext(context);
final IndexSearcher searcher = new IndexSearcher(topLevelContext);
searcher.setQueryCache(null);
final Weight weight = searcher.createNormalizedWeight(query, false);
final Scorer s = weight.scorer(context);
int tombestones = 0;
if (s != null) {
final DocIdSetIterator it = s.iterator();
final Bits liveDocs = reader.getLiveDocs();
if (liveDocs == null) {
// visible docs = query result
bitset = BitSet.of(it, reader.maxDoc());
if (logger.isTraceEnabled())
logger.trace("query={} coreCacheKey={} segment={} cardinality={}", query, key, reader, bitset.cardinality());
} else {
// visible docs = query result AND liveDocs.
tombestones = reader.numDeletedDocs();
DocIdSetIterator fit = new FilteredDocIdSetIterator(it) {
@Override
protected boolean match(int doc) {
return liveDocs.get(doc);
}
};
bitset = BitSet.of(fit, reader.maxDoc());
if (logger.isTraceEnabled())
logger.trace("query={} coreCacheKey={} segment={} cardinality={}", query, key, reader, bitset.cardinality());
}
} else {
bitset = null; // no visible docs.
if (logger.isTraceEnabled())
logger.trace("query={} coreCacheKey={} segment={} cardinality=0 ", query, key, reader);
}
Value newValue = new Value(tombestones, bitset);
Value oldValue = leafCache.put(key, newValue);
if (oldValue != null)
this.bitsetFilterCache.listener.onRemoval(this.bitsetFilterCache.shardId, oldValue);
this.bitsetFilterCache.listener.onCache(this.bitsetFilterCache.shardId, newValue);
}
return bitset;
}
@Override
public String toString() {
return "TokenRangesBitsetProducer("+query.toString()+":"+leafCache+")";
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
final TokenRangesBitsetProducer other = (TokenRangesBitsetProducer) o;
return this.query.equals(other.query);
}
@Override
public int hashCode() {
return 31 * getClass().hashCode() + query.hashCode();
}
@Override
public long ramBytesUsed() {
return QUERY_DEFAULT_RAM_BYTES_USED + leafCache.values().stream().collect(Collectors.summingLong(m -> m.ramBytesUsed()));
}
@Override
public Collection<Accountable> getChildResources() {
return null;
}
}