From d54e848055afe1ba941931048087fb043a02aa58 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:33:40 +0300 Subject: [PATCH 1/7] feat(options): panic when options are nil (#3363) Client creation should panic when options are nil. --- osscluster.go | 3 +++ redis.go | 3 +++ redis_test.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ ring.go | 3 +++ sentinel.go | 11 +++++++++++ universal.go | 4 ++++ 6 files changed, 75 insertions(+) diff --git a/osscluster.go b/osscluster.go index 3b46cbe3..c0278ed0 100644 --- a/osscluster.go +++ b/osscluster.go @@ -924,6 +924,9 @@ type ClusterClient struct { // NewClusterClient returns a Redis Cluster client as described in // http://redis.io/topics/cluster-spec. func NewClusterClient(opt *ClusterOptions) *ClusterClient { + if opt == nil { + panic("redis: NewClusterClient nil options") + } opt.init() c := &ClusterClient{ diff --git a/redis.go b/redis.go index e0159294..f50df568 100644 --- a/redis.go +++ b/redis.go @@ -661,6 +661,9 @@ type Client struct { // NewClient returns a client to the Redis Server specified by Options. func NewClient(opt *Options) *Client { + if opt == nil { + panic("redis: NewClient nil options") + } opt.init() c := Client{ diff --git a/redis_test.go b/redis_test.go index 7d9bf1ce..80e28341 100644 --- a/redis_test.go +++ b/redis_test.go @@ -727,3 +727,54 @@ var _ = Describe("Dialer connection timeouts", func() { Expect(time.Since(start)).To(BeNumerically("<", 2*dialSimulatedDelay)) }) }) +var _ = Describe("Client creation", func() { + Context("simple client with nil options", func() { + It("panics", func() { + Expect(func() { + redis.NewClient(nil) + }).To(Panic()) + }) + }) + Context("cluster client with nil options", func() { + It("panics", func() { + Expect(func() { + redis.NewClusterClient(nil) + }).To(Panic()) + }) + }) + Context("ring client with nil options", func() { + It("panics", func() { + Expect(func() { + redis.NewRing(nil) + }).To(Panic()) + }) + }) + Context("universal client with nil options", func() { + It("panics", func() { + Expect(func() { + redis.NewUniversalClient(nil) + }).To(Panic()) + }) + }) + Context("failover client with nil options", func() { + It("panics", func() { + Expect(func() { + redis.NewFailoverClient(nil) + }).To(Panic()) + }) + }) + Context("failover cluster client with nil options", func() { + It("panics", func() { + Expect(func() { + redis.NewFailoverClusterClient(nil) + }).To(Panic()) + }) + }) + Context("sentinel client with nil options", func() { + It("panics", func() { + Expect(func() { + redis.NewSentinelClient(nil) + }).To(Panic()) + }) + }) +}) diff --git a/ring.go b/ring.go index 8f2dd3c4..555ea2a1 100644 --- a/ring.go +++ b/ring.go @@ -523,6 +523,9 @@ type Ring struct { } func NewRing(opt *RingOptions) *Ring { + if opt == nil { + panic("redis: NewRing nil options") + } opt.init() hbCtx, hbCancel := context.WithCancel(context.Background()) diff --git a/sentinel.go b/sentinel.go index f5b9a52d..cfc848cf 100644 --- a/sentinel.go +++ b/sentinel.go @@ -224,6 +224,10 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions { // for automatic failover. It's safe for concurrent use by multiple // goroutines. func NewFailoverClient(failoverOpt *FailoverOptions) *Client { + if failoverOpt == nil { + panic("redis: NewFailoverClient nil options") + } + if failoverOpt.RouteByLatency { panic("to route commands by latency, use NewFailoverClusterClient") } @@ -313,6 +317,9 @@ type SentinelClient struct { } func NewSentinelClient(opt *Options) *SentinelClient { + if opt == nil { + panic("redis: NewSentinelClient nil options") + } opt.init() c := &SentinelClient{ baseClient: &baseClient{ @@ -828,6 +835,10 @@ func contains(slice []string, str string) bool { // NewFailoverClusterClient returns a client that supports routing read-only commands // to a replica node. func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient { + if failoverOpt == nil { + panic("redis: NewFailoverClusterClient nil options") + } + sentinelAddrs := make([]string, len(failoverOpt.SentinelAddrs)) copy(sentinelAddrs, failoverOpt.SentinelAddrs) diff --git a/universal.go b/universal.go index 46d5640d..a1ce17ba 100644 --- a/universal.go +++ b/universal.go @@ -267,6 +267,10 @@ var ( // a ClusterClient is returned. // 4. Otherwise, a single-node Client is returned. func NewUniversalClient(opts *UniversalOptions) UniversalClient { + if opts == nil { + panic("redis: NewUniversalClient nil options") + } + switch { case opts.MasterName != "" && (opts.RouteByLatency || opts.RouteRandomly || opts.IsClusterMode): return NewFailoverClusterClient(opts.Failover()) From 6d788cbcd40d52657733bb56f91b7401ba50447b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 08:34:14 +0300 Subject: [PATCH 2/7] chore(deps): bump golangci/golangci-lint-action from 7.0.0 to 8.0.0 (#3366) Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 7.0.0 to 8.0.0. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/v7.0.0...v8.0.0) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/golangci-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 5e0ac1d0..8d4135d5 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v7.0.0 + uses: golangci/golangci-lint-action@v8.0.0 with: verify: true From 4cedb5c0370444e739f196a337a3ed6cb1f05c43 Mon Sep 17 00:00:00 2001 From: "fengyun.rui" Date: Wed, 7 May 2025 16:14:48 +0800 Subject: [PATCH 3/7] feat: add more stats for otel (#2930) Signed-off-by: rfyiamcool Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- extra/redisotel/metrics.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/extra/redisotel/metrics.go b/extra/redisotel/metrics.go index 915838f3..4974f4e8 100644 --- a/extra/redisotel/metrics.go +++ b/extra/redisotel/metrics.go @@ -127,6 +127,22 @@ func reportPoolStats(rdb *redis.Client, conf *config) error { return err } + hits, err := conf.meter.Int64ObservableUpDownCounter( + "db.client.connections.hits", + metric.WithDescription("The number of times free connection was found in the pool"), + ) + if err != nil { + return err + } + + misses, err := conf.meter.Int64ObservableUpDownCounter( + "db.client.connections.misses", + metric.WithDescription("The number of times free connection was not found in the pool"), + ) + if err != nil { + return err + } + redisConf := rdb.Options() _, err = conf.meter.RegisterCallback( func(ctx context.Context, o metric.Observer) error { @@ -140,6 +156,8 @@ func reportPoolStats(rdb *redis.Client, conf *config) error { o.ObserveInt64(usage, int64(stats.TotalConns-stats.IdleConns), metric.WithAttributes(usedAttrs...)) o.ObserveInt64(timeouts, int64(stats.Timeouts), metric.WithAttributes(labels...)) + o.ObserveInt64(hits, int64(stats.Hits), metric.WithAttributes(labels...)) + o.ObserveInt64(misses, int64(stats.Misses), metric.WithAttributes(labels...)) return nil }, idleMax, @@ -147,6 +165,8 @@ func reportPoolStats(rdb *redis.Client, conf *config) error { connsMax, usage, timeouts, + hits, + misses, ) return err From c0be87ec5ba1d043ffcf39747715643476d73ca4 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Wed, 7 May 2025 14:40:49 +0300 Subject: [PATCH 4/7] chore(release): sync master after releasing V9.8.0 (#3365) * Bump version to 9.8.0-beta1 Update README.md * Feature more prominently how to enable OpenTelemetry instrumentation (#3316) * Sync master with v9.8.0-beta.1 (#3322) * DOC-4464 examples for llen, lpop, lpush, lrange, rpop, and rpush (#3234) * DOC-4464 examples for llen, lpop, lpush, lrange, rpop, and rpush * DOC-4464 improved variable names --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov * update pubsub.go (#3329) * use 8.0-RC1 (#3330) * drop ft.profile that was never enabled (#3323) * chore(deps): bump rojopolis/spellcheck-github-actions (#3336) Bumps [rojopolis/spellcheck-github-actions](https://github.com/rojopolis/spellcheck-github-actions) from 0.47.0 to 0.48.0. - [Release notes](https://github.com/rojopolis/spellcheck-github-actions/releases) - [Changelog](https://github.com/rojopolis/spellcheck-github-actions/blob/master/CHANGELOG.md) - [Commits](https://github.com/rojopolis/spellcheck-github-actions/compare/0.47.0...0.48.0) --- updated-dependencies: - dependency-name: rojopolis/spellcheck-github-actions dependency-version: 0.48.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Fix FT.Search Limit argument and add CountOnly argument for limit 0 0 (#3338) * Fix Limit argument and add CountOnly argument * Add test and Documentation * Update search_commands.go --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> * fix add missing command in interface (#3344) * Use DB option in NewFailoverClusterClient (#3342) * DOC-5102 added CountOnly search example for docs (#3345) * Add integration tests for Redis 8 behavior changes in Redis Search (#3337) * Add integration tests for Redis 8 behavior changes in Redis Search * Undo changes in ft.search limit * Fix BM25 as the default scorer test * Add more tests and comments on deprecated params * Update search_commands.go * Remove deprication comment for nostopwords --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> * Use correct slot for COUNTKEYSINSLOT command (#3327) * Ensure context isn't exhausted via concurrent query as opposed to sentinel query (#3334) * fix: better error handling when fetching the master node from the sentinels (#3349) * Better error handling when fetching the master node from the sentinels * fix error message generation * close the errCh to not block * use len over errCh * docs: fix documentation comments (#3351) * DOC-5111 added hash search examples (#3357) * fix: Fix panic caused when arg is nil (#3353) * Update README.md, use redis discord guild (#3331) * use redis discord guild * add line in CONTRIBUTING.md * update with badges similar to rest of the libraries. update url * updated with direct invite link * fix discord link in CONTRIBUTING.md * fix stackoverflow tag --------- Co-authored-by: Elena Kolevska * update HExpire command documentation (#3355) * update HExpire command documentation * Apply suggestions from code review Format the links in the documentation. Add missing documentation. --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> * feat: func isEmptyValue support time.Time (#3273) * fix:func isEmptyValue support time.Time * fix: Improve HSet unit tests * feat: Improve HSet unit tests * fix: isEmptyValue Struct only support time.Time * test(hset): add empty custom struct test --------- Co-authored-by: Guo Hui Co-authored-by: Nedyalko Dyakov * fix: `PubSub` isn't concurrency-safe (#3360) * migrate golangci-lint config to v2 format (#3354) * migrate golangci-lint config to v2 format * chore: skip CI on migration [skip ci] * Bump golangci version * Address several golangci-lint/staticcheck warnings * change staticchecks settings * chore(ci): Use redis 8 rc2 image. (#3361) * chore(ci): Use redis 8 rc2 image * test(timeseries): fix duplicatePolicy check * feat(options): panic when options are nil (#3363) Client creation should panic when options are nil. * chore(release): Update version to v9.8.0 - update version in relevant places - add RELEASE-NOTES.md to keep track of release notes --------- Signed-off-by: dependabot[bot] Co-authored-by: Nikolay Dubina Co-authored-by: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: Liu Shuang Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Co-authored-by: Bulat Khasanov Co-authored-by: Naveen Prashanth <78990165+gnpaone@users.noreply.github.com> Co-authored-by: Glenn Co-authored-by: frankj Co-authored-by: Elena Kolevska Co-authored-by: Hui Co-authored-by: Guo Hui Co-authored-by: fukua95 --- .github/workflows/build.yml | 4 +- .github/workflows/codeql-analysis.yml | 5 +- .github/workflows/golangci-lint.yml | 1 + .github/workflows/test-redis-enterprise.yml | 2 +- RELEASE-NOTES.md | 80 +++++++++++++++++++++ example/del-keys-without-ttl/go.mod | 2 +- example/hll/go.mod | 2 +- example/hset-struct/go.mod | 2 +- example/lua-scripting/go.mod | 2 +- example/otel/go.mod | 6 +- example/redis-bloom/go.mod | 2 +- example/scan-struct/go.mod | 2 +- extra/rediscensus/go.mod | 4 +- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 4 +- extra/redisprometheus/go.mod | 2 +- version.go | 2 +- 17 files changed, 102 insertions(+), 22 deletions(-) create mode 100644 RELEASE-NOTES.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 810ab509..a58ebb9c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,9 @@ name: Go on: push: - branches: [master, v9, v9.7] + branches: [master, v9, v9.7, v9.8] pull_request: - branches: [master, v9, v9.7] + branches: [master, v9, v9.7, v9.8] permissions: contents: read diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c4b558f3..1a803d37 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,10 +13,9 @@ name: "CodeQL" on: push: - branches: [ master ] + branches: [master, v9, v9.7, v9.8] pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] + branches: [master, v9, v9.7, v9.8] jobs: analyze: diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 8d4135d5..def3eb79 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -8,6 +8,7 @@ on: - master - main - v9 + - v9.8 pull_request: permissions: diff --git a/.github/workflows/test-redis-enterprise.yml b/.github/workflows/test-redis-enterprise.yml index 459b2edf..47de6478 100644 --- a/.github/workflows/test-redis-enterprise.yml +++ b/.github/workflows/test-redis-enterprise.yml @@ -2,7 +2,7 @@ name: RE Tests on: push: - branches: [master] + branches: [master, v9, v9.7, v9.8] pull_request: permissions: diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md new file mode 100644 index 00000000..fa106cb9 --- /dev/null +++ b/RELEASE-NOTES.md @@ -0,0 +1,80 @@ +# Release Notes + +# 9.8.0 (2025-04-30) + +## ๐Ÿš€ Highlights +- **Redis 8 Support**: Full compatibility with Redis 8.0, including testing and CI integration +- **Enhanced Hash Operations**: Added support for new hash commands (`HGETDEL`, `HGETEX`, `HSETEX`) and `HSTRLEN` command +- **Search Improvements**: Enabled Search DIALECT 2 by default and added `CountOnly` argument for `FT.Search` + +## โœจ New Features +- Added support for new hash commands: `HGETDEL`, `HGETEX`, `HSETEX` ([#3305](https://github.com/redis/go-redis/pull/3305)) +- Added `HSTRLEN` command for hash operations ([#2843](https://github.com/redis/go-redis/pull/2843)) +- Added `Do` method for raw query by single connection from `pool.Conn()` ([#3182](https://github.com/redis/go-redis/pull/3182)) +- Prevent false-positive marshaling by treating zero time.Time as empty in isEmptyValue ([#3273](https://github.com/redis/go-redis/pull/3273)) +- Added FailoverClusterClient support for Universal client ([#2794](https://github.com/redis/go-redis/pull/2794)) +- Added support for cluster mode with `IsClusterMode` config parameter ([#3255](https://github.com/redis/go-redis/pull/3255)) +- Added client name support in `HELLO` RESP handshake ([#3294](https://github.com/redis/go-redis/pull/3294)) +- **Enabled Search DIALECT 2 by default** ([#3213](https://github.com/redis/go-redis/pull/3213)) +- Added read-only option for failover configurations ([#3281](https://github.com/redis/go-redis/pull/3281)) +- Added `CountOnly` argument for `FT.Search` to use `LIMIT 0 0` ([#3338](https://github.com/redis/go-redis/pull/3338)) +- Added `DB` option support in `NewFailoverClusterClient` ([#3342](https://github.com/redis/go-redis/pull/3342)) +- Added `nil` check for the options when creating a client ([#3363](https://github.com/redis/go-redis/pull/3363)) + +## ๐Ÿ› Bug Fixes +- Fixed `PubSub` concurrency safety issues ([#3360](https://github.com/redis/go-redis/pull/3360)) +- Fixed panic caused when argument is `nil` ([#3353](https://github.com/redis/go-redis/pull/3353)) +- Improved error handling when fetching master node from sentinels ([#3349](https://github.com/redis/go-redis/pull/3349)) +- Fixed connection pool timeout issues and increased retries ([#3298](https://github.com/redis/go-redis/pull/3298)) +- Fixed context cancellation error leading to connection spikes on Primary instances ([#3190](https://github.com/redis/go-redis/pull/3190)) +- Fixed RedisCluster client to consider `MASTERDOWN` a retriable error ([#3164](https://github.com/redis/go-redis/pull/3164)) +- Fixed tracing to show complete commands instead of truncated versions ([#3290](https://github.com/redis/go-redis/pull/3290)) +- Fixed OpenTelemetry instrumentation to prevent multiple span reporting ([#3168](https://github.com/redis/go-redis/pull/3168)) +- Fixed `FT.Search` Limit argument and added `CountOnly` argument for limit 0 0 ([#3338](https://github.com/redis/go-redis/pull/3338)) +- Fixed missing command in interface ([#3344](https://github.com/redis/go-redis/pull/3344)) +- Fixed slot calculation for `COUNTKEYSINSLOT` command ([#3327](https://github.com/redis/go-redis/pull/3327)) +- Updated PubSub implementation with correct context ([#3329](https://github.com/redis/go-redis/pull/3329)) + +## ๐Ÿ“š Documentation +- Added hash search examples ([#3357](https://github.com/redis/go-redis/pull/3357)) +- Fixed documentation comments ([#3351](https://github.com/redis/go-redis/pull/3351)) +- Added `CountOnly` search example ([#3345](https://github.com/redis/go-redis/pull/3345)) +- Added examples for list commands: `LLEN`, `LPOP`, `LPUSH`, `LRANGE`, `RPOP`, `RPUSH` ([#3234](https://github.com/redis/go-redis/pull/3234)) +- Added `SADD` and `SMEMBERS` command examples ([#3242](https://github.com/redis/go-redis/pull/3242)) +- Updated `README.md` to use Redis Discord guild ([#3331](https://github.com/redis/go-redis/pull/3331)) +- Updated `HExpire` command documentation ([#3355](https://github.com/redis/go-redis/pull/3355)) +- Featured OpenTelemetry instrumentation more prominently ([#3316](https://github.com/redis/go-redis/pull/3316)) +- Updated `README.md` with additional information ([#310ce55](https://github.com/redis/go-redis/commit/310ce55)) + +## โšก Performance and Reliability +- Bound connection pool background dials to configured dial timeout ([#3089](https://github.com/redis/go-redis/pull/3089)) +- Ensured context isn't exhausted via concurrent query ([#3334](https://github.com/redis/go-redis/pull/3334)) + +## ๐Ÿ”ง Dependencies and Infrastructure +- Updated testing image to Redis 8.0-RC2 ([#3361](https://github.com/redis/go-redis/pull/3361)) +- Enabled CI for Redis CE 8.0 ([#3274](https://github.com/redis/go-redis/pull/3274)) +- Updated various dependencies: + - Bumped golangci/golangci-lint-action from 6.5.0 to 7.0.0 ([#3354](https://github.com/redis/go-redis/pull/3354)) + - Bumped rojopolis/spellcheck-github-actions ([#3336](https://github.com/redis/go-redis/pull/3336)) + - Bumped golang.org/x/net in example/otel ([#3308](https://github.com/redis/go-redis/pull/3308)) +- Migrated golangci-lint configuration to v2 format ([#3354](https://github.com/redis/go-redis/pull/3354)) + +## โš ๏ธ Breaking Changes +- **Enabled Search DIALECT 2 by default** ([#3213](https://github.com/redis/go-redis/pull/3213)) +- Dropped RedisGears (Triggers and Functions) support ([#3321](https://github.com/redis/go-redis/pull/3321)) +- Dropped FT.PROFILE command that was never enabled ([#3323](https://github.com/redis/go-redis/pull/3323)) + +## ๐Ÿ”’ Security +- Fixed network error handling on SETINFO (CVE-2025-29923) ([#3295](https://github.com/redis/go-redis/pull/3295)) + +## ๐Ÿงช Testing +- Added integration tests for Redis 8 behavior changes in Redis Search ([#3337](https://github.com/redis/go-redis/pull/3337)) +- Added vector types INT8 and UINT8 tests ([#3299](https://github.com/redis/go-redis/pull/3299)) +- Added test codes for search_commands.go ([#3285](https://github.com/redis/go-redis/pull/3285)) +- Fixed example test sorting ([#3292](https://github.com/redis/go-redis/pull/3292)) + +## ๐Ÿ‘ฅ Contributors + +We would like to thank all the contributors who made this release possible: + +[@alexander-menshchikov](https://github.com/alexander-menshchikov), [@EXPEbdodla](https://github.com/EXPEbdodla), [@afti](https://github.com/afti), [@dmaier-redislabs](https://github.com/dmaier-redislabs), [@four_leaf_clover](https://github.com/four_leaf_clover), [@alohaglenn](https://github.com/alohaglenn), [@gh73962](https://github.com/gh73962), [@justinmir](https://github.com/justinmir), [@LINKIWI](https://github.com/LINKIWI), [@liushuangbill](https://github.com/liushuangbill), [@golang88](https://github.com/golang88), [@gnpaone](https://github.com/gnpaone), [@ndyakov](https://github.com/ndyakov), [@nikolaydubina](https://github.com/nikolaydubina), [@oleglacto](https://github.com/oleglacto), [@andy-stark-redis](https://github.com/andy-stark-redis), [@rodneyosodo](https://github.com/rodneyosodo), [@dependabot](https://github.com/dependabot), [@rfyiamcool](https://github.com/rfyiamcool), [@frankxjkuang](https://github.com/frankxjkuang), [@fukua95](https://github.com/fukua95), [@soleymani-milad](https://github.com/soleymani-milad), [@ofekshenawa](https://github.com/ofekshenawa), [@khasanovbi](https://github.com/khasanovbi) diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 727fbbd7..6d731f37 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/v9 v9.8.0 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index 775e3e7b..28edd2ca 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.8.0-beta.1 +require github.com/redis/go-redis/v9 v9.8.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/hset-struct/go.mod b/example/hset-struct/go.mod index 33d3ef6d..c10579f1 100644 --- a/example/hset-struct/go.mod +++ b/example/hset-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/v9 v9.8.0 ) require ( diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 363c93c2..50964fa8 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.8.0-beta.1 +require github.com/redis/go-redis/v9 v9.8.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index 5a060d99..da456310 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -11,8 +11,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.8.0-beta.1 - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/extra/redisotel/v9 v9.8.0 + github.com/redis/go-redis/v9 v9.8.0 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -25,7 +25,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0-beta.1 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index 3d7a4caa..86f25db7 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.8.0-beta.1 +require github.com/redis/go-redis/v9 v9.8.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index 33d3ef6d..c10579f1 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/v9 v9.8.0 ) require ( diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index b39f7dd4..65499e2c 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0-beta.1 - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0 + github.com/redis/go-redis/v9 v9.8.0 go.opencensus.io v0.24.0 ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 93cc423d..20c78b9d 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/v9 v9.8.0 ) require ( diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index c5b29dff..c9c63427 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0-beta.1 - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0 + github.com/redis/go-redis/v9 v9.8.0 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index c934767e..25193e46 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/v9 v9.8.0 ) require ( diff --git a/version.go b/version.go index b5479516..c56e04ff 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.8.0-beta.1" + return "9.8.0" } From 8ba559ca5db4ecb34565a6bf3ddda4c130b6b775 Mon Sep 17 00:00:00 2001 From: Lev Zakharov Date: Wed, 7 May 2025 15:54:26 +0300 Subject: [PATCH 5/7] feat: add connection waiting statistics (#2804) Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- internal/pool/pool.go | 22 +++++++++++++------- internal/pool/pool_test.go | 41 ++++++++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/internal/pool/pool.go b/internal/pool/pool.go index b69c75f4..e7d951e2 100644 --- a/internal/pool/pool.go +++ b/internal/pool/pool.go @@ -33,9 +33,11 @@ var timers = sync.Pool{ // Stats contains pool state information and accumulated stats. type Stats struct { - Hits uint32 // number of times free connection was found in the pool - Misses uint32 // number of times free connection was NOT found in the pool - Timeouts uint32 // number of times a wait timeout occurred + Hits uint32 // number of times free connection was found in the pool + Misses uint32 // number of times free connection was NOT found in the pool + Timeouts uint32 // number of times a wait timeout occurred + WaitCount uint32 // number of times a connection was waited + WaitDurationNs int64 // total time spent for waiting a connection in nanoseconds TotalConns uint32 // number of total connections in the pool IdleConns uint32 // number of idle connections in the pool @@ -90,7 +92,8 @@ type ConnPool struct { poolSize int idleConnsLen int - stats Stats + stats Stats + waitDurationNs atomic.Int64 _closed uint32 // atomic } @@ -320,6 +323,7 @@ func (p *ConnPool) waitTurn(ctx context.Context) error { default: } + start := time.Now() timer := timers.Get().(*time.Timer) timer.Reset(p.cfg.PoolTimeout) @@ -331,6 +335,8 @@ func (p *ConnPool) waitTurn(ctx context.Context) error { timers.Put(timer) return ctx.Err() case p.queue <- struct{}{}: + p.waitDurationNs.Add(time.Since(start).Nanoseconds()) + atomic.AddUint32(&p.stats.WaitCount, 1) if !timer.Stop() { <-timer.C } @@ -457,9 +463,11 @@ func (p *ConnPool) IdleLen() int { func (p *ConnPool) Stats() *Stats { return &Stats{ - Hits: atomic.LoadUint32(&p.stats.Hits), - Misses: atomic.LoadUint32(&p.stats.Misses), - Timeouts: atomic.LoadUint32(&p.stats.Timeouts), + Hits: atomic.LoadUint32(&p.stats.Hits), + Misses: atomic.LoadUint32(&p.stats.Misses), + Timeouts: atomic.LoadUint32(&p.stats.Timeouts), + WaitCount: atomic.LoadUint32(&p.stats.WaitCount), + WaitDurationNs: p.waitDurationNs.Load(), TotalConns: uint32(p.Len()), IdleConns: uint32(p.IdleLen()), diff --git a/internal/pool/pool_test.go b/internal/pool/pool_test.go index 99f31bd7..d198ba54 100644 --- a/internal/pool/pool_test.go +++ b/internal/pool/pool_test.go @@ -59,12 +59,14 @@ var _ = Describe("ConnPool", func() { time.Sleep(time.Second) Expect(connPool.Stats()).To(Equal(&pool.Stats{ - Hits: 0, - Misses: 0, - Timeouts: 0, - TotalConns: 0, - IdleConns: 0, - StaleConns: 0, + Hits: 0, + Misses: 0, + Timeouts: 0, + WaitCount: 0, + WaitDurationNs: 0, + TotalConns: 0, + IdleConns: 0, + StaleConns: 0, })) }) @@ -358,4 +360,31 @@ var _ = Describe("race", func() { Expect(stats.IdleConns).To(Equal(uint32(0))) Expect(stats.TotalConns).To(Equal(uint32(opt.PoolSize))) }) + + It("wait", func() { + opt := &pool.Options{ + Dialer: func(ctx context.Context) (net.Conn, error) { + return &net.TCPConn{}, nil + }, + PoolSize: 1, + PoolTimeout: 3 * time.Second, + } + p := pool.NewConnPool(opt) + + wait := make(chan struct{}) + conn, _ := p.Get(ctx) + go func() { + _, _ = p.Get(ctx) + wait <- struct{}{} + }() + time.Sleep(time.Second) + p.Put(ctx, conn) + <-wait + + stats := p.Stats() + Expect(stats.IdleConns).To(Equal(uint32(0))) + Expect(stats.TotalConns).To(Equal(uint32(1))) + Expect(stats.WaitCount).To(Equal(uint32(1))) + Expect(stats.WaitDurationNs).To(BeNumerically("~", time.Second.Nanoseconds(), 100*time.Millisecond.Nanoseconds())) + }) }) From f174acba52ecd16202a99c99a719ced59c7337b5 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Thu, 8 May 2025 15:32:47 +0300 Subject: [PATCH 6/7] ci(redis): update to 8.0.1 (#3372) --- .github/actions/run-tests/action.yml | 2 +- .github/workflows/build.yml | 6 +++--- main_test.go | 3 ++- search_test.go | 7 +++++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index 08323aa5..0696f38d 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -25,7 +25,7 @@ runs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.0-RC2"]="8.0-RC2-pre" + ["8.0.1"]="8.0.1-pre" ["7.4.2"]="rs-7.4.0-v2" ["7.2.7"]="rs-7.2.0-v14" ) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a58ebb9c..bde6cc72 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: redis-version: - - "8.0-RC2" # 8.0 RC2 + - "8.0.1" # 8.0.1 - "7.4.2" # should use redis stack 7.4 go-version: - "1.23.x" @@ -43,7 +43,7 @@ jobs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.0-RC2"]="8.0-RC2-pre" + ["8.0.1"]="8.0.1-pre" ["7.4.2"]="rs-7.4.0-v2" ) if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then @@ -72,7 +72,7 @@ jobs: fail-fast: false matrix: redis-version: - - "8.0-RC2" # 8.0 RC2 + - "8.0.1" # 8.0.1 - "7.4.2" # should use redis stack 7.4 - "7.2.7" # should redis stack 7.2 go-version: diff --git a/main_test.go b/main_test.go index 556e633e..29e6014b 100644 --- a/main_test.go +++ b/main_test.go @@ -100,7 +100,8 @@ var _ = BeforeSuite(func() { fmt.Printf("RECluster: %v\n", RECluster) fmt.Printf("RCEDocker: %v\n", RCEDocker) - fmt.Printf("REDIS_VERSION: %v\n", RedisVersion) + fmt.Printf("REDIS_VERSION: %.1f\n", RedisVersion) + fmt.Printf("CLIENT_LIBS_TEST_IMAGE: %v\n", os.Getenv("CLIENT_LIBS_TEST_IMAGE")) if RedisVersion < 7.0 || RedisVersion > 9 { panic("incorrect or not supported redis version") diff --git a/search_test.go b/search_test.go index 6bc8b111..019acbe3 100644 --- a/search_test.go +++ b/search_test.go @@ -1871,17 +1871,20 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(val).To(BeEquivalentTo("OK")) WaitForIndexing(client, "aggTimeoutHeavy") - const totalDocs = 10000 + const totalDocs = 100000 for i := 0; i < totalDocs; i++ { key := fmt.Sprintf("doc%d", i) _, err := client.HSet(ctx, key, "n", i).Result() Expect(err).NotTo(HaveOccurred()) } + // default behaviour was changed in 8.0.1, set to fail to validate the timeout was triggered + err = client.ConfigSet(ctx, "search-on-timeout", "fail").Err() + Expect(err).NotTo(HaveOccurred()) options := &redis.FTAggregateOptions{ SortBy: []redis.FTAggregateSortBy{{FieldName: "@n", Desc: true}}, LimitOffset: 0, - Limit: 100, + Limit: 100000, Timeout: 1, // 1 ms timeout, expected to trigger a timeout error. } _, err = client.FTAggregateWithArgs(ctx, "aggTimeoutHeavy", "*", options).Result() From 42c32846e6fd227e2bee33046e29f2644f6ab4bc Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Fri, 9 May 2025 12:24:36 +0300 Subject: [PATCH 7/7] utils: export ParseFloat and MustParseFloat wrapping internal utils (#3371) * utils: expose ParseFloat via new public utils package * add tests for special float values in vector search --- helper/helper.go | 11 ++++++ internal/util/convert.go | 30 +++++++++++++++ internal/util/convert_test.go | 40 ++++++++++++++++++++ search_test.go | 71 ++++++++++++++++++++++++++++++++--- 4 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 helper/helper.go create mode 100644 internal/util/convert.go create mode 100644 internal/util/convert_test.go diff --git a/helper/helper.go b/helper/helper.go new file mode 100644 index 00000000..7047c8ae --- /dev/null +++ b/helper/helper.go @@ -0,0 +1,11 @@ +package helper + +import "github.com/redis/go-redis/v9/internal/util" + +func ParseFloat(s string) (float64, error) { + return util.ParseStringToFloat(s) +} + +func MustParseFloat(s string) float64 { + return util.MustParseFloat(s) +} diff --git a/internal/util/convert.go b/internal/util/convert.go new file mode 100644 index 00000000..d326d50d --- /dev/null +++ b/internal/util/convert.go @@ -0,0 +1,30 @@ +package util + +import ( + "fmt" + "math" + "strconv" +) + +// ParseFloat parses a Redis RESP3 float reply into a Go float64, +// handling "inf", "-inf", "nan" per Redis conventions. +func ParseStringToFloat(s string) (float64, error) { + switch s { + case "inf": + return math.Inf(1), nil + case "-inf": + return math.Inf(-1), nil + case "nan", "-nan": + return math.NaN(), nil + } + return strconv.ParseFloat(s, 64) +} + +// MustParseFloat is like ParseFloat but panics on parse errors. +func MustParseFloat(s string) float64 { + f, err := ParseStringToFloat(s) + if err != nil { + panic(fmt.Sprintf("redis: failed to parse float %q: %v", s, err)) + } + return f +} diff --git a/internal/util/convert_test.go b/internal/util/convert_test.go new file mode 100644 index 00000000..ffa3ee9f --- /dev/null +++ b/internal/util/convert_test.go @@ -0,0 +1,40 @@ +package util + +import ( + "math" + "testing" +) + +func TestParseStringToFloat(t *testing.T) { + tests := []struct { + in string + want float64 + ok bool + }{ + {"1.23", 1.23, true}, + {"inf", math.Inf(1), true}, + {"-inf", math.Inf(-1), true}, + {"nan", math.NaN(), true}, + {"oops", 0, false}, + } + + for _, tc := range tests { + got, err := ParseStringToFloat(tc.in) + if tc.ok { + if err != nil { + t.Fatalf("ParseFloat(%q) error: %v", tc.in, err) + } + if math.IsNaN(tc.want) { + if !math.IsNaN(got) { + t.Errorf("ParseFloat(%q) = %v; want NaN", tc.in, got) + } + } else if got != tc.want { + t.Errorf("ParseFloat(%q) = %v; want %v", tc.in, got, tc.want) + } + } else { + if err == nil { + t.Errorf("ParseFloat(%q) expected error, got nil", tc.in) + } + } + } +} diff --git a/search_test.go b/search_test.go index 019acbe3..fdcd0d24 100644 --- a/search_test.go +++ b/search_test.go @@ -1,15 +1,18 @@ package redis_test import ( + "bytes" "context" + "encoding/binary" "fmt" - "strconv" + "math" "strings" "time" . "github.com/bsm/ginkgo/v2" . "github.com/bsm/gomega" "github.com/redis/go-redis/v9" + "github.com/redis/go-redis/v9/helper" ) func WaitForIndexing(c *redis.Client, index string) { @@ -27,6 +30,14 @@ func WaitForIndexing(c *redis.Client, index string) { } } +func encodeFloat32Vector(vec []float32) []byte { + buf := new(bytes.Buffer) + for _, v := range vec { + binary.Write(buf, binary.LittleEndian, v) + } + return buf.Bytes() +} + var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { ctx := context.TODO() var client *redis.Client @@ -693,9 +704,9 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(err).NotTo(HaveOccurred()) Expect(res).ToNot(BeNil()) Expect(len(res.Rows)).To(BeEquivalentTo(2)) - score1, err := strconv.ParseFloat(fmt.Sprintf("%s", res.Rows[0].Fields["__score"]), 64) + score1, err := helper.ParseFloat(fmt.Sprintf("%s", res.Rows[0].Fields["__score"])) Expect(err).NotTo(HaveOccurred()) - score2, err := strconv.ParseFloat(fmt.Sprintf("%s", res.Rows[1].Fields["__score"]), 64) + score2, err := helper.ParseFloat(fmt.Sprintf("%s", res.Rows[1].Fields["__score"])) Expect(err).NotTo(HaveOccurred()) Expect(score1).To(BeNumerically(">", score2)) @@ -712,9 +723,9 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(err).NotTo(HaveOccurred()) Expect(resDM).ToNot(BeNil()) Expect(len(resDM.Rows)).To(BeEquivalentTo(2)) - score1DM, err := strconv.ParseFloat(fmt.Sprintf("%s", resDM.Rows[0].Fields["__score"]), 64) + score1DM, err := helper.ParseFloat(fmt.Sprintf("%s", resDM.Rows[0].Fields["__score"])) Expect(err).NotTo(HaveOccurred()) - score2DM, err := strconv.ParseFloat(fmt.Sprintf("%s", resDM.Rows[1].Fields["__score"]), 64) + score2DM, err := helper.ParseFloat(fmt.Sprintf("%s", resDM.Rows[1].Fields["__score"])) Expect(err).NotTo(HaveOccurred()) Expect(score1DM).To(BeNumerically(">", score2DM)) @@ -1684,6 +1695,56 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(resUint8.Docs[0].ID).To(BeEquivalentTo("doc1")) }) + It("should return special float scores in FT.SEARCH vecsim", Label("search", "ftsearch", "vecsim"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") + + vecField := &redis.FTFlatOptions{ + Type: "FLOAT32", + Dim: 2, + DistanceMetric: "IP", + } + _, err := client.FTCreate(ctx, "idx_vec", + &redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{"doc:"}}, + &redis.FieldSchema{FieldName: "vector", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: vecField}}).Result() + Expect(err).NotTo(HaveOccurred()) + WaitForIndexing(client, "idx_vec") + + bigPos := []float32{1e38, 1e38} + bigNeg := []float32{-1e38, -1e38} + nanVec := []float32{float32(math.NaN()), 0} + negNanVec := []float32{float32(math.Copysign(math.NaN(), -1)), 0} + + client.HSet(ctx, "doc:1", "vector", encodeFloat32Vector(bigPos)) + client.HSet(ctx, "doc:2", "vector", encodeFloat32Vector(bigNeg)) + client.HSet(ctx, "doc:3", "vector", encodeFloat32Vector(nanVec)) + client.HSet(ctx, "doc:4", "vector", encodeFloat32Vector(negNanVec)) + + searchOptions := &redis.FTSearchOptions{WithScores: true, Params: map[string]interface{}{"vec": encodeFloat32Vector(bigPos)}} + res, err := client.FTSearchWithArgs(ctx, "idx_vec", "*=>[KNN 4 @vector $vec]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(4)) + + var scores []float64 + for _, row := range res.Docs { + raw := fmt.Sprintf("%v", row.Fields["__vector_score"]) + f, err := helper.ParseFloat(raw) + Expect(err).NotTo(HaveOccurred()) + scores = append(scores, f) + } + + Expect(scores).To(ContainElement(BeNumerically("==", math.Inf(1)))) + Expect(scores).To(ContainElement(BeNumerically("==", math.Inf(-1)))) + + // For NaN values, use a custom check since NaN != NaN in floating point math + nanCount := 0 + for _, score := range scores { + if math.IsNaN(score) { + nanCount++ + } + } + Expect(nanCount).To(Equal(2)) + }) + It("should fail when using a non-zero offset with a zero limit", Label("search", "ftsearch"), func() { SkipBeforeRedisVersion(7.9, "requires Redis 8.x") val, err := client.FTCreate(ctx, "testIdx", &redis.FTCreateOptions{}, &redis.FieldSchema{