/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2010-2011, Red Hat, Inc. and/or its affiliates or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat, Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.search.query.engine.impl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.FieldSelector;
import org.apache.lucene.search.Collector;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.TimeLimitingCollector;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.TopDocsCollector;
import org.apache.lucene.search.TopFieldCollector;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.search.TotalHitCountCollector;
import org.apache.lucene.search.Weight;
import org.apache.lucene.util.Counter;
import org.hibernate.search.SearchException;
import org.hibernate.search.query.collector.impl.FacetCollector;
import org.hibernate.search.query.collector.impl.FieldCacheCollector;
import org.hibernate.search.query.collector.impl.FieldCacheCollectorFactory;
import org.hibernate.search.query.dsl.impl.FacetingRequestImpl;
import org.hibernate.search.query.engine.spi.TimeoutExceptionFactory;
import org.hibernate.search.query.engine.spi.TimeoutManager;
import org.hibernate.search.query.facet.Facet;
import org.hibernate.search.spatial.impl.DistanceCollector;
import org.hibernate.search.spatial.impl.Point;
/**
* A helper class which gives access to the current query and its hits. This class will dynamically
* reload the underlying {@code TopDocs} if required.
*
* @author Hardy Ferentschik
* @author Sanne Grinovero <sanne@hibernate.org> (C) 2011 Red Hat Inc.
*/
public class QueryHits {
private static final int DEFAULT_TOP_DOC_RETRIEVAL_SIZE = 100;
private final org.apache.lucene.search.Query preparedQuery;
private final IndexSearcherWithPayload searcher;
private final Filter filter;
private final Sort sort;
private final Map<String, FacetingRequestImpl> facetRequests;
private final TimeoutManagerImpl timeoutManager;
private int totalHits;
private TopDocs topDocs;
private Map<String, List<Facet>> facetMap;
private List<FacetCollector> facetCollectors;
private DistanceCollector distanceCollector = null;
private final boolean enableFieldCacheOnClassName;
private Point spatialSearchCenter = null;
private String spatialFieldName = null;
/**
* If enabled, after hits collection it will contain the class name for each hit
*/
private FieldCacheCollector classTypeCollector;
/**
* If enabled, a Collector will collect values from the primary keys
*/
private final FieldCacheCollectorFactory idFieldCollectorFactory;
private FieldCacheCollector idFieldCollector;
private final TimeoutExceptionFactory timeoutExceptionFactory;
public QueryHits(IndexSearcherWithPayload searcher,
org.apache.lucene.search.Query preparedQuery,
Filter filter,
Sort sort,
TimeoutManagerImpl timeoutManager,
Map<String, FacetingRequestImpl> facetRequests,
boolean enableFieldCacheOnTypes,
FieldCacheCollectorFactory idFieldCollector,
TimeoutExceptionFactory timeoutExceptionFactory,
Point spatialSearchCenter,
String spatialFieldName)
throws IOException {
this(
searcher, preparedQuery, filter, sort, DEFAULT_TOP_DOC_RETRIEVAL_SIZE, timeoutManager, facetRequests,
enableFieldCacheOnTypes, idFieldCollector, timeoutExceptionFactory, spatialSearchCenter, spatialFieldName
);
}
public QueryHits(IndexSearcherWithPayload searcher,
org.apache.lucene.search.Query preparedQuery,
Filter filter,
Sort sort,
Integer n,
TimeoutManagerImpl timeoutManager,
Map<String, FacetingRequestImpl> facetRequests,
boolean enableFieldCacheOnTypes,
FieldCacheCollectorFactory idFieldCollector,
TimeoutExceptionFactory timeoutExceptionFactory,
Point spatialSearchCenter,
String spatialFieldName)
throws IOException {
this.timeoutManager = timeoutManager;
this.preparedQuery = preparedQuery;
this.searcher = searcher;
this.filter = filter;
this.sort = sort;
this.facetRequests = facetRequests;
this.enableFieldCacheOnClassName = enableFieldCacheOnTypes;
this.idFieldCollectorFactory = idFieldCollector;
this.timeoutExceptionFactory = timeoutExceptionFactory;
this.spatialSearchCenter = spatialSearchCenter;
this.spatialFieldName = spatialFieldName;
updateTopDocs( n );
}
public Document doc(int index) throws IOException {
return searcher.getSearcher().doc( docId( index ) );
}
public Document doc(int index, FieldSelector selector) throws IOException {
return searcher.getSearcher().doc( docId( index ), selector );
}
public ScoreDoc scoreDoc(int index) throws IOException {
if ( index >= totalHits ) {
throw new SearchException( "Not a valid ScoreDoc index: " + index );
}
// TODO - Is there a better way to get more TopDocs? Get more or less?
if ( index >= topDocs.scoreDocs.length ) {
updateTopDocs( 2 * index );
}
//if the refresh timed out, raise an exception
if ( timeoutManager.isTimedOut() && index >= topDocs.scoreDocs.length ) {
throw timeoutExceptionFactory.createTimeoutException(
"Timeout period exceeded. Cannot load document: " + index,
preparedQuery
);
}
return topDocs.scoreDocs[index];
}
public int docId(int index) throws IOException {
return scoreDoc( index ).doc;
}
public float score(int index) throws IOException {
return scoreDoc( index ).score;
}
public Double spatialDistance(int index) throws IOException {
if ( spatialSearchCenter == null ) {
return null;
}
return Double.valueOf( distanceCollector.getDistance( docId( index ) ) );
}
public Explanation explain(int index) throws IOException {
final Explanation explanation = searcher.getSearcher().explain( preparedQuery, docId( index ) );
timeoutManager.isTimedOut();
return explanation;
}
public int getTotalHits() {
return totalHits;
}
public TopDocs getTopDocs() {
return topDocs;
}
public Map<String, List<Facet>> getFacets() {
if ( facetRequests == null || facetRequests.size() == 0 ) {
return Collections.emptyMap();
}
return facetMap;
}
/**
* @param n the number of {@code TopDoc}s to retrieve. The actual retrieved number of {@code TopDoc}s is n or the
* total number of documents if {@code n > maxDoc}
*
* @throws IOException in case a search exception occurs
*/
private void updateTopDocs(int n) throws IOException {
int totalMaxDocs = searcher.getSearcher().maxDoc();
final int maxDocs = Math.min( n, totalMaxDocs );
final Weight weight = preparedQuery.weight( searcher.getSearcher() );
final TopDocsCollector<?> topDocCollector;
final TotalHitCountCollector hitCountCollector;
Collector collector = null;
if ( maxDocs != 0 ) {
topDocCollector = createTopDocCollector( maxDocs, weight );
hitCountCollector = null;
collector = topDocCollector;
collector = optionallyEnableFieldCacheOnTypes( collector, totalMaxDocs, maxDocs );
collector = optionallyEnableFieldCacheOnIds( collector, totalMaxDocs, maxDocs );
collector = optionallyEnableFacetingCollectors( collector );
collector = optionallyEnableDistanceCollector( collector, maxDocs );
}
else {
topDocCollector = null;
hitCountCollector = new TotalHitCountCollector();
collector = hitCountCollector;
}
collector = decorateWithTimeOutCollector( collector );
boolean timeoutNow = isImmediateTimeout();
if ( !timeoutNow ) {
try {
searcher.getSearcher().search( weight, filter, collector );
}
catch ( TimeLimitingCollector.TimeExceededException e ) {
//we have reached the time limit and stopped before the end
//TimeoutManager.isTimedOut should be above that limit but set if for safety
timeoutManager.forceTimedOut();
}
}
// update top docs and totalHits
if ( maxDocs != 0 ) {
this.topDocs = topDocCollector.topDocs();
this.totalHits = topDocs.totalHits;
// if we were collecting facet data we have to update our instance state
if ( facetCollectors != null && !facetCollectors.isEmpty() ) {
facetMap = new HashMap<String, List<Facet>>();
for ( FacetCollector facetCollector : facetCollectors ) {
facetMap.put( facetCollector.getFacetName(), facetCollector.getFacetList() );
}
}
}
else {
this.topDocs = null;
this.totalHits = hitCountCollector.getTotalHits();
}
timeoutManager.isTimedOut();
}
private Collector optionallyEnableFacetingCollectors(Collector collector) {
if ( facetRequests == null || facetRequests.isEmpty() ) {
return collector;
}
facetCollectors = new ArrayList<FacetCollector>();
Collector nextInChain = collector;
for ( FacetingRequestImpl entry : facetRequests.values() ) {
FacetCollector facetCollector = new FacetCollector( nextInChain, entry );
nextInChain = facetCollector;
facetCollectors.add( facetCollector );
}
return facetCollectors.get( facetCollectors.size() - 1 );
}
private Collector optionallyEnableDistanceCollector(Collector collector, int maxDocs) {
if ( spatialFieldName == null || spatialFieldName.isEmpty() || spatialSearchCenter == null) {
return collector;
}
distanceCollector = new DistanceCollector( collector, spatialSearchCenter, maxDocs, spatialFieldName );
return distanceCollector;
}
private boolean isImmediateTimeout() {
boolean timeoutAt0 = false;
if ( timeoutManager.getType() == TimeoutManager.Type.LIMIT ) {
final Long timeoutLeft = timeoutManager.getTimeoutLeftInMilliseconds();
if ( timeoutLeft != null ) {
if ( timeoutLeft == 0l ) {
if ( timeoutManager.getType() == TimeoutManager.Type.LIMIT && timeoutManager.isTimedOut() ) {
timeoutManager.forceTimedOut();
timeoutAt0 = true;
}
}
}
else {
if ( timeoutManager.isTimedOut() ) {
timeoutManager.forceTimedOut();
}
}
}
return timeoutAt0;
}
private Collector decorateWithTimeOutCollector(Collector collector) {
Collector maybeTimeLimitingCollector = collector;
if ( timeoutManager.getType() == TimeoutManager.Type.LIMIT ) {
final Long timeoutLeft = timeoutManager.getTimeoutLeftInMilliseconds();
if ( timeoutLeft != null ) {
Counter counter = timeoutManager.getLuceneTimeoutCounter();
maybeTimeLimitingCollector = new TimeLimitingCollector( collector, counter, timeoutLeft);
}
}
return maybeTimeLimitingCollector;
}
private TopDocsCollector<?> createTopDocCollector(int maxDocs, Weight weight) throws IOException {
TopDocsCollector<?> topCollector;
if ( sort == null ) {
topCollector = TopScoreDocCollector.create( maxDocs, !weight.scoresDocsOutOfOrder() );
}
else {
boolean fillFields = true;
topCollector = TopFieldCollector.create(
sort,
maxDocs,
fillFields,
searcher.isFieldSortDoTrackScores(),
searcher.isFieldSortDoMaxScore(),
!weight.scoresDocsOutOfOrder()
);
}
return topCollector;
}
private Collector optionallyEnableFieldCacheOnIds(Collector collector, int totalMaxDocs, int maxDocs) {
if ( idFieldCollectorFactory != null ) {
idFieldCollector = idFieldCollectorFactory.createFieldCollector( collector, totalMaxDocs, maxDocs );
return idFieldCollector;
}
return collector;
}
private Collector optionallyEnableFieldCacheOnTypes(Collector collector, int totalMaxDocs, int expectedMatchesCount) {
if ( enableFieldCacheOnClassName ) {
classTypeCollector = FieldCacheCollectorFactory
.CLASS_TYPE_FIELD_CACHE_COLLECTOR_FACTORY
.createFieldCollector( collector, totalMaxDocs, expectedMatchesCount );
return classTypeCollector;
}
else {
return collector;
}
}
public FieldCacheCollector getClassTypeCollector() {
return classTypeCollector;
}
public FieldCacheCollector getIdsCollector() {
return idFieldCollector;
}
}