Files
qdrant-client/qdrant_client/local/utils.py
GeorgeandClaude Opus 5 df683c7524 fix(local): match core's MMR tie-breaking (#1402)
* 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>
2026-09-04 13:40:43 +07:00

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