Skip to content

fix(query): allow rerank on every search operator - #608

Open
dudanogueira wants to merge 3 commits into
mainfrom
fix/rerank-all-operators
Open

fix(query): allow rerank on every search operator#608
dudanogueira wants to merge 3 commits into
mainfrom
fix/rerank-all-operators

Conversation

@dudanogueira

Copy link
Copy Markdown

Motivation

Two independent gaps in the v6 rerank support, both reported against 6.3.1 / Weaviate 1.39.0:

  • v6: rerank cannot be used with BM25, Hybrid or FetchObjects #603rerank(...) was reachable only from the near* operators. BM25, Hybrid and FetchObjects could not be reranked through the client, even though SearchRequest.rerank is a top-level proto field and the server applies it identically for all operators (the issue includes a hand-marshalled BM25+rerank request whose scores match GraphQL exactly). Java was the only client missing from the "Rerank keyword search results" tab set in the docs.
  • v6: rerank score is never unmarshalled from the search reply #604 — the server returns a rerank score per object (MetadataResult.rerank_score) and per group (GroupByResult.rerank), and the client read neither. A reranked search arrived correctly ordered with the number that produced the order discarded, and no public way to recover it short of re-reading the reply through GrpcTransport internals.

Approach

#603: move rerank(Rerank) from BaseVectorSearchBuilder up to BaseQueryOptions.Builder, which every operator builder extends, and marshal it from BaseQueryOptions.appendTo. The alternative — duplicating the setter onto Bm25.Builder, Hybrid.Builder and FetchObjects.Builder — was rejected: rerank is genuinely a common query option, not a per-operator one, and duplication would have kept three more copies of the same marshalling in sync.

Consequence: the near* records no longer carry their own Rerank component. To keep operator.rerank() readable on every operator rather than only the ones that used to have the component, QueryOperator.rerank() now defaults to reading it off common() instead of returning null. QueryRequest.marshal correspondingly drops its separate operator.rerank().appendTo(...) call, since the common options now marshal it.

#604: add rerankScore to QueryMetadata (per object) and to QueryResponseGroup / GenerativeResponseGroup (per group). The per-object value is set only when rerank_score_present is true0.0 is what the dummy reranker returns for a match, so the value alone cannot distinguish "not reranked" from "reranked with score 0". The type is Double, not float, so absence is representable as null.

Generative search reuses the query operators, so it accepts rerank the same way; per-object scores there were already covered because every unmarshal path funnels through QueryResponse.unmarshalResultObject. Only the group-level score needed the separate fix in GenerativeResponseGrouped.

Key areas for review

  • BaseQueryOptions.java — the moved rerank component and its marshalling in appendTo; check the record component ordering matches the canonical constructor call
  • QueryOperator.rerank() — now derives from common(); confirm no operator has a non-null common() that could diverge from what it marshals
  • QueryRequest.marshal — the removed rerank append; make sure nothing marshals rerank twice or not at all
  • QueryResponse.unmarshalResultObject — the getRerankScorePresent() guard, the whole point of v6: rerank score is never unmarshalled from the search reply #604's edge case
  • GenerativeResponseGrouped / QueryResponseGroupedhasRerank() guard for the group score

Risks and mitigations

  • Record shape changes (see Breaking changes) — accessors are preserved; only canonical constructors change arity.
  • Double-marshalling rerank: QueryRequest.marshal used to append rerank separately; the old path is removed in the same commit that adds the new one, and RerankTest.test_rerankIsMarshalled asserts the marshalled request across six operators.
  • Silently swallowing a legitimate 0.0 score: covered explicitly by test_rerankScoreZeroIsNotAbsent, which asserts 0.0 and null are distinguished within one reply.

Testing

Unit tests (no server needed):

  • RerankTest — parameterised over bm25, hybrid, fetchObjects, nearText, nearVector, nearObject: rerank marshals to the top-level proto field and reads back off the operator. Plus: no rerank by default, rerank without a query (query is optional), score unmarshalling, and the 0.0-vs-absent case.
  • GenerativeRerankTest — group-level score unmarshalled, absent without rerank, and per-object score present on grouped generative results.

Integration (SearchITest, DummyReranker):

  • The existing nearText rerank test extended to assert bm25, hybrid and fetchObjects all accept the same Rerank and come back with a non-null score on every object.
  • test_rerankScoreIsReturned — a reranked fetchObjects yields a score on every object; the same query without rerank leaves it null.

Locally: full unit suite green (397 tests), and SearchITest green against a real container (33 run, 2 skipped by version gate).

Breaking changes

Source/binary compatible for normal builder-based usage, but three public record shapes changed:

  • NearText, NearVector, NearObject, NearImage, NearAudio, NearVideo, NearDepth, NearThermal, NearImu — the Rerank rerank component is gone from the canonical constructor. The rerank() accessor still exists via QueryOperator's default method.
  • QueryMetadata — new trailing Double rerankScore component.
  • QueryResponseGroup / GenerativeResponseGroup — new Double rerankScore component before objects.

Anyone calling those canonical constructors directly or destructuring them in a record pattern will need to adjust. Builders and accessors are unaffected.

Closes #603
Closes #604

🤖 Generated with Claude Code

https://claude.ai/code/session_01WmY5dAGWCccWDoqkKNC2JU

dudanogueira and others added 3 commits August 24, 2026 20:44
rerank(Rerank) was declared on BaseVectorSearchBuilder, so only the near*
searches could be reranked. BM25, Hybrid and FetchObjects could not, even
though rerank is a top-level field of the search request and the server
applies it the same way for all of them.

Move the option to BaseQueryOptions, which every operator builder extends,
and marshal it from BaseQueryOptions.appendTo. The near* records no longer
carry their own rerank component; QueryOperator.rerank() now reads it off
the common options, so it stays readable on every operator.

Closes #603

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmY5dAGWCccWDoqkKNC2JU
The server returns a rerank score per object (MetadataResult.rerank_score)
and per group (GroupByResult.rerank), but neither was read: a reranked
search arrived correctly ordered with the number that produced the order
missing, and no way to get it short of re-reading the reply.

Add rerankScore to QueryMetadata and QueryResponseGroup and populate both.
The score is only set when the reply says it is present -- 0.0 is what the
dummy reranker returns for a match, so the value alone cannot distinguish
"not reranked" from "reranked with score 0".

Closes #604

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmY5dAGWCccWDoqkKNC2JU
The generative search reuses the query operators, so it accepts rerank
the same way -- but GenerativeResponseGrouped dropped the group-level
score that QueryResponseGrouped reads, leaving that one path unable to
tell how the groups were ranked.

Per-object scores were already covered: every unmarshal path funnels
through QueryResponse.unmarshalResultObject.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmY5dAGWCccWDoqkKNC2JU

@orca-security-eu orca-security-eu Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orca Security Scan Summary

Status Check Issues by priority
Passed Passed Infrastructure as Code high 0   medium 0   low 0   info 0 View in Orca
Passed Passed SAST high 0   medium 0   low 0   info 0 View in Orca
Passed Passed Secrets high 0   medium 0   low 0   info 0 View in Orca
Passed Passed Vulnerabilities high 0   medium 0   low 0   info 0 View in Orca

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v6: rerank score is never unmarshalled from the search reply v6: rerank cannot be used with BM25, Hybrid or FetchObjects

1 participant