/* * 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.query.sorting; import static org.fest.assertions.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.util.Calendar; import java.util.List; import java.util.Locale; import java.util.TimeZone; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import org.apache.lucene.document.Document; import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.NumericDocValues; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.FieldComparator; import org.apache.lucene.search.FieldComparatorSource; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.SimpleFieldComparator; import org.apache.lucene.search.Sort; import org.apache.lucene.search.SortField; import org.hibernate.Transaction; import org.hibernate.resource.transaction.spi.TransactionStatus; import org.hibernate.search.FullTextQuery; import org.hibernate.search.FullTextSession; import org.hibernate.search.Search; import org.hibernate.search.annotations.Analyze; import org.hibernate.search.annotations.ClassBridge; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.bridge.LuceneOptions; import org.hibernate.search.bridge.MetadataProvidingFieldBridge; import org.hibernate.search.bridge.spi.FieldMetadataBuilder; import org.hibernate.search.bridge.spi.FieldType; import org.hibernate.search.test.SearchTestBase; import org.hibernate.search.test.query.Author; import org.hibernate.search.test.query.Book; import org.hibernate.search.testsupport.TestConstants; import org.hibernate.search.testsupport.TestForIssue; import org.hibernate.search.testsupport.junit.SkipOnElasticsearch; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.experimental.categories.Category; /** * @author Hardy Ferentschik */ public class SortTest extends SearchTestBase { private static FullTextSession fullTextSession; private static QueryParser queryParser; @Override @Before public void setUp() throws Exception { super.setUp(); fullTextSession = Search.getFullTextSession( openSession() ); queryParser = new QueryParser( "title", TestConstants.stopAnalyzer ); createTestBooks(); createTestNumbers(); createTestContractors(); } @Override @After public void tearDown() throws Exception { // check for ongoing transaction which is an indicator that something went wrong // don't call the cleanup methods in this case. Otherwise the original error get swallowed if ( fullTextSession.getTransaction().getStatus() != TransactionStatus.ACTIVE ) { deleteTestBooks(); deleteTestNumbers(); deleteTestContractors(); fullTextSession.close(); } super.tearDown(); } @SuppressWarnings("unchecked") @Test public void testResultOrderedByIdAsString() throws Exception { Transaction tx = fullTextSession.beginTransaction(); Query query = queryParser.parse( "summary:lucene" ); FullTextQuery hibQuery = fullTextSession.createFullTextQuery( query, Book.class ); Sort sort = new Sort( new SortField( "id", SortField.Type.STRING, false ) ); hibQuery.setSort( sort ); List<Book> result = hibQuery.list(); assertNotNull( result ); assertThat( result ).onProperty( "id" ).containsExactly( 1, 10, 2, 3 ); tx.commit(); } @SuppressWarnings("unchecked") @Test public void testResultOrderedByIdAsLong() throws Exception { Transaction tx = fullTextSession.beginTransaction(); Query query = queryParser.parse( "summary:lucene" ); FullTextQuery hibQuery = fullTextSession.createFullTextQuery( query, Book.class ); Sort sort = new Sort( new SortField( "id_forIntegerSort", SortField.Type.INT, false ) ); hibQuery.setSort( sort ); List<Book> result = hibQuery.list(); assertNotNull( result ); assertThat( result ).onProperty( "id" ).containsExactly( 1, 2, 3, 10 ); tx.commit(); } @SuppressWarnings("unchecked") @Test @Category(SkipOnElasticsearch.class) public void testResultOrderedByIdAlteringSortStyle() throws Exception { Transaction tx = fullTextSession.beginTransaction(); Query query = queryParser.parse( "summary:lucene" ); FullTextQuery hibQuery = fullTextSession.createFullTextQuery( query, Book.class ); hibQuery.setSort( new Sort( new SortField( "id", SortField.Type.STRING, false ) ) ); List<Book> result = hibQuery.list(); assertThat( result ).onProperty( "id" ).containsExactly( 1, 10, 2, 3 ); hibQuery.setSort( new Sort( new SortField( "id_forIntegerSort", SortField.Type.INT, false ) ) ); result = hibQuery.list(); assertThat( result ).onProperty( "id" ).containsExactly( 1, 2, 3, 10 ); hibQuery.setSort( new Sort( new SortField( "id", SortField.Type.STRING, false ) ) ); result = hibQuery.list(); assertThat( result ).onProperty( "id" ).containsExactly( 1, 10, 2, 3 ); tx.commit(); } @SuppressWarnings("unchecked") @Test public void testResultOrderedBySummaryStringAscending() throws Exception { Transaction tx = fullTextSession.beginTransaction(); // order by summary Query query = queryParser.parse( "summary:lucene OR summary:action" ); FullTextQuery hibQuery = fullTextSession.createFullTextQuery( query, Book.class ); Sort sort = new Sort( new SortField( "summary_forSort", SortField.Type.STRING ) ); //ASC hibQuery.setSort( sort ); List<Book> result = hibQuery.list(); assertNotNull( result ); assertEquals( "Wrong number of test results.", 5, result.size() ); assertEquals( "Groovy in Action", result.get( 0 ).getSummary() ); tx.commit(); } @SuppressWarnings("unchecked") @Test public void testResultOrderedBySummaryStringDescending() throws Exception { Transaction tx = fullTextSession.beginTransaction(); // order by summary backwards Query query = queryParser.parse( "summary:lucene OR summary:action" ); FullTextQuery hibQuery = fullTextSession.createFullTextQuery( query, Book.class ); Sort sort = new Sort( new SortField( "summary_forSort", SortField.Type.STRING, true ) ); //DESC hibQuery.setSort( sort ); List<Book> result = hibQuery.list(); assertNotNull( result ); assertEquals( "Wrong number of test results.", 5, result.size() ); assertEquals( "Hibernate & Lucene", result.get( 0 ).getSummary() ); tx.commit(); } @SuppressWarnings("unchecked") @Test public void testResultOrderedByDateDescending() throws Exception { Transaction tx = fullTextSession.beginTransaction(); // order by date backwards Query query = queryParser.parse( "summary:lucene OR summary:action" ); FullTextQuery hibQuery = fullTextSession.createFullTextQuery( query, Book.class ); Sort sort = new Sort( new SortField( "publicationDate", SortField.Type.STRING, true ) ); //DESC hibQuery.setSort( sort ); List<Book> result = hibQuery.list(); assertNotNull( result ); assertThat( result ).onProperty( "id" ).containsExactly( 4, 10, 3, 2, 1 ); assertEquals( "Groovy in Action", result.get( 0 ).getSummary() ); tx.commit(); } @SuppressWarnings("unchecked") @Test @Category(SkipOnElasticsearch.class) public void testCustomFieldComparatorAscendingSort() { Transaction tx = fullTextSession.beginTransaction(); Query query = new MatchAllDocsQuery(); FullTextQuery hibQuery = fullTextSession.createFullTextQuery( query, NumberHolder.class ); Sort sort = new Sort( new SortField( "sum", new SumFieldComparatorSource() ) ); hibQuery.setSort( sort ); List<NumberHolder> result = hibQuery.list(); assertNotNull( result ); assertEquals( "Wrong number of test results.", 4, result.size() ); int previousSum = 0; for ( NumberHolder n : result ) { assertTrue( "Documents should be ordered by increasing sum", previousSum < n.getSum() ); previousSum = n.getSum(); } tx.commit(); } @SuppressWarnings("unchecked") @Test @Category(SkipOnElasticsearch.class) public void testCustomFieldComparatorDescendingSort() { Transaction tx = fullTextSession.beginTransaction(); Query query = new MatchAllDocsQuery(); FullTextQuery hibQuery = fullTextSession.createFullTextQuery( query, NumberHolder.class ); Sort sort = new Sort( new SortField( "sum", new SumFieldComparatorSource(), true ) ); hibQuery.setSort( sort ); List<NumberHolder> result = hibQuery.list(); assertNotNull( result ); assertEquals( "Wrong number of test results.", 4, result.size() ); int previousSum = 100; for ( NumberHolder n : result ) { assertTrue( "Documents should be ordered by decreasing sum", previousSum > n.getSum() ); previousSum = n.getSum(); } tx.commit(); } @SuppressWarnings("unchecked") @Test public void testResultOrderedByDocId() throws Exception { Transaction tx = fullTextSession.beginTransaction(); Query query = queryParser.parse( "summary:lucene" ); FullTextQuery hibQuery = fullTextSession.createFullTextQuery( query, Book.class ); Sort sort = new Sort( new SortField( null, SortField.Type.DOC, false ) ); hibQuery.setSort( sort ); List<Book> result = hibQuery.list(); assertNotNull( result ); assertThat( result ).onProperty( "id" ).containsOnly( 1, 2, 3, 10 ); tx.commit(); } @Test @SuppressWarnings("unchecked") public void testResultOrderedByEmbeddedAuthorNameAscending() throws Exception { Transaction tx = fullTextSession.beginTransaction(); // order by summary Query query = queryParser.parse( "summary:lucene OR summary:action" ); FullTextQuery hibQuery = fullTextSession.createFullTextQuery( query, Book.class ); Sort sort = new Sort( new SortField( "mainAuthor.name", SortField.Type.STRING ) ); //ASC hibQuery.setSort( sort ); List<Book> result = hibQuery.list(); assertNotNull( result ); assertThat( result ).onProperty( "id" ).containsExactly( 2, 1, 3, 4, 10 ); tx.commit(); } @Test public void testSortingByMultipleFields() throws Exception { Transaction tx = fullTextSession.beginTransaction(); Query query = queryParser.parse( "name:Bill OR name:Barny OR name:Bart" ); FullTextQuery hibQuery = fullTextSession.createFullTextQuery( query, BrickLayer.class ); Sort sort = new Sort( new SortField( "sortLastName", SortField.Type.STRING ), new SortField( "sortName", SortField.Type.STRING ) ); hibQuery.setSort( sort ); @SuppressWarnings("unchecked") List<Book> result = hibQuery.list(); assertNotNull( result ); assertThat( result ).onProperty( "lastName" ) .containsExactly( "Higgins", "Higgins", "Johnson", "Johnson" ); assertThat( result ).onProperty( "name" ) .containsExactly( "Barny the brick layer", "Bart the brick layer", "Barny the brick layer", "Bill the brick layer" ); tx.commit(); } @SuppressWarnings("unchecked") @Test @TestForIssue(jiraKey = "HSEARCH-2287") public void testChangingSortOrder() throws Exception { Transaction tx = fullTextSession.beginTransaction(); Query query = queryParser.parse( "summary:lucene" ); FullTextQuery hibQuery = fullTextSession.createFullTextQuery( query, Book.class ); hibQuery.setSort( new Sort( new SortField( "id_forIntegerSort", SortField.Type.INT, false ) ) ); List<Book> result = hibQuery.list(); assertThat( result ).onProperty( "id" ).containsExactly( 1, 2, 3, 10 ); hibQuery.setSort( new Sort( new SortField( "id_forIntegerSort", SortField.Type.INT, true ) ) ); result = hibQuery.list(); assertThat( result ).onProperty( "id" ).containsExactly( 10, 3, 2, 1 ); tx.commit(); } /** * Helper method creating three books with the same title and summary. * When searching for these books the results should be returned in the order * they got added to the index. */ private void createTestBooks() { Transaction tx = fullTextSession.beginTransaction(); Calendar cal = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ), Locale.ROOT ); cal.set( 2007, Calendar.JULY, 25, 11, 20, 30 ); Author author = new Author( "Bob" ); fullTextSession.save( author ); Book book = new Book( 1, "Hibernate & Lucene", "This is a test book." ); book.setPublicationDate( cal.getTime() ); book.setMainAuthor( author ); fullTextSession.save( book ); author = new Author( "Anthony" ); fullTextSession.save( author ); cal.add( Calendar.SECOND, 1 ); book = new Book( 2, "Hibernate & Lucene", "This is a test book." ); book.setMainAuthor( author ); book.setPublicationDate( cal.getTime() ); fullTextSession.save( book ); author = new Author( "Calvin" ); fullTextSession.save( author ); cal.add( Calendar.SECOND, 1 ); book = new Book( 3, "Hibernate & Lucene", "This is a test book." ); book.setMainAuthor( author ); book.setPublicationDate( cal.getTime() ); fullTextSession.save( book ); author = new Author( "Ernst" ); fullTextSession.save( author ); cal.add( Calendar.SECOND, 1 ); book = new Book( 10, "Hibernate & Lucene", "This is a test book." ); book.setMainAuthor( author ); book.setPublicationDate( cal.getTime() ); fullTextSession.save( book ); author = new Author( "Dennis" ); fullTextSession.save( author ); cal.add( Calendar.SECOND, 1 ); book = new Book( 4, "Groovy in Action", "The bible of Groovy" ); book.setMainAuthor( author ); book.setPublicationDate( cal.getTime() ); fullTextSession.save( book ); tx.commit(); fullTextSession.clear(); } /** * Helper method creating test data for number holder. */ private void createTestNumbers() { Transaction tx = fullTextSession.beginTransaction(); NumberHolder holder = new NumberHolder( 1, 1 ); fullTextSession.save( holder ); holder = new NumberHolder( 1, 10 ); fullTextSession.save( holder ); holder = new NumberHolder( 1, 5 ); fullTextSession.save( holder ); holder = new NumberHolder( 3, 2 ); fullTextSession.save( holder ); tx.commit(); fullTextSession.clear(); } private void createTestContractors() { Transaction tx = fullTextSession.beginTransaction(); fullTextSession.save( new BrickLayer( 2, "Bill the brick layer", "Johnson" ) ); fullTextSession.save( new BrickLayer( 4, "Barny the brick layer", "Johnson" ) ); fullTextSession.save( new BrickLayer( 5, "Bart the brick layer", "Higgins" ) ); fullTextSession.save( new BrickLayer( 6, "Barny the brick layer", "Higgins" ) ); tx.commit(); fullTextSession.clear(); } private void deleteTestBooks() { Transaction tx = fullTextSession.beginTransaction(); fullTextSession.createQuery( "delete " + Book.class.getName() ).executeUpdate(); tx.commit(); fullTextSession.clear(); } private void deleteTestNumbers() { Transaction tx = fullTextSession.beginTransaction(); fullTextSession.createQuery( "delete " + NumberHolder.class.getName() ).executeUpdate(); tx.commit(); fullTextSession.clear(); } private void deleteTestContractors() { Transaction tx = fullTextSession.beginTransaction(); fullTextSession.createQuery( "delete " + BrickLayer.class.getName() ).executeUpdate(); tx.commit(); fullTextSession.clear(); } @Override public Class<?>[] getAnnotatedClasses() { return new Class[] { Book.class, Author.class, NumberHolder.class, BrickLayer.class }; } @Entity @Indexed @ClassBridge(impl = SortFieldCreatingClassBridge.class) public static class NumberHolder { @Id @GeneratedValue int id; @Field(analyze = Analyze.NO) int num1; @Field(analyze = Analyze.NO) int num2; public NumberHolder(int num1, int num2) { this.num1 = num1; this.num2 = num2; } public NumberHolder() { } public int getSum() { return num1 + num2; } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append( "NumbersHolder" ); sb.append( "{id=" ).append( id ); sb.append( ", num1=" ).append( num1 ); sb.append( ", num2=" ).append( num2 ); sb.append( '}' ); return sb.toString(); } } /** * Class bridge creating doc value fields for custom sorting. * * @author Gunnar Morling */ public static class SortFieldCreatingClassBridge implements MetadataProvidingFieldBridge { @Override public void set(String name, Object value, Document document, LuceneOptions luceneOptions) { NumberHolder numberHolder = (NumberHolder) value; luceneOptions.addNumericDocValuesFieldToDocument( "num1", numberHolder.num1, document ); luceneOptions.addNumericDocValuesFieldToDocument( "num2", numberHolder.num2, document ); } @Override public void configureFieldMetadata(String name, FieldMetadataBuilder builder) { builder.field( "sum", FieldType.INTEGER ) .sortable( true ); } } public static class SumFieldComparatorSource extends FieldComparatorSource { @Override public FieldComparator<?> newComparator(String fieldName, int numHits, int sortPos, boolean reversed) throws IOException { return new SumFieldComparator( numHits, "num1", "num2" ); } } public static class SumFieldComparator extends SimpleFieldComparator<Integer> { private final String field1; private final String field2; private final int[] field1Values; private final int[] field2Values; private NumericDocValues currentReaderValuesField1; private NumericDocValues currentReaderValuesField2; private int bottom; private Integer topValue; public SumFieldComparator(int numHits, String field1, String field2) { this.field1 = field1; this.field2 = field2; this.field1Values = new int[numHits]; this.field2Values = new int[numHits]; } @Override public int compare(int slot1, int slot2) { final int v1 = field1Values[slot1] + field2Values[slot1]; final int v2 = field1Values[slot2] + field2Values[slot2]; return compareValues( v1, v2 ); } private int compareValues(int v1, int v2) { if ( v1 > v2 ) { return 1; } else if ( v1 < v2 ) { return -1; } else { return 0; } } @Override public int compareBottom(int doc) { int v = (int) ( currentReaderValuesField1.get( doc ) + currentReaderValuesField2.get( doc ) ); return compareValues( bottom, v ); } @Override public int compareTop(int doc) throws IOException { return topValue - field1Values[doc] - field2Values[doc]; } @Override public void copy(int slot, int doc) { int v1 = (int) currentReaderValuesField1.get( doc ); field1Values[slot] = v1; int v2 = (int) currentReaderValuesField2.get( doc ); field2Values[slot] = v2; } @Override protected void doSetNextReader(LeafReaderContext context) throws IOException { final LeafReader reader = context.reader(); currentReaderValuesField1 = reader.getNumericDocValues( field1 ); currentReaderValuesField2 = reader.getNumericDocValues( field2 ); } @Override public void setBottom(final int bottom) { this.bottom = field1Values[bottom] + field2Values[bottom]; } @Override public void setTopValue(Integer value) { this.topValue = value; } @Override public Integer value(int slot) { return field1Values[slot] + field2Values[slot]; } } }