/* * 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.test.filters; import static org.fest.assertions.Assertions.assertThat; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.apache.lucene.index.CompositeReaderContext; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.MultiReader; import org.apache.lucene.index.Terms; import org.apache.lucene.index.TermsEnum; import org.apache.lucene.search.BulkScorer; 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.BytesRef; import org.hibernate.search.annotations.DocumentId; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.backend.spi.Work; import org.hibernate.search.backend.spi.WorkType; import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator; import org.hibernate.search.query.dsl.QueryBuilder; import org.hibernate.search.query.engine.spi.EntityInfo; import org.hibernate.search.testsupport.TestForIssue; import org.hibernate.search.testsupport.junit.SearchFactoryHolder; import org.hibernate.search.testsupport.junit.SkipOnElasticsearch; import org.hibernate.search.testsupport.setup.TransactionContextForTest; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; /** * Verified queries don't get stale IndexReader instances after a change is applied. * Note that queries operate on per-segment sub-readers, while we usually expose * top level (recursive) global IndexReader views: this usually should not affect * their usage but is relevant to how we test them. * * @author Sanne Grinovero (C) 2013 Red Hat Inc. * @since 4.2 */ @TestForIssue(jiraKey = "HSEARCH-1230") @Category(SkipOnElasticsearch.class) // IndexReaders are specific to Lucene public class FreshReadersProvidedTest { @Rule public SearchFactoryHolder sfHolder = new SearchFactoryHolder( Guest.class ); @Test public void filtersTest() { ExtendedSearchIntegrator searchFactory = sfHolder.getSearchFactory(); Assert.assertNotNull( searchFactory.getIndexManagerHolder().getIndexManager( "guests" ) ); { // Store guest "Thorin Oakenshield" in the index Guest lastDwarf = new Guest(); lastDwarf.id = 13l; lastDwarf.name = "Thorin Oakenshield"; Work work = new Work( lastDwarf, lastDwarf.id, WorkType.ADD, false ); TransactionContextForTest tc = new TransactionContextForTest(); searchFactory.getWorker().performWork( work, tc ); tc.end(); } QueryBuilder guestQueryBuilder = searchFactory.buildQueryBuilder() .forEntity( Guest.class ) .get(); Query queryAllGuests = guestQueryBuilder.all().createQuery(); List<EntityInfo> queryEntityInfos = searchFactory.createHSQuery( queryAllGuests, Guest.class ) .queryEntityInfos(); Assert.assertEquals( 1, queryEntityInfos.size() ); Assert.assertEquals( 13L, queryEntityInfos.get( 0 ).getId() ); RecordingQueryWrapper recordingWrapper = new RecordingQueryWrapper( queryAllGuests, "name" ); List<EntityInfo> recordingWrapperEntityInfos = searchFactory.createHSQuery( recordingWrapper, Guest.class ) .queryEntityInfos(); checkQueryInspectedAllSegments( recordingWrapper ); expectedTermsForQuery( recordingWrapper, "thorin", "oakenshield" ); Assert.assertEquals( 1, recordingWrapperEntityInfos.size() ); Assert.assertEquals( 13L, recordingWrapperEntityInfos.get( 0 ).getId() ); { // Store guest "Balin" Guest balin = new Guest(); balin.id = 7l; balin.name = "Balin"; Work work = new Work( balin, balin.id, WorkType.ADD, false ); TransactionContextForTest tc = new TransactionContextForTest(); searchFactory.getWorker().performWork( work, tc ); tc.end(); } List<EntityInfo> queryEntityInfosAgain = searchFactory.createHSQuery( queryAllGuests, Guest.class ) .queryEntityInfos(); Assert.assertEquals( 2, queryEntityInfosAgain.size() ); Assert.assertEquals( 13L, queryEntityInfosAgain.get( 0 ).getId() ); Assert.assertEquals( 7L, queryEntityInfosAgain.get( 1 ).getId() ); RecordingQueryWrapper secondRecordingWrapper = new RecordingQueryWrapper( queryAllGuests, "name" ); List<EntityInfo> secondRecordingWrapperEntityInfos = searchFactory.createHSQuery( secondRecordingWrapper, Guest.class ) .queryEntityInfos(); checkQueryInspectedAllSegments( secondRecordingWrapper ); expectedTermsForQuery( secondRecordingWrapper, "thorin", "oakenshield", "balin" ); Assert.assertEquals( 2, secondRecordingWrapperEntityInfos.size() ); Assert.assertEquals( 13L, secondRecordingWrapperEntityInfos.get( 0 ).getId() ); Assert.assertEquals( 7L, secondRecordingWrapperEntityInfos.get( 1 ).getId() ); } private void expectedTermsForQuery(RecordingQueryWrapper recordingWrapper, String... term) { Assert.assertEquals( term.length, recordingWrapper.seenTerms.size() ); assertThat( recordingWrapper.seenTerms ).as( "seen terms" ).contains( (Object[]) term ); } /** * Verifies that the current {@link RecordingQueryWrapper} has been fed all the same sub-readers * which would be obtained from a freshly checked out IndexReader. * * @param recordingWrapper test {@link RecordingQueryWrapper} instance */ private void checkQueryInspectedAllSegments(RecordingQueryWrapper recordingWrapper) { ExtendedSearchIntegrator searchFactory = sfHolder.getSearchFactory(); IndexReader currentIndexReader = searchFactory.getIndexReaderAccessor().open( Guest.class ); try { List<IndexReader> allSubReaders = getSubIndexReaders( (MultiReader) currentIndexReader ); assertThat( recordingWrapper.visitedReaders ).as( "visited readers" ) .contains( allSubReaders.toArray() ); } finally { searchFactory.getIndexReaderAccessor().close( currentIndexReader ); } } public static List<IndexReader> getSubIndexReaders(MultiReader compositeReader) { CompositeReaderContext compositeReaderContext = compositeReader.getContext(); ArrayList<IndexReader> segmentReaders = new ArrayList<IndexReader>( 20 ); for ( LeafReaderContext readerContext : compositeReaderContext.leaves() ) { segmentReaders.add( readerContext.reader() ); } return segmentReaders; } /** * Scorers are created once for each segment, each time being passed a different IndexReader. * These IndexReader instances are "subreaders", not the global kind representing * the whole index. */ private static class RecordingQueryWrapper extends Query { final Query delegate; final List<IndexReader> visitedReaders = new ArrayList<IndexReader>(); final List<String> seenTerms = new ArrayList<String>(); final String fieldName; public RecordingQueryWrapper(Query delegate, String fieldName) { this.delegate = delegate; this.fieldName = fieldName; } @Override public Weight createWeight(IndexSearcher searcher, boolean needsScores) throws IOException { Weight delegateWeight = delegate.createWeight( searcher, needsScores ); return new ForwardingWeight( this, delegateWeight ) { @Override public Scorer scorer(LeafReaderContext context) throws IOException { record( context ); return super.scorer( context ); } @Override public BulkScorer bulkScorer(LeafReaderContext context) throws IOException { record( context ); return super.bulkScorer( context ); } }; } private void record(LeafReaderContext context) throws IOException { final LeafReader reader = context.reader(); this.visitedReaders.add( reader ); Terms terms = reader.terms( fieldName ); TermsEnum iterator = terms.iterator(); BytesRef next = iterator.next(); while ( next != null ) { seenTerms.add( next.utf8ToString() ); next = iterator.next(); } } @Override public String toString(String fieldName) { return new StringBuilder( "RecordingQueryWrapper(" ) .append( this.delegate ) .append( ", " ) .append( this.fieldName ) .append( ")" ) .toString(); } } @Indexed(index = "guests") public static final class Guest { private long id; private String name; @DocumentId public long getId() { return id; } public void setId(long id) { this.id = id; } @Field public String getName() { return name; } public void setName(String name) { this.name = name; } } }