Skip to content

[ENH] RrfRank.Max(0): all-zero collapse on non-positive baselines — add client-side guard or server signaling #498

@tazarov

Description

@tazarov

Observed behavior

Captured from the deferred t.Logf observation in TestCloudClientSearchRRFArithmetic/Max_0 (Phase 21.1 Pass 1 run against Chroma Cloud):

pass1 degenerate Max_0:  err=<nil> IDs=[[1 2 3 4 5]] Scores=[[0 0 0 0 0]]

The server returned searchErr=nil, an outer IDs slice with exactly one inner entry [1 2 3 4 5] in default insertion order, and an outer Scores slice with one inner entry where every score is exactly 0.0.

Expected behavior

EITHER:

  1. Client-side guard (preferred): RrfRank.Max(FloatOperand(0.0)) should reject at construction time (or return an ErrorRank) because max(x, 0) is provably meaningless on RRF's non-positive fusion output. The client has full visibility into this composition and can detect it statically.
  2. Server-side signaling: If the client accepts the composition, the server should signal "degenerate result" via a response header (X-Chroma-Degenerate: all_tied) or a structured warning so the Go client can propagate it to the caller instead of returning an all-tied result set that looks like a valid ranking.

Currently the client accepts rrf.Max(FloatOperand(0.0)) without complaint and the server returns an all-tied result set with insertion-order IDs — this looks like a normal search response from the caller's perspective but carries no ranking information.

Wire-level explanation

From .planning/phases/21.1-rrf-cloud-integration-test-coverage-including-arithmetic-com/21.1-RESEARCH.md § Pitfall 4:

RrfRank.MarshalJSON() in pkg/api/v2/rank.go:1217 auto-negates the fusion score before serializing:

// Negate (RRF gives higher scores for better, Chroma needs lower for better)
result := rrfSum.Negate()
return result.MarshalJSON()

So rrf.Max(FloatOperand(0.0)) on the wire evaluates max(-rrf_sum, 0). Because -rrf_sum <= 0 for every doc when the raw RRF sum is non-negative (which is always true for RRF — sum(1/(k+rank)) >= 0), max(-rrf_sum, 0) = 0 for every doc. Every score collapses to 0.0, producing an all-tied result set with no ranking information.

Classification

Client-API-contract defect per the L1 rubric in .planning/phases/21.1-rrf-cloud-integration-test-coverage-including-arithmetic-com/21.1-REVIEWS.md § Action Item L1. The client could reject rrf.Max(FloatOperand(0.0)) at construction time as a mathematically meaningless composition on RRF's non-positive fusion output. Alternatively (or additionally), the server could signal the degenerate state via a response header.

This is tagged [ENH] (not [BUG]) because:

  • The server is mathematically correct — max(x, 0) = 0 for x <= 0 is the definition.
  • The Go client accepted a composition that it had enough information to reject at construction.
  • The remediation is "add a client-side guard" or "add a server-side warning channel", both of which are enhancements to the existing contract, not fixes to broken behavior.

Surfaced by

pkg/api/v2/client_cloud_test.go :: TestCloudClientSearchRRFArithmetic/Max_0 (added in Phase 21.1 Pass 1; pinned as a regression assertion in Pass 2).

Repro

export CHROMA_API_KEY=<your key>
export CHROMA_DATABASE=<your database>
export CHROMA_TENANT=<your tenant>
go test -tags="basicv2 cloud" -v -run "TestCloudClientSearchRRFArithmetic/Max_0" ./pkg/api/v2/...

The test PASSES today because Pass 2 pins the observed all-zero collapse as a regression assertion (per-score require.Equal(float64(0), s, ...) loop and require.Equal([]DocumentID{"1","2","3","4","5"}, sr.IDs[0])). When the client-side guard lands, the pin will flip as part of the enhancement phase.

Scope

Phase 21.1 pins this behavior as a regression assertion. Client-side guards and server-side signaling are deferred per D-20 (Phase 21.1 does not fold fixes into its own scope — each discovered contract gap becomes its own phase).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions