/* * 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.sorting; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.apache.lucene.analysis.core.KeywordTokenizerFactory; import org.apache.lucene.analysis.core.LowerCaseFilterFactory; import org.apache.lucene.analysis.core.WhitespaceTokenizerFactory; import org.apache.lucene.analysis.miscellaneous.ASCIIFoldingFilterFactory; import org.apache.lucene.search.Query; import org.apache.lucene.search.Sort; import org.apache.lucene.search.SortField; import org.hibernate.search.annotations.Analyze; import org.hibernate.search.annotations.Analyzer; import org.hibernate.search.annotations.AnalyzerDef; import org.hibernate.search.annotations.AnalyzerDefs; import org.hibernate.search.annotations.DocumentId; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.FieldBridge; import org.hibernate.search.annotations.Fields; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.annotations.IndexedEmbedded; import org.hibernate.search.annotations.SortableField; import org.hibernate.search.annotations.SortableFields; import org.hibernate.search.annotations.Store; import org.hibernate.search.annotations.TokenFilterDef; import org.hibernate.search.annotations.TokenizerDef; import org.hibernate.search.backend.spi.Work; import org.hibernate.search.backend.spi.WorkType; import org.hibernate.search.backend.spi.Worker; import org.hibernate.search.bridge.builtin.IntegerBridge; import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator; import org.hibernate.search.exception.SearchException; import org.hibernate.search.query.dsl.QueryBuilder; import org.hibernate.search.query.engine.spi.EntityInfo; import org.hibernate.search.query.engine.spi.HSQuery; 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.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.rules.ExpectedException; /** * Test to verify we apply the right sorting strategy for non-trivial mapped entities * * @author Sanne Grinovero */ public class SortingTest { private static final String SORT_TYPE_ERROR_CODE = "HSEARCH000307"; @Rule public final ExpectedException thrown = ExpectedException.none(); @Rule public SearchFactoryHolder factoryHolder = new SearchFactoryHolder( Person.class, UnsortableToy.class ); @Test public void testSortingOnNumericInt() { // Index all testData: storeTestingData( new Person( 0, 3, "Three" ), new Person( 1, 10, "Ten" ), new Person( 2, 9, "Nine" ), new Person( 3, 5, "Five" ) ); Query query = factoryHolder.getSearchFactory().buildQueryBuilder().forEntity( Person.class ).get().all().createQuery(); // Sorting Age as string: Sort sortAsString = new Sort( new SortField( "ageForStringSorting", SortField.Type.STRING ) ); assertSortedResults( query, sortAsString, 1, 0, 3, 2 ); // Also check reverse, to ensure this wasn't just luck sortAsString = new Sort( new SortField( "ageForStringSorting", SortField.Type.STRING, true ) ); assertSortedResults( query, sortAsString, 2, 3, 0, 1 ); // Sorting Age as Int (numeric): Sort sortAsInt = new Sort( new SortField( "ageForIntSorting", SortField.Type.INT ) ); assertSortedResults( query, sortAsInt, 0, 3, 2, 1 ); // Also check reverse, to ensure this wasn't just luck sortAsInt = new Sort( new SortField( "ageForIntSorting", SortField.Type.INT, true ) ); assertSortedResults( query, sortAsInt, 1, 2, 3, 0 ); } @Test public void testSortingOnString() { // Index all testData: storeTestingData( new Person( 0, 3, "Three" ), new Person( 1, 10, "Ten" ), new Person( 2, 9, "Nine" ), new Person( 3, 5, "Five" ) ); // Sorting Name Query query = factoryHolder.getSearchFactory().buildQueryBuilder().forEntity( Person.class ).get().all().createQuery(); Sort sortAsString = new Sort( new SortField( "name", SortField.Type.STRING ) ); assertSortedResults( query, sortAsString, 3, 2, 1, 0 ); } @Test @TestForIssue(jiraKey = "HSEARCH-2376") public void testSortingOnCollatedString() { // Index all testData: storeTestingData( new Person( 0, 3, "Éléonore" ), new Person( 1, 10, "édouard" ), new Person( 2, 9, "Edric" ), new Person( 3, 5, "aaron" ), new Person( 4, 7, " zach" ) ); // Sorting by collated name Query query = factoryHolder.getSearchFactory().buildQueryBuilder().forEntity( Person.class ).get().all().createQuery(); Sort sortAsString = new Sort( new SortField( "collatedName", SortField.Type.STRING ) ); assertSortedResults( query, sortAsString, 4, 3, 1, 2, 0 ); } @Test @TestForIssue(jiraKey = "HSEARCH-2376") public void testAnalyzedSortableStoredField() { Person person = new Person( 0, 3, "Éléonore" ); // Index all testData: storeTestingData( person ); /* * Check the stored value is the value *before* analysis * This check makes sens mainly because we use DocValues for sorting, and * so should field value storage. */ Query query = factoryHolder.getSearchFactory().buildQueryBuilder().forEntity( Person.class ).get().keyword() .onField( "id" ) .matching( person.id ) .createQuery(); assertStoredValueEquals( query, "collatedName", person.name ); } @Test @TestForIssue(jiraKey = "HSEARCH-2376") @Category(SkipOnElasticsearch.class) // Elasticsearch handles sorts on multi-value fields differently (the default is unclear, but it provides "sort modes" like min, max, avg, etc.) public void testSortingOnTokenizedString() { // Index all testData: storeTestingData( new Person( 0, 3, "elizabeth" ), new Person( 1, 10, "zach other" ), new Person( 2, 9, " edric" ), new Person( 3, 5, "bob" ), new Person( 4, 10, "zach Aaron" ) ); // Sorting by tokenized name: ensure only the first token is taken into account Query query = factoryHolder.getSearchFactory().buildQueryBuilder().forEntity( Person.class ).get().all().createQuery(); Sort sortAsString = new Sort( new SortField( "tokenizedName", SortField.Type.STRING ), SortField.FIELD_DOC // Stabilize the sort for the two zachs ); assertSortedResults( query, sortAsString, 3, 2, 0, 1, 4 ); } @Test public void testSortingOnEmbeddedString() { // Index all testData: storeTestingData( new Person( 0, 3, "Three", new CuddlyToy( "Hippo" ) ), new Person( 1, 10, "Ten", new CuddlyToy( "Giraffe" ) ), new Person( 2, 9, "Nine", new CuddlyToy( "Gorilla" ) ), new Person( 3, 5, "Five" , new CuddlyToy( "Alligator" ) ) ); Query query = factoryHolder.getSearchFactory().buildQueryBuilder().forEntity( Person.class ).get().all().createQuery(); Sort sortAsString = new Sort( new SortField( "favoriteCuddlyToy.type", SortField.Type.STRING ) ); assertSortedResults( query, sortAsString, 3, 1, 2, 0 ); } /** * Sortable fields within an embedded to-many association should be ignored. They should not prevent other sort * fields from working, though. */ @Test @TestForIssue(jiraKey = "HSEARCH-2000") public void testSortingForTypeWithSortableFieldWithinEmbeddedToManyAssociation() { // Index all testData: storeTestingData( new Person( 0, 3, "Three", Arrays.asList( new Friend( new CuddlyToy( "Hippo" ) ), new Friend( new CuddlyToy( "Giraffe" ) ) ) ), new Person( 1, 10, "Ten", Arrays.asList( new Friend( new CuddlyToy( "Gorilla" ) ), new Friend( new CuddlyToy( "Alligator" ) ) ) ) ); Query query = factoryHolder.getSearchFactory().buildQueryBuilder().forEntity( Person.class ).get().all().createQuery(); Sort sortAsString = new Sort( new SortField( "ageForStringSorting", SortField.Type.STRING ) ); assertSortedResults( query, sortAsString, 1, 0 ); } @Test public void testSortOnNullableNumericField() throws Exception { storeTestingData( new Person( 1, 25, "name1" ), new Person( 2, 22, null ), new Person( 3, null, "name3" ) ); HSQuery nameQuery = queryForValueNullAndSorting( "name", SortField.Type.STRING ); assertEquals( nameQuery.queryEntityInfos().size(), 1 ); HSQuery ageQuery = queryForValueNullAndSorting( "ageForNullChecks", SortField.Type.INT ); assertEquals( ageQuery.queryEntityInfos().size(), 1 ); } @Test public void testExceptionSortingStringFieldAsNumeric() throws Exception { thrown.expect( SearchException.class ); thrown.expectMessage( SORT_TYPE_ERROR_CODE ); storeTestingData( new UnsortableToy( "111", "Teddy Bear", 300L, 555 ) ); Class<?> entityType = UnsortableToy.class; ExtendedSearchIntegrator integrator = factoryHolder.getSearchFactory(); QueryBuilder queryBuilder = integrator.buildQueryBuilder().forEntity( entityType ).get(); Query query = queryBuilder .keyword() .onField( "description" ) .matching( "Teddy Bear" ) .createQuery(); Sort sort = new Sort( new SortField( "description", SortField.Type.DOUBLE ) ); HSQuery hsQuery = integrator.createHSQuery( query, entityType ); hsQuery.sort( sort ); hsQuery.queryEntityInfos().size(); } @Test public void testExceptionSortingNumericFieldWithStringType() throws Exception { thrown.expect( SearchException.class ); thrown.expectMessage( SORT_TYPE_ERROR_CODE ); storeTestingData( new UnsortableToy( "111", "Teddy Bear", 300L, 555 ) ); Class<?> entityType = UnsortableToy.class; ExtendedSearchIntegrator integrator = factoryHolder.getSearchFactory(); QueryBuilder queryBuilder = integrator.buildQueryBuilder().forEntity( entityType ).get(); Query query = queryBuilder .keyword() .onField( "description" ) .matching( "Teddy Bear" ) .createQuery(); Sort sort = new Sort( new SortField( "longValue", SortField.Type.STRING ) ); HSQuery hsQuery = integrator.createHSQuery( query, entityType ); hsQuery.sort( sort ); hsQuery.queryEntityInfos().size(); } @Test public void testExceptionSortingNumericFieldWithWrongType() throws Exception { thrown.expect( SearchException.class ); thrown.expectMessage( SORT_TYPE_ERROR_CODE ); storeTestingData( new UnsortableToy( "111", "Teddy Bear", 300L, 555 ) ); Class<?> entityType = UnsortableToy.class; ExtendedSearchIntegrator integrator = factoryHolder.getSearchFactory(); QueryBuilder queryBuilder = integrator.buildQueryBuilder().forEntity( entityType ).get(); Query query = queryBuilder .keyword() .onField( "description" ) .matching( "Teddy Bear" ) .createQuery(); Sort sort = new Sort( new SortField( "longValue", SortField.Type.INT ) ); HSQuery hsQuery = integrator.createHSQuery( query, entityType ); hsQuery.sort( sort ); hsQuery.queryEntityInfos().size(); } @Test public void testSortOnNullableNumericFieldArray() throws Exception { storeTestingData( new Person( 1, 25, "name1", 1, 2, 3 ), new Person( 2, 22, "name2", 1, null, 3 ), new Person( 3, 23, "name3", null, null, null ) ); Query rangeQuery = queryForRangeOnFieldSorted( 0, 2, "array" ); Sort sortAsInt = new Sort( new SortField( "array", SortField.Type.INT ) ); assertNumberOfResults( 2, rangeQuery, sortAsInt ); } private void assertNumberOfResults(int expectedResultsNumber, Query query, Sort sort) { ExtendedSearchIntegrator integrator = factoryHolder.getSearchFactory(); HSQuery hsQuery = integrator.createHSQuery( query, Person.class ); if ( sort != null ) { hsQuery.sort( sort ); } assertEquals( expectedResultsNumber, hsQuery.queryResultSize() ); } private Query queryForRangeOnFieldSorted(int min, int max, String fieldName) { ExtendedSearchIntegrator integrator = factoryHolder.getSearchFactory(); QueryBuilder queryBuilder = integrator.buildQueryBuilder().forEntity( Person.class ).get(); return queryBuilder .range() .onField( fieldName ) .from( min ) .to( max ) .createQuery(); } private void storeTestingData(Person... testData) { Worker worker = factoryHolder.getSearchFactory().getWorker(); TransactionContextForTest tc = new TransactionContextForTest(); for ( int i = 0; i < testData.length; i++ ) { Person p = testData[i]; worker.performWork( new Work( p, p.id, WorkType.INDEX ), tc ); } tc.end(); } private void storeTestingData(UnsortableToy... testData) { Worker worker = factoryHolder.getSearchFactory().getWorker(); TransactionContextForTest tc = new TransactionContextForTest(); for ( int i = 0; i < testData.length; i++ ) { UnsortableToy toy = testData[i]; worker.performWork( new Work( toy, toy.id, WorkType.INDEX ), tc ); } tc.end(); } private void assertSortedResults(Query query, Sort sort, int... expectedIds) { ExtendedSearchIntegrator integrator = factoryHolder.getSearchFactory(); HSQuery hsQuery = integrator.createHSQuery( query, Person.class ); if ( sort != null ) { hsQuery.sort( sort ); } assertEquals( expectedIds.length, hsQuery.queryResultSize() ); List<EntityInfo> queryEntityInfos = hsQuery.queryEntityInfos(); assertEquals( expectedIds.length, queryEntityInfos.size() ); for ( int i = 0; i < expectedIds.length; i++ ) { EntityInfo entityInfo = queryEntityInfos.get( i ); assertNotNull( entityInfo ); assertEquals( expectedIds[i], entityInfo.getId() ); } } private void assertStoredValueEquals(Query query, String fieldName, Object expectedValue) { ExtendedSearchIntegrator integrator = factoryHolder.getSearchFactory(); HSQuery hsQuery = integrator.createHSQuery( query, Person.class ); hsQuery.projection( fieldName ); assertEquals( 1, hsQuery.queryResultSize() ); List<EntityInfo> queryEntityInfos = hsQuery.queryEntityInfos(); assertEquals( 1, queryEntityInfos.size() ); assertEquals( expectedValue, queryEntityInfos.get( 0 ).getProjection()[0] ); } private HSQuery queryForValueNullAndSorting(String fieldName, SortField.Type sortType) { ExtendedSearchIntegrator integrator = factoryHolder.getSearchFactory(); QueryBuilder queryBuilder = integrator.buildQueryBuilder().forEntity( Person.class ).get(); Query query = queryBuilder .keyword() .onField( fieldName ) .matching( null ) .createQuery(); HSQuery hsQuery = integrator.createHSQuery( query, Person.class ); Sort sort = new Sort( new SortField( fieldName, sortType ) ); hsQuery.sort( sort ); return hsQuery; } @AnalyzerDefs({ @AnalyzerDef( name = Person.COLLATING_ANALYZER_NAME, tokenizer = @TokenizerDef(factory = KeywordTokenizerFactory.class), filters = { @TokenFilterDef(factory = ASCIIFoldingFilterFactory.class), @TokenFilterDef(factory = LowerCaseFilterFactory.class) } ), @AnalyzerDef( name = Person.TOKENIZING_ANALYZER_NAME, tokenizer = @TokenizerDef(factory = WhitespaceTokenizerFactory.class) ) }) @Indexed private class Person { public static final String COLLATING_ANALYZER_NAME = "org_hibernate_search_test_sorting_SortingTest_Person_collatingAnalyzer"; public static final String TOKENIZING_ANALYZER_NAME = "org_hibernate_search_test_sorting_SortingTest_Person_tokenizingAnalyzer"; @DocumentId final int id; @SortableFields({ @org.hibernate.search.annotations.SortableField(forField = "ageForStringSorting"), @org.hibernate.search.annotations.SortableField(forField = "ageForIntSorting"), @org.hibernate.search.annotations.SortableField(forField = "ageForNullChecks") }) @Fields({ @Field(name = "ageForStringSorting", store = Store.YES, analyze = Analyze.NO, bridge = @FieldBridge(impl = IntegerBridge.class) ), @Field(name = "ageForIntSorting", store = Store.YES, analyze = Analyze.NO), @Field(name = "ageForNullChecks", store = Store.YES, analyze = Analyze.NO, indexNullAs = "-1") }) final Integer age; @SortableFields({ @org.hibernate.search.annotations.SortableField(forField = "name"), @org.hibernate.search.annotations.SortableField(forField = "collatedName"), @org.hibernate.search.annotations.SortableField(forField = "tokenizedName") }) @Fields({ @Field(name = "name", store = Store.YES, analyze = Analyze.NO, indexNullAs = Field.DEFAULT_NULL_TOKEN), @Field(name = "collatedName", store = Store.YES, analyzer = @Analyzer(definition = COLLATING_ANALYZER_NAME)), @Field(name = "tokenizedName", store = Store.YES, analyzer = @Analyzer(definition = TOKENIZING_ANALYZER_NAME)) }) final String name; @IndexedEmbedded final CuddlyToy favoriteCuddlyToy; @IndexedEmbedded final List<Friend> friends; @Field @SortableField @IndexedEmbedded//TODO improve error message when this is missing Integer[] array; Person(int id, Integer age, String name, CuddlyToy favoriteCuddlyToy) { this.id = id; this.age = age; this.name = name; this.favoriteCuddlyToy = favoriteCuddlyToy; this.friends = new ArrayList<Friend>(); } Person(int id, Integer age, String name, List<Friend> friends) { this.id = id; this.age = age; this.name = name; this.favoriteCuddlyToy = null; this.friends = friends; } Person(int id, Integer age, String name, Integer... arrayItems) { this.id = id; this.age = age; this.name = name; this.array = arrayItems; this.favoriteCuddlyToy = null; this.friends = new ArrayList<Friend>(); } } private static class Friend { @IndexedEmbedded final CuddlyToy favoriteCuddlyToy; public Friend(CuddlyToy favoriteCuddlyToy) { this.favoriteCuddlyToy = favoriteCuddlyToy; } } private class CuddlyToy { @org.hibernate.search.annotations.SortableField @Field(store = Store.YES, analyze = Analyze.NO, indexNullAs = Field.DEFAULT_NULL_TOKEN) String type; public CuddlyToy(String type) { this.type = type; } } @Indexed private class UnsortableToy { @DocumentId String id; @org.hibernate.search.annotations.SortableField @Field(store = Store.YES, analyze = Analyze.YES) String description; @org.hibernate.search.annotations.SortableField @Field(store = Store.YES, analyze = Analyze.NO) Long longValue; @org.hibernate.search.annotations.SortableField @Field(store = Store.YES, analyze = Analyze.NO) Integer integerValue; public UnsortableToy(String id, String description, Long longValue, Integer integerValue) { this.id = id; this.description = description; this.longValue = longValue; this.integerValue = integerValue; } } }