h3. Goal
Restore the scroll feature exposed in Search 5 through {{org.hibernate.search.query.hibernate.impl.FullTextQueryImpl#scroll()}}.
h3. API
All located in the {{org.hibernate.search.engine.search.query}} package.
{code} public interface SearchFetchable {
// ... there's already some code here...
// Add this (+ javadoc): // Throws IllegalArgumentException if passed 0 or less (see the class Contracts). SearchScroll<H> scroll(Integer pageSize); // Add this (+ javadoc): // Throws IllegalArgumentException if passed 0 or less for pageSize (see the class Contracts). // Throws IllegalArgumentException if passed less than 0 for offset (see the class Contracts). // TODO maybe it's not possible to implement this efficiently for Elasticsearch (not sure it accepts an offset when scrolling is enabled). In that case, remove this method. SearchScroll<H> scroll(Integer offset, Integer pageSize);
} {code}
{code} // This will be used like this: // try (SearchScroll<H> scroll = query.scroll(20)) { // for (SearchScrollResult<H> page = scroll.next(); page.hasHits(); page = scroll.next()) { // List<H> hits = page.getHits(); // // ... do something with the page ... // } // } public interface SearchScroll<H> extends AutoCloseable {
@Override void close();
// TODO: javadoc // Returns the next page, with at most "pageSize" hits ("pageSize" defined in the call to query.scroll()). // May return a result with less than "pageSize" elements if only that many hits are left. // This should *not* rely on pre-fetching. Fetching should happen when this method is called, not before. // This is necessary if we want to make it easy for users to clear the ORM session between two pages. // Note there is no "hasNext" method precisely because we do not do pre-fetching. SearchScrollResult<H> next(); } {code}
{code} public interface SearchScrollResult<H> {
// TODO: javadoc // This returns true if there are still hits, false otherwise. // Note hasHits() == true && getHits().isEmpty() *is possible*, in particular if matching entities could not be found in the database. // This methods is mainly useful as a stop condition in loops. boolean hasHits();
// TODO: javadoc List<H> getHits();
// TO BE CHECKED: these may not be implementable efficiently. // First, let's check if Elasticsearch returns the total hit count/aggregations to the first search API call when scrolling is enabled. // If it does, let's check the performance impact... Getting this information might require to execute the search query twice, in which case I'd rather not expose this information here and require users to execute the search query twice, explicitly. // Note that *if* we end up implementing these methods, they will return the same data for every single page. long getTotalHitCount(); <A> A getAggregation(AggregationKey<A> key);
// TO BE DISCUSSED: if we add this, it will probably be better to wrap this information into a SearchExecutionMetadata object, and implement getLastExecutionMetadata() here. // As a first step, I would not implement this and would just create a ticket about it. Duration getTook(); boolean isTimedOut(); } {code}
h3. To-do list
In order:
# Add APIs, with stub implementations (throw UnsupportedOperationException( "Not yet implemented" ); ## Ignore getTotalHitCount/getAggregation/getTook/isTimeout for now. # Copy-paste {{org.hibernate.search.integrationtest.backend.tck.search.query.SearchQueryFetchIT}} to {{SearchQueryScrollIT}} and adapt it to test scrolling. ## Don't forget to test edge cases: not fetching any result (should work fine), fetching some results but not all of them (should work fine), trying to fetch more than the total hit count (should throw an exception). ## Don't forget to check that {{hasMoreHits()}} returns the correct information. # Add tests for timeouts (failAfter/truncateAfter) when scrolling. # Implement scrolling for the stub backend. # Add tests to the ORM mapper. Will probably need to copy/paste {{org.hibernate.search.integrationtest.mapper.orm.search.loading.SearchQueryEntityLoadingBaseIT}} and adapt it to test loading when calling {{scroll()}} instead of just loading when calling {{fetch()}}. # Implement scrolling for Elasticsearch. ## This should be easy enough: the first call to fetch*() will execute a search work with the {{scroll}} parameter set, the next calls with execute a scroll work (already implemented, see {{org.hibernate.search.elasticsearch.work.impl.factory.ElasticsearchWorkFactory#scroll}}). ## On close, we will execute a clearScroll work (already implemented, see {{org.hibernate.search.elasticsearch.work.impl.factory.ElasticsearchWorkFactory#clearScroll}}). # Implement scrolling for Lucene. ## Search 5 code will not be very useful in that regard, as it addresses a lot of problems that are no longer relevant in Search 6. ## Basically, in the SearchScroll implementation we will need to keep around some of the context that we currently store as local variables in {{LuceneSearcherImpl#search}}: the `IndexSearcher` and the `LuceneCollectors` instance in particular. ## This may prove difficult, maybe let's organize a pair-programming session for that? # Implement {{scroll()}} and {{scroll(ScrollMode)}} in {{HibernateOrmSearchQueryAdapter}}, relying on {{scrollAll()}} under the scene. ## Only {{ScrollMode.FORWARD_ONLY}} will be supported. ## We will need to decide on a page size. Let's use the same size as the loading fetch size, which should be accessible from {{org.hibernate.search.mapper.orm.search.loading.impl.MutableEntityLoadingOptions#getFetchSize}}. ## Some internal windowing will probably be necessary. Just copy/paste the {{org.hibernate.search.elasticsearch.util.impl.Window}} class from Search 5 and adapt it. Do not forget to also copy the unit test, {{org.hibernate.search.elasticsearch.test.WindowTest}}. ## See {{org.hibernate.search.query.hibernate.impl.ScrollableResultsImpl}} for an example of how it was done in Search 5 (may or may not be helpful). # Add tests for {{scroll()}} and {{scroll(ScrollMode)}} in {{org.hibernate.search.integrationtest.mapper.orm.hibernateormapis.ToHibernateOrmIT}}: ## Nominal case (create scroll, fetch some hits until all hits have been consumed, close). ## Edge cases: not fetching any result (should work fine), fetching some results but not all of them (should work fine), trying to fetch more than the total hit count (should throw an exception). ## Error cases: trying to scroll back, trying to call the {{get*(int)}} methods... ## Check that using any scroll mode other than ScrollMode.FORWARD_ONLY fails. ## Test {{query.stream()}} too (it's based on {{scroll()}}). # Add tests for {{getResultStream()}} in {{org.hibernate.search.integrationtest.mapper.orm.hibernateormapis.ToJpaIT}}. # Allow backends to extend the SearchScroll interfaces, like they currently do with {{SearchQuery}} ({{ElasticsearchSearchQuery}}, {{LuceneSearchQuery}}): ## Add a generic parameter {{S extends SearchScroll<H>}} to {{ExtendedSearchFetchable}} and override its {{scroll}} methods to return that type. ## Adapt the interfaces that extend {{ExtendedSearchFetchable}} as necessary. ## Create a new {{ExtendedSearchScroll<H>}} interface using the same principle. ## Create specific interfaces for Elasticsearch and Lucene: {{ElasticsearchSearchScroll}} and {{LuceneSearchScroll}}. ## Implement these interfaces where appropriate. ## Test extensions for Lucene and Elasticsearch. Mainly, check that the scroll has the correct type. See how it's done for SearchResult in {{org.hibernate.search.integrationtest.backend.elasticsearch.ElasticsearchExtensionIT#query}}. # Add getTotalHitCount/getAggregation to APIs if relevant and implement them. # Add getTook/isTimeout to APIs if relevant and implement them. |
|