Search code examples
javahibernatehibernate-search

Converting nested Hibernate Search 5 predicates to Hibernate Search 6


In the process of converting an application from Hibernate Search 5 to 6. I've read thru much of the documentation, especially https://docs.jboss.org/hibernate/search/6.0/migration/html_single/#queries-reference on how to convert Query DSL and I understand it in a simple scenario but how would you nest predicates into other nested predicates, and then combine those with others in a larger query?
In Hibernate Search 5 we would use queryBuilder.bool() to create a BooleanJunction that you could then add inside another BooleanJunction by calling createQuery on the bool and do that over and over creating nested predicate queries.

Example of the type of code I'm talking about converting:

    BooleanJunction vendorNameBool = queryBuilder.bool();
    BooleanJunction nameBool = queryBuilder.bool();
    
    nameBool.must(
        qb.keyword()
            .onField(CompanyName)
            .matching(nameToken1)
            .createQuery()
    );
    
    nameBool.must(
        qb.keyword()
            .onField(CompanyName)
            .matching(nameToken2)
            .createQuery()
    );

    vendorNameBool.should(nameBool.createQuery);
    
    // do vendorNameBool.should(...) for as many vendor Names that exist, then createQuery 
    probableVendorNamesQuery = vendorNameBool.createQuery();
    
    // creating a number of Queries from various bools and then combining them:
    Query taxIdOrVendorNameOrPhoneNumberQuery = qb.bool()
        .should(probableVendorNamesQuery)
        .should(taxIdQuery)
        .should(phoneNumberQuery)
        .createQuery();
        
    //and add to the final BooleanQuery along with other Query pieces
    
    Query idQuery = getIdQuery();
    Query fileIdQuery = getFileIdQuery();
    
    BooleanQuery.Builder theQuery = new BooleanQuery.Builder();
    theQuery.add(taxIdOrVendorNameOrPhoneNumberQuery, MUST);
    theQuery.add(fileIdQuery, MUST);    
    theQuery.add(idQuery, MUST_NOT);    
    
    BooleanQuery probableQuery = theQuery.build();
     
    // add some projections and execute query
 

Most of the HS6 code examples are in lambda form. There is a section here that provides a simple example of creating a non-lamba predicate, adding them to a list, but how, for example, would you then add that list of predicates to an outer should clause, and then add that should clause, along with another should clause, to an outer "must" clause, etc. etc. ?


Solution

  • Personally, I'd just go with the lambda syntax and nest a second lambda. Adapting the example from the migration guide:

     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 ) );
                 }
                 // ...
                 // BEGIN NEW CODE
                 SomeComplexParameter complexParam = searchParameters.getSomeComplexParameter();
                 if ( complexParam != null ) {
                     b.must( f.bool( b2 -> {
                         b2.should( f.match().field( "someField1" )
                             .matching( complexParam.getSomeField1() ) );
                         b2.should( f.match().field( "someField2" )
                             .matching( complexParam.getSomeField2() ) );
                     } ) );
                 }
                 // END NEW CODE
             } ) )
             .fetchHits( params.getPageIndex() * params.getPageSize(), params.getPageSize() );
    

    You don't even need a second lambda if you know the number of clauses at compile time:

     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 ) );
                 }
                 // ...
                 // BEGIN NEW CODE
                 SomeComplexParameter complexParam = searchParameters.getSomeComplexParameter();
                 if ( complexParam != null ) {
                     b.must( f.bool()
                             .should( f.match().field( "someField1" )
                                 .matching( complexParam.getSomeField1() ) )
                             .should( f.match().field( "someField2" )
                                 .matching( complexParam.getSomeField2() ) ) );
                 }
                 // END NEW CODE
             } ) )
             .fetchHits( params.getPageIndex() * params.getPageSize(), params.getPageSize() );
    

    If you really don't want to use lambdas at the top level (why?), you could use lambdas for nested predicates, at least:

    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() );
    }
    
    // ...
    
    // BEGIN NEW CODE
    SomeComplexParameter complexParam = searchParameters.getSomeComplexParameter();
    if ( complexParam != null ) {
        predicates.add( pf.bool( b -> {
            b.should( pf.match().field( "someField1" )
                .matching( complexParam.getSomeField1() ) );
            b.should( pf.match().field( "someField2" )
                .matching( complexParam.getSomeField2() ) );
        } )
                .toPredicate() );
    }
    // END NEW CODE
    
    List<Book> hits = searchSession.search( Book.class )
            .where( f -> f.bool( b -> {
                b.must( f.matchAll() );
                for ( SearchPredicate predicate : predicates ) {
                    b.must( predicate );
                }
            } )
    

    And here too, you don't need a lambda as long as you know the number of predicates in advance:

    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() );
    }
    
    // ...
    
    // BEGIN NEW CODE
    SomeComplexParameter complexParam = searchParameters.getSomeComplexParameter();
    if ( complexParam != null ) {
        predicates.add( pf.bool()
                .should( pf.match().field( "someField1" )
                    .matching( complexParam.getSomeField1() ) )
                .should( pf.match().field( "someField2" )
                     .matching( complexParam.getSomeField2() ) )
                .toPredicate() );
    }
    // END NEW CODE
    
    List<Book> hits = searchSession.search( Book.class )
            .where( f -> f.bool( b -> {
                b.must( f.matchAll() );
                for ( SearchPredicate predicate : predicates ) {
                    b.must( predicate );
                }
            } )
            .fetchHits( params.getPageIndex() * params.getPageSize(), params.getPageSize() );
    

    Finally, if you really want to completely stay away from lambdas (but again, why?), you can probably do something like this. Be aware that the generic type parameters of BooleanPredicateClausesStep might change in a minor version of Hibernate Search, though, so this code is more likely to break when upgrading.

    MySearchParameters searchParameters = ...;
    SearchSession session = Search.session( entityManager );
    SearchPredicateFactory pf = session.scope( Book.class ).predicate();
    BooleanPredicateClausesStep<?> boolStep = pf.bool();
    
    boolStep.must( f.matchAll() );
    
    if ( searchParameters.getSearchTerms() != null ) {
        boolStep.must( pf.simpleQueryString().fields( "title", "description" )
                .matching( searchParameters.getSearchTerms() )
                .defaultOperator( BooleanOperator.AND ) );
    }
    
    // ...
    
    SomeComplexParameter complexParam = searchParameters.getSomeComplexParameter();
    if ( complexParam != null ) {
        BooleanPredicateClausesStep<?> boolStep2 = pf.bool();
        boolStep2.should( f.match().field( "someField1" )
                .matching( complexParam.getSomeField1() ) );
        boolStep2.should( f.match().field( "someField2" )
                .matching( complexParam.getSomeField2() ) );
        boolStep.must( boolStep2 );
    }
    
    SearchPredicate boolPredicate = boolStep.toPredicate();
    
    List<Book> hits = searchSession.search( Book.class )
            .where( boolPredicate )
            .fetchHits( params.getPageIndex() * params.getPageSize(), params.getPageSize() );
    

    If you're compiling with JDK 11, a more robust solution would be to use the var keyword:

    MySearchParameters searchParameters = ...;
    SearchSession session = Search.session( entityManager );
    SearchPredicateFactory pf = session.scope( Book.class ).predicate();
    var boolStep = pf.bool();
    
    boolStep.must( f.matchAll() );
    
    if ( searchParameters.getSearchTerms() != null ) {
        boolStep.must( pf.simpleQueryString().fields( "title", "description" )
                .matching( searchParameters.getSearchTerms() )
                .defaultOperator( BooleanOperator.AND ) );
    }
    
    // ...
    
    SomeComplexParameter complexParam = searchParameters.getSomeComplexParameter();
    if ( complexParam != null ) {
        var boolStep2 = pf.bool();
        boolStep2.should( f.match().field( "someField1" )
                .matching( complexParam.getSomeField1() ) );
        boolStep2.should( f.match().field( "someField2" )
                .matching( complexParam.getSomeField2() ) );
        boolStep.must( boolStep2 );
    }
    
    SearchPredicate boolPredicate = boolStep.toPredicate();
    
    List<Book> hits = searchSession.search( Book.class )
            .where( boolPredicate )
            .fetchHits( params.getPageIndex() * params.getPageSize(), params.getPageSize() );