-
Notifications
You must be signed in to change notification settings - Fork 968
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reuse BitSet when there are deleted documents in the index instead of creating new BitSet #12857
base: main
Are you sure you want to change the base?
Conversation
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 working on this @Pulkitg64 . I went through the change but I didn't understand how are we not reusing the bitset in the current approach. We do wrap the BitSetIterator
with a FilteredDocIdSetIterator
when there are deleted docs right which would eventually use the bitset to advance the inner iterator(See this).
Instead in this PR we are unnecessarily always wrapping the BitSetIterator
with a FilteredDocIdSetIterator
even when there are no deletions which is just an added overhead and is avoided in the current code. Additionally, I find the current approach(separate function) more cleaner too. WDYT?
Bits acceptDocs = | ||
new Bits() { | ||
@Override | ||
public boolean get(int index) { | ||
return liveDocs != null ? liveDocs.get(index) & bitSet.get(index) : bitSet.get(index); | ||
} | ||
|
||
@Override | ||
public int length() { | ||
return bitSet.cardinality(); | ||
} | ||
}; |
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.
Why are we creating this when we could directly do int cost = bitSet.cardinality();
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.
Here we are implementing the Bits
interface that's why we need to override both functions.
Thanks @shubhamvishu for taking a look.
Sorry! I think, I should have used different title for this PR. The part in the current approach, which I am trying to optimize is that when the iterator is 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.
Interesting! So instead of greedily collecting all matching + live docs into a BitSet
, we're saving on the filter collection step at the cost of running #approximateSearch
with an upper bound of visitLimit
Can you run some benchmarks for different filters to measure this tradeoff?
@Override | ||
public int length() { | ||
return bitSet.cardinality(); | ||
} |
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.
length()
is more like the maximum doc you can request, this should be bitSet.length()
?
new Bits() { | ||
@Override | ||
public boolean get(int index) { | ||
return liveDocs != null ? liveDocs.get(index) & bitSet.get(index) : bitSet.get(index); |
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.
nit: Can we make this cleaner by return (liveDocs == null || liveDocs.get(index)) && bitSet.get(index)
?
? ((BitSetIterator) iterator).getBitSet() | ||
: BitSet.of(iterator, maxDoc); | ||
Bits acceptDocs = | ||
new Bits() { |
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.
Perhaps we can wrap the BitSet
in a new Bits
only for the case we're trying to optimize (when iterator instanceof BitSetIterator
) -- not changing the BitSet.of
flow like @shubhamvishu also mentioned?
} | ||
}; | ||
|
||
int cost = acceptDocs.length(); |
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.
The cost
here determines what limit to set for #approximateSearch
If we use acceptDocs.length()
, this will be equal to maxDoc
(and always complete graph search without falling back to exact search, even when we want to..)
Perhaps this should be acceptDocs.cardinality()
?
@kaivalnp We could use the On separate note, I'm thinking if there is some use case where we don't require to know this cost upfront and directly go for approximate search only for instance. Currently, this optimization only kicks in when the iterator is of |
Is our goal memory usage or speed? We could use I am honestly not sure if the implementation here is any faster than just creating the bit set upfront and checking it. During search, you now have to check two bitsets now instead of one. If the filter happens to be |
Broad feedback: any "optimizations" without benchmarking aren't optimizations, they are just guesses. I am curious to see if this helps CPU usage in anyway. I could see it helping memory usage. |
This PR has not had activity in the past 2 weeks, labeling it as stale. If the PR is waiting for review, notify the dev@lucene.apache.org list. Thank you for your contribution! |
I don't fully understand this change, but it looks like it is stalled on proving it shows lower CPU and/or heap/GC load? Could we benchmark this change using luceneutil? It's able to create vector indices that have X% deletions and then run |
Thanks @mikemccand for the pointers. Will try to run benchmarks on this change. |
This PR has not had activity in the past 2 weeks, labeling it as stale. If the PR is waiting for review, notify the dev@lucene.apache.org list. Thank you for your contribution! |
Description
Fixes issue: #12414
Before this change we were creating new BitSet every time when there are deletions in the index with use of matched Docs and Live Docs, which required iteration over all matched docs which is a time consuming process with linear time complexity. This is not required when the iterator is of BitSetIterator instance.
With this change we have wrapped matching Docs and live Docs under single Bits instance which can be directly passed for HNSW search. So, now during HNSW Search when a node is explored, we will check if the doc is accepted or not by checking bits of both matchedDocs and liveDocs which is constant time operation. Cost (
int cost = acceptDocs.length()
) of the new acceptedDocs is not exactly accurate but gives an upper bound on number, as it is not considering live-docs count.