Java tutorial
/* * 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.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.setup.TransactionContextForTest; import org.junit.Rule; import org.junit.Test; 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")); // Non sorted, expect results in indexing order: Query query = factoryHolder.getSearchFactory().buildQueryBuilder().forEntity(Person.class).get().all() .createQuery(); assertSortedResults(query, null, 0, 1, 2, 3); // Sorting Age as string: Sort sortAsString = new Sort(new SortField("ageForStringSorting", SortField.Type.STRING)); assertSortedResults(query, sortAsString, 1, 0, 3, 2); // Sorting Age as Int (numeric): Sort sortAsInt = new Sort(new SortField("ageForIntSorting", SortField.Type.INT)); assertSortedResults(query, sortAsInt, 0, 3, 2, 1); } @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, "lonore"), 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, "lonore"); // 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") 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 = "collatingAnalyzer", tokenizer = @TokenizerDef(factory = KeywordTokenizerFactory.class), filters = { @TokenFilterDef(factory = ASCIIFoldingFilterFactory.class), @TokenFilterDef(factory = LowerCaseFilterFactory.class) }), @AnalyzerDef(name = "tokenizingAnalyzer", tokenizer = @TokenizerDef(factory = WhitespaceTokenizerFactory.class)) }) @Indexed private class Person { @DocumentId final int id; @SortableFields({ @org.hibernate.search.annotations.SortableField(forField = "ageForStringSorting"), @org.hibernate.search.annotations.SortableField(forField = "ageForIntSorting") }) @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 = "collatingAnalyzer")), @Field(name = "tokenizedName", store = Store.YES, analyzer = @Analyzer(definition = "tokenizingAnalyzer")) }) final String name; @IndexedEmbedded final CuddlyToy favoriteCuddlyToy; @IndexedEmbedded final List<Friend> friends; @Field @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; } } }