/* * 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.query.dsl; import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.Term; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.fest.assertions.Condition; import org.hibernate.Session; import org.hibernate.Transaction; import org.hibernate.search.FullTextSession; import org.hibernate.search.Search; import org.hibernate.search.engine.ProjectionConstants; import org.hibernate.search.exception.SearchException; import org.hibernate.search.query.dsl.QueryBuilder; import org.hibernate.search.test.SearchTestBase; import org.hibernate.search.testsupport.TestForIssue; import org.hibernate.search.util.logging.impl.Log; import org.hibernate.search.util.logging.impl.LoggerFactory; import org.junit.Before; import org.junit.Test; import static org.fest.assertions.Assertions.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * @author Emmanuel Bernard * @author Hardy Ferentschik */ public class MoreLikeThisTest extends SearchTestBase { private static final Log log = LoggerFactory.make(); private FullTextSession fullTextSession; private final boolean outputLogs = false; @Override @Before public void setUp() throws Exception { super.setUp(); Session session = openSession(); fullTextSession = Search.getFullTextSession( session ); indexTestData(); } @Test @SuppressWarnings("unchecked") public void testMoreLikeThisBasicBehavior() throws Exception { Transaction transaction = fullTextSession.beginTransaction(); try { QueryBuilder qb = getCoffeeQueryBuilder(); Coffee decaffInstance = getDecaffInstance( qb ); Query mltQuery = qb .moreLikeThis() .favorSignificantTermsWithFactor( 1 ) .comparingAllFields() .toEntityWithId( decaffInstance.getId() ) .createQuery(); List<Object[]> results = fullTextSession .createFullTextQuery( mltQuery, Coffee.class ) .setProjection( ProjectionConstants.THIS, ProjectionConstants.SCORE ) .list(); assertThat( results ).isNotEmpty(); Set<Term> terms = extractTerms( mltQuery, Coffee.class ); assertThat( terms ) .describedAs( "internalDescription should be ignored" ) .doesNotSatisfy( new Condition<Collection<?>>() { @Override public boolean matches(Collection<?> value) { for ( Term term : (Collection<Term>) value ) { if ( "internalDescription".equals( term.field() ) ) { return true; } } return false; } } ); outputQueryAndResults( decaffInstance, mltQuery, results ); //custom fields mltQuery = qb .moreLikeThis() .comparingField( "summary" ).boostedTo( 10f ) .andField( "description" ) .toEntityWithId( decaffInstance.getId() ) .createQuery(); results = fullTextSession .createFullTextQuery( mltQuery, Coffee.class ) .setProjection( ProjectionConstants.THIS, ProjectionConstants.SCORE ) .list(); assertThat( results ).isNotEmpty(); assertThat( mltQuery instanceof BooleanQuery ); BooleanQuery topMltQuery = (BooleanQuery) mltQuery; // FIXME: I'd prefer a test that uses data instead of how the query is actually built assertThat( topMltQuery.getClauses() ).onProperty( "query.boost" ).contains( 1f, 10f ); outputQueryAndResults( decaffInstance, mltQuery, results ); //using non compatible field try { qb .moreLikeThis() .comparingField( "summary" ) .andField( "internalDescription" ) .toEntityWithId( decaffInstance.getId() ) .createQuery(); } catch (SearchException e) { assertThat( e.getMessage() ) .as( "Internal description is neither stored nor store termvectors" ) .contains( "internalDescription" ); } } finally { transaction.commit(); } } private Set<Term> extractTerms(Query query, Class<?> indexedType) throws IOException { IndexReader reader = null; try { Set<Term> terms = new HashSet<Term>( 100 ); reader = fullTextSession.getSearchFactory().getIndexReaderAccessor().open( indexedType ); query.createWeight( new IndexSearcher( reader ), false ).extractTerms( terms ); return terms; } finally { if ( reader != null ) { reader.close(); } } } @Test @SuppressWarnings("unchecked") public void testMoreLikeThisToEntity() { Transaction transaction = fullTextSession.beginTransaction(); Query mltQuery; try { QueryBuilder qb = getCoffeeQueryBuilder(); Coffee decaffInstance = getDecaffInstance( qb ); // query results to compare toEntity() results against mltQuery = qb .moreLikeThis() .comparingField( "summary" ).boostedTo( 10f ) .andField( "description" ) .toEntityWithId( decaffInstance.getId() ) .createQuery(); List<Object[]> results = fullTextSession .createFullTextQuery( mltQuery, Coffee.class ) .setProjection( ProjectionConstants.THIS, ProjectionConstants.SCORE ) .list(); // pass entity itself in a managed state mltQuery = qb .moreLikeThis() .comparingField( "summary" ).boostedTo( 10f ) .andField( "description" ) .toEntity( decaffInstance ) .createQuery(); List<Object[]> entityResults = fullTextSession .createFullTextQuery( mltQuery, Coffee.class ) .setProjection( ProjectionConstants.THIS, ProjectionConstants.SCORE ) .list(); // query from id and from the managed entity should match assertThat( entityResults ).isNotEmpty(); assertThat( entityResults ).hasSize( results.size() ); for ( int index = 0; index < entityResults.size(); index++ ) { Object[] real = entityResults.get( index ); Object[] expected = results.get( index ); assertThat( real[1] ).isEqualTo( expected[1] ); assertThat( ( (Coffee) real[0] ).getId() ).isEqualTo( ( (Coffee) expected[0] ).getId() ); } outputQueryAndResults( decaffInstance, mltQuery, entityResults ); // pass entity itself with a matching id but different values // the id should take precedene Coffee nonMatchingOne = (Coffee) results.get( results.size() - 1 )[0]; Coffee copyOfDecaffInstance = new Coffee(); copyOfDecaffInstance.setId( decaffInstance.getId() ); copyOfDecaffInstance.setInternalDescription( nonMatchingOne.getInternalDescription() ); copyOfDecaffInstance.setName( nonMatchingOne.getName() ); copyOfDecaffInstance.setDescription( nonMatchingOne.getDescription() ); copyOfDecaffInstance.setIntensity( nonMatchingOne.getIntensity() ); copyOfDecaffInstance.setSummary( nonMatchingOne.getSummary() ); mltQuery = qb .moreLikeThis() .comparingField( "summary" ).boostedTo( 10f ) .andField( "description" ) .toEntity( copyOfDecaffInstance ) .createQuery(); entityResults = fullTextSession .createFullTextQuery( mltQuery, Coffee.class ) .setProjection( ProjectionConstants.THIS, ProjectionConstants.SCORE ) .list(); // query from id and from the managed entity should match assertThat( entityResults ).isNotEmpty(); assertThat( entityResults ).hasSize( results.size() ); for ( int index = 0; index < entityResults.size(); index++ ) { Object[] real = entityResults.get( index ); Object[] expected = results.get( index ); assertThat( real[1] ).isEqualTo( expected[1] ); assertThat( ( (Coffee) real[0] ).getId() ).isEqualTo( ( (Coffee) expected[0] ).getId() ); } outputQueryAndResults( decaffInstance, mltQuery, entityResults ); // pass entity itself with the right values but no id copyOfDecaffInstance = new Coffee(); copyOfDecaffInstance.setInternalDescription( decaffInstance.getInternalDescription() ); copyOfDecaffInstance.setName( decaffInstance.getName() ); copyOfDecaffInstance.setDescription( decaffInstance.getDescription() ); copyOfDecaffInstance.setIntensity( decaffInstance.getIntensity() ); copyOfDecaffInstance.setSummary( decaffInstance.getSummary() ); mltQuery = qb .moreLikeThis() .comparingField( "summary" ).boostedTo( 10f ) .andField( "description" ) .toEntity( copyOfDecaffInstance ) .createQuery(); entityResults = fullTextSession .createFullTextQuery( mltQuery, Coffee.class ) .setProjection( ProjectionConstants.THIS, ProjectionConstants.SCORE ) .list(); // query from id and from the managed entity should match assertThat( entityResults ).isNotEmpty(); assertThat( entityResults ).hasSize( results.size() ); for ( int index = 0; index < entityResults.size(); index++ ) { Object[] real = entityResults.get( index ); Object[] expected = results.get( index ); assertThat( real[1] ).isEqualTo( expected[1] ); assertThat( ( (Coffee) real[0] ).getId() ).isEqualTo( ( (Coffee) expected[0] ).getId() ); } outputQueryAndResults( decaffInstance, mltQuery, entityResults ); } finally { transaction.commit(); } } @Test @SuppressWarnings("unchecked") public void testMoreLikeThisExcludingEntityBeingCompared() { Transaction transaction = fullTextSession.beginTransaction(); Query mltQuery; List<Object[]> results; try { QueryBuilder qb = getCoffeeQueryBuilder(); Coffee decaffInstance = getDecaffInstance( qb ); // exclude comparing entity mltQuery = qb .moreLikeThis() .comparingField( "summary" ).boostedTo( 10f ) .andField( "description" ) .toEntityWithId( decaffInstance.getId() ) .createQuery(); results = fullTextSession .createFullTextQuery( mltQuery, Coffee.class ) .setProjection( ProjectionConstants.THIS, ProjectionConstants.SCORE ) .list(); mltQuery = qb .moreLikeThis() .excludeEntityUsedForComparison() .comparingField( "summary" ).boostedTo( 10f ) .andField( "description" ) .toEntityWithId( decaffInstance.getId() ) .createQuery(); List<Object[]> resultsWoComparingEntity = fullTextSession .createFullTextQuery( mltQuery, Coffee.class ) .setProjection( ProjectionConstants.THIS, ProjectionConstants.SCORE ) .list(); assertThat( resultsWoComparingEntity ).hasSize( results.size() - 1 ); for ( int index = 0; index < resultsWoComparingEntity.size(); index++ ) { Object[] real = resultsWoComparingEntity.get( index ); Object[] expected = results.get( index + 1 ); assertThat( real[1] ).isEqualTo( expected[1] ); assertThat( ( (Coffee) real[0] ).getId() ).isEqualTo( ( (Coffee) expected[0] ).getId() ); } outputQueryAndResults( decaffInstance, mltQuery, resultsWoComparingEntity ); } finally { transaction.commit(); } } @Test @SuppressWarnings("unchecked") public void testMoreLikeThisOnCompressedFields() { Transaction transaction = fullTextSession.beginTransaction(); Query mltQuery; List<Object[]> entityResults; try { QueryBuilder qb = getCoffeeQueryBuilder(); Coffee decaffInstance = getDecaffInstance( qb ); // using compressed field mltQuery = qb .moreLikeThis() .comparingField( "brand.description" ) .toEntityWithId( decaffInstance.getId() ) .createQuery(); entityResults = fullTextSession .createFullTextQuery( mltQuery, Coffee.class ) .setProjection( ProjectionConstants.THIS, ProjectionConstants.SCORE ) .list(); assertThat( entityResults ).isNotEmpty(); Long matchingElements = (Long) fullTextSession.createQuery( "select count(*) from Coffee c where c.brand.name like '%pony'" ).uniqueResult(); assertThat( entityResults ).hasSize( matchingElements.intValue() ); float score = -1; for ( Object[] element : entityResults ) { if ( score == -1 ) { score = (Float) element[1]; } assertThat( element[1] ).as( "All scores should be equal as the same brand is used" ) .isEqualTo( score ); } outputQueryAndResults( decaffInstance, mltQuery, entityResults ); } finally { transaction.commit(); } } @Test @SuppressWarnings("unchecked") public void testMoreLikeThisOnEmbeddedFields() { Transaction transaction = fullTextSession.beginTransaction(); Query mltQuery; List<Object[]> entityResults; try { QueryBuilder qb = getCoffeeQueryBuilder(); Coffee decaffInstance = getDecaffInstance( qb ); // using fields from IndexedEmbedded mltQuery = qb .moreLikeThis() .comparingField( "brand.name" ) .toEntityWithId( decaffInstance.getId() ) .createQuery(); entityResults = fullTextSession .createFullTextQuery( mltQuery, Coffee.class ) .setProjection( ProjectionConstants.THIS, ProjectionConstants.SCORE ) .list(); assertThat( entityResults ).isNotEmpty(); Long matchingElements = (Long) fullTextSession.createQuery( "select count(*) from Coffee c where c.brand.name like '%pony'" ).uniqueResult(); assertThat( entityResults ).hasSize( matchingElements.intValue() ); float score = -1; for ( Object[] element : entityResults ) { if ( score == -1 ) { score = (Float) element[1]; } assertThat( element[1] ).as( "All scores should be equal as the same brand is used" ) .isEqualTo( score ); } outputQueryAndResults( decaffInstance, mltQuery, entityResults ); // using indexed embedded id from document try { qb .moreLikeThis() .comparingField( "brand.id" ) .toEntityWithId( decaffInstance.getId() ) .createQuery(); } catch (SearchException e) { assertThat( e.getMessage() ) .as( "Field cannot be used" ) .contains( "brand.id" ); } } finally { transaction.commit(); } } @Test @TestForIssue(jiraKey = "HSEARCH-1614") public void testMoreLikeThisOnUnknownFieldThrowsException() { QueryBuilder queryBuilder = getCoffeeQueryBuilder(); Coffee decaffInstance = getDecaffInstance( queryBuilder ); try { queryBuilder.moreLikeThis() .comparingField( "foo" ) .toEntityWithId( decaffInstance.getId() ) .createQuery(); fail( "Creating the query should have failed" ); } catch (SearchException e) { assertTrue( "Unexpected error message: " + e.getMessage(), e.getMessage().startsWith( "HSEARCH000218" ) ); } } @Override public Class<?>[] getAnnotatedClasses() { return new Class<?>[] { Coffee.class, CoffeeBrand.class }; } private QueryBuilder getCoffeeQueryBuilder() { return fullTextSession.getSearchFactory() .buildQueryBuilder() .forEntity( Coffee.class ) .get(); } private Coffee getDecaffInstance(QueryBuilder qb) { Query decaff = qb.keyword().onField( "name" ).matching( "Decaffeinato" ).createQuery(); return (Coffee) fullTextSession.createFullTextQuery( decaff, Coffee.class ) .list() .get( 0 ); } private void outputQueryAndResults(Coffee originalInstance, Query mltQuery, List<Object[]> results) { // set to true to display results if ( outputLogs ) { StringBuilder builder = new StringBuilder( "Initial coffee: " ) .append( originalInstance ).append( "\n\n" ) .append( "Query: " ).append( mltQuery.toString() ).append( "\n\n" ) .append( "Matching coffees" ).append( "\n" ); for ( Object[] entry : results ) { builder.append( " Score: " ).append( entry[1] ); builder.append( " | Coffee: " ).append( entry[0] ).append( "\n" ); } log.debug( builder.toString() ); } } private void indexTestData() { Transaction tx = fullTextSession.beginTransaction(); CoffeeBrand brandPony = new CoffeeBrand(); brandPony.setName( "My little pony" ); brandPony.setDescription( "Sells goods for horseback riding and good coffee blends" ); fullTextSession.persist( brandPony ); CoffeeBrand brandMonkey = new CoffeeBrand(); brandMonkey.setName( "Monkey Monkey Do" ); brandPony.setDescription( "Offers mover services via monkeys instead of trucks for difficult terrains. Coffees from this brand make monkeys work much faster." ); fullTextSession.persist( brandMonkey ); createCoffee( "Kazaar", "EXCEPTIONALLY INTENSE AND SYRUPY", "A daring blend of two Robustas from Brazil and Guatemala, specially prepared for Nespresso, and a separately roasted Arabica from South America, Kazaar is a coffee of exceptional intensity. Its powerful bitterness and notes of pepper are balanced by a full and creamy texture.", 12, brandMonkey ); createCoffee( "Dharkan", "LONG ROASTED AND VELVETY", "This blend of Arabicas from Latin America and Asia fully unveils its character thanks to the technique of long roasting at a low temperature. Its powerful personality reveals intense roasted notes together with hints of bitter cocoa powder and toasted cereals that express themselves in a silky and velvety txture.", 11, brandPony ); createCoffee( "Ristretto", "POWERFUL AND CONTRASTING", "A blend of South American and East African Arabicas, with a touch of Robusta, roasted separately to create the subtle fruity note of this full-bodied, intense espresso.", 10, brandMonkey ); createCoffee( "Arpeggio", "INTENSE AND CREAMY", "A dark roast of pure South and Central American Arabicas, Arpeggio has a strong character and intense body, enhanced by cocoa notes.", 9, brandPony ); createCoffee( "Roma", "FULL AND BALANCED", "The balance of lightly roasted South and Central American Arabicas with Robusta, gives Roma sweet and woody notes and a full, lasting taste on the palate.", 8, brandMonkey ); createCoffee( "Livanto", "ROUND AND BALANCED", "A pure Arabica from South and Central America, Livanto is a well-balanced espresso characterised by a roasted caramelised note.", 6, brandMonkey ); createCoffee( "Capriccio", "RICH AND DISTINCTIVE", "Blending South American Arabicas with a touch of Robusta, Capriccio is an espresso with a rich aroma and a strong typical cereal note.", 5, brandMonkey ); createCoffee( "Volluto", "SWEET AND LIGHT", "A pure and lightly roasted Arabica from South America, Volluto reveals sweet and biscuity flavours, reinforced by a little acidity and a fruity note.", 4, brandMonkey ); createCoffee( "Cosi", "LIGHT AND LEMONY", "Pure, lightly roasted East African, Central and South American Arabicas make Cosi a light-bodied espresso with refreshing citrus notes.", 3, brandMonkey ); createCoffee( "Indriya from India", "POWERFUL AND SPICY", "Indriya from India is the noble marriage of Arabicas with a hint of Robusta from southern India. It is a full-bodied espresso, which has a distinct personality with notes of spices.", 10, brandMonkey ); createCoffee( "Rosabaya de Colombia", "FRUITY AND BALANCED", "This blend of fine, individually roasted Colombian Arabicas, develops a subtle acidity with typical red fruit and winey notes.", 6, brandMonkey ); createCoffee( "Dulsão do Brasil", "SWEET AND SATINY SMOOTH", "A pure Arabica coffee, Dulsão do Brasil is a delicate blend of red and yellow Bourbon beans from Brazil. Its satiny smooth, elegantly balanced flavor is enhanced with a note of delicately toasted grain.", 4, brandMonkey ); createCoffee( "Bukeela ka Ethiopia", "", "This delicate Lungo expresses a floral bouquet reminiscent of jasmine, white lily, bergamot and orange blossom together with notes of wood. A pure Arabica blend composed of two very different coffees coming from the birthplace of coffee, Ethiopia. The blend’s coffees are roasted separately: one portion short and dark to guarantee the body, the other light but longer to preserve the delicate notes.", 3, brandMonkey ); createCoffee( "Fortissio Lungo", "RICH AND INTENSE", "Made from Central and South American Arabicas with just a hint of Robusta, Fortissio Lungo is an intense full-bodied blend with bitterness, which develops notes of dark roasted beans.", 7, brandMonkey ); createCoffee( "Vivalto Lungo", "COMPLEX AND BALANCED", "Vivalto Lungo is a balanced coffee made from a complex blend of separately roasted South American and East African Arabicas, combining roasted and subtle floral notes.", 4, brandMonkey ); createCoffee( "Linizio Lungo", "ROUND AND SMOOTH", "Mild and well-rounded on the palate, Linizio Lungo is a blend of fine Arabicas enhancing malt and cereal notes.", 4, brandMonkey ); createCoffee( "Decaffeinato Intenso", "DENSE AND POWERFUL", "Dark roasted South American Arabicas with a touch of Robusta bring out the subtle cocoa and roasted cereal notes of this full-bodied decaffeinated espresso.", 7, brandMonkey ); createCoffee( "Decaffeinato Lungo", "LIGHT AND FULL-FLAVOURED", "The slow roasting of this blend of South American Arabicas with a touch of Robusta gives Decaffeinato Lungo a smooth, creamy body and roasted cereal flavour.", 3, brandMonkey ); createCoffee( "Decaffeinato", "FRUITY AND DELICATE", "A blend of South American Arabicas reinforced with just a touch of Robusta is lightly roasted to reveal an aroma of red fruit.", 2, brandPony ); createCoffee( "Caramelito", "CARAMEL FLAVOURED", "The sweet flavour of caramel softens the roasted notes of the Livanto Grand Cru. This delicate gourmet marriage evokes the creaminess of soft toffee.", 6, brandMonkey ); createCoffee( "Ciocattino", "CHOCOLATE FLAVOURED", "Dark and bitter chocolate notes meet the caramelized roast of the Livanto Grand Cru. A rich combination reminiscent of a square of dark chocolate.", 6, brandMonkey ); createCoffee( "Vanilio", "VANILLA FLAVOURED", "A balanced harmony between the rich and the velvety aromas of vanilla and the mellow flavour of the Livanto Grand Cru. A blend distinguished by its full flavour, infinitely smooth and silky on the palate.", 6, brandMonkey ); tx.commit(); fullTextSession.clear(); } private void createCoffee(String title, String summary, String description, int intensity, CoffeeBrand brand) { Coffee coffee = new Coffee(); coffee.setName( title ); coffee.setSummary( summary ); coffee.setDescription( description ); coffee.setIntensity( intensity ); coffee.setInternalDescription( "Same internal description of coffee and blend that would make things look quite the same." ); coffee.setBrand( brand ); fullTextSession.persist( coffee ); } }