hibernate-search

how to test two conditions on each child of a OneToMany in hibernate search?


Given two entities Organization and Job and their indexing with Hibernate Search (I am forced to still use HS 5.12).

@Entity
@Indexed(index = "idx_organization")
public class Organization {
    // ...

    @OneToMany(mappedBy = "orga", cascade = CascadeType.ALL, orphanRemoval = true)
    @IndexEmbedded
    final private Set<Job> jobs = new HashSet<>();
}
@Entity
@Indexed(index = "idx_job")
public class Job {
    // ...

    @NotNull
    @ManyToOne
    @JoinColumn(nullable = false)
    @ContainedIn
    private Organization orga;

    @NotNull
    @Column(nullable = false)
    private String title;

    @Nullable
    @Field
    private OffsetDateTime archivedOn;

    @Nullable
    @Field
    private OffsetDateTime publishedOn;
}

I want to get all organizations that have at least one Job a with a.publishedOn != null and a.archivedOn == null (i.e. organizations that have at least one published and non-archived job)

This is what I tried, but the result does not include organizations that have 2 published jobs of which one is archived.

FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(em);

QueryBuilder qb = fullTextEntityManager
        .getSearchFactory()
        .buildQueryBuilder()
        .forEntity(Organization.class)
        .get()
final var q = qb.bool();
q.must(qb.keyword()
        .wildcard()
        .onField(Organization_.JOBS + "." + Job_.PUBLISHED_ON)
        .ignoreFieldBridge()
        .matching("*")
        .createQuery()
);
q.must(qb.bool().must(
            qb.keyword()
                .wildcard()
                .onField(Organization_.JOBS + "." + Job_.ARCHIVED_ON)
                .ignoreFieldBridge()
                .matching("*")
                .createQuery()
        ).not() // <-- note the "not"
        .createQuery()
);

Solution

  • I am forced to still use HS 5.12

    I guess you mean 5.11, as 5.12 doesn't exist.

    I want to get all organizations that have at least one Job a with a.publishedOn != null and a.archivedOn == null

    This is not possible at query time in Hibernate Search 5, because Lucene flattens the document structure in the index, unless you use Lucene's nested document feature which Hibernate Search 5 doesn't support. See this section of Hibernate Search 6's documentation for more information.

    The best you can do in Hibernate Search 5 is to perform the combination of the two criteria at indexing time, as shown below.

    @Entity
    @Indexed(index = "idx_job")
    public class Job {
        // ...
    
        @NotNull
        @ManyToOne
        @JoinColumn(nullable = false)
        @ContainedIn
        private Organization orga;
    
        @NotNull
        @Column(nullable = false)
        private String title;
    
        @Nullable
        @Field
        private OffsetDateTime archivedOn;
    
        @Nullable
        @Field
        private OffsetDateTime publishedOn;
    
        @Field
        @javax.persistence.Transient
        public boolean isLive() {
            return publishedOn != null && archivedOn == null;
        } 
    }
    

    Reindex your data, then you can use the new field as shown below.

    FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(em);
    
    QueryBuilder qb = fullTextEntityManager
            .getSearchFactory()
            .buildQueryBuilder()
            .forEntity(Organization.class)
            .get()
    final var q = qb.bool();
    q.must(qb.keyword()
            .wildcard()
            .onField(Organization_.JOBS + ".live")
            .matching(true)
            .createQuery()
    );