Skip to content

RFC-52: Add score fusion (revision 14)#160

Draft
anirudhlakhotia wants to merge 2 commits into
couchbaselabs:masterfrom
anirudhlakhotia:rfc-52-score-fusion
Draft

RFC-52: Add score fusion (revision 14)#160
anirudhlakhotia wants to merge 2 commits into
couchbaselabs:masterfrom
anirudhlakhotia:rfc-52-score-fusion

Conversation

@anirudhlakhotia

Copy link
Copy Markdown

No description provided.

Comment thread rfc/0052-sdk3-full-text-search.md Outdated
Comment thread rfc/0052-sdk3-full-text-search.md Outdated
Each strategy's tuning parameters are exposed in the same way the SDK exposes traditional FTS query parameters (fluent-style methods on the object, or an options block, following the SDK's existing convention):

* `rankConstant` (`uint32`, RRF only). Sent as `params.score_rank_constant`. The server defaults it to `60`. Not exposed on `rsf()`.
* `windowSize` (`uint32`). Sent as `params.score_window_size`. How many results per list are considered for fusion. The server defaults it to the request `size` (`limit`).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It does? Weird. Seems the whole point is to have a significantly larger window than the final result size, so you know you're fusing a good set of results from both sources.

Comment thread rfc/0052-sdk3-full-text-search.md Outdated
* `rankConstant` (`uint32`, RRF only). Sent as `params.score_rank_constant`. The server defaults it to `60`. Not exposed on `rsf()`.
* `windowSize` (`uint32`). Sent as `params.score_window_size`. How many results per list are considered for fusion. The server defaults it to the request `size` (`limit`).

`rrf()` exposes both `rankConstant` and `windowSize`; `rsf()` exposes only `windowSize`. As with `numCandidates` (`k`) and `knn_operator`, the SDK names differ from the JSON field names. In SDKs where a single class exposes both `rrf()` and `rsf()` rather than separate types, `rankConstant` cannot be hidden on `rsf()`; if it is set there, the SDK must ignore it and not send it.

@programmatix programmatix Jul 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

where a single class exposes both rrf() and rsf()

I would explicitly ban that here. It already doesn't really work, and won't scale if more scoring options get added later.

Comment thread rfc/0052-sdk3-full-text-search.md Outdated

* Cluster does not support fusion. Detectable only through the cluster capability (see FeatureNotAvailable handling). Once that capability exists, the SDK raises `FeatureNotAvailableException` before sending; until then, the request silently degrades to additive scoring with no error.

* Non-hybrid request (only an FTS query, or only a vector search). The SDK does not check for this; it sends the `score` field regardless, and the request is a silent no-op rather than an error.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

While not a blocker, personally I would InvalidArgument this. It just helps guide the user to the correct use of a complex FTS API.

Comment thread rfc/0052-sdk3-full-text-search.md Outdated
## Score fusion
This is a feature being added to Couchbase Server 8.1 in the FTS service. It extends hybrid search, where a traditional FTS query is combined with one or more vector queries.

As with vector search, all SDK additions related to it should be annotated with the platform equivalent of @Stability.Volatile, as changes may be required following user feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

N.B. Vector search is no longer volatile, the section above probably needs to be updated as well.

Do we still want search fusion to be volatile?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Based on the discussion below for the ns_server cap, I think it would make sense to keep search fusion volatile at least until the ns_server cluster cap is added. When we implement the cluster cap check it will technically be a breaking change, so if it's 'volatile' that would be acceptable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yup this makes sense. The cap check landing later is a breaking change (silent fallback becomes an exception), and volatile lets us add it without a deprecation cycle

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Filed MB-72837 for this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Still unsure about Volatile to be honest, mainly because of the "boost" reuse
But otherwise resolved with the new ns_server cluster cap

What does everyone think about this?

Comment thread rfc/0052-sdk3-full-text-search.md Outdated
Comment thread rfc/0052-sdk3-full-text-search.md Outdated
Comment thread rfc/0052-sdk3-full-text-search.md Outdated

If neither is set, the SDK sends no `score` field and the server keeps its existing additive-boost behaviour, so existing queries are unaffected.

A unified `scoring(...)` setter was rejected: `disableScoring` is already shipped across the SDKs, so unifying would force a deprecation everywhere.

@dnault dnault Jul 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A unified scoring(...) setter sounds great to me (at least in Java and Kotlin and any other language where it makes sense). Is there a link to a discussion somewhere that might change my mind?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OOI what would you put in there?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OOI what would you put in there?

@programmatix When I wrote that comment, I was thinking it would follow the usual Scala/Kotlin pattern:

scoring(SearchScoring.none()  / SearchScoring.somethingElse())

but I'm backing away from this, since it doesn't really solve the problem of invalid arguments.

For Kotlin, we can make it impossible to specify an invalid arg; the scoring fusion can be an optional parameter of the method that builds the hybrid query. For example:

cluster.search(
    indexName = "myIndex",
    spec = SearchSpec.mixedMode(
        searchQuery = SearchSpec.queryString("foo"),
        vectorQuery = SearchSpec.vector("someField", vector),
        scoreFusion = SearchScoreFusion.rsf(windowSize = ...)   // <-- HERE!
    )
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nb I like a unified scoring method, in discussions with @anirudhlakhotia and think this is going to make it in.
Something like SearchOptions.searchOptions().scoring(SearchScoring.reciprocalRank())

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

We would need to soft deprecate disableScoring across all SDKs though, but this is clean and lets us extend the method to other non-fusion scoring methods if we have any in the future


A unified `scoring(...)` setter was rejected: `disableScoring` is already shipped across the SDKs, so unifying would force a deprecation everywhere.

### The boost field as a fusion weight

@dnault dnault Jul 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does an SDK developer need to know the info in this section to implement the feature, or is it just background information?

Copilot AI 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.

Pull request overview

Adds an RFC section describing “score fusion” for hybrid (FTS + vector) search, including the wire encoding, SDK API surface (SearchScoreFusion), and option placement on SearchOptions.

Changes:

  • Documented score-fusion strategies (RRF/RSF), tuning parameters, and JSON encoding (score + params).
  • Specified the SDK API shape (SearchScoreFusion) and SearchOptions.scoreFusion(...) behavior and validation.
  • Updated the RFC revision history to include Revision #14.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +922 to +931
{
"//": "FTS query; its top-level boost is its fusion weight.",
"query": { "match": "wireless headphones", "field": "desc", "boost": 2.0 },
"//": "Vector query; its boost is the vector side's fusion weight.",
"knn": [ { "field": "emb", "vector": [ ... ], "k": 200, "boost": 1.0 } ],
"//": "Fusion strategy: new values on the existing score field.",
"score": "rrf",
"//": "Tuning params; omitted entirely when unset.",
"params": { "score_rank_constant": 60, "score_window_size": 200 }
}
Comment thread rfc/0052-sdk3-full-text-search.md Outdated
### Fusion strategies
Score fusion supports two strategies:
* Reciprocal Rank Fusion (`"rrf"`). Merges by rank rather than raw score, and works well with the server defaults. The recommended strategy.
* Relative Score Fusion (`"rsf"`). Merges by normalised score rather than rank.
* April 2nd, 2025 - Revision #13 (by Jared Casey)
* Added prefilter option to vector search.

* TBD, 2026 - Revision #14 (by Anirudh Lakhotia)

Copilot AI 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.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

rfc/0052-sdk3-full-text-search.md:1191

  • The revision history entry uses "TBD" for the date. This makes the document’s change log ambiguous once merged; please replace it with an actual date (or omit the entry until the date is known).
* TBD, 2026 - Revision #14 (by Anirudh Lakhotia)

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.

5 participants