Skip to content

feat(search): add WITHSCORES support to FT.SEARCH (#2143) - #3432

Open
watersRand wants to merge 1 commit into
redis:masterfrom
watersRand:feat/search-withscores
Open

feat(search): add WITHSCORES support to FT.SEARCH (#2143)#3432
watersRand wants to merge 1 commit into
redis:masterfrom
watersRand:feat/search-withscores

Conversation

@watersRand

@watersRand watersRand commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Description

This PR resolves issue #2143 by adding full support for the WITHSCORES option in FT.SEARCH commands within @redis/search.

Previously, the WITHSCORES flag was either ignored during argument parsing or its returned scores were dropped during reply transformation, causing the relevance score to be missing from search results.

This change ensures that:

  1. WITHSCORES is properly serialized into the command arguments when requested.
  2. The relevance score is correctly parsed and extracted in both RESP2 and RESP3 reply transformers.
  3. The SearchReply type definition is updated to expose an optional score?: number property on each returned document.

Checklist

  • Does npm test pass with this change (including linting)?
  • Is the new or changed code fully tested?
  • Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?

Note

Low Risk
Additive search option and reply parsing with tests; no auth, persistence, or breaking type changes beyond an optional score field.

Overview
Adds end-to-end WITHSCORES support for client.ft.search / FT.SEARCH in @redis/search.

When { WITHSCORES: true } is passed, the command builder now emits the WITHSCORES argument (alongside the default DIALECT). Reply transformers for RESP2 and RESP3 parse relevance scores and attach an optional score on each document in SearchReply; without the flag, behavior stays the same.

Coverage includes argument-serialization tests and an integration test that asserts documents[0].score is a number when scores are requested.

Reviewed by Cursor Bugbot for commit 7741133. Bugbot is set up for automated code reviews on this repo. Configure here.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7741133c79

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +181 to +182
if(typeof reply[i] === 'number' || (typeof reply[i] === 'string' && !isNaN(Number(reply[i])) && Array.isArray(reply[i + 1]))){
score = Number(reply[i++]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Parse RESP2 scores after the document ID

For RESP2, FT.SEARCH ... WITHSCORES returns each row as id, score, fields, but this checks for and consumes a score before consuming the ID. A reply such as [1, 'doc', '1', ['field', 'value']] is therefore parsed with '1' as the document value and then produces a spurious row. Moreover, without WITHSCORES, a numeric document ID in [1, '1', ['field', 'value']] is mistaken for a score, corrupting ordinary RESP2 searches as well; consume the ID first and determine score presence from the requested reply layout rather than the ID's numeric shape.

Useful? React with 👍 / 👎.

VERBATIM?: boolean;
NOSTOPWORDS?: boolean;
INKEYS?: RedisVariadicArgument;
WITHSCORES?: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle scores in the no-content search variant

SEARCH_NOCONTENT.parseCommand reuses Parameters<typeof SEARCH.parseCommand>, so adding WITHSCORES here also exposes and serializes it for ft.searchNoContent. That command's separate RESP2 transformer returns reply.slice(1), treating every score as another document ID, while its RESP3 transformer maps the main result back to IDs and silently discards every score. Callers can therefore request scores through the public type but cannot receive a valid no-content result; either exclude this option from that variant or preserve ID/score pairs in its reply contract.

Useful? React with 👍 / 👎.

Comment on lines +220 to +221
const rawScore = getMapValue(resultMap,['score']);
const score = rawScore !== undefined ? Number(rawScore) : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve configured double type mappings for scores

When a caller uses withTypeMapping({ [RESP_TYPES.DOUBLE]: String }), the RESP3 decoder supplies the score as a string, but this unconditional Number conversion changes it back to a number, so the newly exposed field ignores the client's requested reply mapping. Scored commands normally pass RESP3 doubles through and use transformDoubleReply[2] for RESP2; this field should follow the same pattern and use a DoubleReply-compatible type so the runtime value and inferred type remain consistent.

Useful? React with 👍 / 👎.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 7741133. Configure here.

}
documents.push({
id: reply[i++] as string,
...(score !== undefined ? {score} : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

RESP2 parser misreads scores and IDs

High Severity

The RESP2 transformer treats a numeric string followed by a fields array as a leading score, but default FT.SEARCH replies are id then fields, so numeric keys are consumed as scores. WITHSCORES replies are id, then score, then fields, and withoutDocuments treats the score at reply[2] as missing content, so hits lose fields and extra documents appear.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7741133. Configure here.


if (options?.WITHSCORES) {
parser.push('WITHSCORES');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

NOCONTENT WITHSCORES mixes scores into IDs

Medium Severity

WITHSCORES is on shared FtSearchOptions, so searchNoContent can send it. That command's RESP2 transformer still treats every element after total as an id, so interleaved scores show up as extra document ids. RESP3 drops scores and keeps ids, so the same call disagrees across protocols.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7741133. Configure here.

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.

1 participant