Search code examples
hibernatehibernate-searchfacetfaceted-search

How to get the object related to hibernate search facet


I am following hibernate-search document about faceting from: https://docs.jboss.org/hibernate/stable/search/reference/en-US/html_single/#query-faceting

I was able to generate my facets, for example I have my authorNames + count:

name1 = 100
name2 = 200
and so on..

But my problem is how am I going to query the Author object, if I want to display some other details of it.

Going by the example I now have:

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

org.apache.lucene.search.Query luceneQuery = qb.all().createQuery();
FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(luceneQuery, Book.class);

FacetingRequest authorFacet = qb.facet().name("authorFacetRequest").onField("authors.name_facet").discrete()
        .orderedBy(FacetSortOrder.FIELD_VALUE).includeZeroCounts(false).createFacetingRequest();

// retrieve facet manager and apply faceting request
FacetManager facetManager = fullTextQuery.getFacetManager();
facetManager.enableFaceting(authorFacet);

// retrieve the faceting results
List<Facet> facets = facetManager.getFacets("authorFacetRequest");
facets.forEach(p -> log.info(p.getValue() + " - " + p.getCount()));

But in the GUI I want to create a link with id=author.id. But author.id is not available using facet. So what I would like is:

List<facetName (authorName), count, referenceId>

What's the simplest and efficient way to implement this? Is there a way to do this without multiple queries?


Solution

  • You could target the author identifier instead of the author name.

    Write a class to hold your results:

    public class EntityFacet<T> implements Facet {
      private final Facet delegate;
      private final T entity;
      public EntityFacet(Facet delegate, T entity) {
        // ...
      }
    
      // delegate all Facet methods to the delegate
    
      public T getEntity() {
        return entity;
      }
    }
    

    Add a Field and Facet to the author ID:

    @Indexed
    @Entity
    public class Author {
    
      @Id
      // Discrete faceting only works on text fields, so we don't use the default bridge
      @Field(name = "id_for_facet", analyze = Analyze.NO, bridge = @Bridge(impl = org.hibernate.search.bridge.builtin.IntegerBridge))
      @Facet(name = "id_facet", forField = "id_for_facet")
      private Integer id;
    
      // ...
    }
    

    (If you have any restriction in place in your @IndexedEmbedded in the Book class, such as includePaths, make sure to update them to include this new facet)

    If you only need the name and ID, then you can do two queries on the two facets and be done with it.

    If you need more information, then change your code to target the ID facet:

    FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(em);
    QueryBuilder qb = fullTextEntityManager.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get();
    
    org.apache.lucene.search.Query luceneQuery = qb.all().createQuery();
    FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(luceneQuery, Book.class);
    
    FacetingRequest authorFacet = qb.facet().name("authorFacetRequest").onField("authors.id_facet").discrete()
            .includeZeroCounts(false).createFacetingRequest();
    
    // retrieve facet manager and apply faceting request
    FacetManager facetManager = fullTextQuery.getFacetManager();
    facetManager.enableFaceting(authorFacet);
    
    // retrieve the faceting results
    List<Facet> facets = facetManager.getFacets("authorFacetRequest");
    

    And finally, do some post-processing:

    List<Integer> authorIds = facets.stream().map( f -> {
              try {
                return Integer.parseInt(f.getValue());
              }
              catch (NumberFormatException e) {
                throw new RuntimeException("Unexpected author ID format", e);
              }
            } )
            .collect(Collectors.asList());
    List<Author> authors = fullTextEntityManager.unwrap(Session.class)
        .byId(Author.class)
        .multiLoad( authorIds );
    List<EntityFacet<Author>> entityFacets = new ArrayList<>(facets.size());
    for (int i = 0; i < facets.size() ) {
      entityFacets.add(new EntityFacet(facets.get(i), authors.get(i)));
    }
    Collator nameSort = new Collator();
    nameSort.setStrength(Collator.PRIMARY);
    Collections.sort(entityFacets, Comparator.comparing(f -> f.getEntity().getName(), nameSort));
    entityFacets.forEach(p -> log.info(p.getEntity() + " - " + p.getCount()));
    

    I didn't try to run the code, but that should be it, give or take a few syntax errors.

    Granted, it's a bit too complex for its own good, especially the ID parsing stuff. But I don't think you can do better until we improved faceting (which should happen in Search 6).


    EDIT: A (much) simpler solution with Hibernate Search 6:

    Add an aggregable field to the author ID:

    @Indexed
    @Entity
    public class Author {
    
      @Id
      @GenericField(aggregable = Aggregable.YES)
      private Integer id;
    
      // ...
    }
    

    (If you have any restriction in place in your @IndexedEmbedded in the Book class, such as includePaths, make sure to update them to include this new facet)

    If you only need the name and ID, then you can do two queries on the two facets and be done with it.

    If you need more information, then change your code to target the ID facet:

    AggregationKey<Map<Integer, Long>> countByAuthorIdKey = AggregationKey.of("countByAuthorId");
    Map<Integer, Long> countByAuthorId = Search.session(em)
            .search(Book.class)
            .where(f -> f.matchAll())
            .aggregation(countByAuthorIdKey, f -> f.terms()
                    .field("authors.id", Integer.class) )
            .fetch(0)
            .aggregation(countByAuthorIdKey);
    

    And finally, do some post-processing:

    // Pre-load all authors into the session in a single operation,
    // for better performance
    em.unwrap(Session.class)
        .byId(Author.class)
        .multiLoad(new ArrayList<>(countByAuthorId.keys()));
    Map<Author, Long> countByAuthor = new LinkedHashMap<>(countByAuthorId.size());
    for (Map.Entry<Integer, Long> entry : countByAuthorId.entrySet()) {
        countByAuthor.put(em.getReference(Author.class, entry.getKey()), entry.getValue());
    }
    
    // Use the map whatever way you want
    countByAuthor.forEach((author, count) -> log.info(author + " - " + count));