mirror of
https://github.com/qdrant/qdrant-client.git
synced 2026-09-30 09:58:12 -05:00
* fix(local): match core's MMR tie-breaking Local mode ordered MMR results differently from core whenever two candidates tied exactly, on relevance or on MMR score. The MMR score itself was already correct; the selection rules around it were not: * the first point was seeded from `candidate_ids[0]`, i.e. whatever `search` happened to return first, instead of the most relevant candidate; * `np.argmax` picked the *first* maximum, while core's `max_by_key` returns the *last* one on ties; * pending candidates were kept in an order-preserving list, while core holds them in an `IndexSet` and drops the selected one with `swap_remove`, which moves the last candidate into the freed slot and therefore changes the order candidates are visited in - and so which one wins a tie. Reproduce the three rules in `_mmr`. The divergence was spotted on a MAX_SIM multivector field with DOT, but it is specific to neither: plain dense vectors and EUCLID diverge the same way once an exact tie is constructed. The added congruence tests keep relevance scores distinct on purpose: core orders equally relevant candidates by search order, which is not stable, so only ties in the MMR score can be asserted on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor: move utils from collection, move tests * tests: update comments --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
27 lines
970 B
Python
27 lines
970 B
Python
"""Small helpers whose semantics have to match the Rust implementation in core."""
|
|
|
|
|
|
def last_argmax(values: list[float]) -> int:
|
|
"""Index of the maximum value, resolving ties in favour of the *last* maximum.
|
|
|
|
Mirrors Rust's `Iterator::max_by_key`, which core uses to pick MMR candidates.
|
|
`np.argmax` returns the first maximum instead, which orders exact ties differently.
|
|
"""
|
|
best_index = 0
|
|
for index in range(1, len(values)):
|
|
if values[index] >= values[best_index]:
|
|
best_index = index
|
|
return best_index
|
|
|
|
|
|
def swap_remove(items: list[int], position: int) -> int:
|
|
"""Remove and return `items[position]`, moving the last item into the freed slot.
|
|
|
|
Mirrors `IndexSet::swap_remove`, which core uses to drop a selected MMR candidate,
|
|
and which therefore decides the order the remaining candidates are visited in.
|
|
"""
|
|
value = items[position]
|
|
items[position] = items[-1]
|
|
items.pop()
|
|
return value
|