/* * Hibernate, Relational Persistence for Idiomatic Java * * Copyright (c) 2010, 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.test.query; import java.io.IOException; import java.util.Calendar; import java.util.List; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import org.apache.lucene.index.IndexReader; import org.apache.lucene.queryParser.QueryParser; import org.apache.lucene.search.FieldCache; 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.Sort; import org.apache.lucene.search.SortField; import org.hibernate.Transaction; 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.Field; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.test.SearchTestCase; import org.hibernate.search.test.TestConstants; /** * @author Hardy Ferentschik */ public class SortTest extends SearchTestCase { private static FullTextSession fullTextSession; private static QueryParser queryParser; public void setUp() throws Exception { super.setUp(); fullTextSession = Search.getFullTextSession( openSession() ); queryParser = new QueryParser( TestConstants.getTargetLuceneVersion(), "title", TestConstants.stopAnalyzer ); createTestBooks(); createTestNumbers(); } 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().isActive() ) { deleteTestBooks(); deleteTestNumbers(); fullTextSession.close(); } super.tearDown(); } @SuppressWarnings("unchecked") public void testResultOrderedById() 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.STRING, false ) ); hibQuery.setSort( sort ); List<Book> result = hibQuery.list(); assertNotNull( result ); assertEquals( "Wrong number of test results.", 3, result.size() ); int id = 1; for ( Book b : result ) { assertEquals( "Expected another id", Integer.valueOf( id ), b.getId() ); id++; } tx.commit(); } @SuppressWarnings("unchecked") 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.STRING ) ); //ASC hibQuery.setSort( sort ); List<Book> result = hibQuery.list(); assertNotNull( result ); assertEquals( "Wrong number of test results.", 4, result.size() ); assertEquals( "Groovy in Action", result.get( 0 ).getSummary() ); tx.commit(); } @SuppressWarnings("unchecked") 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.STRING, true ) ); //DESC hibQuery.setSort( sort ); List<Book> result = hibQuery.list(); assertNotNull( result ); assertEquals( "Wrong number of test results.", 4, result.size() ); assertEquals( "Hibernate & Lucene", result.get( 0 ).getSummary() ); tx.commit(); } @SuppressWarnings("unchecked") 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.STRING, true ) ); //DESC hibQuery.setSort( sort ); List<Book> result = hibQuery.list(); assertNotNull( result ); assertEquals( "Wrong number of test results.", 4, result.size() ); for ( Book book : result ) { System.out.println( book.getSummary() + " : " + book.getPublicationDate() ); } assertEquals( "Groovy in Action", result.get( 0 ).getSummary() ); tx.commit(); } @SuppressWarnings("unchecked") 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") 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(); } /** * 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(); cal.set( 2007, Calendar.JULY, 25, 11, 20, 30 ); Book book = new Book( 1, "Hibernate & Lucene", "This is a test book." ); book.setPublicationDate( cal.getTime() ); fullTextSession.save( book ); cal.add( Calendar.SECOND, 1 ); book = new Book( 2, "Hibernate & Lucene", "This is a test book." ); book.setPublicationDate( cal.getTime() ); fullTextSession.save( book ); cal.add( Calendar.SECOND, 1 ); book = new Book( 3, "Hibernate & Lucene", "This is a test book." ); book.setPublicationDate( cal.getTime() ); fullTextSession.save( book ); cal.add( Calendar.SECOND, 1 ); book = new Book( 4, "Groovy in Action", "The bible of Groovy" ); 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 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(); } protected Class<?>[] getAnnotatedClasses() { return new Class[] { Book.class, Author.class, NumberHolder.class }; } @Entity @Indexed 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 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(); } @SuppressWarnings("unused") private NumberHolder() { } } 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 FieldComparator<Integer> { private final String field1; private final String field2; private final int[] field1Values; private final int[] field2Values; private int[] currentReaderValuesField1; private int[] currentReaderValuesField2; private int bottom; 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 = currentReaderValuesField1[doc] + currentReaderValuesField2[doc]; return compareValues( bottom, v ); } @Override public void copy(int slot, int doc) { int v1 = currentReaderValuesField1[doc]; field1Values[slot] = v1; int v2 = currentReaderValuesField2[doc]; field2Values[slot] = v2; } @Override public void setNextReader(IndexReader reader, int docBase) throws IOException { currentReaderValuesField1 = FieldCache.DEFAULT .getInts( reader, field1, FieldCache.DEFAULT_INT_PARSER, false ); currentReaderValuesField2 = FieldCache.DEFAULT .getInts( reader, field2, FieldCache.DEFAULT_INT_PARSER, false ); } @Override public void setBottom(final int bottom) { this.bottom = field1Values[bottom] + field2Values[bottom]; } @Override public Integer value(int slot) { return field1Values[slot] + field2Values[slot]; } } }