Migrating complex search from Hibernate Search 5 to 6

I’m trying to migrate my search queries from Hibernate Search 5 to 6 while using Lucene.

There are few components I made:

  • SearchRequest POJO that holds all the data needed for single search request
  • SearchService which manages all searches for my project
  • many util classes that creates tiny Query objects that are later linked by BooleanJunction into the main query

Is there any chance to do similar with Hibernate Search 6?
I’m really lost with all those new classes and syntax and I just don’t know how to convert.


Few code snippets to get idea what I’m using while building my search query:

@Builder
class SearchReq {
    private String q;
    private Integer categoryId;
    private Integer manufacturerId;
    private Integer manufacturerName;
    private Integer providerId;
    private Integer providerName;
    private Integer countryId;
    private Integer cityId;
    private Integer tagId;
    // ... etc.
    private Integer pageSize;
    private Integer page;
    private String sortField;    
}

Creating search request:

SearchReq.builder().q("tofu").cityId(12).build();

Properties holding NULL are skipped, so in this case you’d get everything that contains “tofu” and is available in city with ID == 12.

Once I have POJO filled up, I can create list of tiny queryies that will be later joined.

final List<Query> queries = new LinkedList<>();
if( q != null ) { queries.add(  QueryUtil.byQ( q ) ); }
if( categoryId != null ) { queries.add( QueryUtil.byCategoryId( categoryId ) ); }
// etc.

QueryUtil.java

private static Query byCategoryId(final QueryBuilder queryBuilder, final Integer categoryId) {
	return queryBuilder.createBooleanQuery("category",  categoryId);
}

Another step is to apply boosting factors:

public static void article(final QueryBuilder queryBuilder, final String searchTerm, final BooleanJunction<?> bj) {
    	bj.should(QueryForField.q(SearchField.NAME,         searchTerm, queryBuilder)).boostedTo(SearchField.NAME.getBoostFactor());
    	bj.should(QueryForField.q(SearchField.SHORT_NAME,   searchTerm, queryBuilder)).boostedTo(SearchField.SHORT_NAME.getBoostFactor());
    	bj.should(QueryForField.q(SearchField.DESCRIPTION,  searchTerm, queryBuilder)).boostedTo(SearchField.DESCRIPTION.getBoostFactor());
    	bj.should(QueryForField.q(SearchField.SEARCH_AID,   searchTerm, queryBuilder)).boostedTo(SearchField.SEARCH_AID.getBoostFactor());
    	bj.should(QueryForField.q(SearchField.MANUFACTURER, searchTerm, queryBuilder)).boostedTo(SearchField.MANUFACTURER.getBoostFactor());
    	bj.should(QueryForField.q(SearchField.PRICE_INFOS,  searchTerm, queryBuilder)).boostedTo(SearchField.PRICE_INFOS.getBoostFactor());
    	bj.should(QueryForField.q(SearchField.CATEGORIES,   searchTerm, queryBuilder)).boostedTo(SearchField.CATEGORIES.getBoostFactor());
    	bj.should(QueryForField.q(SearchField.TAGS,         searchTerm, queryBuilder)).boostedTo(SearchField.TAGS.getBoostFactor());
    }

Finally I join all the queries into main query:

final BooleanJunction<?> master = queryBuilder.bool();
queries.stream().filter(Objects::nonNull).forEach(master::must);
final Query theQuery = master.createQuery();

Hello,

Please read the migration guide, there’s an example that does pretty much exactly what you want: Hibernate Search 6.0.10.Final: Migration Guide from 5.11

As to migrating existing complex queries, let’s consider the query below:

MySearchParameters searchParameters = ...;
FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager( em );
QueryBuilder qb = fullTextEntityManager.getSearchFactory()
        .buildQueryBuilder().forEntity( Book.class ).get();

BooleanJunction junction = qb.bool();
junction.must(qb.all().createQuery());

if ( searchParameters.getSearchTerms() != null ) {
    junction.must( qb.simpleQueryString().onFields( "title", "description" )
            .withAndAsDefaultOperator()
            .matching( searchParameters.getSearchTerms() )
            .createQuery() );
}
if ( searchParameters.getMaxBookLength() != null ) {
    junction.must( qb.range().onField( "pageCount" )
            .below( searchParameters.getMaxBookLength() ) );
}
if ( !searchParameters.getGenres().isEmpty() ) {
    BooleanJunction junction2 = qb.bool();
    for ( Genre genre : searchParameters.getGenres() ) {
        junction2.should( qb.keyword().onField( "genre" )
                .matching( genre ) );
    }
    junction.must( junction2.createQuery() );
}

org.apache.lucene.search.Query luceneQuery = junction.createQuery();

FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery( luceneQuery, Book.class );
fullTextQuery.setFirstResult( params.getPageIndex() * params.getPageSize() );
fullTextQuery.setMaxResults( params.getPageSize() );

List hits = fullTextQuery.getResultList();

It would look like this in Hibernate Search 6:

MySearchParameters searchParameters = ...;
SearchSession session = Search.session( entityManager );
List<Book> hits = searchSession.search( Book.class )
        .where( f -> f.bool( b -> {
            b.must( f.matchAll() );
            if ( searchParameters.getSearchTerms() != null ) {
                b.must( f.simpleQueryString().fields( "title", "description" )
                        .matching( searchParameters.getSearchTerms() )
                        .defaultOperator( BooleanOperator.AND ) );
            }
            if ( searchParameters.getMaxBookLength() != null ) {
                b.must( f.range().field( "pageCount" )
                        .atMost( searchParameters.getMaxBookLength() ) );
            }
            if ( !searchParameters.getGenres().isEmpty() ) {
                b.must( f.bool( b2 -> {
                    for ( Genre genre : searchParameters.getGenres() ) {
                        b2.should( f.match().field( "genre" )
                                .matching( genre ) );
                    }
                } ) );
            }
        } ) )
        .fetchHits( searchParameters.getPageIndex() * searchParameters.getPageSize(),
                searchParameters.getPageSize() );

Alternatively, if for some reasons predicate objects are necessary:

MySearchParameters searchParameters = ...;
SearchSession session = Search.session( entityManager );
SearchPredicateFactory pf = session.scope( Book.class ).predicate();
List<SearchPredicate> predicates = new ArrayList<>();

if ( searchParameters.getSearchTerms() != null ) {
    predicates.add( pf.simpleQueryString().fields( "title", "description" )
            .matching( searchParameters.getSearchTerms() )
            .defaultOperator( BooleanOperator.AND )
            .toPredicate() );
}
if ( searchParameters.getMaxBookLength() != null ) {
    predicates.add( pf.range().field( "pageCount" )
            .atMost( searchParameters.getMaxBookLength() )
            .toPredicate() );
}
if ( !searchParameters.getGenres().isEmpty() ) {
    predicates.add( f.bool( b -> {
        for ( Genre genre : searchParameters.getGenres() ) {
            b.should( f.match().field( "genre" )
                    .matching( genre ) );
        }
    } )
            .toPredicate() );
}

SearchPredicate topLevelPredicate = pf.bool( b -> {
    b.must( f.matchAll() );
    for ( SearchPredicate predicate : predicates ) {
        b.must( predicate );
    }
} );

List<Book> hits = searchSession.search( Book.class )
        .where( topLevelPredicate )
        .fetchHits( searchParameters.getPageIndex() * searchParameters.getPageSize(),
                searchParameters.getPageSize() );

If that doesn’t help, I’ll need a more precise question, for example with a snippet of code showing where you got and what got you stuck.

1 Like

Another situation :frowning:
This won’t compile because I get this error:

Type mismatch: cannot convert from PredicateFinalStep to SearchPredicate

import org.hibernate.search.engine.search.predicate.SearchPredicate;
import org.hibernate.search.engine.search.predicate.dsl.SearchPredicateFactory;
import org.hibernate.search.mapper.orm.session.SearchSession;

// ...

SearchPredicateFactory pf = searchSession.scope( Article.class ).predicate();
List<SearchPredicate> predicates = new ArrayList<>();

// ... build the list of predicates ...

SearchPredicate classPredicate = pf.bool( b -> {
	for( SearchPredicate predicate : predicates ) {
		b.should( predicate );
	}
	b.minimumShouldMatchNumber( 1 );
} );

When I put a cast in front of pf.bool() the error is gone.
The example in migration guide does not have cast and I’m not sure I’m doing it right.
Since I have tons of code to convert, I’m not able to test it and I guess I won’t be able for days and days.

SearchPredicate classPredicate = (SearchPredicate) pf.bool( b -> {
	for( SearchPredicate predicate : predicates ) {
		b.should( predicate );
	}
	b.minimumShouldMatchNumber( 1 );
} );

hey,

There’s PredicateFinalStep#toPredicate() which would return you a SearchPredicate. You wouldn’t want to do the cast.

This ^. The cast would fail anyway.

EDIT: created [HSEARCH-4786] - Hibernate JIRA to fix the migration guide.

Use the migration helper: Hibernate Search 6.0.10.Final: Migration Guide from 5.11

Thank you both very much!!!
I am willing to rewrite all of my search code from scratch if needed, just to avoid helpers and hacks.
I want to say, I’m going for clean code written for Hibernate Search 6, not some adjusted code hacked just to work.

I’ll see what to do with hint from @mbekhta and really hope not to waste more of your time.

Thanks again!

P.S. My project is already live with Hibernate Search 5 and doing well which means I have all the time in the world to make it the right way. :slight_smile: