Issue
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() );
Answered By - yrodiere
Answer Checked By - David Marino (JavaFixing Volunteer)