From 73a3f64379f459155b6709261a66f25e4d7164e9 Mon Sep 17 00:00:00 2001 From: Aarti Sonigra <23amtics292@gmail.com> Date: Sat, 6 Jun 2026 18:59:25 +0530 Subject: [PATCH 1/2] fix(search): support exact match with params --- packages/search/lib/commands/SEARCH.spec.ts | 5 ++- packages/search/lib/commands/SEARCH.ts | 43 ++++++++++++++------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/search/lib/commands/SEARCH.spec.ts b/packages/search/lib/commands/SEARCH.spec.ts index 758ac2a1b2..277d20788c 100644 --- a/packages/search/lib/commands/SEARCH.spec.ts +++ b/packages/search/lib/commands/SEARCH.spec.ts @@ -1,4 +1,5 @@ import { strict as assert } from 'node:assert'; + import testUtils, { GLOBAL } from '../test-utils'; import SEARCH from './SEARCH'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; @@ -260,7 +261,9 @@ describe('FT.SEARCH', () => { number: 1 } }), - ['FT.SEARCH', 'index', 'query', 'PARAMS', '6', 'string', 'string', 'buffer', Buffer.from('buffer'), 'number', '1', 'DIALECT', DEFAULT_DIALECT] + + ['FT.SEARCH', 'index', 'query', 'PARAMS', '3', 'string', 'string', 'buffer', Buffer.from('buffer'), 'number', '1', 'DIALECT', DEFAULT_DIALECT] + ); }); diff --git a/packages/search/lib/commands/SEARCH.ts b/packages/search/lib/commands/SEARCH.ts index 8f8da9d9bc..fc4791e1e8 100644 --- a/packages/search/lib/commands/SEARCH.ts +++ b/packages/search/lib/commands/SEARCH.ts @@ -8,24 +8,37 @@ import { getMapValue, mapLikeToObject, mapLikeValues, parseDocumentValue, parseS export type FtSearchParams = Record; export function parseParamsArgument(parser: CommandParser, params?: FtSearchParams) { - if (params) { - parser.push('PARAMS'); - - const args: Array = []; - for (const key in params) { - if (!Object.hasOwn(params, key)) continue; - - const value = params[key]; - args.push( - key, - typeof value === 'number' ? value.toString() : value - ); - } - - parser.pushVariadicWithLength(args); + if (!params) return; + + parser.push('PARAMS'); + + // FT.SEARCH expects: PARAMS ... + // Where is the *number of pairs*. + // + // The previous implementation incorrectly used `pushVariadicWithLength(args)` + // which sets the length to the number of *arguments*, not the required + // number of pairs. This causes exact-match queries like `@field:"$x"` + // to bind parameters incorrectly on some Redis Stack/FT versions. + const pairArgs: Array = []; + let pairs = 0; + + for (const key in params) { + if (!Object.hasOwn(params, key)) continue; + + const value = params[key]; + pairArgs.push( + key, + typeof value === 'number' ? value.toString() : value + ); + pairs++; } + + // is the number of pairs, so it must be `pairs`. + parser.push(pairs.toString()); + parser.pushVariadic(pairArgs); } + export interface FtSearchOptions { VERBATIM?: boolean; NOSTOPWORDS?: boolean; From f9228b9eb0d499c8ee55a768b1b74c02ca394b9f Mon Sep 17 00:00:00 2001 From: Aarti Sonigra <23amtics292@gmail.com> Date: Thu, 18 Jun 2026 16:24:20 +0530 Subject: [PATCH 2/2] fix(cluster): resolve sharded pubsub migration event handling (#3311) --- packages/client/lib/client/index.ts | 4 +++ packages/client/lib/cluster/cluster-slots.ts | 31 +++++++++++--------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index ce04064068..cde71a2b66 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -748,6 +748,10 @@ export default class RedisClient< return new RedisCommandsQueue( this.#options.RESP ?? DEFAULT_RESP, this.#options.commandsQueueMaxLength, + (channel, listeners) => { + this.emit('sharded-channel-moved', channel, listeners); + this.emit('server-sunsubscribe', channel, listeners); + }, (channel, listeners) => this.emit('sharded-channel-moved', channel, listeners), clientId ); diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index 907a6b378c..3a5e93aae1 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -946,20 +946,21 @@ export default class RedisClusterSlots< } async #initiateShardedPubSubClient(master: MasterNode) { - const client = this.#createClient(master, false) - .on('server-sunsubscribe', async (channel, listeners) => { - try { - await this.rediscover(client); - const redirectTo = await this.getShardedPubSubClient(channel); - await redirectTo.extendPubSubChannelListeners( - PUBSUB_TYPE.SHARDED, - channel, - listeners - ); - } catch (err) { - this.#emit('sharded-shannel-moved-error', err, channel, listeners); - } - }); + const client = this.#createClient(master, false); + + client.on('server-sunsubscribe', async (channel, listeners) => { + try { + await this.rediscover(client); + const redirectTo = await this.getShardedPubSubClient(channel); + await redirectTo.extendPubSubChannelListeners( + PUBSUB_TYPE.SHARDED, + channel, + listeners + ); + } catch (err) { + this.#emit('sharded-channel-moved-error', err, channel, listeners); + } + }); master.pubSub = { client, @@ -977,6 +978,8 @@ export default class RedisClusterSlots< return master.pubSub.connectPromise!; } + + async executeShardedUnsubscribeCommand( channel: string, unsubscribe: (client: RedisClientType) => Promise