Luke
Luke

Reputation: 768

Hibernate Search Order by Collection Count within Date Range

I have two objects - a Document object, and a Hit object:

Document
    id
    description
    hits (Collection of type Hits)
    ...

Hit
    documentId (Type Document)
    date
    ...

I'm attempting to create a Hibernate Search query that searches the document description, then orders the results by the count of hits within a given date range.

I'm having a bit of trouble wrapping my head around how to do this. I've taken a look at this, but it takes into account the total number of hits, and does not allow for dynamic date range queries, as the bridged field is updated independent of date.

I currently have an index for my Document object with searching on description working fine. I'm thinking I may need to create an index for the Hit object, run a query against it for the date range, group by documentId, and then run a second query using the searched term against the Document index.

Given this strategy, I'm still not sure how to:

  1. group the hits by documentId
  2. maintain the order of the results from the hits query
  3. handle pagination.

My guess is that I would need to get all results from the Hits index (using some type of faceting logic for grouping/ordering maybe?), then match on documentId/term and handle pagination in the second query. I'm just not sure how all of this would look when it comes to building a query with the QueryBuilder interface, or if there's a completely different way to approach this that I haven't thought of.

UPDATE

public class DateRangeDownloadsFieldComparator extends FieldComparator<Integer> {

    class DownloadsParser implements FieldCache.IntParser {

        @Override
        public int parseInt(String string) {
           /* 
            * What do I pass here and what do I do with it? 
            * Ideally, I would pass the downloads collection and return the size of the
            * collection where the download appears within the provided date range, but 
            * passing a collection here does nothing, as it's silently ignored.
            */

            return 0;
        }
    }

    private Calendar startDate;

    private Calendar endDate;

    private final int[] fieldValues;

    private int bottom;

    private int[] currentReaderValues;

    public DateRangeDownloadsFieldComparator(int numHits, Calendar startDate, Calendar endDate) {

        super();
        this.startDate = startDate;
        this.endDate = endDate;

        fieldValues = new int[numHits];
    }

    @Override
    public int compare(int slot1, int slot2) {

        return compareValues(fieldValues[slot1], fieldValues[slot2]);
    }

    @Override
    public int compareBottom(int doc) throws IOException {

        int currentDoc = currentReaderValues[doc];

        return compareValues(bottom, currentDoc);
    }

    @Override
    public int compareValues(Integer v1, Integer v2) {

        if (v1 > v2) {
            return 1;
        }
        else if (v1 < v2) {
            return -1;
        }
        else {
            return 0;
        }
    }

    @Override
    public void copy(int slot, int doc) throws IOException {

        int v1 = currentReaderValues[doc];
        fieldValues[slot] = v1;

    }

    @Override
    public void setBottom(int slot) {

        bottom = fieldValues[slot];

    }

    @Override
    public void setNextReader(IndexReader reader, int docBase) throws IOException {

        currentReaderValues = FieldCache.DEFAULT
                .getInts(reader, "downloads", new DownloadsParser());
    }

    @Override
    public Integer value(int slot) {

        return fieldValues[slot];
    }

}

UPDATE 2

Document Entity

@Entity                                                                                                                                
@Table(name = "documents")                                                                                                             
@Indexed(index = "documents")                                                                                                          
public class EDocument {                                                                                                               

    public static final String FIELD_NAME = "name";                                                                                    

    public static final String FIELD_CREATED = "created";                                                                              

    public static final String FIELD_DESCRIPTION = "description";                                                                      

    @Id                                                                                                                                
    @GeneratedValue                                                                                                                    
    @Column(name = "id")                                                                                                               
    private Long id;                                                                                                                   

    @Temporal(TemporalType.TIMESTAMP)                                                                                                  
    @Column(name = "created")                                                                                                          
    @Field(name = FIELD_CREATED)                                                                                                       
    private Calendar created;                                                                                                          

    @Column(name = "name")                                                                                                             
    @Field(name = FIELD_NAME)                                                                                                          
    private String name;                                                                                                               

    @Column(name = "description")                                                                                                      
    @Field(name = FIELD_DESCRIPTION)                                                                                                   
    private String description;                                                                                                        

    @ManyToOne(fetch = FetchType.LAZY)                                                                                                 
    @JoinColumn(name = "user_id", nullable = false)                                                                                    
    @IndexedEmbedded(depth = 1)                                                                                                        
    private EUser user;                                                                                                                

    @OneToMany(                                                                                                                        
            fetch = FetchType.LAZY,                                                                                                    
            targetEntity = EDownload.class,                                                                                            
            mappedBy = "document",                                                                                                     
            orphanRemoval = true,                                                                                                      
            cascade = CascadeType.ALL)                                                                                                 
    @IndexedEmbedded(depth = 1)                                                                             
    private Set<EDownload> downloads;                                                                                                  

    public EDocument() {                                                                                                               

        created = Calendar.getInstance();                                                                                              
    }                                                                                                                                  

    // getters and setters                                                                                                             

}        

Download Entity

@Entity
@Table(name = "downloads")
public class EDownload {

    public static final String FIELD_REQUESTED = "requested";

    @Id
    @GeneratedValue
    @Column(name = "id")
    private Long id;

    @Column(name = "requested")
    @Field(name = FIELD_REQUESTED)
    private Calendar requested;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "document_id", nullable = false)
    private EDocument document;

    public EDownload() {

        requested = Calendar.getInstance();
    }

    // getters and setters
}                                                                                                                              

Upvotes: 0

Views: 662

Answers (2)

Luke
Luke

Reputation: 768

I ended up going with something similar to what @gmansoor suggested - a first query for the terms, which returns IDs, and then a second query based on those IDs. It doesn't appear that there's a way to do this in a single query in Hibernate Search, so this seems to be the best solution for now. If anyone knows of a better way to do this, please let me know.

Upvotes: 0

Hardy
Hardy

Reputation: 19129

I probably would index the hits as part of the document index using @IndexedEmbedded. Then you run your query against the description field, providing a custom Sort implementation. The custom sort will need the data range as parameter and then count the hits in this date range and order accordingly.

Upvotes: 0

Related Questions