-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Add support for intra-segment search concurrency #13542
Conversation
lucene/core/src/java/org/apache/lucene/search/PointRangeQuery.java
Outdated
Show resolved
Hide resolved
lucene/core/src/test/org/apache/lucene/index/TestSegmentToThreadMapping.java
Outdated
Show resolved
Hide resolved
lucene/core/src/test/org/apache/lucene/search/TestSortRandom.java
Outdated
Show resolved
Hide resolved
for (LeafReaderContext ctx : sortedLeaves) { | ||
if (ctx.reader().maxDoc() > maxDocsPerSlice) { | ||
assert group == null; | ||
groupedLeaves.add(Collections.singletonList(ctx)); | ||
// if the segment does not fit in a single slice, we split it in multiple partitions of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for moving forward on this issue @javanna!
I had a different strategy in mind for slicing the index. With the current implementation, we deduce the number of slices based on a given per-slice doc count. What if the number of slices was given instead? Each segment would not be divided into n slices, but a slice could straddle over a few segments. I think it gives us better control over the concurrency and leaves us with fewer slices per segment while achieving the same level of concurrency, which is better because it reduces the fixed cost of search that we pay per slice per segment. Are there challenges that make that hard to implement?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I honestly don't even remember how I divided segments into multiple slices. That's subject to change and I'd expect that to be the last details to look at. First, make search across segment partitions work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused by the slice vs partition terminology (sorry -- I have not yet looked at the code changes!).
Are we aiming to replace the notion of slices (what IndexSearcher
implements today) with partitions? In which case, it does seem important that a partition should support putting N small segments together into a single quanta (one work unit for the searcher thread).
Or, will we keep the notion of a slice, and it's a list of partitions in a single quanta, in which case slices append multiple (small) things (partitions) together, while partitions break up a single big segment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your feedback @mikemccand ! You are right to be confused, this is a POC and details about abstractions needs refining.
Here is how I got to this place: we have slices that target multiple leaf reader contexts, and I wanted to be able to have a slice still targeting multiple segments, at the same time targeting a partition of a segment. The easier way to do that was to wrap a leaf reader context into a new abstraction that holds a leaf reader context and defines a range of doc ids as well. A partition can still point to the entire segment though.
Technically, a slice is a set of segments, yet a partition of an index, while a leaf reader context partition is a partition of a segment, hence the confusion :) One natural next step could be to fold the range of doc ids into the leaf reader context perhaps, but that is a substantial API change. Otherwise keeping the structure i came up with, but renaming things to clarify the concepts and reduce confusion. Do you have other options in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had worked up a version of this where I modified LeafReaderContext/IndexReaderContext to create a new kind of context that models the range within a segment. I had added interval start/end to LRC, but I suspect a cleaner way would be to make a new thing (IntervalReaderContext or so) and then change APIs to expect IndexReaderContext instead of CompositeReaderContext? If we do it this way it might make it easier to handle some cases like the single-threaded execution you mentioned. But this is more about cleaning up the APIs than making it work and we can argue endlessly about what is neater, so I think your approach to delay such questions makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going back to this, especially on @stefanvodita 's comment above, it may make sense to introduce a different way to control the number of slices, instead of using the existing maxDocsPerSlice argument. It is kind of nice that we can apply the existing value and partition segments accordingly, but that can also be surprising and generate way too many partitions perhaps. Good thing to get further feedback on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I haven't been able to keep up with all the updates on the PR. I still think there's a better partitioning [name up for debate] strategy, that will make searching more efficient.
partitions or intervals (in my view) are a logical division of the index into roughly equal-sized contiguous (in docid space) portions and they overlay the segments arbitrarily
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about that, and I guess it's not incompatible with this strategy but can be built on top of it EG if we have segments of size 100, 30, 20 and we want to have size 25 partitions (maybe we have 6 threads) we can implement that using 4 partitions for the 1st segment, 2 for the second one (of size 25 and 5) and 1 for the last, assigning the last two partitions to the same thread. IE we can still build slices (collections of partitions) on top.
Nice to see this PR @javanna! I know it might be too early to ask(as changes are not yet consolidated), but curious if we have any early benchmarking numbers when intra concurrency is enabled? |
Thank you for tackling this @javanna! This is long overdue ... it's crazy that a fully optimized (
I think queries that do substantial work in |
Thanks for looking into this!
I wonder if it should be on |
Bulk reply to some of the feedback I got: hi @shubhamvishu ,
I have run no benchmarks so far. I focused more on making tests happy, which felt like a good initial step. We do know that there will be a substantial gain when we search force merged indices.
Yes, definitely, I mentioned this above in a comment / TODO. Removing duplicated effort across multiple slices pointing to the same segment would be great, but I focused on making things work to start with. What you mention is a natural next step. It may though end up influencing the design depending on where we decide to de-duplicate work: does the notion of segment partition needs to be visible to consumers, or can it be an implementation detail?
Agreed, that adjustment is a hack. The change in expectation should be reflected in the |
I was referring to
Something like that indeed. And only try to apply the |
I wonder if we should tackle the issue with caching / cloning scorers? We have scorers/scorerSuppliers that do a lot of up-front work when created and we don't want to duplicate that work when two threads search the same segment. OTOH two threads cannot share the same Scorer instance since it has internal state namely the iterator it advances, and maybe more. This seems like the biggest problem to tackle - maybe it would make sense to start there? OTOH we need some kind of execution framework (this PR) that we can use to exercise such changes - I guess it's a kind of leapfrogging problem, gotta start somewhere. Given that, maybe we create a long-running branch we can use to iterate on this in multiple steps before merging to main? As for the Scorer-cloning I poked at it and it seems tricky. I think that Weight can cache Scorer and/or ScorerSupplier, and I guess BulkScorer. Then when asked for a scorer-type object that it already has it can create a shallowCopy(). Shallow-copying should be implemented so as to do the least amount of work to create a new instance that can be iterated independently. This will be up to each Scorer/ScorerSupplier/BulkScorer to figure out -- maybe some of them just return null and let Weight create a new one as it does today. |
for (LeafReaderContext ctx : sortedLeaves) { | ||
if (ctx.reader().maxDoc() > maxDocsPerSlice) { | ||
assert group == null; | ||
groupedLeaves.add(Collections.singletonList(ctx)); | ||
// if the segment does not fit in a single slice, we split it in multiple partitions of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had worked up a version of this where I modified LeafReaderContext/IndexReaderContext to create a new kind of context that models the range within a segment. I had added interval start/end to LRC, but I suspect a cleaner way would be to make a new thing (IntervalReaderContext or so) and then change APIs to expect IndexReaderContext instead of CompositeReaderContext? If we do it this way it might make it easier to handle some cases like the single-threaded execution you mentioned. But this is more about cleaning up the APIs than making it work and we can argue endlessly about what is neater, so I think your approach to delay such questions makes sense.
groupedLeaves.add(Collections.singletonList(ctx)); | ||
// if the segment does not fit in a single slice, we split it in multiple partitions of | ||
// equal size | ||
int numSlices = Math.ceilDiv(ctx.reader().maxDoc(), maxDocsPerSlice); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My mental model of the whole slice/partition/segment/interval concept is: existing physical segments (leaves) divide the index into arbitrary sizes. existing slices (what we have today, not what is called slices in this PR) group segments together. partitions or intervals (in my view) are a logical division of the index into roughly equal-sized contiguous (in docid space) portions and they overlay the segments arbitrarily. Then it is the job of IndexSearcher to map this logical division of work into the underlying physical segments. The main comment here is - let's not confuse ourselves by re-using the word "slice" which already means something else!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
partitions or intervals (in my view) are a logical division of the index into roughly equal-sized contiguous (in docid space) portions and they overlay the segments arbitrarily
I like the idea of calling them virtual segments in contrast to physical segments. Interval (and less so partition) seems to imply the part about contiguity in docid space, but is that a necessary assumption? It's efficient for it to work that way, but maybe we can allow more flexibility in how we determine these partitions / intervals (virtual segments).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are changing the meaning of slices from group of segments to a partition of segment if I understand correctly. I think its ok to change its definition in this PR(we are not adding anything thats conflicting just changing its meaning). But if it really creating confusion maybe renaming to totalNumLeafPartitions
(or something better as naming is hard) could do the job?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the feedback! I'd suggest postponing detailed reviews around how slices are generated, and naming, for now. I don't plan on addressing these details at the moment, I'd rather focus on functionality and high-level API design (where do we expose the range of ids within the different Lucene API, what problems could there be with the current approach, find better solutions for the hack I came up with).
ef8b961
to
790191f
Compare
lucene/core/src/java/org/apache/lucene/search/TotalHitCountCollectorManager.java
Outdated
Show resolved
Hide resolved
lucene/core/src/test/org/apache/lucene/search/TestSynonymQuery.java
Outdated
Show resolved
Hide resolved
// An alternative is to document the requirement to not reuse collector managers across | ||
// searches, that would be a | ||
// breaking change. Perhaps not needed for now. | ||
seenContexts.clear(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the collector manager holds state, either we clean it in-between searches (reduce is the current place where that can be done, but that may change over time), or we declare that collector managers cannot be re-used across multiple searches (potential breaking change).
I am unsure whether it is necessary to make a breaking change for this, perhaps clearing state in reduce is not that bad.
980e966
to
8413125
Compare
@@ -80,7 +80,7 @@ public void test() throws Exception { | |||
// System.out.println("query clone count=" + queryCloneCount); | |||
assertTrue( | |||
"too many calls to IndexInput.clone during TermRangeQuery: " + queryCloneCount, | |||
queryCloneCount < 50); | |||
queryCloneCount <= Math.max(s.getLeafContexts().size(), s.getSlices().length) * 4); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to account for higher number of slices created while running tests, and the number of calls to clone being a factor of that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, shouldn't the multiplicative factor be the number of leaf context partitions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no idea, it varied a lot in my tests and I could not align my expectations with what I observed in practice, I ended up applying a magic factor that seems to be good for all the cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did more digging on this, things were much more predictable before because we only have 20 docs and a couple of segments. 50
was then high enough for all cases.
With intra-segment slicing, we end up with more slices, and the number of clone calls is indeed a factor of the number of slices, but there is more to it which depends on the collected terms, and I am not sure how to calculate that exactly or make it predictable. The test does not seem to do that either so far, it just has a high enough value.
I see two clone calls per partition done when Weight#scorerSupplier
is called. They are from two different places in the IntersectTermsEnum
constructor (line 89 and 98).
I see other two clone calls per partition done when ScorerSupplier#bulkScorer
is called.
That is where my current factor of 4 came from. It seems to work in the vast majority of the cases. I have a seed (F02FB6F046EE8C80
) where I have 9 partitions, but a total of 40 clone calls as part of search. Those additional 4 calls are rather unpredictable, I think that it depends on whether the query is rewritten as a boolean query or not.
After all this analysis though, I realize that I came up with a formula that is more restrictive than the previous fixed value. Here we are searching 9 partitions, and the total becomes 36
(against 40
real value), while previously the fixed limit was 50
. I think I can increase the factor further without worrying too much. Key is that we are currently limiting the max number of partitions per segments to 5. We will need to adapt things in tests if we remove that limit in the future, which is likely.
@@ -211,7 +211,7 @@ private void doTestScores(int totalHitsThreshold) throws IOException { | |||
} | |||
|
|||
public void testBoosts() throws IOException { | |||
doTestBoosts(2); | |||
doTestBoosts(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two changes are to account for single document leaf partitions being created in some scenarios, for which we would not be able to ever early terminate provided 2
as a total hits threshold (as there is no partition with more than one doc).
I did some work on this draft PR and made the fixes around hits counting early termination and caching more future proof. They are now contained to
I am getting some facets failures too, which are predictable, given I have not updated the facets code in any way. I need help figuring out a way forward. List of facet test failures
It would be good to align and decide on terminology: I have no strong opinions. To me, the Next thing I'd like to start thinking about is whether support for intra-segment concurrency requires breaking changes to make, as we have a chance to make those with Lucene 10. I wonder specifically if we are going to be able to hide the notion of leaf partition and the range of doc ids like I currently do, or if that will need to become more of a first class citizen, for instance when we'll want to remove duplicated work across partitions. I need to do a bit more digging to form an opinion on this. |
search( | ||
Arrays.stream(leaves).map(leafReaderContextSlice -> leafReaderContextSlice.ctx).toList(), | ||
weight, | ||
collector); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question about this: this is used in a single test (TestBooleanQueryVisitSubscorers
), which never sets the executor, hence there's always ever going to be a single slice and no parallelism. It is meaningless to do anything better than what I did here, but ScorerIndexSearcher
is public and part of the lucene test framework, so even if we never use the ScorerIndexSearcher(IndexReader, Executor)
constructor internally, there may be external users?
I want to double check that this is worth iterating further on, to support concurrent search across leaf partitions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a check that verifies that only entire segments are scored using this search subclass. This is to cover potential existing subclasses of it. We have no such scenario in our own codebase.
…or) method There's a couple of places in the codebase where we extend IndexSearcher to customize per leaf behaviour, and in order to do that, we need to override the entire search method that loops through the leaves. A good example is ScorerIndexSearcher. Adding a searchLeaf method that provides the per leaf behaviour makes those cases a little easier to deal with. Also, it paves the road for apache#13542 where we will want to replace search(List<LeafReaderContext>, Weight, Collector) with something along the lines of search(LeafReaderContextPartition[], Weight, Collector).
scores = newScores; | ||
} | ||
scores[totalHits] = scorer.score(); | ||
scores[doc] = scorer.score(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gsmiller could you double check the changes I made around facets collection? Tests are green and they were not before, but always good to double check . The ones around scoring here are caused by my recent change to support keepScores
set to true in FacetsCollectorManager
. These I am slightly nervous about because of the potentially big array to keep scores around and then merge them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Below, FacetsCollectorManager
has now logic to merge back results from multiple FacetsCollector
referring to the same LeafReaderContext
, which seems to be the expectation in how we expose results from FacetsCollectorManager
. It is not particularly nice but I thought it requires no changes for users and should not introduce particular overhead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Saw you already merged this but I made some time to look at the facet collecting changes anyway. They look correct to me. I share your concern with keeping around large sparse score arrays, but I don't think keepScores
is a particularly common use-case (could be wrong...) and we can always look into ways of improving this as a follow-up if necessary. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for looking! I noticed that we document the scores array as non-sparse and this change makes it indeed sparse. I think I could have been smarter here and keep it non-sparse. I will try to address this. It feels bad that we allocate such a big array and keep on resizing it as well. We should rather then build in advance knowing how many docs the leaf has. I will see what I can do about this.
@@ -142,7 +142,7 @@ protected Facets buildFacetsResult( | |||
private IndexSearcher getNewSearcher(IndexReader reader) { | |||
// Do not wrap with an asserting searcher, since DrillSidewaysQuery doesn't | |||
// implement all the required components like Weight#scorer. | |||
IndexSearcher searcher = newSearcher(reader, true, false, random().nextBoolean()); | |||
IndexSearcher searcher = newSearcher(reader, true, false, Concurrency.INTER_SEGMENT); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the tests that rely on DrillSideways need to explicitly disable intra-segment concurrency, but can use inter-segment concurrency, hence I introduced a way to distinguish between the two.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I opened #13753 to track this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you everyone for persisting on this change! This is long, long overdue for Lucene. I'm happy we are getting a start in 10.0.0.
Also thank you for the numerous comments/javadocs/MIGRATE entries explaining the change so well. I mostly only reviewed those publicly visible docs / API changes.
lucene/core/src/java/org/apache/lucene/search/CollectorManager.java
Outdated
Show resolved
Hide resolved
lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java
Outdated
Show resolved
Hide resolved
lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java
Outdated
Show resolved
Hide resolved
lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java
Outdated
Show resolved
Hide resolved
lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java
Outdated
Show resolved
Hide resolved
lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java
Outdated
Show resolved
Hide resolved
lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java
Outdated
Show resolved
Hide resolved
lucene/core/src/java/org/apache/lucene/search/TotalHitCountCollectorManager.java
Outdated
Show resolved
Hide resolved
+1, but have we marked these APIs as |
Thanks all for the feedback and reviews! The journey only begins as this PR is getting merged, looking forward to making intra-segment concurrency support shine soon :) |
No indeed, but we have been effectively treating them as unstable API for as long as I've been a committer on this project. Would be good to align our javadocs with our practices. |
We have removed BulkScorer#score(LeafReaderContext, Bits) in main in favour of BulkScorer#score(LeafCollector collector, Bits acceptDocs, int min, int max) as part of apache#13542. This commit deprecates the method in 9.x. Internal usages of it are left untouched as there may be subclasses that override it, which we don't want to break in a minor release.
We have removed BulkScorer#score(LeafReaderContext, Bits) in main in favour of BulkScorer#score(LeafCollector collector, Bits acceptDocs, int min, int max) as part of #13542. This commit deprecates the method in 9.x. Internal usages of it are left untouched as there may be subclasses that override it, which we don't want to break in a minor release.
This PR introduces support for optionally creating slices that target leaf reader context partitions, which allow them to be searched concurrently. This is good to maximize resource usage when searching force-merged indices, or indices with rather big segments, by parallelizig search execution across subsets of segments being searched.
Note: this PR does not affect default generation of slices. Segments can be partitioned by overriding the
IndexSearcher#slices(List<LeafReaderContext>)
method to plug in ad-hoc slices creation. Moreover, the existingIndexSearcher#slices
static method now creates segment partitions when the additionalallowSegmentsPartitions
argument is set totrue
.The overall design of this change is based on the existing search concurrency support that is based on
LeafSlice
andCollectorManager
. A newLeafReaderContextPartition
abstraction is introduced, that holds a reference to aLeafReaderContext
and the range of doc ids it targets. ALeafSlice
noew targets segment partitions, each identified by aLeafReaderContext
instance and a range of doc ids. It is possible for a partition to target a whole segment, and for partitions of different segments to be combined into the same leaf slices freely, hence searched by the same thread. It is not possible for multiple partitions of the same segment to be added to the same leaf slice.Segment partitions are searched concurrently leveraging the existing
BulkScorer#score(LeafCollector collector, Bits acceptDocs, int min, int max)
method, that allows to score a specific subset of documents for a providedLeafCollector
, in place of theBulkScorer#score(LeafCollector collector, Bits acceptDocs)
that would instead score all documents.Changes that require migration
The migrate guide has the following new clarifying items around the contract and breaking changes required to support intra-segment concurrency:
Collector#getLeafCollector
may be called multiple times for the same leaf across distinctCollector
instances created by aCollectorManager
. Logic that relies ongetLeafCollector
being called once per leaf per search needs updating.Scorer
,ScorerSupplier
orBulkScorer
may be requested multiple times for the same leafIndexSearcher#searchLeaf
change of signature to accept the range of doc idsBulkScorer#score(LeafCollector, BitSet)
is removed in favour ofBulkScorer#score(LeafCollector, BitSet, int, int)
IndexSearcher#slices
method changed to take a last boolean argument that optionally enables the creation of segment partitionsTotalHitCountCollectorManager
now requires that an array ofLeafSlice
s, retrieved viaIndexSearcher#getSlices
,is provided to its constructor
Note:
DrillSideways
is the only component that does not support intra-segment concurrency and needs considerable work to do so, due to its requirement that the entire set of docs in a segment gets scored in one go.The default searcher slicing is not affected by this PR, but
LuceneTestCase
now randomly leverages intra-segment concurrency. An additionalnewSearcher
method is added that takes aConcurrency
enum as the last argument in place of theuseThreads
boolean flag. This is important to disable intra-segment concurrency forDrillSideways
related tests that do support inter-segment concurrency but not intra-segment concurrency.Next step
While this change introduces support for intra-segment concurrency, it only sets up the foundations of it. There is still a performance penalty for queries that require segment-level computation ahead of time, such as points/range queries. This is an implementation limitation that we expect to improve in future releases, see #13745.
Additionally, we will need to decide what to do about the lack of support for intra-segment concurrency in
DrillSideways
before we can enable intra-segment slicing by default. See #13753 .More details: issues found
core
I have found the following two issues that revolved around total hits counting, which have now been addressed by adapting
TotalHitCountCollectorManager
's behaviour:IndexSearcher#count /
TotalHitCountCollectorrely on
Weight#count(LeafReaderContext)`, which now gets called multiple times against the same leaf and leads to excessive counting of hits.LRUQueryCache
caches the return value ofWeight#count
. When we execute the same query against the same segment multiple times (as part of a single search call), the first time we do the actual counting for the docs that the first partition holds, and subsequent times we should do the same, count hits in each partition of the same segment instead of retrieving the count from the cache.Both these issues are addressed by modifying
TotalHitCountCollectorManager
to return an enhanced version ofTotalHitCountCollector
that tracks leaves that it sees, and whether they early terminated or not. The goal is to ensure consistency and correctness across multiple times that a leaf collector for the same leaf is requested.If a partition for a certain leaf early terminates, all of the other partitions of that same segment should also early terminate and not contribute to the total hits count. If a partition for a certain leaf computes hit counts, all of the other partitions of that same segment should also compute hit counts ( as opposed to potentially being early terminated or retrieved from the cache, which only work globally per leaf, and not per leaf partition)
I have also adjusted
CheckHits
to not go outside of the bounds of the doc id range of the current slice.I fixed
TestSortRandom
s customScorerSupplier
to not rely on a scorer supplier being pulled once and only once perLeafReaderContext
facet
I have found additional issues in the facet module:
DrillSideways
does not support scoring via the provided range of doc ids, it throws an exception whenever the range is not0
andInteger.MAX_VALUE
. This is not easy to solve. I have disabled intra-segment concurrency in tests whenDrillSideways
is used.FacetsCollectorManager
andRandomSampligFacetsCollector#createManager
returned multipleMatchingDocs
instances that pointed to the sameLeafReaderContext
which causedTestStringValueFacetCounts
failures. I added an extra round of de-duplication in the reduce method that merges back multiple matching docs from multiple partitions of the same segment into a single per-segment instance.Closes #9721