/* * 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.dsl; import static org.junit.Assert.assertThat; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.apache.lucene.document.Document; import org.apache.lucene.search.Query; import org.apache.lucene.search.Sort; import org.apache.lucene.search.SortField; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import org.hibernate.search.annotations.DocumentId; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.FieldBridge; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.annotations.IndexedEmbedded; import org.hibernate.search.annotations.SortableField; import org.hibernate.search.annotations.Spatial; import org.hibernate.search.annotations.SpatialMode; import org.hibernate.search.backend.spi.Work; import org.hibernate.search.backend.spi.WorkType; import org.hibernate.search.bridge.LuceneOptions; import org.hibernate.search.bridge.MetadataProvidingFieldBridge; import org.hibernate.search.bridge.StringBridge; import org.hibernate.search.bridge.spi.FieldMetadataBuilder; import org.hibernate.search.bridge.spi.FieldType; 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.spatial.Coordinates; import org.hibernate.search.testsupport.TestForIssue; import org.hibernate.search.testsupport.junit.SearchFactoryHolder; import org.hibernate.search.testsupport.setup.TransactionContextForTest; import org.junit.Before; import org.junit.Rule; import org.junit.Test; /** * @author Emmanuel Bernard * @author Yoann Rodiere */ @TestForIssue(jiraKey = "HSEARCH-1872") public class SortDSLTest { @Rule public SearchFactoryHolder sfHolder = new SearchFactoryHolder( IndexedEntry.class ); @Before public void prepareTestData() { IndexedEntry entry0 = new IndexedEntry( 0 ) .setTextField( "infrequent1 infrequent2 infrequent1" + " inMultipleDocsWithUniqueScores" + " inMultipleDocsWithVariousScores inMultipleDocsWithVariousScores" ) .setNonUniqueIntgerField( 1 ) .setUniqueIntegerField( 3 ) .setUniqueDoubleField( 2d ) /* * Distances: * - to (24,32) with arc method: 10.16km * - to (24,32) with plane method: 11.12km (exact same as entry 1) */ .setLocation( 24.0d, 31.9d ); IndexedEntry entry1 = new IndexedEntry( 1 ) .setTextField( "inMultipleDocsWithUniqueScores inMultipleDocsWithUniqueScores inMultipleDocsWithUniqueScores" + " inMultipleDocsWithVariousScores" ) .setNonUniqueIntgerField( 2 ) .setUniqueIntegerField( 1 ) .setUniqueDoubleField( 1d ) /* * Distances: * - to (24,32) with arc method: 11.12km * - to (24,32) with plane method: 11.12km (exact same as entry 0) */ .setLocation( 23.9d, 32.0d ) .setPrevious( entry0 ); IndexedEntry entry2 = new IndexedEntry( 2 ) .setNonUniqueIntgerField( 1 ) .setPrevious( entry1 ); IndexedEntry entry3 = new IndexedEntry( 3 ) .setTextField( "infrequent1" + " inMultipleDocsWithUniqueScores inMultipleDocsWithUniqueScores" + " inMultipleDocsWithVariousScores" ) .setNonUniqueIntgerField( 1 ) .setUniqueIntegerField( 4 ) .setUniqueDoubleField( 3d ) /* * Distances: * - to (24,32) with arc method: 15.06km * - to (24,32) with plane method: 15.73km */ .setLocation( 23.9d, 32.1d ) .setPrevious( entry2 ); entry0.setPrevious( entry3 ); storeData( entry0 ); storeData( entry1 ); storeData( entry2 ); storeData( entry3 ); } private QueryBuilder builder() { return sfHolder.getSearchFactory().buildQueryBuilder().forEntity( IndexedEntry.class ).get(); } @Test public void score() throws Exception { Query query = builder().keyword() .onField( "textField" ) .matching( "infrequent1" ) .createQuery(); Sort sort = builder().sort() .byScore() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 0, 3 ) ); sort = builder().sort() .byScore() .asc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 0 ) ); sort = builder().sort() .byScore() .desc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 0, 3 ) ); } @Test public void docID() throws Exception { Query query = builder().all().createQuery(); Sort sort = builder().sort() .byIndexOrder() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 0, 1, 2, 3 ) ); sort = builder().sort() .byIndexOrder() .asc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 0, 1, 2, 3 ) ); sort = builder().sort() .byIndexOrder() .desc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 2, 1, 0 ) ); } @Test public void singleField() throws Exception { Query query = builder().all().createQuery(); // Missing value is not provided; the missing values should be considered as 0 Sort sort = builder().sort() .byField( "uniqueDoubleField" ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 1, 0, 3 ) ); sort = builder().sort() .byField( "uniqueDoubleField" ) .asc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 1, 0, 3 ) ); sort = builder().sort() .byField( "uniqueDoubleField" ) .desc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 0, 1, 2 ) ); } @Test public void singleField_double_missingValue_use() throws Exception { Query query = builder().all().createQuery(); Sort sort = builder().sort() .byField( "uniqueDoubleField" ) .onMissingValue().use( 1.5d ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 1, 2, 0, 3 ) ); sort = builder().sort() .byField( "uniqueDoubleField" ) .asc() .onMissingValue().use( 1.5d ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 1, 2, 0, 3 ) ); sort = builder().sort() .byField( "uniqueDoubleField" ) .desc() .onMissingValue().use( 1.5d ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 0, 2, 1 ) ); } @Test public void singleField_integer_missingValue_use() throws Exception { Query query = builder().all().createQuery(); Sort sort = builder().sort() .byField( "uniqueIntegerField" ) .onMissingValue().use( 2 ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 1, 2, 0, 3 ) ); sort = builder().sort() .byField( "uniqueIntegerField" ) .asc() .onMissingValue().use( 2 ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 1, 2, 0, 3 ) ); sort = builder().sort() .byField( "uniqueIntegerField" ) .desc() .onMissingValue().use( 2 ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 0, 2, 1 ) ); } @Test public void singleField_stringFieldBridge() throws Exception { Query query = builder().all().createQuery(); Sort sort = builder().sort() .byField( "fieldBridgedStringField" ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 1, 0, 3 ) ); sort = builder().sort() .byField( "fieldBridgedStringField" ) .asc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 1, 0, 3 ) ); sort = builder().sort() .byField( "fieldBridgedStringField" ) .desc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 0, 1, 2 ) ); } @Test(expected = SearchException.class) public void singleField_stringFieldBridge_missingValue_use() throws Exception { builder().sort() .byField( "fieldBridgedStringField" ) .onMissingValue().use( "1.5" ) .createSort(); } @Test public void singleField_numericFieldBridge() throws Exception { Query query = builder().all().createQuery(); // Missing value is not provided; the missing values should be considered as 0 Sort sort = builder().sort() .byField( "fieldBridgedNumericField" ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 1, 0, 3 ) ); sort = builder().sort() .byField( "fieldBridgedNumericField" ) .asc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 1, 0, 3 ) ); sort = builder().sort() .byField( "fieldBridgedNumericField" ) .desc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 0, 1, 2 ) ); } @Test public void singleField_numericFieldBridge_missingValue_use() throws Exception { Query query = builder().all().createQuery(); Sort sort = builder().sort() .byField( "fieldBridgedNumericField" ) .onMissingValue().use( 1.5d ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 1, 2, 0, 3 ) ); sort = builder().sort() .byField( "fieldBridgedNumericField" ) .asc() .onMissingValue().use( 1.5d ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 1, 2, 0, 3 ) ); sort = builder().sort() .byField( "fieldBridgedNumericField" ) .desc() .onMissingValue().use( 1.5d ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 0, 2, 1 ) ); } @Test(expected = ClassCastException.class) public void singleField_numericFieldBridge_missingValue_use_nonRaw() throws Exception { Query query = builder().all().createQuery(); Sort sort = builder().sort() .byField( "fieldBridgedNumericField" ) .onMissingValue().use( new WrappedDoubleValue( 1.5d ) ) .createSort(); query( query, sort ); } @Test public void singleField_double_missingValue_sortFirst() throws Exception { Query query = builder().all().createQuery(); Sort sort = builder().sort() .byField( "uniqueDoubleField" ) .onMissingValue().sortFirst() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 1, 0, 3 ) ); sort = builder().sort() .byField( "uniqueDoubleField" ) .asc() .onMissingValue().sortFirst() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 1, 0, 3 ) ); sort = builder().sort() .byField( "uniqueDoubleField" ) .desc() .onMissingValue().sortFirst() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 3, 0, 1 ) ); } @Test public void singleField_integer_missingValue_sortFirst() throws Exception { Query query = builder().all().createQuery(); Sort sort = builder().sort() .byField( "uniqueIntegerField" ) .onMissingValue().sortFirst() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 1, 0, 3 ) ); sort = builder().sort() .byField( "uniqueIntegerField" ) .asc() .onMissingValue().sortFirst() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 1, 0, 3 ) ); sort = builder().sort() .byField( "uniqueIntegerField" ) .desc() .onMissingValue().sortFirst() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 3, 0, 1 ) ); } @Test public void singleField_missingValue_sortLast() throws Exception { Query query = builder().all().createQuery(); Sort sort = builder().sort() .byField( "uniqueDoubleField" ) .onMissingValue().sortLast() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 1, 0, 3, 2 ) ); sort = builder().sort() .byField( "uniqueDoubleField" ) .asc() .onMissingValue().sortLast() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 1, 0, 3, 2 ) ); sort = builder().sort() .byField( "uniqueDoubleField" ) .desc() .onMissingValue().sortLast() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 0, 1, 2 ) ); } @Test public void multipleFields() throws Exception { Query query = builder().all().createQuery(); Sort sort = builder().sort() .byField( "nonUniqueIntegerField" ) .andByField( "uniqueDoubleField" ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 0, 3, 1 ) ); sort = builder().sort() .byField( "nonUniqueIntegerField" ) .andByField( "uniqueDoubleField" ) .asc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 0, 3, 1 ) ); sort = builder().sort() .byField( "nonUniqueIntegerField" ) .andByField( "uniqueDoubleField" ) .desc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 0, 2, 1 ) ); } @Test public void distance() throws Exception { Query query = builder().all().createQuery(); Sort sort = builder().sort() .byDistance() .onField( "location_hash" ) .fromLatitude( 24 ).andLongitude( 32 ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 0, 1, 3, 2 ) ); sort = builder().sort() .byDistance() .onField( "location_hash" ) .fromLatitude( 24 ).andLongitude( 32 ) .asc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 0, 1, 3, 2 ) ); sort = builder().sort() .byDistance() .onField( "location_hash" ) .fromLatitude( 24 ).andLongitude( 32 ) .desc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 3, 1, 0 ) ); } @Test public void nativeLucene() throws Exception { Query query = builder().all().createQuery(); // Missing value is not provided; the missing values should be considered as 0 Sort sort = builder().sort() .byNative( new SortField( "uniqueDoubleField", SortField.Type.DOUBLE ) ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 1, 0, 3 ) ); sort = builder().sort() .byNative( new SortField( "uniqueDoubleField", SortField.Type.DOUBLE, false ) ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 2, 1, 0, 3 ) ); sort = builder().sort() .byNative( new SortField( "uniqueDoubleField", SortField.Type.DOUBLE, true ) ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 0, 1, 2 ) ); } @Test public void fieldThenScore() throws Exception { Query query = builder().keyword() .onField( "textField" ) .matching( "inMultipleDocsWithUniqueScores" ) .createQuery(); Sort sort = builder().sort() .byField( "nonUniqueIntegerField" ) .andByScore() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 0, 1 ) ); sort = builder().sort() .byField( "nonUniqueIntegerField" ) .asc() .andByScore() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 0, 1 ) ); sort = builder().sort() .byField( "nonUniqueIntegerField" ) .desc() .andByScore() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 1, 3, 0 ) ); } @Test public void scoreThenField() throws Exception { Query query = builder().keyword() .onField( "textField" ) .matching( "inMultipleDocsWithVariousScores" ) .createQuery(); Sort sort = builder().sort() .byScore() .andByField( "uniqueDoubleField" ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 0, 1, 3 ) ); sort = builder().sort() .byScore() .asc() .andByField( "uniqueDoubleField" ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 1, 3, 0 ) ); sort = builder().sort() .byScore() .desc() .andByField( "uniqueDoubleField" ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 0, 1, 3 ) ); } @Test @TestForIssue(jiraKey = "HSEARCH-2587") public void embeddedField() throws Exception { Query query = builder().all().createQuery(); // Missing value is not provided; the missing values should be considered as 0 Sort sort = builder().sort() .byField( "previous.uniqueDoubleField" ) .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 2, 1, 0 ) ); sort = builder().sort() .byField( "previous.uniqueDoubleField" ) .asc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 3, 2, 1, 0 ) ); sort = builder().sort() .byField( "previous.uniqueDoubleField" ) .desc() .createSort(); assertThat( query( query, sort ), returnsIDsInOrder( 0, 1, 2, 3 ) ); } private Matcher<List<EntityInfo>> returnsIDsInOrder(Integer ... idsInOrder) { final List<Integer> idsInOrderList = Arrays.asList( idsInOrder ); return new TypeSafeMatcher<List<EntityInfo>>() { @Override public void describeTo(Description description) { description.appendText( "a list containing exactly (and in the same order) " ) .appendValue( idsInOrderList ); } @Override protected void describeMismatchSafely(List<EntityInfo> item, Description mismatchDescription) { mismatchDescription.appendText( "was " ).appendValue( toIds( item ) ); } private List<Object> toIds(List<EntityInfo> entityInfos) { List<Object> result = new ArrayList<>(); for ( EntityInfo entityInfo : entityInfos ) { result.add( entityInfo.getProjection()[0] ); } return result; } @Override protected boolean matchesSafely(List<EntityInfo> actual) { return idsInOrderList.equals( toIds( actual ) ); } }; } private List<EntityInfo> query(Query luceneQuery, Sort sort) { ExtendedSearchIntegrator sf = sfHolder.getSearchFactory(); HSQuery hsQuery = sf.createHSQuery( luceneQuery, IndexedEntry.class ); return hsQuery .projection( "id" ) .sort( sort ) .queryEntityInfos(); } private void storeData(IndexedEntry entry) { Work work = new Work( entry, entry.id, WorkType.ADD, false ); TransactionContextForTest tc = new TransactionContextForTest(); sfHolder.getSearchFactory().getWorker().performWork( work, tc ); tc.end(); } public static class WrappedDoubleValue { final Double value; public WrappedDoubleValue(Double value) { super(); this.value = value; } } public static class WrappedDoubleValueFieldBridge implements MetadataProvidingFieldBridge, StringBridge { @Override public void configureFieldMetadata(String name, FieldMetadataBuilder builder) { builder.field( name, FieldType.DOUBLE ) .sortable( true ); } @Override public String objectToString(Object object) { if ( object == null ) { return null; } return object.toString(); } @Override public void set(String name, Object value, Document document, LuceneOptions luceneOptions) { if ( value == null ) { return; } Double doubleValue = ((WrappedDoubleValue) value).value; if ( doubleValue == null ) { return; } luceneOptions.addNumericFieldToDocument( name, doubleValue, document ); luceneOptions.addNumericDocValuesFieldToDocument( name, doubleValue, document ); } } public static class WrappedStringValue { final String value; public WrappedStringValue(String value) { super(); this.value = value; } } public static class WrappedStringValueFieldBridge implements MetadataProvidingFieldBridge, StringBridge { @Override public void configureFieldMetadata(String name, FieldMetadataBuilder builder) { builder.field( name, FieldType.STRING ) .sortable( true ); } @Override public String objectToString(Object object) { if ( object == null ) { return null; } return object.toString(); } @Override public void set(String name, Object value, Document document, LuceneOptions luceneOptions) { if ( value == null ) { return; } String stringValue = ((WrappedStringValue) value).value; if ( stringValue == null ) { return; } luceneOptions.addFieldToDocument( name, stringValue, document ); luceneOptions.addSortedDocValuesFieldToDocument( name, stringValue, document ); } } @Indexed @Spatial(name = "location_hash", spatialMode = SpatialMode.HASH) public static class IndexedEntry implements Coordinates { @DocumentId @Field(name = "idSort") @SortableField(forField = "idSort") int id; @Field String textField; @Field @SortableField Integer nonUniqueIntegerField; @Field @SortableField Double uniqueDoubleField; @Field @SortableField Integer uniqueIntegerField; @Field(bridge = @FieldBridge(impl = WrappedStringValueFieldBridge.class)) WrappedStringValue fieldBridgedStringField; @Field(bridge = @FieldBridge(impl = WrappedDoubleValueFieldBridge.class)) WrappedDoubleValue fieldBridgedNumericField; @IndexedEmbedded(depth = 1) IndexedEntry previous; Double latitude; Double longitude; public IndexedEntry() { } public IndexedEntry(int id) { super(); this.id = id; } @Override public Double getLatitude() { return latitude; } @Override public Double getLongitude() { return longitude; } public IndexedEntry setTextField(String textField) { this.textField = textField; return this; } public IndexedEntry setNonUniqueIntgerField(Integer nonUniqueIntegerField) { this.nonUniqueIntegerField = nonUniqueIntegerField; return this; } public IndexedEntry setUniqueIntegerField(Integer uniqueIntegerField) { this.uniqueIntegerField = uniqueIntegerField; return this; } public IndexedEntry setUniqueDoubleField(Double uniqueDoubleField) { this.uniqueDoubleField = uniqueDoubleField; this.fieldBridgedStringField = new WrappedStringValue( uniqueDoubleField == null ? null : String.valueOf( uniqueDoubleField ) ); this.fieldBridgedNumericField = new WrappedDoubleValue( uniqueDoubleField ); return this; } public IndexedEntry setLocation(Double latitude, Double longitude) { this.latitude = latitude; this.longitude = longitude; return this; } public IndexedEntry setPrevious(IndexedEntry previous) { this.previous = previous; return this; } } }