/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.filter.impl;
import java.io.IOException;
import java.util.Set;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.ConstantScoreScorer;
import org.apache.lucene.search.ConstantScoreWeight;
import org.apache.lucene.search.DocIdSet;
import org.apache.lucene.search.DocIdSetIterator;
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.util.RoaringDocIdSet;
import org.hibernate.search.util.impl.SoftLimitMRUCache;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;
/**
* A slightly different version of Lucene's original <code>CachingWrapperQuery</code> which
* uses <code>SoftReferences</code> instead of <code>WeakReferences</code> in order to cache
* the filter <code>DocIdSet</code>.
*
* @author Hardy Ferentschik
* @author Sanne Grinovero
* @see org.apache.lucene.search.CachingWrapperQuery
* @see <a href="https://hibernate.atlassian.net/browse/HSEARCH-174">HSEARCH-174</a>
*/
@SuppressWarnings("serial")
public class CachingWrapperQuery extends Query implements Cloneable {
private static final Log log = LoggerFactory.make();
public static final int DEFAULT_SIZE = 5;
/**
* Under memory pressure the JVM will release all Soft references,
* so pushing it too high will invalidate all eventually useful other caches.
*/
private static final int HARD_TO_SOFT_RATIO = 15;
private Query query; // not final because of clone
/**
* The cache using soft references in order to store the filter bit sets.
*/
private final SoftLimitMRUCache cache;
/**
* @param query Query to cache results of
*/
public CachingWrapperQuery(Query query) {
this( query, DEFAULT_SIZE );
}
/**
* @param query Query to cache results of
* @param size soft reference size (gets multiplied by {@link #HARD_TO_SOFT_RATIO}.
*/
public CachingWrapperQuery(Query query, int size) {
this.query = query;
final int softRefSize = size * HARD_TO_SOFT_RATIO;
if ( log.isDebugEnabled() ) {
log.debugf( "Initialising SoftLimitMRUCache with hard ref size of %d and a soft ref of %d", (Integer) size, (Integer) softRefSize );
}
this.cache = new SoftLimitMRUCache( size, softRefSize );
}
/**
* Gets the contained query.
*
* @return the contained query.
*/
public Query getQuery() {
return query;
}
/**
* Default cache implementation: uses {@link RoaringDocIdSet}.
*/
protected DocIdSet cacheImpl(DocIdSetIterator iterator, LeafReader reader) throws IOException {
return new RoaringDocIdSet.Builder( reader.maxDoc() ).add( iterator ).build();
}
@Override
public Query rewrite(IndexReader reader) throws IOException {
if ( getBoost() != 1f ) {
return super.rewrite( reader );
}
final Query rewritten = query.rewrite( reader );
if ( query == rewritten ) {
return super.rewrite( reader );
}
else {
CachingWrapperQuery clone = (CachingWrapperQuery) clone();
clone.query = rewritten;
return clone;
}
}
@Override
public Weight createWeight(IndexSearcher searcher, boolean needsScores) throws IOException {
final Weight weight = query.createWeight( searcher, needsScores );
if ( needsScores ) {
// our cache is not sufficient, we need scores too
return weight;
}
return new ConstantScoreWeight( weight.getQuery() ) {
@Override
public void extractTerms(Set<Term> terms) {
weight.extractTerms( terms );
}
@Override
public Scorer scorer(LeafReaderContext context) throws IOException {
final LeafReader reader = context.reader();
final Object key = reader.getCoreCacheKey();
DocIdSet docIdSet = getDocIdSet( context );
assert docIdSet != null;
if ( docIdSet == DocIdSet.EMPTY ) {
return null;
}
final DocIdSetIterator disi = docIdSet.iterator();
if ( disi == null ) {
return null;
}
return new ConstantScoreScorer( this, 0f, disi );
}
private DocIdSet getDocIdSet(LeafReaderContext context) throws IOException {
final LeafReader reader = context.reader();
final Object key = reader.getCoreCacheKey();
Object cached = cache.get( key );
if ( cached != null ) {
return (DocIdSet) cached;
}
synchronized ( cache ) {
cached = cache.get( key );
if ( cached != null ) {
return (DocIdSet) cached;
}
final DocIdSet docIdSet;
final Scorer scorer = weight.scorer( context );
if ( scorer == null ) {
docIdSet = DocIdSet.EMPTY;
}
else {
docIdSet = cacheImpl( scorer.iterator(), reader );
}
cache.put( key, docIdSet );
return docIdSet;
}
}
};
}
@Override
public String toString(String field) {
return getClass().getSimpleName() + "(" + query.toString( field ) + ")";
}
@Override
public boolean equals(Object o) {
if ( !super.equals( o ) ) {
return false;
}
final CachingWrapperQuery other = (CachingWrapperQuery) o;
return this.query.equals( other.query );
}
@Override
public int hashCode() {
return ( query.hashCode() ^ super.hashCode() );
}
}