/*
* 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 java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
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.fest.assertions.Assertions;
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.SortableField;
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.LuceneOptions;
import org.hibernate.search.cfg.Environment;
import org.hibernate.search.query.engine.spi.HSQuery;
import org.hibernate.search.spi.CustomTypeMetadata;
import org.hibernate.search.test.util.impl.ExpectedLog4jLog;
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.hibernate.search.util.impl.CollectionHelper;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
@TestForIssue(jiraKey = "HSEARCH-2561")
@Category(SkipOnElasticsearch.class) // This test is specific to Lucene
public class CustomTypeMetadataSortingTest {
private static final String UNINVERTING_READER_LOG_CODE = "HSEARCH000289";
private static final Sort FIRST_NAME_SORT = new Sort( new SortField( "properties.firstName", SortField.Type.STRING ) );
private static final Sort FIRST_NAME_SORT_REVERSED =
new Sort( new SortField( "properties.firstName", SortField.Type.STRING, true ) );
@Rule
public final ExpectedLog4jLog logged = ExpectedLog4jLog.create();
@Rule
public final SearchFactoryHolder factoryHolder = new SearchFactoryHolder( PropertySet.class, ExtendedPropertySet.class, Person.class )
.withProperty( Environment.INDEX_UNINVERTING_ALLOWED, "true" );
@Test
public void undeclaredSortableField_defaultMetadata() {
storeTestingData(
new PropertySet( 0 )
.put( "firstName", "Aaron" )
.put( "lastName", "Zahnd" )
.put( "nonSortableField", "zzz" ),
new ExtendedPropertySet( 1 )
.put( "firstName", "Mike" )
.put( "lastName", "Myers" )
.put( "nonSortableField", "mmm" ),
new Person( 2, "Zach" )
);
logged.expectMessage( UNINVERTING_READER_LOG_CODE );
Query luceneQuery = factoryHolder.getSearchFactory().buildQueryBuilder().forEntity( PropertySet.class ).get().all().createQuery();
HSQuery query = factoryHolder.getSearchFactory().createHSQuery( luceneQuery, PropertySet.class, Person.class )
.sort( FIRST_NAME_SORT );
Assertions.assertThat( query.queryEntityInfos() ).onProperty( "id" ).as( "Sorted IDs" )
.containsExactly( 0, 1, 2 );
query.sort( FIRST_NAME_SORT_REVERSED );
Assertions.assertThat( query.queryEntityInfos() ).onProperty( "id" ).as( "Sorted IDs" )
.containsExactly( 2, 1, 0 );
}
@Test
public void undeclaredSortableField_incorrectCustomMetadata() {
storeTestingData(
new PropertySet( 0 )
.put( "firstName", "Aaron" )
.put( "lastName", "Zahnd" )
.put( "nonSortableField", "zzz" ),
new ExtendedPropertySet( 1 )
.put( "firstName", "Mike" )
.put( "lastName", "Myers" )
.put( "nonSortableField", "mmm" ),
new Person( 2, "Zach" )
);
logged.expectMessage( UNINVERTING_READER_LOG_CODE );
Query luceneQuery = factoryHolder.getSearchFactory().buildQueryBuilder().forEntity( PropertySet.class ).get().all().createQuery();
CustomTypeMetadata incorrectCustomMetadata = new PropertySetMetadata() {
@Override
public Set<String> getSortableFields() {
return Collections.singleton( "properties.nonSortableField" );
}
};
HSQuery query = factoryHolder.getSearchFactory().createHSQuery( luceneQuery, incorrectCustomMetadata, new PersonMetadata() )
.sort( FIRST_NAME_SORT );
Assertions.assertThat( query.queryEntityInfos() ).onProperty( "id" ).as( "Sorted IDs" )
.containsExactly( 0, 1, 2 );
query.sort( FIRST_NAME_SORT_REVERSED );
Assertions.assertThat( query.queryEntityInfos() ).onProperty( "id" ).as( "Sorted IDs" )
.containsExactly( 2, 1, 0 );
}
@Test
public void undeclaredSortableField_correctCustomMetadata() {
storeTestingData(
new PropertySet( 0 )
.put( "firstName", "Aaron" )
.put( "lastName", "Zahnd" )
.put( "nonSortableField", "zzz" ),
new ExtendedPropertySet( 1 )
.put( "firstName", "Mike" )
.put( "lastName", "Myers" )
.put( "nonSortableField", "mmm" ),
new Person( 2, "Zach" )
);
// We expect HSearch to *not* use an uninverting reader
logged.expectMessageMissing( UNINVERTING_READER_LOG_CODE );
Query luceneQuery = factoryHolder.getSearchFactory().buildQueryBuilder().forEntity( PropertySet.class ).get().all().createQuery();
HSQuery query = factoryHolder.getSearchFactory().createHSQuery( luceneQuery, new PropertySetMetadata(), new PersonMetadata() )
.sort( FIRST_NAME_SORT );
Assertions.assertThat( query.queryEntityInfos() ).onProperty( "id" ).as( "Sorted IDs" )
.containsExactly( 0, 1, 2 );
query.sort( FIRST_NAME_SORT_REVERSED );
Assertions.assertThat( query.queryEntityInfos() ).onProperty( "id" ).as( "Sorted IDs" )
.containsExactly( 2, 1, 0 );
}
private void storeTestingData(Identifiable... testData) {
Worker worker = factoryHolder.getSearchFactory().getWorker();
TransactionContextForTest tc = new TransactionContextForTest();
for ( int i = 0; i < testData.length; i++ ) {
Identifiable identifiable = testData[i];
worker.performWork( new Work( identifiable, identifiable.getId(), WorkType.INDEX ), tc );
}
tc.end();
}
private interface Identifiable {
int getId();
}
@Indexed(index = "propertySet")
private static class PropertySet implements Identifiable {
@DocumentId
int id;
@Field(bridge = @FieldBridge(impl = PropertiesBridge.class))
Map<String, String> properties = new LinkedHashMap<>();
public PropertySet(int id) {
this.id = id;
}
@Override
public int getId() {
return id;
}
public PropertySet put(String name, String value) {
properties.put( name, value );
return this;
}
}
@Indexed(index = "propertySet")
private static class ExtendedPropertySet extends PropertySet {
public ExtendedPropertySet(int id) {
super( id );
}
}
@Indexed(index = "person")
private static class Person implements Identifiable {
@DocumentId
int id;
@Field(name = "properties.firstName")
@SortableField(forField = "properties.firstName")
String firstName;
public Person(int id, String firstName) {
this.id = id;
this.firstName = firstName;
}
@Override
public int getId() {
return id;
}
}
/**
* This bridge adds sortable fields without those being declared in the metadata,
* which is why it is useful to be able to override sortable fields when querying.
*/
public static class PropertiesBridge implements org.hibernate.search.bridge.FieldBridge {
public static final Set<String> SORTABLE_PROPERTIES = CollectionHelper.asImmutableSet( new String[] { "firstName", "lastName" } );
@Override
public void set(String name, Object value, Document document, LuceneOptions luceneOptions) {
@SuppressWarnings("unchecked")
Map<String, String> properties = (Map<String, String>) value;
for ( Map.Entry<String, String> property : properties.entrySet() ) {
String fieldName = name + "." + property.getKey();
luceneOptions.addFieldToDocument( fieldName, property.getValue(), document );
if ( SORTABLE_PROPERTIES.contains( property.getKey() ) ) {
luceneOptions.addSortedDocValuesFieldToDocument( fieldName, property.getValue(), document );
}
}
}
}
private static class PropertySetMetadata implements CustomTypeMetadata {
private final Set<String> sortableFields;
public PropertySetMetadata() {
Set<String> sortableFields = new TreeSet<>();
for ( String propertyName : PropertiesBridge.SORTABLE_PROPERTIES ) {
sortableFields.add( "properties." + propertyName );
}
this.sortableFields = Collections.unmodifiableSet( sortableFields );
}
@Override
public Class<?> getEntityType() {
return PropertySet.class;
}
@Override
public Set<String> getSortableFields() {
return sortableFields;
}
}
private static class PersonMetadata implements CustomTypeMetadata {
@Override
public Class<?> getEntityType() {
return Person.class;
}
@Override
public Set<String> getSortableFields() {
return Collections.emptySet();
}
}
}