/* * 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.query.engine.impl; import java.io.Closeable; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.apache.lucene.document.Document; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.StoredFieldVisitor; import org.apache.lucene.search.Collector; import org.apache.lucene.search.Explanation; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.similarities.Similarity; import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator; import org.hibernate.search.engine.spi.EntityIndexBinding; import org.hibernate.search.exception.SearchException; import org.hibernate.search.metadata.FieldDescriptor; import org.hibernate.search.metadata.IndexedTypeDescriptor; import org.hibernate.search.metadata.PropertyDescriptor; import org.hibernate.search.reader.impl.MultiReaderFactory; import org.hibernate.search.util.StringHelper; import org.hibernate.search.util.logging.impl.Log; import org.hibernate.search.util.logging.impl.DefaultLogCategories; import org.hibernate.search.util.logging.impl.LoggerFactory; /** * @author Sanne Grinovero */ public final class LazyQueryState implements Closeable { private static final Log log = LoggerFactory.make(); private static final Log QUERY_LOG = LoggerFactory.make( DefaultLogCategories.QUERY ); private final Query userQuery; private final IndexSearcher searcher; private final boolean fieldSortDoTrackScores; private final boolean fieldSortDoMaxScore; private final ExtendedSearchIntegrator extendedIntegrator; private final Collection<EntityIndexBinding> targetedEntityBindings; private final QueryFilters facetingFilters; private Query rewrittenQuery; public LazyQueryState(Query userQuery, QueryFilters facetingFilters, IndexReader reader, Similarity searcherSimilarity, ExtendedSearchIntegrator extendedIntegrator, Collection<EntityIndexBinding> targetedEntityBindings, boolean fieldSortDoTrackScores, boolean fieldSortDoMaxScore) { this.userQuery = userQuery; this.facetingFilters = facetingFilters; this.fieldSortDoTrackScores = fieldSortDoTrackScores; this.fieldSortDoMaxScore = fieldSortDoMaxScore; this.searcher = new IndexSearcher( reader ); this.searcher.setSimilarity( searcherSimilarity ); this.extendedIntegrator = extendedIntegrator; this.targetedEntityBindings = targetedEntityBindings; } public boolean isFieldSortDoTrackScores() { return fieldSortDoTrackScores; } public boolean isFieldSortDoMaxScore() { return fieldSortDoMaxScore; } public Explanation explain(QueryFilters filters, int documentId) throws IOException { return searcher.explain( filters.filterOrPassthrough( rewrittenQuery() ), documentId ); } public Document doc(final int docId) throws IOException { return searcher.doc( docId ); } public void doc(final int docId, final StoredFieldVisitor fieldVisitor) throws IOException { searcher.doc( docId, fieldVisitor ); } public int maxDoc() { //pointless to try caching this one return searcher.getIndexReader().maxDoc(); } public void search(final QueryFilters filters, final Collector collector) throws IOException { validateQuery(); QUERY_LOG.executingLuceneQuery( userQuery ); searcher.search( filters.filterOrPassthrough( rewrittenQuery() ), collector ); } public IndexReader getIndexReader() { return searcher.getIndexReader(); } @Override public void close() { final IndexReader indexReader = searcher.getIndexReader(); try { MultiReaderFactory.closeReader( indexReader ); } catch (SearchException e) { log.unableToCloseSearcherDuringQuery( userQuery.toString(), e ); } } public String describeQuery() { return userQuery.toString(); } private void validateQuery() { // HSEARCH-763 we make an attempt to validate whether the queries target appropriate fields, eg // numeric fields should not be part of a text based query, instead a NumericRangeQuery should be used. // Validation is based on the original user specified query, since the rewritten query amongst other things // rewrites queries to use filters Set<String> stringEncodedFieldNames = new HashSet<>(); Set<String> numericEncodedFieldNames = new HashSet<>(); for ( EntityIndexBinding binding : targetedEntityBindings ) { IndexedTypeDescriptor indexedTypeDescriptor = extendedIntegrator.getIndexedTypeDescriptor( binding.getDocumentBuilder().getBeanClass() ); // get the field contributions from the type (class bridges) for ( FieldDescriptor fieldDescriptor : indexedTypeDescriptor.getIndexedFields() ) { putInFieldTypeBucket( stringEncodedFieldNames, numericEncodedFieldNames, fieldDescriptor ); } // get the field contributions from the properties for ( PropertyDescriptor propertyDescriptor : indexedTypeDescriptor.getIndexedProperties() ) { for ( FieldDescriptor fieldDescriptor : propertyDescriptor.getIndexedFields() ) { putInFieldTypeBucket( stringEncodedFieldNames, numericEncodedFieldNames, fieldDescriptor ); } } } FieldNameCollector.FieldCollection fieldCollection = FieldNameCollector.extractFieldNames( userQuery ); if ( !Collections.disjoint( stringEncodedFieldNames, fieldCollection.getNumericFieldNames() ) ) { Set<String> invalidFields = new HashSet<>(); for ( String numericField : fieldCollection.getNumericFieldNames() ) { if ( stringEncodedFieldNames.contains( numericField ) ) { invalidFields.add( numericField ); } } throw log.stringEncodedFieldsAreTargetedWithNumericQuery( userQuery.toString(), StringHelper.join( invalidFields, "," ) ); } if ( !Collections.disjoint( numericEncodedFieldNames, fieldCollection.getStringFieldNames() ) ) { Set<String> invalidFields = new HashSet<>(); for ( String stringField : fieldCollection.getStringFieldNames() ) { if ( numericEncodedFieldNames.contains( stringField ) ) { invalidFields.add( stringField ); } } throw log.numericEncodedFieldsAreTargetedWithStringQuery( userQuery.toString(), StringHelper.join( invalidFields, "," ) ); } } private void putInFieldTypeBucket(Set<String> stringEncodedFieldNames, Set<String> numericEncodedFieldNames, FieldDescriptor fieldDescriptor) { if ( FieldDescriptor.Type.NUMERIC.equals( fieldDescriptor.getType() ) ) { // If the property allows to index a string based null token, a non numeric query is ok // We are currently skipping this case completely from the validation. In a full on implementation we would // need to take term values into account and in the case of a string query only allow the null token. // For now we cast a net which is potentially too wide (HF) if ( !fieldDescriptor.indexNull() ) { numericEncodedFieldNames.add( fieldDescriptor.getName() ); } } else { stringEncodedFieldNames.add( fieldDescriptor.getName() ); } } private Query rewrittenQuery() throws IOException { if ( rewrittenQuery == null ) { //Apply faceting filters: final Query effectiveQuery = facetingFilters.filterOrPassthrough( userQuery ); rewrittenQuery = effectiveQuery.rewrite( searcher.getIndexReader() ); } return rewrittenQuery; } }