Home > other >  Hibernate search : Sorting with filter on nested object, how to?
Hibernate search : Sorting with filter on nested object, how to?

Time:10-05

I have to code an hibernate search query (for elastic search database backend) which include a conditionnal sort of this kind :

Date dateOfBirth = new Date('01/01/2000');
Integer age = 10;
if (dateOfBirth == null) {
   //then sort by age
}
else {
   //sort by date of birth
}

I found an example to code this conditionnal sort inside Hibernate Search Reference, it can be done like this (quoted example) :

List<Author> hits = searchSession.search( Author.class )
.where( f -> f.matchAll() )
.sort( f -> f.field( "books.pageCount" )
.mode( SortMode.AVG )
.filter( pf -> pf.match().field( "books.genre" )
.matching( Genre.CRIME_FICTION ) ) )
.fetchHits( 20 );

My problem is that I hibernate search throws an exception at runtime. My sort filter code :

 case DATE_SIGNATURE:
                FieldSortOptionsStep bivSortFirst = f.field(Depot_.VENTE   "."   Vente_.DATE_SIGNATURE)
                        .filter(fa ->
                                {
                                    PredicateFinalStep a = fa.bool(bo -> bo.must(fa.exists().field(Depot_.VENTE   "."   Vente_.DATE_SIGNATURE)));
                                    return fa.bool(b0 -> b0.must(a));
                                }
                        );
                FieldSortOptionsStep bivSortSecond = f.field(Depot_.VENTE   "."   Vente_.ACTE   "."   Acte_.SIGNATURE)
                        .filter(fa ->
                                {
                                    PredicateFinalStep a = fa.bool(bo -> bo.mustNot(fa.exists().field(Depot_.VENTE   "."   Vente_.DATE_SIGNATURE)));
                                    PredicateFinalStep b = fa.bool(bo -> bo.must(fa.exists().field(Depot_.VENTE   "."   Vente_.ACTE   "."   Acte_.SIGNATURE)));
                                    return fa.bool(b0 -> b0.must(a).must(b));
                                }
                        );
                sortFieldOrderedList.add(bivSortFirst);
                sortFieldOrderedList.add(bivSortSecond);
                break;

In the above example, I sort on two fields by priority. The first is assimilable to 'date of birth' and the second to 'age'. At runtime, the filter are not accepted by hibernate search and then throws an exception like follows :

The error message :

HSEARCH400604: Invalid sort filter: field 'vente.acte.signature' is not contained in a nested object. Sort filters are only available if the field to sort on is contained in a nested object. Context: field 'vente.acte.signature'

I read to do so, I need to go for 'inner_hits' query for elastic search. But how do I do this with hibernate search API ?

Thanks.

EDIT : Hibernate mapping of classes :

@Entity
@Indexed
public class Depot {
    ...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "vente_fk")
    protected Vente vente;
                
    @IndexedEmbedded(includePaths = {
    Vente_.ID,
    Vente_.DATE_SIGNATURE,
    Vente_.DATE_SIGNATURE_ACTE,
    Vente_.ACTE   "."   Acte_.SIGNATURE,
        and much more
    }
    public Vente getVente() {
            return this.vente;
        }
    ...
}

@Entity
public class Vente {

    @OneToMany(mappedBy = Depot_.VENTE, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    protected Set<Depot> depot = new HashSet<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "acte_fk")
    protected Acte acte;
...
    @AssociationInverseSide(inversePath = @ObjectPath(@PropertyValue(propertyName = Acte_.VENTE)))
    @IndexedEmbedded
    public Acte getActe() {
        return this.acte;
    }
...
}

@Entity
public class Acte {
...
    @GenericField(projectable = Projectable.YES, sortable = Sortable.YES, aggregable = Aggregable.YES)
    protected Date signature;
    
    @OneToMany(mappedBy = Vente_.ACTE)
    protected Set<Vente> vente = new HashSet<>();
    
    public Date getSignature() {
        return this.signature;
    }
...
}

CodePudding user response:

From what I can see, for each Depot, there is at most one Acte and one Vente. So what you're trying to do is a bit exotic, as filtering in sorts is generally used on multi-valued nested objects.

The reason it's not working is you didn't mark the @IndexedEmbedded objects (vente, acte) as "nested"; as explained in the documentation, filtering only works on nested objects. And "nested" has a very precise meaning, it's not synonmymous with "indexed-embedded".

However, I think the whole approach is wrong in this case: you shouldn't use filtering. I'm quite sure that even if you mark the @IndexedEmbedded objects as "nested", you will face other problems, because what you're trying to do isn't the intended purpose of filtering. One of those problems could be performance; nested documents mean runtime joins, and runtime joins aren't cheap.

Instead, consider solving this problem at indexing time. Instead of trying to figure out which date to use for each document when searching, do that when indexing:

@Entity
@Indexed
public class Depot {
    //...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "vente_fk")
    protected Vente vente;
                
    @IndexedEmbedded(includePaths = {
    Vente_.ID,
    Vente_.DATE_FOR_SORT, // <================= ADD THIS
    Vente_.DATE_SIGNATURE,
    Vente_.DATE_SIGNATURE_ACTE,
    Vente_.ACTE   "."   Acte_.SIGNATURE,
        //and much more
    })
    public Vente getVente() {
            return this.vente;
        }

}

@Entity
public class Vente {

    @OneToMany(mappedBy = Depot_.VENTE, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    protected Set<Depot> depot = new HashSet<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "acte_fk")
    protected Acte acte;
//...
    @AssociationInverseSide(inversePath = @ObjectPath(@PropertyValue(propertyName = Acte_.VENTE)))
    @IndexedEmbedded
    public Acte getActe() {
        return this.acte;
    }

    // v================= ADD THIS
    @Transient
    @IndexingDependency(derivedFrom = {
        @ObjectPath(@PropertyValue(propertyName = Vente_.DATE_SIGNATURE)),
        @ObjectPath(@PropertyValue(propertyName = Vente_.ACTE), @PropertyValue(propertyName = Acte_.SIGNATURE)),
    })
    public Date getDateForSort() {
        if ( getDateSignature() != null ) {
            return getDateSignature();
        }
        else {
            return getActe().getSignature();
        }
    }
    // ^================= ADD THIS
//...
}
  • Related