go-sync icon indicating copy to clipboard operation
go-sync copied to clipboard

Update module github.com/redis/go-redis/v9 to v9.7.3

Open renovate[bot] opened this issue 1 year ago • 6 comments

This PR contains the following updates:

Package Change Age Adoption Passing Confidence
github.com/redis/go-redis/v9 v9.5.1 -> v9.7.3 age adoption passing confidence

Release Notes

redis/go-redis (github.com/redis/go-redis/v9)

v9.7.3

Compare Source

What's Changed

  • fix: handle network error on SETINFO (#​3295) (CVE-2025-29923)
  • Deprecating misspelled DisableIndentity flag in the client options.
  • Introducing DisableIdentity flag in the client options.
  • Updating the documentation related to the new flag and the one that was deprecated.

Full Changelog: https://github.com/redis/go-redis/compare/v9.7.1...v9.7.3

v9.7.2

Compare Source

v9.7.1

Compare Source

Changes
  • Recognize byte slice for key argument in cluster client hash slot computation (#​3049)
  • fix(search&aggregate):fix error overwrite and typo #​3220 (#​3224)
  • fix: linter configuration (#​3279)
  • fix(search): if ft.aggregate use limit when limitoffset is zero (#​3275)
  • Reinstate read-only lock on hooks access in dialHook to fix data race (#​3225)
  • fix: flaky ClientKillByFilter test (#​3268)
  • chore: fix some comments (#​3226)
  • fix(aggregate, search): ft.aggregate bugfixes (#​3263)
  • fix: add unstableresp3 to cluster client (#​3266)
  • Fix race condition in clusterNodes.Addrs() (#​3219)
  • SortByWithCount FTSearchOptions fix (#​3201)
  • Eliminate redundant dial mutex causing unbounded connection queue contention (#​3088)
  • Add guidance on unstable RESP3 support for RediSearch commands to README (#​3177)
🚀 New Features
  • Add guidance on unstable RESP3 support for RediSearch commands to README (#​3177)
🐛 Bug Fixes
  • fix(search): if ft.aggregate use limit when limitoffset is zero (#​3275)
  • fix: add unstableresp3 to cluster client (#​3266)
  • fix(aggregate, search): ft.aggregate bugfixes (#​3263)
  • SortByWithCount FTSearchOptions fix (#​3201)
  • Recognize byte slice for key argument in cluster client hash slot computation (#​3049)
Contributors

We'd like to thank all the contributors who worked on this release!

@​ofekshenawa, @​Cgol9, @​LINKIWI, @​shawnwgit, @​zhuhaicity, @​bitsark, @​vladvildanov, @​ndyakov

Full Changelog: https://github.com/redis/go-redis/compare/v9.7.0...v9.7.1

v9.7.0: 9.7.0

Compare Source

Changes

🚀 New Features

  • Support Redis search and query capabilities (#​2801, #​3098)
  • Support indexing and querying empty values (#​3053)
  • Support for Redis JSON with RESP2 protocol (#​3146)

🛠️ Improvements

We're glad to announce that we added a search and query support in the current release.

🧰 Maintenance

Contributors

We'd like to thank all the contributors who worked on this release!

@​andy-stark-redis, @​ipechorin, @​ofekshenawa and @​vladvildanov

v9.6.3

Compare Source

What's Changed

Full Changelog: https://github.com/redis/go-redis/compare/v9.6.2...v9.6.3

v9.6.2: 9.6.2

Compare Source

Changes

🐛 Bug Fixes

  • Fixed bug with broken TLS sessions (#​3145)

Contributors

We'd like to thank all the contributors who worked on this release!

@​ofekshenawa @​vladvildanov @​rentziass

v9.6.1: 9.6.1

Compare Source

Changes

9.6

This release contains all new features from version 9.6.

🚀 New Features
  • Support Hash-field expiration commands (#​2991)
  • Support Hash-field expiration commands in Pipeline & Fix HExpire HExpireWithArgs expiration (#​3038)
  • Support NOVALUES parameter for HSCAN (#​2925)
  • Added test case for CLIENT KILL with MAXAGE option (#​2971)
  • Add support for XREAD last entry (#​3005)
  • Reduce the type assertion of CheckConn (#​3066)

9.6.1

In addition minor changes were performed to retract version 9.5.3 and 9.5.4 that were released accidentally.

🧰 Maintenance
🎁 Package Distribution
  • Retract versions 9.5.3 and 9.5.4 (#​3069)

Contributors

We'd like to thank all the contributors who worked on this release!

@​LINKIWI, @​b1ron, @​gerzse, @​haines, @​immersedin, @​naiqianz, @​ofekshenawa, @​srikar-jilugu, @​tzongw, @​vladvildanov, @​vmihailenco and @​monkey92t

v9.6.0: 9.6.0

Compare Source

Changes

🚀 New Features

  • Support Hash-field expiration commands (#​2991)
  • Support Hash-field expiration commands in Pipeline & Fix HExpire HExpireWithArgs expiration (#​3038)
  • Support NOVALUES parameter for HSCAN (#​2925)
  • Added test case for CLIENT KILL with MAXAGE option (#​2971)
  • Add support for XREAD last entry (#​3005)
  • Reduce the type assertion of CheckConn (#​3066)

🛠️ Improvements

This release includes support for Redis Community Edition (CE) 7.4.0, ensuring compatibility with the latest features and improvements introduced in Redis CE 7.4.0.

🧰 Maintenance

  • chore(deps): bump golangci/golangci-lint-action from 4 to 6 (#​2993)
  • Avoid unnecessary retry delay in cluster client following MOVED and ASK redirection (#​3048)
  • add test for tls connCheck #​3025 (#​3047)
  • fix node routing in slotClosestNode (#​3043)
  • Update pubsub.go (#​3042)
  • Change monitor test to run manually (#​3041)
  • chore(deps): bump rojopolis/spellcheck-github-actions from 0.36.0 to 0.38.0 (#​3028)
  • Add (*StatusCmd).Bytes() method (#​3030)
  • chore(deps): bump golang.org/x/net from 0.20.0 to 0.23.0 in /example/otel (#​3000)

Contributors

We'd like to thank all the contributors who worked on this release!

@​LINKIWI, @​b1ron, @​dependabot, @​dependabot[bot], @​gerzse, @​haines, @​immersedin, @​naiqianz, @​ofekshenawa, @​srikar-jilugu, @​tzongw, @​vladvildanov and @​vmihailenco @​monkey92t

v9.5.5

Compare Source

What's Changed

Full Changelog: https://github.com/redis/go-redis/compare/v9.5.4...v9.5.5

v9.5.4

Compare Source

v9.5.3

Compare Source

v9.5.2: 9.5.2

Compare Source

Changes

  • fix: fix #​2681 (#​2998)
  • Remove skipping span creation by checking parent spans (#​2980)
  • Handle IPv6 in isMovedError (#​2981)
  • Fix XGroup first pos key (#​2983)
  • Adding BitfieldRo in BitMapCmdable interface (#​2962)
  • Optimize docs useless imports and typo (#​2970)
  • chore: fix some comments (#​2967)
  • Fix for issues #​2959 and #​2960 (#​2961)
  • fix: #​2956 (#​2957)
  • fix misuses of a vs an (#​2936)
  • add server address and port span attributes to redis otel trace instrumentation (#​2826)
  • chore(deps): bump google.golang.org/protobuf from 1.32.0 to 1.33.0 in /example/otel (#​2944)
  • Remove secrets from Redis Enterprise CI (#​2938)
  • Fix monitor on go 1.19 (#​2908)
  • chore(deps): bump google.golang.org/protobuf from 1.28.1 to 1.33.0 in /extra/redisprometheus (#​2942)
  • Change RE image to public RE image (#​2935)

Contributors

We'd like to thank all the contributors who worked on this release!

@​XSAM, @​akash14darshan, @​daviddzxy, @​dependabot, @​dependabot[bot], @​esara, @​hakusai22, @​hishope, @​kindknow, @​monkey92t, @​ofekshenawa, @​singular-seal and deferdeter


Configuration

📅 Schedule: Branch creation - "* * * * 2-4" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • [ ] If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

renovate[bot] avatar Jun 05 '24 00:06 renovate[bot]

[puLL-Merge] - redis/[email protected]

Description

This pull request updates the go-redis library, incorporating various improvements, bug fixes, and compatibility updates. The changes span across multiple files and include updates to dependencies, code refactoring, and new feature additions.

Changes

Changes

  1. .github/workflows/build.yml:

    • Added Go version 1.19.x to the build matrix.
  2. .github/workflows/test-redis-enterprise.yml:

    • Updated Redis Enterprise version to 7.4.2-54.
    • Replaced sensitive information with placeholder values.
  3. CHANGELOG.md:

    • Added an entry for an unreleased version, mentioning changes to span creation behavior.
  4. Makefile:

    • Modified the test command to skip certain tests for Go version 1.19 in example directories.
  5. README.md:

    • Minor changes to the import statements in the example code.
  6. bitmap_commands.go:

    • Added a new BitFieldRO command.
    • Optimized the BitCount command implementation.
  7. command.go:

    • Improved the readMonitor function to reduce lock contention.
  8. error.go:

    • Added a call to internal.GetAddr for better address handling.
  9. Various go.mod and go.sum files:

    • Updated dependencies, including upgrading go-redis version to v9.5.3.
  10. internal/util.go and internal/util_test.go:

    • Added a new GetAddr function and corresponding tests.
  11. monitor_test.go:

    • Added a new test function TestMonitorCommand.
  12. options.go:

    • Added a new CredentialsProviderContext field to Options struct.
  13. osscluster.go:

    • Added CredentialsProviderContext to ClusterOptions.
    • Improved error handling in pipeline operations.
  14. pubsub.go:

    • Fixed a typo in a comment.
  15. redis.go:

    • Implemented support for CredentialsProviderContext.
  16. stream_commands.go:

    • Added SetFirstKeyPos calls to various XGroup commands.
  17. version.go:

    • Updated version to 9.5.3.

Possible Issues

  • The removal of Docker access token and other sensitive information from the Redis Enterprise test workflow might affect CI/CD processes if not properly configured elsewhere.

Security Hotspots

  • The replacement of sensitive information in .github/workflows/test-redis-enterprise.yml with placeholder values is a good security practice, but ensure that the actual sensitive data is properly managed and securely injected during the CI/CD process.

github-actions[bot] avatar Jul 03 '24 11:07 github-actions[bot]

[puLL-Merge] - redis/[email protected]

Description

This PR introduces several changes to the go-redis library, including updates to dependencies, improvements to tracing and monitoring functionality, and various bug fixes and enhancements.

Changes

Changes

  1. .github/workflows/build.yml:

    • Added Go version 1.19.x to the test matrix.
  2. .github/workflows/test-redis-enterprise.yml:

    • Updated Redis Enterprise version to 7.4.2-54.
    • Simplified credentials and configuration setup.
  3. CHANGELOG.md:

    • Added entry for unreleased changes regarding span creation behavior.
  4. Makefile:

    • Added logic to skip tests in example directories for Go version 1.19.
  5. README.md:

    • Minor cleanup in code example.
  6. bitmap_commands.go:

    • Added BitFieldRO command.
    • Optimized BitCount command implementation.
  7. command.go:

    • Improved concurrency handling in MonitorCmd.
  8. error.go:

    • Added support for IPv6 addresses in moved error parsing.
  9. example/otel/go.mod:

    • Updated OpenTelemetry dependencies.
  10. extra/rediscensus/go.mod, extra/rediscmd/go.mod, extra/redisotel/go.mod, extra/redisprometheus/go.mod:

    • Updated Go version and dependencies.
  11. extra/redisotel/config.go, extra/redisotel/tracing.go:

    • Updated OpenTelemetry semantic conventions.
    • Added server address and port attributes to tracing.
  12. internal/util.go, internal/util_test.go:

    • Added GetAddr function to handle different address formats.
  13. monitor_test.go:

    • Added new test for monitor command.
  14. options.go:

    • Added CredentialsProviderContext for enhanced credential handling.
  15. osscluster.go:

    • Added CredentialsProviderContext to ClusterOptions.
    • Improved error handling in pipeline processing.
  16. pubsub.go:

    • Fixed typo in comment.
  17. redis.go:

    • Implemented CredentialsProviderContext in connection initialization.
  18. stream_commands.go:

    • Added SetFirstKeyPos to stream-related commands for better key tracking.

Possible Issues

  • The change in span creation behavior (CHANGELOG.md) might affect existing tracing setups and may require adjustments in applications using this library.

Security Hotspots

No significant security issues were identified in this PR. However, the changes to credential handling (introduction of CredentialsProviderContext) should be carefully reviewed to ensure they don't introduce any security vulnerabilities.

github-actions[bot] avatar Jul 17 '24 07:07 github-actions[bot]

[puLL-Merge] - redis/[email protected]

Description

This PR introduces several significant changes and improvements to the go-redis library. The changes include new features, bug fixes, optimizations, and updates to dependencies. Some key changes involve enhancing connection handling, improving cluster operations, adding new Redis commands support, and updating OpenTelemetry integration.

Possible Issues

  1. The changes to connection handling and cluster operations might introduce subtle behavioral changes that could affect existing applications.
  2. Updates to dependencies, especially OpenTelemetry, may require adjustments in applications using these features.

Security Hotspots

No significant security vulnerabilities are apparent in this change set. However, changes to connection handling and authentication should be carefully reviewed to ensure they don't introduce any security weaknesses.

Changes

Changes

  1. .github/workflows/build.yml:

    • Updated branch targets and Go versions for CI/CD.
  2. .github/workflows/golangci-lint.yml and .github/workflows/spellcheck.yml:

    • Updated versions of linting and spellchecking tools.
  3. .github/workflows/test-redis-enterprise.yml:

    • Updated Redis Enterprise version and simplified test configuration.
  4. CHANGELOG.md:

    • Added entry for changes in OpenTelemetry span creation behavior.
  5. Makefile:

    • Updated Redis version and added conditional test skipping for older Go versions.
  6. README.md:

    • Minor code example update.
  7. bitmap_commands.go:

    • Optimized BitCount command implementation.
  8. command.go:

    • Added new fields and methods to various command structures.
    • Enhanced MonitorCmd to improve concurrency safety.
  9. commands_test.go:

    • Added new tests for various Redis commands, including hash expiration commands.
  10. error.go:

    • Improved error handling for moved errors in cluster operations.
  11. extra/redisotel/:

    • Updated OpenTelemetry integration with latest conventions and improved tracing.
  12. hash_commands.go:

    • Added new hash commands for expiration and TTL operations.
  13. internal/pool/conn.go and internal/pool/pool.go:

    • Improved connection health checking and management.
  14. options.go:

    • Added CredentialsProviderContext for enhanced credential management.
  15. osscluster.go:

    • Improved cluster node selection logic and error handling.
  16. pubsub.go:

    • Minor improvements to context handling in PubSub operations.
  17. redis.go:

    • Enhanced connection initialization with new credential provider support.
  18. stream_commands.go:

    • Added support for the "ID" field in XReadArgs.
  19. Various test files:

    • Added and updated tests to cover new features and edge cases.
  20. version.go:

    • Updated version to 9.6.0.

These changes collectively represent a significant update to the go-redis library, improving its functionality, performance, and compatibility with the latest Redis features.

github-actions[bot] avatar Jul 23 '24 09:07 github-actions[bot]

[puLL-Merge] - redis/[email protected]

Description

This PR introduces several improvements and new features to the go-redis library. It includes updates to dependencies, enhancements to existing functionalities, and the addition of new Redis commands support. The changes span across multiple files and involve modifications to core functionalities, test cases, and documentation.

Changes

Changes

  1. bitmap_commands.go:

    • Optimized BitCount implementation.
    • Added BitFieldRO command support.
  2. command.go:

    • Enhanced ClientInfo struct with new fields.
    • Improved MonitorCmd implementation.
  3. commands_test.go:

    • Added new test cases for various Redis commands.
    • Updated existing tests to accommodate new functionalities.
  4. error.go:

    • Improved error handling for moved errors.
  5. hash_commands.go:

    • Added new hash commands support (HExpire, HPExpire, HExpireAt, HPExpireAt, HPersist, HExpireTime, HPExpireTime, HTTL, HPTTL).
  6. internal/pool/conn.go and internal/pool/pool.go:

    • Enhanced connection handling and health checks.
  7. osscluster.go:

    • Improved cluster node selection logic.
    • Enhanced error handling and retries for cluster operations.
  8. options.go:

    • Added CredentialsProviderContext for enhanced credential management.
  9. pubsub.go:

    • Minor improvements in PubSub implementation.
  10. redis.go:

    • Enhanced connection initialization process.
  11. stream_commands.go:

    • Added support for new stream-related commands.
  12. Various test files:

    • Updated and added new test cases to cover new functionalities.
  13. version.go:

    • Updated version to 9.6.1.

Possible Issues

  • The changes to cluster node selection logic in osscluster.go might affect the behavior of node selection in edge cases. Thorough testing in various cluster configurations is recommended.

Security Hotspots

No significant security issues were identified in this change. However, as always, careful review of credential handling changes is recommended.

github-actions[bot] avatar Aug 21 '24 15:08 github-actions[bot]

[puLL-Merge] - redis/[email protected]

Description

This pull request introduces several changes and improvements to the go-redis library. The main changes include updates to various commands, enhancements to connection handling, improvements in cluster operations, and additions to hash commands for expiration management.

Changes

Changes

  1. .github/workflows/:

    • Updated Go versions and Redis image versions in CI workflows.
  2. CHANGELOG.md:

    • Added an unreleased section mentioning changes to span creation behavior.
  3. Makefile:

    • Updated Redis version and added Go version check for tests.
  4. bitmap_commands.go:

    • Optimized BitCount command implementation.
  5. command.go:

    • Added Watch field to ClientInfo struct.
    • Enhanced MonitorCmd implementation.
  6. commands_test.go:

    • Added tests for new hash expiration commands.
  7. error.go:

    • Improved handling of moved errors.
  8. extra/redisotel/:

    • Updated OpenTelemetry dependencies and implementation.
  9. hash_commands.go:

    • Added new commands for hash field expiration management (HExpire, HPExpire, HExpireAt, HPExpireAt, HPersist, HExpireTime, HPExpireTime, HTTL, HPTTL).
  10. internal/pool/conn.go:

    • Improved connection health checking.
  11. osscluster.go:

    • Enhanced cluster node selection logic.
    • Improved handling of MOVED and ASK redirections.
  12. options.go:

    • Added CredentialsProviderContext for enhanced credential management.
  13. pubsub.go:

    • Fixed context usage in writeCmd method.
  14. redis.go:

    • Implemented CredentialsProviderContext in connection initialization.
  15. stream_commands.go:

    • Added ID field to XReadArgs struct.
    • Set first key position for various XGroup commands.
  16. version.go:

    • Updated version to 9.6.2.

Possible Issues

  • The changes to span creation behavior in OpenTelemetry integration might affect existing tracing setups.
  • Updates to cluster node selection logic could potentially change the behavior of cluster operations in edge cases.

Security Hotspots

No significant security hotspots were identified in this change set.

github-actions[bot] avatar Oct 18 '24 12:10 github-actions[bot]

👍 Dependency issues cleared. Learn more about Socket for GitHub ↗︎

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report↗︎

socket-security[bot] avatar Mar 26 '25 21:03 socket-security[bot]

[puLL-Merge] - redis/[email protected]

Diff

diff --git .github/wordlist.txt .github/wordlist.txt
index dceddff46..1fc34f733 100644
--- .github/wordlist.txt
+++ .github/wordlist.txt
@@ -1,4 +1,5 @@
 ACLs
+APIs
 autoload
 autoloader
 autoloading
@@ -46,11 +47,14 @@ runtime
 SHA
 sharding
 SETNAME
+SpellCheck
 SSL
 struct
 stunnel
+SynDump
 TCP
 TLS
+UnstableResp
 uri
 URI
 url
@@ -59,3 +63,5 @@ RedisStack
 RedisGears
 RedisTimeseries
 RediSearch
+RawResult
+RawVal
\ No newline at end of file
diff --git .github/workflows/build.yml .github/workflows/build.yml
index 4061bbdff..eb0c20ba2 100644
--- .github/workflows/build.yml
+++ .github/workflows/build.yml
@@ -2,9 +2,9 @@ name: Go
 
 on:
   push:
-    branches: [master, v9]
+    branches: [master, v9, v9.7]
   pull_request:
-    branches: [master, v9]
+    branches: [master, v9, v9.7]
 
 permissions:
   contents: read
@@ -20,7 +20,7 @@ jobs:
 
     services:
       redis:
-        image: redis/redis-stack-server:edge
+        image: redis/redis-stack-server:latest
         options: >-
           --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
         ports:
@@ -37,3 +37,9 @@ jobs:
 
       - name: Test
         run: make test
+
+      - name: Upload to Codecov
+        uses: codecov/codecov-action@v4
+        with:
+          files: coverage.txt
+          token: ${{ secrets.CODECOV_TOKEN }}
\ No newline at end of file
diff --git .github/workflows/golangci-lint.yml .github/workflows/golangci-lint.yml
index a139f5dab..d9e53f706 100644
--- .github/workflows/golangci-lint.yml
+++ .github/workflows/golangci-lint.yml
@@ -12,15 +12,13 @@ on:
 
 permissions:
   contents: read
+  pull-requests: read  # for golangci/golangci-lint-action to fetch pull requests
 
 jobs:
   golangci:
-    permissions:
-      contents: read  # for actions/checkout to fetch code
-      pull-requests: read  # for golangci/golangci-lint-action to fetch pull requests
     name: lint
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@v4
+        uses: golangci/[email protected]
diff --git .github/workflows/spellcheck.yml .github/workflows/spellcheck.yml
index 62e38997e..cc6d828c9 100644
--- .github/workflows/spellcheck.yml
+++ .github/workflows/spellcheck.yml
@@ -8,7 +8,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v4
       - name: Check Spelling
-        uses: rojopolis/[email protected]
+        uses: rojopolis/[email protected]
         with:
           config_path: .github/spellcheck-settings.yml
           task_name: Markdown
diff --git .golangci.yml .golangci.yml
index de514554a..285aca6b3 100644
--- .golangci.yml
+++ .golangci.yml
@@ -1,4 +1,3 @@
 run:
-  concurrency: 8
-  deadline: 5m
+  timeout: 5m
   tests: false
diff --git Makefile Makefile
index d8d007596..1a6bd1786 100644
--- Makefile
+++ Makefile
@@ -14,6 +14,7 @@ test: testdeps
 	    go test ./... -short -race && \
 	    go test ./... -run=NONE -bench=. -benchmem && \
 	    env GOOS=linux GOARCH=386 go test && \
+	    go test -coverprofile=coverage.txt -covermode=atomic ./... && \
 	    go vet); \
 	done
 	cd internal/customvet && go build .
diff --git README.md README.md
index e7df5dfd6..9395c652f 100644
--- README.md
+++ README.md
@@ -3,6 +3,7 @@
 [![build workflow](https://github.com/redis/go-redis/actions/workflows/build.yml/badge.svg)](https://github.com/redis/go-redis/actions)
 [![PkgGoDev](https://pkg.go.dev/badge/github.com/redis/go-redis/v9)](https://pkg.go.dev/github.com/redis/go-redis/v9?tab=doc)
 [![Documentation](https://img.shields.io/badge/redis-documentation-informational)](https://redis.uptrace.dev/)
+[![codecov](https://codecov.io/github/redis/go-redis/graph/badge.svg?token=tsrCZKuSSw)](https://codecov.io/github/redis/go-redis)
 [![Chat](https://discordapp.com/api/guilds/752070105847955518/widget.png)](https://discord.gg/rWtp5Aj)
 
 > go-redis is brought to you by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace).
@@ -169,19 +170,39 @@ By default, go-redis automatically sends the client library name and version dur
 
 #### Disabling Identity Verification
 
-When connection identity verification is not required or needs to be explicitly disabled, a `DisableIndentity` configuration option exists. In V10 of this library, `DisableIndentity` will become `DisableIdentity` in order to fix the associated typo.
+When connection identity verification is not required or needs to be explicitly disabled, a `DisableIdentity` configuration option exists.
+Initially there was a typo and the option was named `DisableIndentity` instead of `DisableIdentity`. The misspelled option is marked as Deprecated and will be removed in V10 of this library.
+Although both options will work at the moment, the correct option is `DisableIdentity`. The deprecated option will be removed in V10 of this library, so please use the correct option name to avoid any issues.
 
-To disable verification, set the `DisableIndentity` option to `true` in the Redis client options:
+To disable verification, set the `DisableIdentity` option to `true` in the Redis client options:
 
 ```go
 rdb := redis.NewClient(&redis.Options{
     Addr:            "localhost:6379",
     Password:        "",
     DB:              0,
-    DisableIndentity: true, // Disable set-info on connect
+    DisableIdentity: true, // Disable set-info on connect
 })
 ```
 
+#### Unstable RESP3 Structures for RediSearch Commands
+When integrating Redis with application functionalities using RESP3, it's important to note that some response structures aren't final yet. This is especially true for more complex structures like search and query results. We recommend using RESP2 when using the search and query capabilities, but we plan to stabilize the RESP3-based API-s in the coming versions. You can find more guidance in the upcoming release notes.
+
+To enable unstable RESP3, set the option in your client configuration:
+
+```go
+redis.NewClient(&redis.Options{
+			UnstableResp3: true,
+		})
+```
+**Note:** When UnstableResp3 mode is enabled, it's necessary to use RawResult() and RawVal() to retrieve a raw data.
+          Since, raw response is the only option for unstable search commands Val() and Result() calls wouldn't have any affect on them:
+
+```go
+res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawResult()
+val1 := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawVal()
+```
+
 ## Contributing
 
 Please see [out contributing guidelines](CONTRIBUTING.md) to help us improve this library!
diff --git bench_decode_test.go bench_decode_test.go
index 16bdf2cd3..d61a901a0 100644
--- bench_decode_test.go
+++ bench_decode_test.go
@@ -30,7 +30,7 @@ func NewClientStub(resp []byte) *ClientStub {
 		Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
 			return stub.stubConn(initHello), nil
 		},
-		DisableIndentity: true,
+		DisableIdentity: true,
 	})
 	return stub
 }
@@ -46,7 +46,7 @@ func NewClusterClientStub(resp []byte) *ClientStub {
 		Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
 			return stub.stubConn(initHello), nil
 		},
-		DisableIndentity: true,
+		DisableIdentity: true,
 
 		ClusterSlots: func(_ context.Context) ([]ClusterSlot, error) {
 			return []ClusterSlot{
diff --git command.go command.go
index 9ae97a95a..f3d0e49b7 100644
--- command.go
+++ command.go
@@ -40,7 +40,7 @@ type Cmder interface {
 
 	readTimeout() *time.Duration
 	readReply(rd *proto.Reader) error
-
+	readRawReply(rd *proto.Reader) error
 	SetErr(error)
 	Err() error
 }
@@ -122,11 +122,11 @@ func cmdString(cmd Cmder, val interface{}) string {
 //------------------------------------------------------------------------------
 
 type baseCmd struct {
-	ctx    context.Context
-	args   []interface{}
-	err    error
-	keyPos int8
-
+	ctx          context.Context
+	args         []interface{}
+	err          error
+	keyPos       int8
+	rawVal       interface{}
 	_readTimeout *time.Duration
 }
 
@@ -167,6 +167,8 @@ func (cmd *baseCmd) stringArg(pos int) string {
 	switch v := arg.(type) {
 	case string:
 		return v
+	case []byte:
+		return string(v)
 	default:
 		// TODO: consider using appendArg
 		return fmt.Sprint(v)
@@ -197,6 +199,11 @@ func (cmd *baseCmd) setReadTimeout(d time.Duration) {
 	cmd._readTimeout = &d
 }
 
+func (cmd *baseCmd) readRawReply(rd *proto.Reader) (err error) {
+	cmd.rawVal, err = rd.ReadReply()
+	return err
+}
+
 //------------------------------------------------------------------------------
 
 type Cmd struct {
diff --git commands_test.go commands_test.go
index 9554bf9a9..64800705d 100644
--- commands_test.go
+++ commands_test.go
@@ -217,7 +217,7 @@ var _ = Describe("Commands", func() {
 
 			killed := client.ClientKillByFilter(ctx, "MAXAGE", "1")
 			Expect(killed.Err()).NotTo(HaveOccurred())
-			Expect(killed.Val()).To(SatisfyAny(Equal(int64(2)), Equal(int64(3))))
+			Expect(killed.Val()).To(BeNumerically(">=", 2))
 
 			select {
 			case <-done:
diff --git a/doctests/bf_tutorial_test.go b/doctests/bf_tutorial_test.go
new file mode 100644
index 000000000..67545f1d5
--- /dev/null
+++ doctests/bf_tutorial_test.go
@@ -0,0 +1,83 @@
+// EXAMPLE: bf_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+
+func ExampleClient_bloom() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:models")
+	// REMOVE_END
+
+	// STEP_START bloom
+	res1, err := rdb.BFReserve(ctx, "bikes:models", 0.01, 1000).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res1) // >>> OK
+
+	res2, err := rdb.BFAdd(ctx, "bikes:models", "Smoky Mountain Striker").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res2) // >>> true
+
+	res3, err := rdb.BFExists(ctx, "bikes:models", "Smoky Mountain Striker").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res3) // >>> true
+
+	res4, err := rdb.BFMAdd(ctx, "bikes:models",
+		"Rocky Mountain Racer",
+		"Cloudy City Cruiser",
+		"Windy City Wippet",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res4) // >>> [true true true]
+
+	res5, err := rdb.BFMExists(ctx, "bikes:models",
+		"Rocky Mountain Racer",
+		"Cloudy City Cruiser",
+		"Windy City Wippet",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res5) // >>> [true true true]
+	// STEP_END
+
+	// Output:
+	// OK
+	// true
+	// true
+	// [true true true]
+	// [true true true]
+}
diff --git a/doctests/bitmap_tutorial_test.go b/doctests/bitmap_tutorial_test.go
new file mode 100644
index 000000000..dbfc247ac
--- /dev/null
+++ doctests/bitmap_tutorial_test.go
@@ -0,0 +1,92 @@
+// EXAMPLE: bitmap_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+
+func ExampleClient_ping() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "pings:2024-01-01-00:00")
+	// REMOVE_END
+
+	// STEP_START ping
+	res1, err := rdb.SetBit(ctx, "pings:2024-01-01-00:00", 123, 1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res1) // >>> 0
+
+	res2, err := rdb.GetBit(ctx, "pings:2024-01-01-00:00", 123).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res2) // >>> 1
+
+	res3, err := rdb.GetBit(ctx, "pings:2024-01-01-00:00", 456).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res3) // >>> 0
+	// STEP_END
+
+	// Output:
+	// 0
+	// 1
+	// 0
+}
+
+func ExampleClient_bitcount() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	_, err := rdb.SetBit(ctx, "pings:2024-01-01-00:00", 123, 1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+	// REMOVE_END
+
+	// STEP_START bitcount
+	res4, err := rdb.BitCount(ctx, "pings:2024-01-01-00:00",
+		&redis.BitCount{
+			Start: 0,
+			End:   456,
+		}).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res4) // >>> 1
+	// STEP_END
+
+	// Output:
+	// 1
+}
diff --git a/doctests/hash_tutorial_test.go b/doctests/hash_tutorial_test.go
new file mode 100644
index 000000000..8b0b1ce9a
--- /dev/null
+++ doctests/hash_tutorial_test.go
@@ -0,0 +1,281 @@
+// EXAMPLE: hash_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+
+func ExampleClient_set_get_all() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bike:1")
+	// REMOVE_END
+
+	// STEP_START set_get_all
+	hashFields := []string{
+		"model", "Deimos",
+		"brand", "Ergonom",
+		"type", "Enduro bikes",
+		"price", "4972",
+	}
+
+	res1, err := rdb.HSet(ctx, "bike:1", hashFields).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res1) // >>> 4
+
+	res2, err := rdb.HGet(ctx, "bike:1", "model").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res2) // >>> Deimos
+
+	res3, err := rdb.HGet(ctx, "bike:1", "price").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res3) // >>> 4972
+
+	cmdReturn := rdb.HGetAll(ctx, "bike:1")
+	res4, err := cmdReturn.Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res4)
+	// >>> map[brand:Ergonom model:Deimos price:4972 type:Enduro bikes]
+
+	type BikeInfo struct {
+		Model string `redis:"model"`
+		Brand string `redis:"brand"`
+		Type  string `redis:"type"`
+		Price int    `redis:"price"`
+	}
+
+	var res4a BikeInfo
+
+	if err := cmdReturn.Scan(&res4a); err != nil {
+		panic(err)
+	}
+
+	fmt.Printf("Model: %v, Brand: %v, Type: %v, Price: $%v\n",
+		res4a.Model, res4a.Brand, res4a.Type, res4a.Price)
+	// >>> Model: Deimos, Brand: Ergonom, Type: Enduro bikes, Price: $4972
+	// STEP_END
+
+	// Output:
+	// 4
+	// Deimos
+	// 4972
+	// map[brand:Ergonom model:Deimos price:4972 type:Enduro bikes]
+	// Model: Deimos, Brand: Ergonom, Type: Enduro bikes, Price: $4972
+}
+
+func ExampleClient_hmget() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bike:1")
+	// REMOVE_END
+
+	hashFields := []string{
+		"model", "Deimos",
+		"brand", "Ergonom",
+		"type", "Enduro bikes",
+		"price", "4972",
+	}
+
+	_, err := rdb.HSet(ctx, "bike:1", hashFields).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START hmget
+	cmdReturn := rdb.HMGet(ctx, "bike:1", "model", "price")
+	res5, err := cmdReturn.Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res5) // >>> [Deimos 4972]
+
+	type BikeInfo struct {
+		Model string `redis:"model"`
+		Brand string `redis:"-"`
+		Type  string `redis:"-"`
+		Price int    `redis:"price"`
+	}
+
+	var res5a BikeInfo
+
+	if err := cmdReturn.Scan(&res5a); err != nil {
+		panic(err)
+	}
+
+	fmt.Printf("Model: %v, Price: $%v\n", res5a.Model, res5a.Price)
+	// >>> Model: Deimos, Price: $4972
+	// STEP_END
+
+	// Output:
+	// [Deimos 4972]
+	// Model: Deimos, Price: $4972
+}
+
+func ExampleClient_hincrby() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bike:1")
+	// REMOVE_END
+
+	hashFields := []string{
+		"model", "Deimos",
+		"brand", "Ergonom",
+		"type", "Enduro bikes",
+		"price", "4972",
+	}
+
+	_, err := rdb.HSet(ctx, "bike:1", hashFields).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START hincrby
+	res6, err := rdb.HIncrBy(ctx, "bike:1", "price", 100).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res6) // >>> 5072
+
+	res7, err := rdb.HIncrBy(ctx, "bike:1", "price", -100).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res7) // >>> 4972
+	// STEP_END
+
+	// Output:
+	// 5072
+	// 4972
+}
+
+func ExampleClient_incrby_get_mget() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bike:1:stats")
+	// REMOVE_END
+
+	// STEP_START incrby_get_mget
+	res8, err := rdb.HIncrBy(ctx, "bike:1:stats", "rides", 1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res8) // >>> 1
+
+	res9, err := rdb.HIncrBy(ctx, "bike:1:stats", "rides", 1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res9) // >>> 2
+
+	res10, err := rdb.HIncrBy(ctx, "bike:1:stats", "rides", 1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res10) // >>> 3
+
+	res11, err := rdb.HIncrBy(ctx, "bike:1:stats", "crashes", 1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res11) // >>> 1
+
+	res12, err := rdb.HIncrBy(ctx, "bike:1:stats", "owners", 1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res12) // >>> 1
+
+	res13, err := rdb.HGet(ctx, "bike:1:stats", "rides").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res13) // >>> 3
+
+	res14, err := rdb.HMGet(ctx, "bike:1:stats", "crashes", "owners").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res14) // >>> [1 1]
+	// STEP_END
+
+	// Output:
+	// 1
+	// 2
+	// 3
+	// 1
+	// 1
+	// 3
+	// [1 1]
+}
diff --git a/doctests/json_tutorial_test.go b/doctests/json_tutorial_test.go
new file mode 100644
index 000000000..4e9787330
--- /dev/null
+++ doctests/json_tutorial_test.go
@@ -0,0 +1,1149 @@
+// EXAMPLE: json_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+func ExampleClient_setget() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bike")
+	// REMOVE_END
+
+	// STEP_START set_get
+	res1, err := rdb.JSONSet(ctx, "bike", "$",
+		"\"Hyperion\"",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res1) // >>> OK
+
+	res2, err := rdb.JSONGet(ctx, "bike", "$").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res2) // >>> ["Hyperion"]
+
+	res3, err := rdb.JSONType(ctx, "bike", "$").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res3) // >>> [[string]]
+	// STEP_END
+
+	// Output:
+	// OK
+	// ["Hyperion"]
+	// [[string]]
+}
+
+func ExampleClient_str() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bike")
+	// REMOVE_END
+
+	_, err := rdb.JSONSet(ctx, "bike", "$",
+		"\"Hyperion\"",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START str
+	res4, err := rdb.JSONStrLen(ctx, "bike", "$").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(*res4[0]) // >>> 8
+
+	res5, err := rdb.JSONStrAppend(ctx, "bike", "$", "\" (Enduro bikes)\"").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(*res5[0]) // >>> 23
+
+	res6, err := rdb.JSONGet(ctx, "bike", "$").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res6) // >>> ["Hyperion (Enduro bikes)"]
+	// STEP_END
+
+	// Output:
+	// 8
+	// 23
+	// ["Hyperion (Enduro bikes)"]
+}
+
+func ExampleClient_num() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "crashes")
+	// REMOVE_END
+
+	// STEP_START num
+	res7, err := rdb.JSONSet(ctx, "crashes", "$", 0).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res7) // >>> OK
+
+	res8, err := rdb.JSONNumIncrBy(ctx, "crashes", "$", 1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res8) // >>> [1]
+
+	res9, err := rdb.JSONNumIncrBy(ctx, "crashes", "$", 1.5).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res9) // >>> [2.5]
+
+	res10, err := rdb.JSONNumIncrBy(ctx, "crashes", "$", -0.75).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res10) // >>> [1.75]
+	// STEP_END
+
+	// Output:
+	// OK
+	// [1]
+	// [2.5]
+	// [1.75]
+}
+
+func ExampleClient_arr() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "newbike")
+	// REMOVE_END
+
+	// STEP_START arr
+	res11, err := rdb.JSONSet(ctx, "newbike", "$",
+		[]interface{}{
+			"Deimos",
+			map[string]interface{}{"crashes": 0},
+			nil,
+		},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res11) // >>> OK
+
+	res12, err := rdb.JSONGet(ctx, "newbike", "$").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res12) // >>> [["Deimos",{"crashes":0},null]]
+
+	res13, err := rdb.JSONGet(ctx, "newbike", "$[1].crashes").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res13) // >>> [0]
+
+	res14, err := rdb.JSONDel(ctx, "newbike", "$.[-1]").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res14) // >>> 1
+
+	res15, err := rdb.JSONGet(ctx, "newbike", "$").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res15) // >>> [["Deimos",{"crashes":0}]]
+	// STEP_END
+
+	// Output:
+	// OK
+	// [["Deimos",{"crashes":0},null]]
+	// [0]
+	// 1
+	// [["Deimos",{"crashes":0}]]
+}
+
+func ExampleClient_arr2() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "riders")
+	// REMOVE_END
+
+	// STEP_START arr2
+	res16, err := rdb.JSONSet(ctx, "riders", "$", []interface{}{}).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res16) // >>> OK
+
+	res17, err := rdb.JSONArrAppend(ctx, "riders", "$", "\"Norem\"").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res17) // >>> [1]
+
+	res18, err := rdb.JSONGet(ctx, "riders", "$").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res18) // >>> [["Norem"]]
+
+	res19, err := rdb.JSONArrInsert(ctx, "riders", "$", 1,
+		"\"Prickett\"", "\"Royce\"", "\"Castilla\"",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res19) // [3]
+
+	res20, err := rdb.JSONGet(ctx, "riders", "$").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res20) // >>> [["Norem", "Prickett", "Royce", "Castilla"]]
+
+	rangeStop := 1
+
+	res21, err := rdb.JSONArrTrimWithArgs(ctx, "riders", "$",
+		&redis.JSONArrTrimArgs{Start: 1, Stop: &rangeStop},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res21) // >>> [1]
+
+	res22, err := rdb.JSONGet(ctx, "riders", "$").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res22) // >>> [["Prickett"]]
+
+	res23, err := rdb.JSONArrPop(ctx, "riders", "$", -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res23) // >>> [["Prickett"]]
+
+	res24, err := rdb.JSONArrPop(ctx, "riders", "$", -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res24) // []
+	// STEP_END
+
+	// Output:
+	// OK
+	// [1]
+	// [["Norem"]]
+	// [4]
+	// [["Norem","Prickett","Royce","Castilla"]]
+	// [1]
+	// [["Prickett"]]
+	// ["Prickett"]
+	// []
+}
+
+func ExampleClient_obj() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bike:1")
+	// REMOVE_END
+
+	// STEP_START obj
+	res25, err := rdb.JSONSet(ctx, "bike:1", "$",
+		map[string]interface{}{
+			"model": "Deimos",
+			"brand": "Ergonom",
+			"price": 4972,
+		},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res25) // >>> OK
+
+	res26, err := rdb.JSONObjLen(ctx, "bike:1", "$").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(*res26[0]) // >>> 3
+
+	res27, err := rdb.JSONObjKeys(ctx, "bike:1", "$").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res27) // >>> [brand model price]
+	// STEP_END
+
+	// Output:
+	// OK
+	// 3
+	// [[brand model price]]
+}
+
+var inventory_json = map[string]interface{}{
+	"inventory": map[string]interface{}{
+		"mountain_bikes": []interface{}{
+			map[string]interface{}{
+				"id":    "bike:1",
+				"model": "Phoebe",
+				"description": "This is a mid-travel trail slayer that is a fantastic " +
+					"daily driver or one bike quiver. The Shimano Claris 8-speed groupset " +
+					"gives plenty of gear range to tackle hills and there\u2019s room for " +
+					"mudguards and a rack too.  This is the bike for the rider who wants " +
+					"trail manners with low fuss ownership.",
+				"price":  1920,
+				"specs":  map[string]interface{}{"material": "carbon", "weight": 13.1},
+				"colors": []interface{}{"black", "silver"},
+			},
+			map[string]interface{}{
+				"id":    "bike:2",
+				"model": "Quaoar",
+				"description": "Redesigned for the 2020 model year, this bike " +
+					"impressed our testers and is the best all-around trail bike we've " +
+					"ever tested. The Shimano gear system effectively does away with an " +
+					"external cassette, so is super low maintenance in terms of wear " +
+					"and tear. All in all it's an impressive package for the price, " +
+					"making it very competitive.",
+				"price":  2072,
+				"specs":  map[string]interface{}{"material": "aluminium", "weight": 7.9},
+				"colors": []interface{}{"black", "white"},
+			},
+			map[string]interface{}{
+				"id":    "bike:3",
+				"model": "Weywot",
+				"description": "This bike gives kids aged six years and older " +
+					"a durable and uberlight mountain bike for their first experience " +
+					"on tracks and easy cruising through forests and fields. A set of " +
+					"powerful Shimano hydraulic disc brakes provide ample stopping " +
+					"ability. If you're after a budget option, this is one of the best " +
+					"bikes you could get.",
+				"price": 3264,
+				"specs": map[string]interface{}{"material": "alloy", "weight": 13.8},
+			},
+		},
+		"commuter_bikes": []interface{}{
+			map[string]interface{}{
+				"id":    "bike:4",
+				"model": "Salacia",
+				"description": "This bike is a great option for anyone who just " +
+					"wants a bike to get about on With a slick-shifting Claris gears " +
+					"from Shimano\u2019s, this is a bike which doesn\u2019t break the " +
+					"bank and delivers craved performance.  It\u2019s for the rider " +
+					"who wants both efficiency and capability.",
+				"price":  1475,
+				"specs":  map[string]interface{}{"material": "aluminium", "weight": 16.6},
+				"colors": []interface{}{"black", "silver"},
+			},
+			map[string]interface{}{
+				"id":    "bike:5",
+				"model": "Mimas",
+				"description": "A real joy to ride, this bike got very high " +
+					"scores in last years Bike of the year report. The carefully " +
+					"crafted 50-34 tooth chainset and 11-32 tooth cassette give an " +
+					"easy-on-the-legs bottom gear for climbing, and the high-quality " +
+					"Vittoria Zaffiro tires give balance and grip.It includes " +
+					"a low-step frame , our memory foam seat, bump-resistant shocks and " +
+					"conveniently placed thumb throttle. Put it all together and you " +
+					"get a bike that helps redefine what can be done for this price.",
+				"price": 3941,
+				"specs": map[string]interface{}{"material": "alloy", "weight": 11.6},
+			},
+		},
+	},
+}
+
+func ExampleClient_setbikes() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:inventory")
+	// REMOVE_END
+
+	// STEP_START set_bikes
+	var inventory_json = map[string]interface{}{
+		"inventory": map[string]interface{}{
+			"mountain_bikes": []interface{}{
+				map[string]interface{}{
+					"id":    "bike:1",
+					"model": "Phoebe",
+					"description": "This is a mid-travel trail slayer that is a fantastic " +
+						"daily driver or one bike quiver. The Shimano Claris 8-speed groupset " +
+						"gives plenty of gear range to tackle hills and there\u2019s room for " +
+						"mudguards and a rack too.  This is the bike for the rider who wants " +
+						"trail manners with low fuss ownership.",
+					"price":  1920,
+					"specs":  map[string]interface{}{"material": "carbon", "weight": 13.1},
+					"colors": []interface{}{"black", "silver"},
+				},
+				map[string]interface{}{
+					"id":    "bike:2",
+					"model": "Quaoar",
+					"description": "Redesigned for the 2020 model year, this bike " +
+						"impressed our testers and is the best all-around trail bike we've " +
+						"ever tested. The Shimano gear system effectively does away with an " +
+						"external cassette, so is super low maintenance in terms of wear " +
+						"and tear. All in all it's an impressive package for the price, " +
+						"making it very competitive.",
+					"price":  2072,
+					"specs":  map[string]interface{}{"material": "aluminium", "weight": 7.9},
+					"colors": []interface{}{"black", "white"},
+				},
+				map[string]interface{}{
+					"id":    "bike:3",
+					"model": "Weywot",
+					"description": "This bike gives kids aged six years and older " +
+						"a durable and uberlight mountain bike for their first experience " +
+						"on tracks and easy cruising through forests and fields. A set of " +
+						"powerful Shimano hydraulic disc brakes provide ample stopping " +
+						"ability. If you're after a budget option, this is one of the best " +
+						"bikes you could get.",
+					"price": 3264,
+					"specs": map[string]interface{}{"material": "alloy", "weight": 13.8},
+				},
+			},
+			"commuter_bikes": []interface{}{
+				map[string]interface{}{
+					"id":    "bike:4",
+					"model": "Salacia",
+					"description": "This bike is a great option for anyone who just " +
+						"wants a bike to get about on With a slick-shifting Claris gears " +
+						"from Shimano\u2019s, this is a bike which doesn\u2019t break the " +
+						"bank and delivers craved performance.  It\u2019s for the rider " +
+						"who wants both efficiency and capability.",
+					"price":  1475,
+					"specs":  map[string]interface{}{"material": "aluminium", "weight": 16.6},
+					"colors": []interface{}{"black", "silver"},
+				},
+				map[string]interface{}{
+					"id":    "bike:5",
+					"model": "Mimas",
+					"description": "A real joy to ride, this bike got very high " +
+						"scores in last years Bike of the year report. The carefully " +
+						"crafted 50-34 tooth chainset and 11-32 tooth cassette give an " +
+						"easy-on-the-legs bottom gear for climbing, and the high-quality " +
+						"Vittoria Zaffiro tires give balance and grip.It includes " +
+						"a low-step frame , our memory foam seat, bump-resistant shocks and " +
+						"conveniently placed thumb throttle. Put it all together and you " +
+						"get a bike that helps redefine what can be done for this price.",
+					"price": 3941,
+					"specs": map[string]interface{}{"material": "alloy", "weight": 11.6},
+				},
+			},
+		},
+	}
+
+	res1, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res1) // >>> OK
+	// STEP_END
+
+	// Output:
+	// OK
+}
+
+func ExampleClient_getbikes() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:inventory")
+	// REMOVE_END
+
+	_, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START get_bikes
+	res2, err := rdb.JSONGetWithArgs(ctx, "bikes:inventory",
+		&redis.JSONGetArgs{Indent: "  ", Newline: "\n", Space: " "},
+		"$.inventory.*",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res2)
+	// >>>
+	// [
+	//   [
+	//     {
+	//       "colors": [
+	//         "black",
+	//         "silver"
+	// ...
+	// STEP_END
+
+	// Output:
+	// [
+	//   [
+	//     {
+	//       "colors": [
+	//         "black",
+	//         "silver"
+	//       ],
+	//       "description": "This bike is a great option for anyone who just wants a bike to get about on With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.  It’s for the rider who wants both efficiency and capability.",
+	//       "id": "bike:4",
+	//       "model": "Salacia",
+	//       "price": 1475,
+	//       "specs": {
+	//         "material": "aluminium",
+	//         "weight": 16.6
+	//       }
+	//     },
+	//     {
+	//       "description": "A real joy to ride, this bike got very high scores in last years Bike of the year report. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. Put it all together and you get a bike that helps redefine what can be done for this price.",
+	//       "id": "bike:5",
+	//       "model": "Mimas",
+	//       "price": 3941,
+	//       "specs": {
+	//         "material": "alloy",
+	//         "weight": 11.6
+	//       }
+	//     }
+	//   ],
+	//   [
+	//     {
+	//       "colors": [
+	//         "black",
+	//         "silver"
+	//       ],
+	//       "description": "This is a mid-travel trail slayer that is a fantastic daily driver or one bike quiver. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  This is the bike for the rider who wants trail manners with low fuss ownership.",
+	//       "id": "bike:1",
+	//       "model": "Phoebe",
+	//       "price": 1920,
+	//       "specs": {
+	//         "material": "carbon",
+	//         "weight": 13.1
+	//       }
+	//     },
+	//     {
+	//       "colors": [
+	//         "black",
+	//         "white"
+	//       ],
+	//       "description": "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. All in all it's an impressive package for the price, making it very competitive.",
+	//       "id": "bike:2",
+	//       "model": "Quaoar",
+	//       "price": 2072,
+	//       "specs": {
+	//         "material": "aluminium",
+	//         "weight": 7.9
+	//       }
+	//     },
+	//     {
+	//       "description": "This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. If you're after a budget option, this is one of the best bikes you could get.",
+	//       "id": "bike:3",
+	//       "model": "Weywot",
+	//       "price": 3264,
+	//       "specs": {
+	//         "material": "alloy",
+	//         "weight": 13.8
+	//       }
+	//     }
+	//   ]
+	// ]
+}
+
+func ExampleClient_getmtnbikes() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:inventory")
+	// REMOVE_END
+
+	_, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START get_mtnbikes
+	res3, err := rdb.JSONGet(ctx, "bikes:inventory",
+		"$.inventory.mountain_bikes[*].model",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res3)
+	// >>> ["Phoebe","Quaoar","Weywot"]
+
+	res4, err := rdb.JSONGet(ctx,
+		"bikes:inventory", "$.inventory[\"mountain_bikes\"][*].model",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res4)
+	// >>> ["Phoebe","Quaoar","Weywot"]
+
+	res5, err := rdb.JSONGet(ctx,
+		"bikes:inventory", "$..mountain_bikes[*].model",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res5)
+	// >>> ["Phoebe","Quaoar","Weywot"]
+	// STEP_END
+
+	// Output:
+	// ["Phoebe","Quaoar","Weywot"]
+	// ["Phoebe","Quaoar","Weywot"]
+	// ["Phoebe","Quaoar","Weywot"]
+}
+
+func ExampleClient_getmodels() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:inventory")
+	// REMOVE_END
+
+	_, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START get_models
+	res6, err := rdb.JSONGet(ctx, "bikes:inventory", "$..model").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res6) // >>> ["Salacia","Mimas","Phoebe","Quaoar","Weywot"]
+	// STEP_END
+
+	// Output:
+	// ["Salacia","Mimas","Phoebe","Quaoar","Weywot"]
+}
+
+func ExampleClient_get2mtnbikes() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:inventory")
+	// REMOVE_END
+
+	_, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START get2mtnbikes
+	res7, err := rdb.JSONGet(ctx, "bikes:inventory", "$..mountain_bikes[0:2].model").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res7) // >>> ["Phoebe","Quaoar"]
+	// STEP_END
+
+	// Output:
+	// ["Phoebe","Quaoar"]
+}
+
+func ExampleClient_filter1() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:inventory")
+	// REMOVE_END
+
+	_, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START filter1
+	res8, err := rdb.JSONGetWithArgs(ctx, "bikes:inventory",
+		&redis.JSONGetArgs{Indent: "  ", Newline: "\n", Space: " "},
+		"$..mountain_bikes[?(@.price < 3000 && @.specs.weight < 10)]",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res8)
+	// >>>
+	// [
+	//   {
+	//     "colors": [
+	//       "black",
+	//       "white"
+	//     ],
+	//     "description": "Redesigned for the 2020 model year
+	// ...
+	// STEP_END
+
+	// Output:
+	// [
+	//   {
+	//     "colors": [
+	//       "black",
+	//       "white"
+	//     ],
+	//     "description": "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. All in all it's an impressive package for the price, making it very competitive.",
+	//     "id": "bike:2",
+	//     "model": "Quaoar",
+	//     "price": 2072,
+	//     "specs": {
+	//       "material": "aluminium",
+	//       "weight": 7.9
+	//     }
+	//   }
+	// ]
+}
+
+func ExampleClient_filter2() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:inventory")
+	// REMOVE_END
+
+	_, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START filter2
+	res9, err := rdb.JSONGet(ctx,
+		"bikes:inventory",
+		"$..[?(@.specs.material == 'alloy')].model",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res9) // >>> ["Mimas","Weywot"]
+	// STEP_END
+
+	// Output:
+	// ["Mimas","Weywot"]
+}
+
+func ExampleClient_filter3() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:inventory")
+	// REMOVE_END
+
+	_, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START filter3
+	res10, err := rdb.JSONGet(ctx,
+		"bikes:inventory",
+		"$..[?(@.specs.material =~ '(?i)al')].model",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res10) // >>> ["Salacia","Mimas","Quaoar","Weywot"]
+	// STEP_END
+
+	// Output:
+	// ["Salacia","Mimas","Quaoar","Weywot"]
+}
+
+func ExampleClient_filter4() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:inventory")
+	// REMOVE_END
+
+	_, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START filter4
+	res11, err := rdb.JSONSet(ctx,
+		"bikes:inventory",
+		"$.inventory.mountain_bikes[0].regex_pat",
+		"\"(?i)al\"",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res11) // >>> OK
+
+	res12, err := rdb.JSONSet(ctx,
+		"bikes:inventory",
+		"$.inventory.mountain_bikes[1].regex_pat",
+		"\"(?i)al\"",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res12) // >>> OK
+
+	res13, err := rdb.JSONSet(ctx,
+		"bikes:inventory",
+		"$.inventory.mountain_bikes[2].regex_pat",
+		"\"(?i)al\"",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res13) // >>> OK
+
+	res14, err := rdb.JSONGet(ctx,
+		"bikes:inventory",
+		"$.inventory.mountain_bikes[?(@.specs.material =~ @.regex_pat)].model",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res14) // >>> ["Quaoar","Weywot"]
+	// STEP_END
+
+	// Output:
+	// OK
+	// OK
+	// OK
+	// ["Quaoar","Weywot"]
+}
+
+func ExampleClient_updatebikes() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:inventory")
+	// REMOVE_END
+
+	_, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START update_bikes
+	res15, err := rdb.JSONGet(ctx, "bikes:inventory", "$..price").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res15) // >>> [1475,3941,1920,2072,3264]
+
+	res16, err := rdb.JSONNumIncrBy(ctx, "bikes:inventory", "$..price", -100).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res16) // >>> [1375,3841,1820,1972,3164]
+
+	res17, err := rdb.JSONNumIncrBy(ctx, "bikes:inventory", "$..price", 100).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res17) // >>> [1475,3941,1920,2072,3264]
+	// STEP_END
+
+	// Output:
+	// [1475,3941,1920,2072,3264]
+	// [1375,3841,1820,1972,3164]
+	// [1475,3941,1920,2072,3264]
+}
+
+func ExampleClient_updatefilters1() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:inventory")
+	// REMOVE_END
+
+	_, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START update_filters1
+	res18, err := rdb.JSONSet(ctx,
+		"bikes:inventory",
+		"$.inventory.*[?(@.price<2000)].price",
+		1500,
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res18) // >>> OK
+
+	res19, err := rdb.JSONGet(ctx, "bikes:inventory", "$..price").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res19) // >>> [1500,3941,1500,2072,3264]
+	// STEP_END
+
+	// Output:
+	// OK
+	// [1500,3941,1500,2072,3264]
+}
+
+func ExampleClient_updatefilters2() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:inventory")
+	// REMOVE_END
+
+	_, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START update_filters2
+	res20, err := rdb.JSONArrAppend(ctx,
+		"bikes:inventory",
+		"$.inventory.*[?(@.price<2000)].colors",
+		"\"pink\"",
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res20) // >>> [3 3]
+
+	res21, err := rdb.JSONGet(ctx, "bikes:inventory", "$..[*].colors").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res21)
+	// >>> [["black","silver","pink"],["black","silver","pink"],["black","white"]]
+	// STEP_END
+
+	// Output:
+	// [3 3]
+	// [["black","silver","pink"],["black","silver","pink"],["black","white"]]
+}
diff --git a/doctests/list_tutorial_test.go b/doctests/list_tutorial_test.go
new file mode 100644
index 000000000..908469ce0
--- /dev/null
+++ doctests/list_tutorial_test.go
@@ -0,0 +1,766 @@
+// EXAMPLE: list_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+
+func ExampleClient_queue() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:repairs")
+	// REMOVE_END
+
+	// STEP_START queue
+	res1, err := rdb.LPush(ctx, "bikes:repairs", "bike:1").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res1) // >>> 1
+
+	res2, err := rdb.LPush(ctx, "bikes:repairs", "bike:2").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res2) // >>> 2
+
+	res3, err := rdb.RPop(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res3) // >>> bike:1
+
+	res4, err := rdb.RPop(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res4) // >>> bike:2
+	// STEP_END
+
+	// Output:
+	// 1
+	// 2
+	// bike:1
+	// bike:2
+}
+
+func ExampleClient_stack() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:repairs")
+	// REMOVE_END
+
+	// STEP_START stack
+	res5, err := rdb.LPush(ctx, "bikes:repairs", "bike:1").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res5) // >>> 1
+
+	res6, err := rdb.LPush(ctx, "bikes:repairs", "bike:2").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res6) // >>> 2
+
+	res7, err := rdb.LPop(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res7) // >>> bike:2
+
+	res8, err := rdb.LPop(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res8) // >>> bike:1
+	// STEP_END
+
+	// Output:
+	// 1
+	// 2
+	// bike:2
+	// bike:1
+}
+
+func ExampleClient_llen() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:repairs")
+	// REMOVE_END
+
+	// STEP_START llen
+	res9, err := rdb.LLen(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res9) // >>> 0
+	// STEP_END
+
+	// Output:
+	// 0
+}
+
+func ExampleClient_lmove_lrange() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:repairs")
+	rdb.Del(ctx, "bikes:finished")
+	// REMOVE_END
+
+	// STEP_START lmove_lrange
+	res10, err := rdb.LPush(ctx, "bikes:repairs", "bike:1").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res10) // >>> 1
+
+	res11, err := rdb.LPush(ctx, "bikes:repairs", "bike:2").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res11) // >>> 2
+
+	res12, err := rdb.LMove(ctx, "bikes:repairs", "bikes:finished", "LEFT", "LEFT").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res12) // >>> bike:2
+
+	res13, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res13) // >>> [bike:1]
+
+	res14, err := rdb.LRange(ctx, "bikes:finished", 0, -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res14) // >>> [bike:2]
+	// STEP_END
+
+	// Output:
+	// 1
+	// 2
+	// bike:2
+	// [bike:1]
+	// [bike:2]
+}
+
+func ExampleClient_lpush_rpush() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:repairs")
+	// REMOVE_END
+
+	// STEP_START lpush_rpush
+	res15, err := rdb.RPush(ctx, "bikes:repairs", "bike:1").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res15) // >>> 1
+
+	res16, err := rdb.RPush(ctx, "bikes:repairs", "bike:2").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res16) // >>> 2
+
+	res17, err := rdb.LPush(ctx, "bikes:repairs", "bike:important_bike").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res17) // >>> 3
+
+	res18, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res18) // >>> [bike:important_bike bike:1 bike:2]
+	// STEP_END
+
+	// Output:
+	// 1
+	// 2
+	// 3
+	// [bike:important_bike bike:1 bike:2]
+}
+
+func ExampleClient_variadic() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:repairs")
+	// REMOVE_END
+
+	// STEP_START variadic
+	res19, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res19) // >>> 3
+
+	res20, err := rdb.LPush(ctx, "bikes:repairs", "bike:important_bike", "bike:very_important_bike").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res20) // >>> 5
+
+	res21, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res21) // >>> [bike:very_important_bike bike:important_bike bike:1 bike:2 bike:3]
+	// STEP_END
+
+	// Output:
+	// 3
+	// 5
+	// [bike:very_important_bike bike:important_bike bike:1 bike:2 bike:3]
+}
+
+func ExampleClient_lpop_rpop() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:repairs")
+	// REMOVE_END
+
+	// STEP_START lpop_rpop
+	res22, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res22) // >>> 3
+
+	res23, err := rdb.RPop(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res23) // >>> bike:3
+
+	res24, err := rdb.LPop(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res24) // >>> bike:1
+
+	res25, err := rdb.RPop(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res25) // >>> bike:2
+
+	res26, err := rdb.RPop(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		fmt.Println(err) // >>> redis: nil
+	}
+
+	fmt.Println(res26) // >>> <empty string>
+
+	// STEP_END
+
+	// Output:
+	// 3
+	// bike:3
+	// bike:1
+	// bike:2
+	// redis: nil
+	//
+}
+
+func ExampleClient_ltrim() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:repairs")
+	// REMOVE_END
+
+	// STEP_START ltrim
+	res27, err := rdb.LPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res27) // >>> 5
+
+	res28, err := rdb.LTrim(ctx, "bikes:repairs", 0, 2).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res28) // >>> OK
+
+	res29, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res29) // >>> [bike:5 bike:4 bike:3]
+	// STEP_END
+
+	// Output:
+	// 5
+	// OK
+	// [bike:5 bike:4 bike:3]
+}
+
+func ExampleClient_ltrim_end_of_list() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:repairs")
+	// REMOVE_END
+
+	// STEP_START ltrim_end_of_list
+	res30, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res30) // >>> 5
+
+	res31, err := rdb.LTrim(ctx, "bikes:repairs", -3, -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res31) // >>> OK
+
+	res32, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res32) // >>> [bike:3 bike:4 bike:5]
+	// STEP_END
+
+	// Output:
+	// 5
+	// OK
+	// [bike:3 bike:4 bike:5]
+}
+
+func ExampleClient_brpop() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:repairs")
+	// REMOVE_END
+
+	// STEP_START brpop
+	res33, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res33) // >>> 2
+
+	res34, err := rdb.BRPop(ctx, 1, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res34) // >>> [bikes:repairs bike:2]
+
+	res35, err := rdb.BRPop(ctx, 1, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res35) // >>> [bikes:repairs bike:1]
+
+	res36, err := rdb.BRPop(ctx, 1, "bikes:repairs").Result()
+
+	if err != nil {
+		fmt.Println(err) // >>> redis: nil
+	}
+
+	fmt.Println(res36) // >>> []
+	// STEP_END
+
+	// Output:
+	// 2
+	// [bikes:repairs bike:2]
+	// [bikes:repairs bike:1]
+	// redis: nil
+	// []
+}
+
+func ExampleClient_rule1() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "new_bikes")
+	// REMOVE_END
+
+	// STEP_START rule_1
+	res37, err := rdb.Del(ctx, "new_bikes").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res37) // >>> 0
+
+	res38, err := rdb.LPush(ctx, "new_bikes", "bike:1", "bike:2", "bike:3").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res38) // >>> 3
+	// STEP_END
+
+	// Output:
+	// 0
+	// 3
+}
+
+func ExampleClient_rule11() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "new_bikes")
+	// REMOVE_END
+
+	// STEP_START rule_1.1
+	res39, err := rdb.Set(ctx, "new_bikes", "bike:1", 0).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res39) // >>> OK
+
+	res40, err := rdb.Type(ctx, "new_bikes").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res40) // >>> string
+
+	res41, err := rdb.LPush(ctx, "new_bikes", "bike:2", "bike:3").Result()
+
+	if err != nil {
+		fmt.Println(err)
+		// >>> WRONGTYPE Operation against a key holding the wrong kind of value
+	}
+
+	fmt.Println(res41)
+	// STEP_END
+
+	// Output:
+	// OK
+	// string
+	// WRONGTYPE Operation against a key holding the wrong kind of value
+	// 0
+}
+
+func ExampleClient_rule2() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:repairs")
+	// REMOVE_END
+
+	// STEP_START rule_2
+	res42, err := rdb.LPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res42) // >>> 3
+
+	res43, err := rdb.Exists(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res43) // >>> 1
+
+	res44, err := rdb.LPop(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res44) // >>> bike:3
+
+	res45, err := rdb.LPop(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res45) // >>> bike:2
+
+	res46, err := rdb.LPop(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res46) // >>> bike:1
+
+	res47, err := rdb.Exists(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res47) // >>> 0
+	// STEP_END
+
+	// Output:
+	// 3
+	// 1
+	// bike:3
+	// bike:2
+	// bike:1
+	// 0
+}
+
+func ExampleClient_rule3() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:repairs")
+	// REMOVE_END
+
+	// STEP_START rule_3
+	res48, err := rdb.Del(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res48) // >>> 0
+
+	res49, err := rdb.LLen(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res49) // >>> 0
+
+	res50, err := rdb.LPop(ctx, "bikes:repairs").Result()
+
+	if err != nil {
+		fmt.Println(err) // >>> redis: nil
+	}
+
+	fmt.Println(res50) // >>> <empty string>
+	// STEP_END
+
+	// Output:
+	// 0
+	// 0
+	// redis: nil
+	//
+}
+
+func ExampleClient_ltrim1() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:repairs")
+	// REMOVE_END
+
+	// STEP_START ltrim.1
+	res51, err := rdb.LPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res51) // >>> 5
+
+	res52, err := rdb.LTrim(ctx, "bikes:repairs", 0, 2).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res52) // >>> OK
+
+	res53, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res53) // >>> [bike:5 bike:4 bike:3]
+	// STEP_END
+
+	// Output:
+	// 5
+	// OK
+	// [bike:5 bike:4 bike:3]
+}
diff --git a/doctests/sets_example_test.go b/doctests/sets_example_test.go
new file mode 100644
index 000000000..7446a2789
--- /dev/null
+++ doctests/sets_example_test.go
@@ -0,0 +1,442 @@
+// EXAMPLE: sets_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+func ExampleClient_sadd() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:racing:france")
+	rdb.Del(ctx, "bikes:racing:usa")
+	// REMOVE_END
+
+	// STEP_START sadd
+	res1, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res1) // >>> 1
+
+	res2, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res2) // >>> 0
+
+	res3, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:2", "bike:3").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res3) // >>> 2
+
+	res4, err := rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res4) // >>> 2
+	// STEP_END
+
+	// Output:
+	// 1
+	// 0
+	// 2
+	// 2
+}
+
+func ExampleClient_sismember() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:racing:france")
+	rdb.Del(ctx, "bikes:racing:usa")
+	// REMOVE_END
+
+	_, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	_, err = rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START sismember
+	res5, err := rdb.SIsMember(ctx, "bikes:racing:usa", "bike:1").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res5) // >>> true
+
+	res6, err := rdb.SIsMember(ctx, "bikes:racing:usa", "bike:2").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res6) // >>> false
+	// STEP_END
+
+	// Output:
+	// true
+	// false
+}
+
+func ExampleClient_sinter() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:racing:france")
+	rdb.Del(ctx, "bikes:racing:usa")
+	// REMOVE_END
+
+	_, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	_, err = rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START sinter
+	res7, err := rdb.SInter(ctx, "bikes:racing:france", "bikes:racing:usa").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res7) // >>> [bike:1]
+	// STEP_END
+
+	// Output:
+	// [bike:1]
+}
+
+func ExampleClient_scard() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:racing:france")
+	// REMOVE_END
+
+	_, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START scard
+	res8, err := rdb.SCard(ctx, "bikes:racing:france").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res8) // >>> 3
+	// STEP_END
+
+	// Output:
+	// 3
+}
+
+func ExampleClient_saddsmembers() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:racing:france")
+	// REMOVE_END
+
+	// STEP_START sadd_smembers
+	res9, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res9) // >>> 3
+
+	res10, err := rdb.SMembers(ctx, "bikes:racing:france").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res10) // >>> [bike:1 bike:2 bike:3]
+	// STEP_END
+
+	// Output:
+	// 3
+	// [bike:1 bike:2 bike:3]
+}
+
+func ExampleClient_smismember() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:racing:france")
+	// REMOVE_END
+
+	_, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START smismember
+	res11, err := rdb.SIsMember(ctx, "bikes:racing:france", "bike:1").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res11) // >>> true
+
+	res12, err := rdb.SMIsMember(ctx, "bikes:racing:france", "bike:2", "bike:3", "bike:4").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res12) // >>> [true true false]
+	// STEP_END
+
+	// Output:
+	// true
+	// [true true false]
+}
+
+func ExampleClient_sdiff() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+,	// REMOVE_START
+	rdb.Del(ctx, "bikes:racing:france")
+	rdb.Del(ctx, "bikes:racing:usa")
+	// REMOVE_END
+
+	// STEP_START sdiff
+	_, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	_, err = rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result()
+
+	res13, err := rdb.SDiff(ctx, "bikes:racing:france", "bikes:racing:usa").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res13) // >>> [bike:2 bike:3]
+	// STEP_END
+
+	// Output:
+	// [bike:2 bike:3]
+}
+
+func ExampleClient_multisets() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:racing:france")
+	rdb.Del(ctx, "bikes:racing:usa")
+	rdb.Del(ctx, "bikes:racing:italy")
+	// REMOVE_END
+
+	// STEP_START multisets
+	_, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	_, err = rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	_, err = rdb.SAdd(ctx, "bikes:racing:italy", "bike:1", "bike:2", "bike:3", "bike:4").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	res14, err := rdb.SInter(ctx, "bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res14) // >>> [bike:1]
+
+	res15, err := rdb.SUnion(ctx, "bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res15) // >>> [bike:1 bike:2 bike:3 bike:4]
+
+	res16, err := rdb.SDiff(ctx, "bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res16) // >>> []
+
+	res17, err := rdb.SDiff(ctx, "bikes:racing:usa", "bikes:racing:france").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res17) // >>> [bike:4]
+
+	res18, err := rdb.SDiff(ctx, "bikes:racing:france", "bikes:racing:usa").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res18) // >>> [bike:2 bike:3]
+	// STEP_END
+
+	// Output:
+	// [bike:1]
+	// [bike:1 bike:2 bike:3 bike:4]
+	// []
+	// [bike:4]
+	// [bike:2 bike:3]
+}
+
+func ExampleClient_srem() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bikes:racing:france")
+	// REMOVE_END
+
+	// STEP_START srem
+	_, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	res19, err := rdb.SRem(ctx, "bikes:racing:france", "bike:1").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res19) // >>> 1
+
+	res20, err := rdb.SPop(ctx, "bikes:racing:france").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res20) // >>> <random element>
+
+	res21, err := rdb.SMembers(ctx, "bikes:racing:france").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res21) // >>> <remaining elements>
+
+	res22, err := rdb.SRandMember(ctx, "bikes:racing:france").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res22) // >>> <random element>
+	// STEP_END
+
+	// Testable examples not available because the test output
+	// is not deterministic.
+}
diff --git a/doctests/ss_tutorial_test.go b/doctests/ss_tutorial_test.go
new file mode 100644
index 000000000..2a6924458
--- /dev/null
+++ doctests/ss_tutorial_test.go
@@ -0,0 +1,437 @@
+// EXAMPLE: ss_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+func ExampleClient_zadd() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "racer_scores")
+	// REMOVE_END
+
+	// STEP_START zadd
+	res1, err := rdb.ZAdd(ctx, "racer_scores",
+		redis.Z{Member: "Norem", Score: 10},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res1) // >>> 1
+
+	res2, err := rdb.ZAdd(ctx, "racer_scores",
+		redis.Z{Member: "Castilla", Score: 12},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res2) // >>> 1
+
+	res3, err := rdb.ZAdd(ctx, "racer_scores",
+		redis.Z{Member: "Norem", Score: 10},
+		redis.Z{Member: "Sam-Bodden", Score: 8},
+		redis.Z{Member: "Royce", Score: 10},
+		redis.Z{Member: "Ford", Score: 6},
+		redis.Z{Member: "Prickett", Score: 14},
+		redis.Z{Member: "Castilla", Score: 12},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res3) // >>> 4
+	// STEP_END
+
+	// Output:
+	// 1
+	// 1
+	// 4
+}
+
+func ExampleClient_zrange() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "racer_scores")
+	// REMOVE_END
+
+	_, err := rdb.ZAdd(ctx, "racer_scores",
+		redis.Z{Member: "Norem", Score: 10},
+		redis.Z{Member: "Sam-Bodden", Score: 8},
+		redis.Z{Member: "Royce", Score: 10},
+		redis.Z{Member: "Ford", Score: 6},
+		redis.Z{Member: "Prickett", Score: 14},
+		redis.Z{Member: "Castilla", Score: 12},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START zrange
+	res4, err := rdb.ZRange(ctx, "racer_scores", 0, -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res4)
+	// >>> [Ford Sam-Bodden Norem Royce Castilla Prickett]
+
+	res5, err := rdb.ZRevRange(ctx, "racer_scores", 0, -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res5)
+	// >>> [Prickett Castilla Royce Norem Sam-Bodden Ford]
+	// STEP_END
+
+	// Output:
+	// [Ford Sam-Bodden Norem Royce Castilla Prickett]
+	// [Prickett Castilla Royce Norem Sam-Bodden Ford]
+}
+
+func ExampleClient_zrangewithscores() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "racer_scores")
+	// REMOVE_END
+
+	_, err := rdb.ZAdd(ctx, "racer_scores",
+		redis.Z{Member: "Norem", Score: 10},
+		redis.Z{Member: "Sam-Bodden", Score: 8},
+		redis.Z{Member: "Royce", Score: 10},
+		redis.Z{Member: "Ford", Score: 6},
+		redis.Z{Member: "Prickett", Score: 14},
+		redis.Z{Member: "Castilla", Score: 12},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START zrange_withscores
+	res6, err := rdb.ZRangeWithScores(ctx, "racer_scores", 0, -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res6)
+	// >>> [{6 Ford} {8 Sam-Bodden} {10 Norem} {10 Royce} {12 Castilla} {14 Prickett}]
+	// STEP_END
+
+	// Output:
+	// [{6 Ford} {8 Sam-Bodden} {10 Norem} {10 Royce} {12 Castilla} {14 Prickett}]
+}
+
+func ExampleClient_zrangebyscore() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "racer_scores")
+	// REMOVE_END
+
+	_, err := rdb.ZAdd(ctx, "racer_scores",
+		redis.Z{Member: "Norem", Score: 10},
+		redis.Z{Member: "Sam-Bodden", Score: 8},
+		redis.Z{Member: "Royce", Score: 10},
+		redis.Z{Member: "Ford", Score: 6},
+		redis.Z{Member: "Prickett", Score: 14},
+		redis.Z{Member: "Castilla", Score: 12},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START zrangebyscore
+	res7, err := rdb.ZRangeByScore(ctx, "racer_scores",
+		&redis.ZRangeBy{Min: "-inf", Max: "10"},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res7)
+	// >>> [Ford Sam-Bodden Norem Royce]
+	// STEP_END
+
+	// Output:
+	// [Ford Sam-Bodden Norem Royce]
+}
+
+func ExampleClient_zremrangebyscore() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "racer_scores")
+	// REMOVE_END
+
+	_, err := rdb.ZAdd(ctx, "racer_scores",
+		redis.Z{Member: "Norem", Score: 10},
+		redis.Z{Member: "Sam-Bodden", Score: 8},
+		redis.Z{Member: "Royce", Score: 10},
+		redis.Z{Member: "Ford", Score: 6},
+		redis.Z{Member: "Prickett", Score: 14},
+		redis.Z{Member: "Castilla", Score: 12},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START zremrangebyscore
+	res8, err := rdb.ZRem(ctx, "racer_scores", "Castilla").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res8) // >>> 1
+
+	res9, err := rdb.ZRemRangeByScore(ctx, "racer_scores", "-inf", "9").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res9) // >>> 2
+
+	res10, err := rdb.ZRange(ctx, "racer_scores", 0, -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res10)
+	// >>> [Norem Royce Prickett]
+	// STEP_END
+
+	// Output:
+	// 1
+	// 2
+	// [Norem Royce Prickett]
+}
+
+func ExampleClient_zrank() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "racer_scores")
+	// REMOVE_END
+
+	_, err := rdb.ZAdd(ctx, "racer_scores",
+		redis.Z{Member: "Norem", Score: 10},
+		redis.Z{Member: "Royce", Score: 10},
+		redis.Z{Member: "Prickett", Score: 14},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START zrank
+	res11, err := rdb.ZRank(ctx, "racer_scores", "Norem").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res11) // >>> 0
+
+	res12, err := rdb.ZRevRank(ctx, "racer_scores", "Norem").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res12) // >>> 2
+	// STEP_END
+
+	// Output:
+	// 0
+	// 2
+}
+
+func ExampleClient_zaddlex() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "racer_scores")
+	// REMOVE_END
+
+	_, err := rdb.ZAdd(ctx, "racer_scores",
+		redis.Z{Member: "Norem", Score: 0},
+		redis.Z{Member: "Royce", Score: 0},
+		redis.Z{Member: "Prickett", Score: 0},
+	).Result()
+
+	// STEP_START zadd_lex
+	res13, err := rdb.ZAdd(ctx, "racer_scores",
+		redis.Z{Member: "Norem", Score: 0},
+		redis.Z{Member: "Sam-Bodden", Score: 0},
+		redis.Z{Member: "Royce", Score: 0},
+		redis.Z{Member: "Ford", Score: 0},
+		redis.Z{Member: "Prickett", Score: 0},
+		redis.Z{Member: "Castilla", Score: 0},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res13) // >>> 3
+
+	res14, err := rdb.ZRange(ctx, "racer_scores", 0, -1).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res14)
+	// >>> [Castilla Ford Norem Prickett Royce Sam-Bodden]
+
+	res15, err := rdb.ZRangeByLex(ctx, "racer_scores", &redis.ZRangeBy{
+		Min: "[A", Max: "[L",
+	}).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res15) // >>> [Castilla Ford]
+	// STEP_END
+
+	// Output:
+	// 3
+	// [Castilla Ford Norem Prickett Royce Sam-Bodden]
+	// [Castilla Ford]
+}
+
+func ExampleClient_leaderboard() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "racer_scores")
+	// REMOVE_END
+
+	// STEP_START leaderboard
+	res16, err := rdb.ZAdd(ctx, "racer_scores",
+		redis.Z{Member: "Wood", Score: 100},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res16) // >>> 1
+
+	res17, err := rdb.ZAdd(ctx, "racer_scores",
+		redis.Z{Member: "Henshaw", Score: 100},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res17) // >>> 1
+
+	res18, err := rdb.ZAdd(ctx, "racer_scores",
+		redis.Z{Member: "Henshaw", Score: 150},
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res18) // >>> 0
+
+	res19, err := rdb.ZIncrBy(ctx, "racer_scores", 50, "Wood").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res19) // >>> 150
+
+	res20, err := rdb.ZIncrBy(ctx, "racer_scores", 50, "Henshaw").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res20) // >>> 200
+	// STEP_END
+
+	// Output:
+	// 1
+	// 1
+	// 0
+	// 150
+	// 200
+}
diff --git a/doctests/string_example_test.go b/doctests/string_example_test.go
new file mode 100644
index 000000000..20ca85548
--- /dev/null
+++ doctests/string_example_test.go
@@ -0,0 +1,173 @@
+// EXAMPLE: set_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+func ExampleClient_set_get() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bike:1")
+	// REMOVE_END
+
+	// STEP_START set_get
+	res1, err := rdb.Set(ctx, "bike:1", "Deimos", 0).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res1) // >>> OK
+
+	res2, err := rdb.Get(ctx, "bike:1").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res2) // >>> Deimos
+	// STEP_END
+
+	// Output:
+	// OK
+	// Deimos
+}
+
+func ExampleClient_setnx_xx() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Set(ctx, "bike:1", "Deimos", 0)
+	// REMOVE_END
+
+	// STEP_START setnx_xx
+	res3, err := rdb.SetNX(ctx, "bike:1", "bike", 0).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res3) // >>> false
+
+	res4, err := rdb.Get(ctx, "bike:1").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res4) // >>> Deimos
+
+	res5, err := rdb.SetXX(ctx, "bike:1", "bike", 0).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res5) // >>> OK
+	// STEP_END
+
+	// Output:
+	// false
+	// Deimos
+	// true
+}
+
+func ExampleClient_mset() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "bike:1", "bike:2", "bike:3")
+	// REMOVE_END
+
+	// STEP_START mset
+	res6, err := rdb.MSet(ctx, "bike:1", "Deimos", "bike:2", "Ares", "bike:3", "Vanth").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res6) // >>> OK
+
+	res7, err := rdb.MGet(ctx, "bike:1", "bike:2", "bike:3").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res7) // >>> [Deimos Ares Vanth]
+	// STEP_END
+
+	// Output:
+	// OK
+	// [Deimos Ares Vanth]
+}
+
+func ExampleClient_incr() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "total_crashes")
+	// REMOVE_END
+
+	// STEP_START incr
+	res8, err := rdb.Set(ctx, "total_crashes", "0", 0).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res8) // >>> OK
+
+	res9, err := rdb.Incr(ctx, "total_crashes").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res9) // >>> 1
+
+	res10, err := rdb.IncrBy(ctx, "total_crashes", 10).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res10) // >>> 11
+	// STEP_END
+
+	// Output:
+	// OK
+	// 1
+	// 11
+}
diff --git a/doctests/tdigest_tutorial_test.go b/doctests/tdigest_tutorial_test.go
new file mode 100644
index 000000000..7589b0ec8
--- /dev/null
+++ doctests/tdigest_tutorial_test.go
@@ -0,0 +1,251 @@
+// EXAMPLE: tdigest_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+
+func ExampleClient_tdigstart() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "racer_ages", "bikes:sales")
+	// REMOVE_END
+
+	// STEP_START tdig_start
+	res1, err := rdb.TDigestCreate(ctx, "bikes:sales").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res1) // >>> OK
+
+	res2, err := rdb.TDigestAdd(ctx, "bikes:sales", 21).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res2) // >>> OK
+
+	res3, err := rdb.TDigestAdd(ctx, "bikes:sales",
+		150, 95, 75, 34,
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res3) // >>> OK
+
+	// STEP_END
+
+	// Output:
+	// OK
+	// OK
+	// OK
+}
+
+func ExampleClient_tdigcdf() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "racer_ages", "bikes:sales")
+	// REMOVE_END
+
+	// STEP_START tdig_cdf
+	res4, err := rdb.TDigestCreate(ctx, "racer_ages").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res4) // >>> OK
+
+	res5, err := rdb.TDigestAdd(ctx, "racer_ages",
+		45.88, 44.2, 58.03, 19.76, 39.84, 69.28,
+		50.97, 25.41, 19.27, 85.71, 42.63,
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res5) // >>> OK
+
+	res6, err := rdb.TDigestRank(ctx, "racer_ages", 50).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res6) // >>> [7]
+
+	res7, err := rdb.TDigestRank(ctx, "racer_ages", 50, 40).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res7) // >>> [7 4]
+	// STEP_END
+
+	// Output:
+	// OK
+	// OK
+	// [7]
+	// [7 4]
+}
+
+func ExampleClient_tdigquant() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "racer_ages")
+	// REMOVE_END
+
+	_, err := rdb.TDigestCreate(ctx, "racer_ages").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	_, err = rdb.TDigestAdd(ctx, "racer_ages",
+		45.88, 44.2, 58.03, 19.76, 39.84, 69.28,
+		50.97, 25.41, 19.27, 85.71, 42.63,
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START tdig_quant
+	res8, err := rdb.TDigestQuantile(ctx, "racer_ages", 0.5).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res8) // >>> [44.2]
+
+	res9, err := rdb.TDigestByRank(ctx, "racer_ages", 4).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res9) // >>> [42.63]
+	// STEP_END
+
+	// Output:
+	// [44.2]
+	// [42.63]
+}
+
+func ExampleClient_tdigmin() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "racer_ages")
+	// REMOVE_END
+
+	_, err := rdb.TDigestCreate(ctx, "racer_ages").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	_, err = rdb.TDigestAdd(ctx, "racer_ages",
+		45.88, 44.2, 58.03, 19.76, 39.84, 69.28,
+		50.97, 25.41, 19.27, 85.71, 42.63,
+	).Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START tdig_min
+	res10, err := rdb.TDigestMin(ctx, "racer_ages").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res10) // >>> 19.27
+
+	res11, err := rdb.TDigestMax(ctx, "racer_ages").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res11) // >>> 85.71
+	// STEP_END
+
+	// Output:
+	// 19.27
+	// 85.71
+}
+
+func ExampleClient_tdigreset() {
+	ctx := context.Background()
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "", // no password docs
+		DB:       0,  // use default DB
+	})
+
+	// REMOVE_START
+	rdb.Del(ctx, "racer_ages")
+	// REMOVE_END
+	_, err := rdb.TDigestCreate(ctx, "racer_ages").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	// STEP_START tdig_reset
+	res12, err := rdb.TDigestReset(ctx, "racer_ages").Result()
+
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(res12) // >>> OK
+	// STEP_END
+
+	// Output:
+	// OK
+}
diff --git example/del-keys-without-ttl/go.mod example/del-keys-without-ttl/go.mod
index 468d0a54f..c631a1bdf 100644
--- example/del-keys-without-ttl/go.mod
+++ 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.5.3
+	github.com/redis/go-redis/v9 v9.7.3
 	go.uber.org/zap v1.24.0
 )
 
diff --git example/hll/go.mod example/hll/go.mod
index 0126764ef..885573780 100644
--- example/hll/go.mod
+++ 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.5.3
+require github.com/redis/go-redis/v9 v9.7.3
 
 require (
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
diff --git example/lua-scripting/go.mod example/lua-scripting/go.mod
index 3f4c29d12..c15f410be 100644
--- example/lua-scripting/go.mod
+++ 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.5.3
+require github.com/redis/go-redis/v9 v9.7.3
 
 require (
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
diff --git example/otel/go.mod example/otel/go.mod
index fea4e72a5..de6fc56d0 100644
--- example/otel/go.mod
+++ example/otel/go.mod
@@ -9,8 +9,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.5.3
-	github.com/redis/go-redis/v9 v9.5.3
+	github.com/redis/go-redis/extra/redisotel/v9 v9.7.3
+	github.com/redis/go-redis/v9 v9.7.3
 	github.com/uptrace/uptrace-go v1.21.0
 	go.opentelemetry.io/otel v1.22.0
 )
@@ -23,7 +23,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.5.3 // indirect
+	github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3 // 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 example/redis-bloom/go.mod example/redis-bloom/go.mod
index 09fb5bed4..7e46b42db 100644
--- example/redis-bloom/go.mod
+++ 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.5.3
+require github.com/redis/go-redis/v9 v9.7.3
 
 require (
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
diff --git example/scan-struct/go.mod example/scan-struct/go.mod
index c01e31293..b1bf250f3 100644
--- example/scan-struct/go.mod
+++ 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.5.3
+	github.com/redis/go-redis/v9 v9.7.3
 )
 
 require (
diff --git extra/rediscensus/go.mod extra/rediscensus/go.mod
index d623cef36..bf8bb290d 100644
--- extra/rediscensus/go.mod
+++ 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.5.3
-	github.com/redis/go-redis/v9 v9.5.3
+	github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3
+	github.com/redis/go-redis/v9 v9.7.3
 	go.opencensus.io v0.24.0
 )
 
@@ -17,3 +17,8 @@ require (
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 )
+
+retract (
+	v9.5.3 // This version was accidentally released.
+	v9.7.2 // This version was accidentally released.
+)
diff --git extra/rediscmd/go.mod extra/rediscmd/go.mod
index a035c8694..d9dbfefb7 100644
--- extra/rediscmd/go.mod
+++ extra/rediscmd/go.mod
@@ -7,10 +7,15 @@ 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.5.3
+	github.com/redis/go-redis/v9 v9.7.3
 )
 
 require (
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 )
+
+retract (
+	v9.5.3 // This version was accidentally released.
+	v9.7.2 // This version was accidentally released.
+)
diff --git extra/redisotel/go.mod extra/redisotel/go.mod
index 587d3bc3a..f51799ed0 100644
--- extra/redisotel/go.mod
+++ 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.5.3
-	github.com/redis/go-redis/v9 v9.5.3
+	github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3
+	github.com/redis/go-redis/v9 v9.7.3
 	go.opentelemetry.io/otel v1.22.0
 	go.opentelemetry.io/otel/metric v1.22.0
 	go.opentelemetry.io/otel/sdk v1.22.0
@@ -22,3 +22,8 @@ require (
 	github.com/go-logr/stdr v1.2.2 // indirect
 	golang.org/x/sys v0.16.0 // indirect
 )
+
+retract (
+	v9.5.3 // This version was accidentally released.
+	v9.7.2 // This version was accidentally released.
+)
diff --git extra/redisprometheus/go.mod extra/redisprometheus/go.mod
index fcc35b9bd..477d3f000 100644
--- extra/redisprometheus/go.mod
+++ 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.5.3
+	github.com/redis/go-redis/v9 v9.7.3
 )
 
 require (
@@ -21,3 +21,8 @@ require (
 	golang.org/x/sys v0.4.0 // indirect
 	google.golang.org/protobuf v1.33.0 // indirect
 )
+
+retract (
+	v9.5.3 // This version was accidentally released.
+	v9.7.2 // This version was accidentally released.
+)
diff --git go.mod go.mod
index 6c65f094f..7a2c500a7 100644
--- go.mod
+++ go.mod
@@ -8,3 +8,9 @@ require (
 	github.com/cespare/xxhash/v2 v2.2.0
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
 )
+
+retract (
+	v9.7.2 // This version was accidentally released. Please use version 9.7.3 instead.
+	v9.5.4 // This version was accidentally released. Please use version 9.6.0 instead.
+	v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead.
+)
diff --git hash_commands.go hash_commands.go
index dcffdcdd9..6596c6f5f 100644
--- hash_commands.go
+++ hash_commands.go
@@ -225,7 +225,7 @@ func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Durati
 	return cmd
 }
 
-// HExpire - Sets the expiration time for specified fields in a hash in seconds.
+// HExpireWithArgs - Sets the expiration time for specified fields in a hash in seconds.
 // It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields.
 // The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields.
 // For more information - https://redis.io/commands/hexpire/
diff --git internal/pool/conn_check.go internal/pool/conn_check.go
index 07c261c2b..83190d394 100644
--- internal/pool/conn_check.go
+++ internal/pool/conn_check.go
@@ -3,7 +3,6 @@
 package pool
 
 import (
-	"crypto/tls"
 	"errors"
 	"io"
 	"net"
@@ -17,10 +16,6 @@ func connCheck(conn net.Conn) error {
 	// Reset previous timeout.
 	_ = conn.SetDeadline(time.Time{})
 
-	// Check if tls.Conn.
-	if c, ok := conn.(*tls.Conn); ok {
-		conn = c.NetConn()
-	}
 	sysConn, ok := conn.(syscall.Conn)
 	if !ok {
 		return nil
diff --git internal/pool/conn_check_test.go internal/pool/conn_check_test.go
index 214993339..2ade8a0b9 100644
--- internal/pool/conn_check_test.go
+++ internal/pool/conn_check_test.go
@@ -3,7 +3,6 @@
 package pool
 
 import (
-	"crypto/tls"
 	"net"
 	"net/http/httptest"
 	"time"
@@ -15,17 +14,12 @@ import (
 var _ = Describe("tests conn_check with real conns", func() {
 	var ts *httptest.Server
 	var conn net.Conn
-	var tlsConn *tls.Conn
 	var err error
 
 	BeforeEach(func() {
 		ts = httptest.NewServer(nil)
 		conn, err = net.DialTimeout(ts.Listener.Addr().Network(), ts.Listener.Addr().String(), time.Second)
 		Expect(err).NotTo(HaveOccurred())
-		tlsTestServer := httptest.NewUnstartedServer(nil)
-		tlsTestServer.StartTLS()
-		tlsConn, err = tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, tlsTestServer.Listener.Addr().Network(), tlsTestServer.Listener.Addr().String(), &tls.Config{InsecureSkipVerify: true})
-		Expect(err).NotTo(HaveOccurred())
 	})
 
 	AfterEach(func() {
@@ -39,23 +33,11 @@ var _ = Describe("tests conn_check with real conns", func() {
 		Expect(connCheck(conn)).To(HaveOccurred())
 	})
 
-	It("good tls conn check", func() {
-		Expect(connCheck(tlsConn)).NotTo(HaveOccurred())
-
-		Expect(tlsConn.Close()).NotTo(HaveOccurred())
-		Expect(connCheck(tlsConn)).To(HaveOccurred())
-	})
-
 	It("bad conn check", func() {
 		Expect(conn.Close()).NotTo(HaveOccurred())
 		Expect(connCheck(conn)).To(HaveOccurred())
 	})
 
-	It("bad tls conn check", func() {
-		Expect(tlsConn.Close()).NotTo(HaveOccurred())
-		Expect(connCheck(tlsConn)).To(HaveOccurred())
-	})
-
 	It("check conn deadline", func() {
 		Expect(conn.SetDeadline(time.Now())).NotTo(HaveOccurred())
 		time.Sleep(time.Millisecond * 10)
diff --git json.go json.go
index ca731db3a..b3cadf4b7 100644
--- json.go
+++ json.go
@@ -60,7 +60,7 @@ type JSONArrTrimArgs struct {
 type JSONCmd struct {
 	baseCmd
 	val      string
-	expanded []interface{}
+	expanded interface{}
 }
 
 var _ Cmder = (*JSONCmd)(nil)
@@ -100,11 +100,11 @@ func (cmd *JSONCmd) Result() (string, error) {
 	return cmd.Val(), cmd.Err()
 }
 
-func (cmd JSONCmd) Expanded() (interface{}, error) {
+func (cmd *JSONCmd) Expanded() (interface{}, error) {
 	if len(cmd.val) != 0 && cmd.expanded == nil {
 		err := json.Unmarshal([]byte(cmd.val), &cmd.expanded)
 		if err != nil {
-			return "", err
+			return nil, err
 		}
 	}
 
@@ -494,7 +494,7 @@ func (c cmdable) JSONMSet(ctx context.Context, params ...interface{}) *StatusCmd
 }
 
 // JSONNumIncrBy increments the number value stored at the specified path by the provided number.
-// For more information, see https://redis.io/commands/json.numincreby
+// For more information, see https://redis.io/docs/latest/commands/json.numincrby/
 func (c cmdable) JSONNumIncrBy(ctx context.Context, key, path string, value float64) *JSONCmd {
 	args := []interface{}{"JSON.NUMINCRBY", key, path, value}
 	cmd := newJSONCmd(ctx, args...)
diff --git json_test.go json_test.go
index d1ea24290..9139be3ac 100644
--- json_test.go
+++ json_test.go
@@ -2,6 +2,8 @@ package redis_test
 
 import (
 	"context"
+	"encoding/json"
+	"time"
 
 	. "github.com/bsm/ginkgo/v2"
 	. "github.com/bsm/gomega"
@@ -17,644 +19,800 @@ var _ = Describe("JSON Commands", Label("json"), func() {
 	ctx := context.TODO()
 	var client *redis.Client
 
-	BeforeEach(func() {
-		client = redis.NewClient(&redis.Options{Addr: ":6379"})
-		Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred())
-	})
-
-	AfterEach(func() {
-		Expect(client.Close()).NotTo(HaveOccurred())
-	})
-
-	Describe("arrays", Label("arrays"), func() {
-		It("should JSONArrAppend", Label("json.arrappend", "json"), func() {
-			cmd1 := client.JSONSet(ctx, "append2", "$", `{"a": [10], "b": {"a": [12, 13]}}`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-
-			cmd2 := client.JSONArrAppend(ctx, "append2", "$..a", 10)
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(Equal([]int64{2, 3}))
+	setupRedisClient := func(protocolVersion int) *redis.Client {
+		return redis.NewClient(&redis.Options{
+			Addr:          "localhost:6379",
+			DB:            0,
+			Protocol:      protocolVersion,
+			UnstableResp3: true,
 		})
+	}
 
-		It("should JSONArrIndex and JSONArrIndexWithArgs", Label("json.arrindex", "json"), func() {
-			cmd1, err := client.JSONSet(ctx, "index1", "$", `{"a": [10], "b": {"a": [12, 10]}}`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd1).To(Equal("OK"))
-
-			cmd2, err := client.JSONArrIndex(ctx, "index1", "$.b.a", 10).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd2).To(Equal([]int64{1}))
-
-			cmd3, err := client.JSONSet(ctx, "index2", "$", `[0,1,2,3,4]`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd3).To(Equal("OK"))
-
-			res, err := client.JSONArrIndex(ctx, "index2", "$", 1).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res[0]).To(Equal(int64(1)))
-
-			res, err = client.JSONArrIndex(ctx, "index2", "$", 1, 2).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res[0]).To(Equal(int64(-1)))
-
-			res, err = client.JSONArrIndex(ctx, "index2", "$", 4).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res[0]).To(Equal(int64(4)))
-
-			res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{}, 4).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res[0]).To(Equal(int64(4)))
-
-			stop := 5000
-			res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res[0]).To(Equal(int64(4)))
-
-			stop = -1
-			res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res[0]).To(Equal(int64(-1)))
-		})
-
-		It("should JSONArrIndex and JSONArrIndexWithArgs with $", Label("json.arrindex", "json"), func() {
-			doc := `{
-				"store": {
-					"book": [
-						{
-							"category": "reference",
-							"author": "Nigel Rees",
-							"title": "Sayings of the Century",
-							"price": 8.95,
-							"size": [10, 20, 30, 40]
-						},
-						{
-							"category": "fiction",
-							"author": "Evelyn Waugh",
-							"title": "Sword of Honour",
-							"price": 12.99,
-							"size": [50, 60, 70, 80]
-						},
-						{
-							"category": "fiction",
-							"author": "Herman Melville",
-							"title": "Moby Dick",
-							"isbn": "0-553-21311-3",
-							"price": 8.99,
-							"size": [5, 10, 20, 30]
-						},
-						{
-							"category": "fiction",
-							"author": "J. R. R. Tolkien",
-							"title": "The Lord of the Rings",
-							"isbn": "0-395-19395-8",
-							"price": 22.99,
-							"size": [5, 6, 7, 8]
-						}
-					],
-					"bicycle": {"color": "red", "price": 19.95}
-				}
-			}`
-			res, err := client.JSONSet(ctx, "doc1", "$", doc).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			resGet, err := client.JSONGet(ctx, "doc1", "$.store.book[?(@.price<10)].size").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(resGet).To(Equal("[[10,20,30,40],[5,10,20,30]]"))
-
-			resArr, err := client.JSONArrIndex(ctx, "doc1", "$.store.book[?(@.price<10)].size", 20).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(resArr).To(Equal([]int64{1, 2}))
-		})
-
-		It("should JSONArrInsert", Label("json.arrinsert", "json"), func() {
-			cmd1 := client.JSONSet(ctx, "insert2", "$", `[100, 200, 300, 200]`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-
-			cmd2 := client.JSONArrInsert(ctx, "insert2", "$", -1, 1, 2)
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(Equal([]int64{6}))
-
-			cmd3 := client.JSONGet(ctx, "insert2")
-			Expect(cmd3.Err()).NotTo(HaveOccurred())
-			// RESP2 vs RESP3
-			Expect(cmd3.Val()).To(Or(
-				Equal(`[100,200,300,1,2,200]`),
-				Equal(`[[100,200,300,1,2,200]]`)))
-		})
-
-		It("should JSONArrLen", Label("json.arrlen", "json"), func() {
-			cmd1 := client.JSONSet(ctx, "length2", "$", `{"a": [10], "b": {"a": [12, 10, 20, 12, 90, 10]}}`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-
-			cmd2 := client.JSONArrLen(ctx, "length2", "$..a")
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(Equal([]int64{1, 6}))
-		})
-
-		It("should JSONArrPop", Label("json.arrpop"), func() {
-			cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-
-			cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2)
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(Equal([]string{"300"}))
-
-			cmd3 := client.JSONGet(ctx, "pop4", "$")
-			Expect(cmd3.Err()).NotTo(HaveOccurred())
-			Expect(cmd3.Val()).To(Equal("[[100,200,200]]"))
-		})
-
-		It("should JSONArrTrim", Label("json.arrtrim", "json"), func() {
-			cmd1, err := client.JSONSet(ctx, "trim1", "$", `[0,1,2,3,4]`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd1).To(Equal("OK"))
-
-			stop := 3
-			cmd2, err := client.JSONArrTrimWithArgs(ctx, "trim1", "$", &redis.JSONArrTrimArgs{Start: 1, Stop: &stop}).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd2).To(Equal([]int64{3}))
-
-			res, err := client.JSONGet(ctx, "trim1", "$").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal(`[[1,2,3]]`))
-
-			cmd3, err := client.JSONSet(ctx, "trim2", "$", `[0,1,2,3,4]`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd3).To(Equal("OK"))
-
-			stop = 3
-			cmd4, err := client.JSONArrTrimWithArgs(ctx, "trim2", "$", &redis.JSONArrTrimArgs{Start: -1, Stop: &stop}).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd4).To(Equal([]int64{0}))
-
-			cmd5, err := client.JSONSet(ctx, "trim3", "$", `[0,1,2,3,4]`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd5).To(Equal("OK"))
-
-			stop = 99
-			cmd6, err := client.JSONArrTrimWithArgs(ctx, "trim3", "$", &redis.JSONArrTrimArgs{Start: 3, Stop: &stop}).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd6).To(Equal([]int64{2}))
-
-			cmd7, err := client.JSONSet(ctx, "trim4", "$", `[0,1,2,3,4]`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd7).To(Equal("OK"))
-
-			stop = 1
-			cmd8, err := client.JSONArrTrimWithArgs(ctx, "trim4", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd8).To(Equal([]int64{0}))
-
-			cmd9, err := client.JSONSet(ctx, "trim5", "$", `[0,1,2,3,4]`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd9).To(Equal("OK"))
-
-			stop = 11
-			cmd10, err := client.JSONArrTrimWithArgs(ctx, "trim5", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd10).To(Equal([]int64{0}))
-		})
-
-		It("should JSONArrPop", Label("json.arrpop", "json"), func() {
-			cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-
-			cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2)
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(Equal([]string{"300"}))
-
-			cmd3 := client.JSONGet(ctx, "pop4", "$")
-			Expect(cmd3.Err()).NotTo(HaveOccurred())
-			Expect(cmd3.Val()).To(Equal("[[100,200,200]]"))
-		})
+	AfterEach(func() {
+		if client != nil {
+			client.FlushDB(ctx)
+			client.Close()
+		}
 	})
 
-	Describe("get/set", Label("getset"), func() {
-		It("should JSONSet", Label("json.set", "json"), func() {
-			cmd := client.JSONSet(ctx, "set1", "$", `{"a": 1, "b": 2, "hello": "world"}`)
-			Expect(cmd.Err()).NotTo(HaveOccurred())
-			Expect(cmd.Val()).To(Equal("OK"))
+	protocols := []int{2, 3}
+	for _, protocol := range protocols {
+		BeforeEach(func() {
+			client = setupRedisClient(protocol)
+			Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred())
 		})
 
-		It("should JSONGet", Label("json.get", "json", "NonRedisEnterprise"), func() {
-			res, err := client.JSONSet(ctx, "get3", "$", `{"a": 1, "b": 2}`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-"}).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal(`{-"a":1,-"b":2}`))
-
-			res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-", Newline: `~`, Space: `!`}).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal(`{~-"a":!1,~-"b":!2~}`))
+		Describe("arrays", Label("arrays"), func() {
+			It("should JSONArrAppend", Label("json.arrappend", "json"), func() {
+				cmd1 := client.JSONSet(ctx, "append2", "$", `{"a": [10], "b": {"a": [12, 13]}}`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+
+				cmd2 := client.JSONArrAppend(ctx, "append2", "$..a", 10)
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(Equal([]int64{2, 3}))
+			})
+
+			It("should JSONArrIndex and JSONArrIndexWithArgs", Label("json.arrindex", "json"), func() {
+				cmd1, err := client.JSONSet(ctx, "index1", "$", `{"a": [10], "b": {"a": [12, 10]}}`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd1).To(Equal("OK"))
+
+				cmd2, err := client.JSONArrIndex(ctx, "index1", "$.b.a", 10).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd2).To(Equal([]int64{1}))
+
+				cmd3, err := client.JSONSet(ctx, "index2", "$", `[0,1,2,3,4]`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd3).To(Equal("OK"))
+
+				res, err := client.JSONArrIndex(ctx, "index2", "$", 1).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res[0]).To(Equal(int64(1)))
+
+				res, err = client.JSONArrIndex(ctx, "index2", "$", 1, 2).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res[0]).To(Equal(int64(-1)))
+
+				res, err = client.JSONArrIndex(ctx, "index2", "$", 4).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res[0]).To(Equal(int64(4)))
+
+				res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{}, 4).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res[0]).To(Equal(int64(4)))
+
+				stop := 5000
+				res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res[0]).To(Equal(int64(4)))
+
+				stop = -1
+				res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res[0]).To(Equal(int64(-1)))
+			})
+
+			It("should JSONArrIndex and JSONArrIndexWithArgs with $", Label("json.arrindex", "json"), func() {
+				doc := `{
+					"store": {
+						"book": [
+							{
+								"category": "reference",
+								"author": "Nigel Rees",
+								"title": "Sayings of the Century",
+								"price": 8.95,
+								"size": [10, 20, 30, 40]
+							},
+							{
+								"category": "fiction",
+								"author": "Evelyn Waugh",
+								"title": "Sword of Honour",
+								"price": 12.99,
+								"size": [50, 60, 70, 80]
+							},
+							{
+								"category": "fiction",
+								"author": "Herman Melville",
+								"title": "Moby Dick",
+								"isbn": "0-553-21311-3",
+								"price": 8.99,
+								"size": [5, 10, 20, 30]
+							},
+							{
+								"category": "fiction",
+								"author": "J. R. R. Tolkien",
+								"title": "The Lord of the Rings",
+								"isbn": "0-395-19395-8",
+								"price": 22.99,
+								"size": [5, 6, 7, 8]
+							}
+						],
+						"bicycle": {"color": "red", "price": 19.95}
+					}
+				}`
+				res, err := client.JSONSet(ctx, "doc1", "$", doc).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				resGet, err := client.JSONGet(ctx, "doc1", "$.store.book[?(@.price<10)].size").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(resGet).To(Equal("[[10,20,30,40],[5,10,20,30]]"))
+
+				resArr, err := client.JSONArrIndex(ctx, "doc1", "$.store.book[?(@.price<10)].size", 20).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(resArr).To(Equal([]int64{1, 2}))
+			})
+
+			It("should JSONArrInsert", Label("json.arrinsert", "json"), func() {
+				cmd1 := client.JSONSet(ctx, "insert2", "$", `[100, 200, 300, 200]`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+
+				cmd2 := client.JSONArrInsert(ctx, "insert2", "$", -1, 1, 2)
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(Equal([]int64{6}))
+
+				cmd3 := client.JSONGet(ctx, "insert2")
+				Expect(cmd3.Err()).NotTo(HaveOccurred())
+				// RESP2 vs RESP3
+				Expect(cmd3.Val()).To(Or(
+					Equal(`[100,200,300,1,2,200]`),
+					Equal(`[[100,200,300,1,2,200]]`)))
+			})
+
+			It("should JSONArrLen", Label("json.arrlen", "json"), func() {
+				cmd1 := client.JSONSet(ctx, "length2", "$", `{"a": [10], "b": {"a": [12, 10, 20, 12, 90, 10]}}`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+
+				cmd2 := client.JSONArrLen(ctx, "length2", "$..a")
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(Equal([]int64{1, 6}))
+			})
+
+			It("should JSONArrPop", Label("json.arrpop"), func() {
+				cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+
+				cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2)
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(Equal([]string{"300"}))
+
+				cmd3 := client.JSONGet(ctx, "pop4", "$")
+				Expect(cmd3.Err()).NotTo(HaveOccurred())
+				Expect(cmd3.Val()).To(Equal("[[100,200,200]]"))
+			})
+
+			It("should JSONArrTrim", Label("json.arrtrim", "json"), func() {
+				cmd1, err := client.JSONSet(ctx, "trim1", "$", `[0,1,2,3,4]`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd1).To(Equal("OK"))
+
+				stop := 3
+				cmd2, err := client.JSONArrTrimWithArgs(ctx, "trim1", "$", &redis.JSONArrTrimArgs{Start: 1, Stop: &stop}).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd2).To(Equal([]int64{3}))
+
+				res, err := client.JSONGet(ctx, "trim1", "$").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal(`[[1,2,3]]`))
+
+				cmd3, err := client.JSONSet(ctx, "trim2", "$", `[0,1,2,3,4]`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd3).To(Equal("OK"))
+
+				stop = 3
+				cmd4, err := client.JSONArrTrimWithArgs(ctx, "trim2", "$", &redis.JSONArrTrimArgs{Start: -1, Stop: &stop}).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd4).To(Equal([]int64{0}))
+
+				cmd5, err := client.JSONSet(ctx, "trim3", "$", `[0,1,2,3,4]`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd5).To(Equal("OK"))
+
+				stop = 99
+				cmd6, err := client.JSONArrTrimWithArgs(ctx, "trim3", "$", &redis.JSONArrTrimArgs{Start: 3, Stop: &stop}).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd6).To(Equal([]int64{2}))
+
+				cmd7, err := client.JSONSet(ctx, "trim4", "$", `[0,1,2,3,4]`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd7).To(Equal("OK"))
+
+				stop = 1
+				cmd8, err := client.JSONArrTrimWithArgs(ctx, "trim4", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd8).To(Equal([]int64{0}))
+
+				cmd9, err := client.JSONSet(ctx, "trim5", "$", `[0,1,2,3,4]`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd9).To(Equal("OK"))
+
+				stop = 11
+				cmd10, err := client.JSONArrTrimWithArgs(ctx, "trim5", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd10).To(Equal([]int64{0}))
+			})
+
+			It("should JSONArrPop", Label("json.arrpop", "json"), func() {
+				cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+
+				cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2)
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(Equal([]string{"300"}))
+
+				cmd3 := client.JSONGet(ctx, "pop4", "$")
+				Expect(cmd3.Err()).NotTo(HaveOccurred())
+				Expect(cmd3.Val()).To(Equal("[[100,200,200]]"))
+			})
 		})
 
-		It("should JSONMerge", Label("json.merge", "json"), func() {
-			res, err := client.JSONSet(ctx, "merge1", "$", `{"a": 1, "b": 2}`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			res, err = client.JSONMerge(ctx, "merge1", "$", `{"b": 3, "c": 4}`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			res, err = client.JSONGet(ctx, "merge1", "$").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal(`[{"a":1,"b":3,"c":4}]`))
+		Describe("get/set", Label("getset"), func() {
+			It("should JSONSet", Label("json.set", "json"), func() {
+				cmd := client.JSONSet(ctx, "set1", "$", `{"a": 1, "b": 2, "hello": "world"}`)
+				Expect(cmd.Err()).NotTo(HaveOccurred())
+				Expect(cmd.Val()).To(Equal("OK"))
+			})
+
+			It("should JSONGet", Label("json.get", "json", "NonRedisEnterprise"), func() {
+				res, err := client.JSONSet(ctx, "get3", "$", `{"a": 1, "b": 2}`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-"}).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal(`{-"a":1,-"b":2}`))
+
+				res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-", Newline: `~`, Space: `!`}).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal(`{~-"a":!1,~-"b":!2~}`))
+			})
+
+			It("should JSONMerge", Label("json.merge", "json"), func() {
+				res, err := client.JSONSet(ctx, "merge1", "$", `{"a": 1, "b": 2}`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				res, err = client.JSONMerge(ctx, "merge1", "$", `{"b": 3, "c": 4}`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				res, err = client.JSONGet(ctx, "merge1", "$").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal(`[{"a":1,"b":3,"c":4}]`))
+			})
+
+			It("should JSONMSet", Label("json.mset", "json", "NonRedisEnterprise"), func() {
+				doc1 := redis.JSONSetArgs{Key: "mset1", Path: "$", Value: `{"a": 1}`}
+				doc2 := redis.JSONSetArgs{Key: "mset2", Path: "$", Value: 2}
+				docs := []redis.JSONSetArgs{doc1, doc2}
+
+				mSetResult, err := client.JSONMSetArgs(ctx, docs).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(mSetResult).To(Equal("OK"))
+
+				res, err := client.JSONMGet(ctx, "$", "mset1").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal([]interface{}{`[{"a":1}]`}))
+
+				res, err = client.JSONMGet(ctx, "$", "mset1", "mset2").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal([]interface{}{`[{"a":1}]`, "[2]"}))
+
+				_, err = client.JSONMSet(ctx, "mset1", "$.a", 2, "mset3", "$", `[1]`).Result()
+				Expect(err).NotTo(HaveOccurred())
+			})
+
+			It("should JSONMGet", Label("json.mget", "json", "NonRedisEnterprise"), func() {
+				cmd1 := client.JSONSet(ctx, "mget2a", "$", `{"a": ["aa", "ab", "ac", "ad"], "b": {"a": ["ba", "bb", "bc", "bd"]}}`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+				cmd2 := client.JSONSet(ctx, "mget2b", "$", `{"a": [100, 200, 300, 200], "b": {"a": [100, 200, 300, 200]}}`)
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(Equal("OK"))
+
+				cmd3 := client.JSONMGet(ctx, "$..a", "mget2a", "mget2b")
+				Expect(cmd3.Err()).NotTo(HaveOccurred())
+				Expect(cmd3.Val()).To(HaveLen(2))
+				Expect(cmd3.Val()[0]).To(Equal(`[["aa","ab","ac","ad"],["ba","bb","bc","bd"]]`))
+				Expect(cmd3.Val()[1]).To(Equal(`[[100,200,300,200],[100,200,300,200]]`))
+			})
+
+			It("should JSONMget with $", Label("json.mget", "json", "NonRedisEnterprise"), func() {
+				res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "b": 2, "nested": {"a": 3}, "c": "", "nested2": {"a": ""}}`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				res, err = client.JSONSet(ctx, "doc2", "$", `{"a": 4, "b": 5, "nested": {"a": 6}, "c": "", "nested2": {"a": [""]}}`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				iRes, err := client.JSONMGet(ctx, "$..a", "doc1").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(iRes).To(Equal([]interface{}{`[1,3,""]`}))
+
+				iRes, err = client.JSONMGet(ctx, "$..a", "doc1", "doc2").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(iRes).To(Equal([]interface{}{`[1,3,""]`, `[4,6,[""]]`}))
+
+				iRes, err = client.JSONMGet(ctx, "$..a", "non_existing_doc", "non_existing_doc1").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(iRes).To(Equal([]interface{}{nil, nil}))
+			})
 		})
 
-		It("should JSONMSet", Label("json.mset", "json", "NonRedisEnterprise"), func() {
-			doc1 := redis.JSONSetArgs{Key: "mset1", Path: "$", Value: `{"a": 1}`}
-			doc2 := redis.JSONSetArgs{Key: "mset2", Path: "$", Value: 2}
-			docs := []redis.JSONSetArgs{doc1, doc2}
-
-			mSetResult, err := client.JSONMSetArgs(ctx, docs).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(mSetResult).To(Equal("OK"))
-
-			res, err := client.JSONMGet(ctx, "$", "mset1").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal([]interface{}{`[{"a":1}]`}))
-
-			res, err = client.JSONMGet(ctx, "$", "mset1", "mset2").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal([]interface{}{`[{"a":1}]`, "[2]"}))
-
-			_, err = client.JSONMSet(ctx, "mset1", "$.a", 2, "mset3", "$", `[1]`).Result()
-			Expect(err).NotTo(HaveOccurred())
+		Describe("Misc", Label("misc"), func() {
+			It("should JSONClear", Label("json.clear", "json"), func() {
+				cmd1 := client.JSONSet(ctx, "clear1", "$", `[1]`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+
+				cmd2 := client.JSONClear(ctx, "clear1", "$")
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(Equal(int64(1)))
+
+				cmd3 := client.JSONGet(ctx, "clear1", "$")
+				Expect(cmd3.Err()).NotTo(HaveOccurred())
+				Expect(cmd3.Val()).To(Equal(`[[]]`))
+			})
+
+			It("should JSONClear with $", Label("json.clear", "json"), func() {
+				doc := `{
+					"nested1": {"a": {"foo": 10, "bar": 20}},
+					"a": ["foo"],
+					"nested2": {"a": "claro"},
+					"nested3": {"a": {"baz": 50}}
+				}`
+				res, err := client.JSONSet(ctx, "doc1", "$", doc).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				iRes, err := client.JSONClear(ctx, "doc1", "$..a").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(iRes).To(Equal(int64(3)))
+
+				resGet, err := client.JSONGet(ctx, "doc1", `$`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":[],"nested2":{"a":"claro"},"nested3":{"a":{}}}]`))
+
+				res, err = client.JSONSet(ctx, "doc1", "$", doc).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				iRes, err = client.JSONClear(ctx, "doc1", "$.nested1.a").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(iRes).To(Equal(int64(1)))
+
+				resGet, err = client.JSONGet(ctx, "doc1", `$`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":["foo"],"nested2":{"a":"claro"},"nested3":{"a":{"baz":50}}}]`))
+			})
+
+			It("should JSONDel", Label("json.del", "json"), func() {
+				cmd1 := client.JSONSet(ctx, "del1", "$", `[1]`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+
+				cmd2 := client.JSONDel(ctx, "del1", "$")
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(Equal(int64(1)))
+
+				cmd3 := client.JSONGet(ctx, "del1", "$")
+				Expect(cmd3.Err()).NotTo(HaveOccurred())
+				Expect(cmd3.Val()).To(HaveLen(0))
+			})
+
+			It("should JSONDel with $", Label("json.del", "json"), func() {
+				res, err := client.JSONSet(ctx, "del1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				iRes, err := client.JSONDel(ctx, "del1", "$..a").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(iRes).To(Equal(int64(2)))
+
+				resGet, err := client.JSONGet(ctx, "del1", "$").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`))
+
+				res, err = client.JSONSet(ctx, "del2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				iRes, err = client.JSONDel(ctx, "del2", "$..a").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(iRes).To(Equal(int64(1)))
+
+				resGet, err = client.JSONGet(ctx, "del2", "$").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`))
+
+				doc := `[
+					{
+						"ciao": ["non ancora"],
+						"nested": [
+							{"ciao": [1, "a"]},
+							{"ciao": [2, "a"]},
+							{"ciaoc": [3, "non", "ciao"]},
+							{"ciao": [4, "a"]},
+							{"e": [5, "non", "ciao"]}
+						]
+					}
+				]`
+				res, err = client.JSONSet(ctx, "del3", "$", doc).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				iRes, err = client.JSONDel(ctx, "del3", `$.[0]["nested"]..ciao`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(iRes).To(Equal(int64(3)))
+
+				resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]`
+				resGet, err = client.JSONGet(ctx, "del3", "$").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(resGet).To(Equal(resVal))
+			})
+
+			It("should JSONForget", Label("json.forget", "json"), func() {
+				cmd1 := client.JSONSet(ctx, "forget3", "$", `{"a": [1,2,3], "b": {"a": [1,2,3], "b": "annie"}}`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+
+				cmd2 := client.JSONForget(ctx, "forget3", "$..a")
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(Equal(int64(2)))
+
+				cmd3 := client.JSONGet(ctx, "forget3", "$")
+				Expect(cmd3.Err()).NotTo(HaveOccurred())
+				Expect(cmd3.Val()).To(Equal(`[{"b":{"b":"annie"}}]`))
+			})
+
+			It("should JSONForget with $", Label("json.forget", "json"), func() {
+				res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				iRes, err := client.JSONForget(ctx, "doc1", "$..a").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(iRes).To(Equal(int64(2)))
+
+				resGet, err := client.JSONGet(ctx, "doc1", "$").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`))
+
+				res, err = client.JSONSet(ctx, "doc2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				iRes, err = client.JSONForget(ctx, "doc2", "$..a").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(iRes).To(Equal(int64(1)))
+
+				resGet, err = client.JSONGet(ctx, "doc2", "$").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`))
+
+				doc := `[
+					{
+						"ciao": ["non ancora"],
+						"nested": [
+							{"ciao": [1, "a"]},
+							{"ciao": [2, "a"]},
+							{"ciaoc": [3, "non", "ciao"]},
+							{"ciao": [4, "a"]},
+							{"e": [5, "non", "ciao"]}
+						]
+					}
+				]`
+				res, err = client.JSONSet(ctx, "doc3", "$", doc).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				iRes, err = client.JSONForget(ctx, "doc3", `$.[0]["nested"]..ciao`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(iRes).To(Equal(int64(3)))
+
+				resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]`
+				resGet, err = client.JSONGet(ctx, "doc3", "$").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(resGet).To(Equal(resVal))
+			})
+
+			It("should JSONNumIncrBy", Label("json.numincrby", "json"), func() {
+				cmd1 := client.JSONSet(ctx, "incr3", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+
+				cmd2 := client.JSONNumIncrBy(ctx, "incr3", "$..a[1]", float64(1))
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(Equal(`[3,0]`))
+			})
+
+			It("should JSONNumIncrBy with $", Label("json.numincrby", "json"), func() {
+				res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 2).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal(`[7]`))
+
+				res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 3.5).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal(`[10.5]`))
+
+				res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				res, err = client.JSONNumIncrBy(ctx, "doc2", "$.b[0].a", 3).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal(`[5]`))
+			})
+
+			It("should JSONObjKeys", Label("json.objkeys", "json"), func() {
+				cmd1 := client.JSONSet(ctx, "objkeys1", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+
+				cmd2 := client.JSONObjKeys(ctx, "objkeys1", "$..*")
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(HaveLen(7))
+				Expect(cmd2.Val()).To(Equal([]interface{}{nil, []interface{}{"a"}, nil, nil, nil, nil, nil}))
+			})
+
+			It("should JSONObjKeys with $", Label("json.objkeys", "json"), func() {
+				doc := `{
+					"nested1": {"a": {"foo": 10, "bar": 20}},
+					"a": ["foo"],
+					"nested2": {"a": {"baz": 50}}
+				}`
+				cmd1, err := client.JSONSet(ctx, "objkeys1", "$", doc).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd1).To(Equal("OK"))
+
+				cmd2, err := client.JSONObjKeys(ctx, "objkeys1", "$.nested1.a").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd2).To(Equal([]interface{}{[]interface{}{"foo", "bar"}}))
+
+				cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".*.a").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd2).To(Equal([]interface{}{"foo", "bar"}))
+
+				cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".nested2.a").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd2).To(Equal([]interface{}{"baz"}))
+
+				_, err = client.JSONObjKeys(ctx, "non_existing_doc", "..a").Result()
+				Expect(err).To(HaveOccurred())
+			})
+
+			It("should JSONObjLen", Label("json.objlen", "json"), func() {
+				cmd1 := client.JSONSet(ctx, "objlen2", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+
+				cmd2 := client.JSONObjLen(ctx, "objlen2", "$..*")
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(HaveLen(7))
+				Expect(cmd2.Val()[0]).To(BeNil())
+				Expect(*cmd2.Val()[1]).To(Equal(int64(1)))
+			})
+
+			It("should JSONStrLen", Label("json.strlen", "json"), func() {
+				cmd1 := client.JSONSet(ctx, "strlen2", "$", `{"a": "alice", "b": "bob", "c": {"a": "alice", "b": "bob"}}`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+
+				cmd2 := client.JSONStrLen(ctx, "strlen2", "$..*")
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(HaveLen(5))
+				var tmp int64 = 20
+				Expect(cmd2.Val()[0]).To(BeAssignableToTypeOf(&tmp))
+				Expect(*cmd2.Val()[0]).To(Equal(int64(5)))
+				Expect(*cmd2.Val()[1]).To(Equal(int64(3)))
+				Expect(cmd2.Val()[2]).To(BeNil())
+				Expect(*cmd2.Val()[3]).To(Equal(int64(5)))
+				Expect(*cmd2.Val()[4]).To(Equal(int64(3)))
+			})
+
+			It("should JSONStrAppend", Label("json.strappend", "json"), func() {
+				cmd1, err := client.JSONSet(ctx, "strapp1", "$", `"foo"`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd1).To(Equal("OK"))
+				cmd2, err := client.JSONStrAppend(ctx, "strapp1", "$", `"bar"`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(*cmd2[0]).To(Equal(int64(6)))
+				cmd3, err := client.JSONGet(ctx, "strapp1", "$").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cmd3).To(Equal(`["foobar"]`))
+			})
+
+			It("should JSONStrAppend and JSONStrLen with $", Label("json.strappend", "json.strlen", "json"), func() {
+				res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(,Equal("OK"))
+
+				intArrayResult, err := client.JSONStrAppend(ctx, "doc1", "$.nested1.a", `"baz"`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(*intArrayResult[0]).To(Equal(int64(8)))
+
+				res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(res).To(Equal("OK"))
+
+				intResult, err := client.JSONStrLen(ctx, "doc2", "$.nested1.a").Result()
+				Expect(err).NotTo(HaveOccurred())
+				Expect(*intResult[0]).To(Equal(int64(5)))
+			})
+
+			It("should JSONToggle", Label("json.toggle", "json"), func() {
+				cmd1 := client.JSONSet(ctx, "toggle1", "$", `[true]`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+
+				cmd2 := client.JSONToggle(ctx, "toggle1", "$[0]")
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(HaveLen(1))
+				Expect(*cmd2.Val()[0]).To(Equal(int64(0)))
+			})
+
+			It("should JSONType", Label("json.type", "json"), func() {
+				cmd1 := client.JSONSet(ctx, "type1", "$", `[true]`)
+				Expect(cmd1.Err()).NotTo(HaveOccurred())
+				Expect(cmd1.Val()).To(Equal("OK"))
+
+				cmd2 := client.JSONType(ctx, "type1", "$[0]")
+				Expect(cmd2.Err()).NotTo(HaveOccurred())
+				Expect(cmd2.Val()).To(HaveLen(1))
+				// RESP2 v RESP3
+				Expect(cmd2.Val()[0]).To(Or(Equal([]interface{}{"boolean"}), Equal("boolean")))
+			})
 		})
+	}
+})
 
-		It("should JSONMGet", Label("json.mget", "json", "NonRedisEnterprise"), func() {
-			cmd1 := client.JSONSet(ctx, "mget2a", "$", `{"a": ["aa", "ab", "ac", "ad"], "b": {"a": ["ba", "bb", "bc", "bd"]}}`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-			cmd2 := client.JSONSet(ctx, "mget2b", "$", `{"a": [100, 200, 300, 200], "b": {"a": [100, 200, 300, 200]}}`)
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(Equal("OK"))
-
-			cmd3 := client.JSONMGet(ctx, "$..a", "mget2a", "mget2b")
-			Expect(cmd3.Err()).NotTo(HaveOccurred())
-			Expect(cmd3.Val()).To(HaveLen(2))
-			Expect(cmd3.Val()[0]).To(Equal(`[["aa","ab","ac","ad"],["ba","bb","bc","bd"]]`))
-			Expect(cmd3.Val()[1]).To(Equal(`[[100,200,300,200],[100,200,300,200]]`))
+var _ = Describe("Go-Redis Advanced JSON and RediSearch Tests", func() {
+	var client *redis.Client
+	var ctx = context.Background()
+
+	setupRedisClient := func(protocolVersion int) *redis.Client {
+		return redis.NewClient(&redis.Options{
+			Addr:          "localhost:6379",
+			DB:            0,
+			Protocol:      protocolVersion, // Setting RESP2 or RESP3 protocol
+			UnstableResp3: true,            // Enable RESP3 features
 		})
+	}
 
-		It("should JSONMget with $", Label("json.mget", "json", "NonRedisEnterprise"), func() {
-			res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "b": 2, "nested": {"a": 3}, "c": "", "nested2": {"a": ""}}`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			res, err = client.JSONSet(ctx, "doc2", "$", `{"a": 4, "b": 5, "nested": {"a": 6}, "c": "", "nested2": {"a": [""]}}`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			iRes, err := client.JSONMGet(ctx, "$..a", "doc1").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(iRes).To(Equal([]interface{}{`[1,3,""]`}))
-
-			iRes, err = client.JSONMGet(ctx, "$..a", "doc1", "doc2").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(iRes).To(Equal([]interface{}{`[1,3,""]`, `[4,6,[""]]`}))
-
-			iRes, err = client.JSONMGet(ctx, "$..a", "non_existing_doc", "non_existing_doc1").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(iRes).To(Equal([]interface{}{nil, nil}))
-		})
+	AfterEach(func() {
+		if client != nil {
+			client.FlushDB(ctx)
+			client.Close()
+		}
 	})
 
-	Describe("Misc", Label("misc"), func() {
-		It("should JSONClear", Label("json.clear", "json"), func() {
-			cmd1 := client.JSONSet(ctx, "clear1", "$", `[1]`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-
-			cmd2 := client.JSONClear(ctx, "clear1", "$")
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(Equal(int64(1)))
-
-			cmd3 := client.JSONGet(ctx, "clear1", "$")
-			Expect(cmd3.Err()).NotTo(HaveOccurred())
-			Expect(cmd3.Val()).To(Equal(`[[]]`))
-		})
-
-		It("should JSONClear with $", Label("json.clear", "json"), func() {
-			doc := `{
-				"nested1": {"a": {"foo": 10, "bar": 20}},
-				"a": ["foo"],
-				"nested2": {"a": "claro"},
-				"nested3": {"a": {"baz": 50}}
-			}`
-			res, err := client.JSONSet(ctx, "doc1", "$", doc).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			iRes, err := client.JSONClear(ctx, "doc1", "$..a").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(iRes).To(Equal(int64(3)))
-
-			resGet, err := client.JSONGet(ctx, "doc1", `$`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":[],"nested2":{"a":"claro"},"nested3":{"a":{}}}]`))
-
-			res, err = client.JSONSet(ctx, "doc1", "$", doc).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			iRes, err = client.JSONClear(ctx, "doc1", "$.nested1.a").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(iRes).To(Equal(int64(1)))
-
-			resGet, err = client.JSONGet(ctx, "doc1", `$`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":["foo"],"nested2":{"a":"claro"},"nested3":{"a":{"baz":50}}}]`))
-		})
-
-		It("should JSONDel", Label("json.del", "json"), func() {
-			cmd1 := client.JSONSet(ctx, "del1", "$", `[1]`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-
-			cmd2 := client.JSONDel(ctx, "del1", "$")
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(Equal(int64(1)))
-
-			cmd3 := client.JSONGet(ctx, "del1", "$")
-			Expect(cmd3.Err()).NotTo(HaveOccurred())
-			Expect(cmd3.Val()).To(HaveLen(0))
-		})
-
-		It("should JSONDel with $", Label("json.del", "json"), func() {
-			res, err := client.JSONSet(ctx, "del1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			iRes, err := client.JSONDel(ctx, "del1", "$..a").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(iRes).To(Equal(int64(2)))
-
-			resGet, err := client.JSONGet(ctx, "del1", "$").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`))
-
-			res, err = client.JSONSet(ctx, "del2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			iRes, err = client.JSONDel(ctx, "del2", "$..a").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(iRes).To(Equal(int64(1)))
-
-			resGet, err = client.JSONGet(ctx, "del2", "$").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`))
-
-			doc := `[
-				{
-					"ciao": ["non ancora"],
-					"nested": [
-						{"ciao": [1, "a"]},
-						{"ciao": [2, "a"]},
-						{"ciaoc": [3, "non", "ciao"]},
-						{"ciao": [4, "a"]},
-						{"e": [5, "non", "ciao"]}
-					]
-				}
-			]`
-			res, err = client.JSONSet(ctx, "del3", "$", doc).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			iRes, err = client.JSONDel(ctx, "del3", `$.[0]["nested"]..ciao`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(iRes).To(Equal(int64(3)))
-
-			resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]`
-			resGet, err = client.JSONGet(ctx, "del3", "$").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(resGet).To(Equal(resVal))
-		})
-
-		It("should JSONForget", Label("json.forget", "json"), func() {
-			cmd1 := client.JSONSet(ctx, "forget3", "$", `{"a": [1,2,3], "b": {"a": [1,2,3], "b": "annie"}}`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-
-			cmd2 := client.JSONForget(ctx, "forget3", "$..a")
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(Equal(int64(2)))
-
-			cmd3 := client.JSONGet(ctx, "forget3", "$")
-			Expect(cmd3.Err()).NotTo(HaveOccurred())
-			Expect(cmd3.Val()).To(Equal(`[{"b":{"b":"annie"}}]`))
-		})
-
-		It("should JSONForget with $", Label("json.forget", "json"), func() {
-			res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			iRes, err := client.JSONForget(ctx, "doc1", "$..a").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(iRes).To(Equal(int64(2)))
-
-			resGet, err := client.JSONGet(ctx, "doc1", "$").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`))
-
-			res, err = client.JSONSet(ctx, "doc2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			iRes, err = client.JSONForget(ctx, "doc2", "$..a").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(iRes).To(Equal(int64(1)))
-
-			resGet, err = client.JSONGet(ctx, "doc2", "$").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`))
-
-			doc := `[
-				{
-					"ciao": ["non ancora"],
-					"nested": [
-						{"ciao": [1, "a"]},
-						{"ciao": [2, "a"]},
-						{"ciaoc": [3, "non", "ciao"]},
-						{"ciao": [4, "a"]},
-						{"e": [5, "non", "ciao"]}
-					]
-				}
-			]`
-			res, err = client.JSONSet(ctx, "doc3", "$", doc).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			iRes, err = client.JSONForget(ctx, "doc3", `$.[0]["nested"]..ciao`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(iRes).To(Equal(int64(3)))
-
-			resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]`
-			resGet, err = client.JSONGet(ctx, "doc3", "$").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(resGet).To(Equal(resVal))
-		})
-
-		It("should JSONNumIncrBy", Label("json.numincrby", "json"), func() {
-			cmd1 := client.JSONSet(ctx, "incr3", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-
-			cmd2 := client.JSONNumIncrBy(ctx, "incr3", "$..a[1]", float64(1))
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(Equal(`[3,0]`))
-		})
-
-		It("should JSONNumIncrBy with $", Label("json.numincrby", "json"), func() {
-			res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 2).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal(`[7]`))
-
-			res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 3.5).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal(`[10.5]`))
-
-			res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			res, err = client.JSONNumIncrBy(ctx, "doc2", "$.b[0].a", 3).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal(`[5]`))
-		})
-
-		It("should JSONObjKeys", Label("json.objkeys", "json"), func() {
-			cmd1 := client.JSONSet(ctx, "objkeys1", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-
-			cmd2 := client.JSONObjKeys(ctx, "objkeys1", "$..*")
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(HaveLen(7))
-			Expect(cmd2.Val()).To(Equal([]interface{}{nil, []interface{}{"a"}, nil, nil, nil, nil, nil}))
-		})
-
-		It("should JSONObjKeys with $", Label("json.objkeys", "json"), func() {
-			doc := `{
-				"nested1": {"a": {"foo": 10, "bar": 20}},
-				"a": ["foo"],
-				"nested2": {"a": {"baz": 50}}
-			}`
-			cmd1, err := client.JSONSet(ctx, "objkeys1", "$", doc).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd1).To(Equal("OK"))
-
-			cmd2, err := client.JSONObjKeys(ctx, "objkeys1", "$.nested1.a").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd2).To(Equal([]interface{}{[]interface{}{"foo", "bar"}}))
-
-			cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".*.a").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd2).To(Equal([]interface{}{"foo", "bar"}))
-
-			cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".nested2.a").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd2).To(Equal([]interface{}{"baz"}))
-
-			_, err = client.JSONObjKeys(ctx, "non_existing_doc", "..a").Result()
-			Expect(err).To(HaveOccurred())
-		})
-
-		It("should JSONObjLen", Label("json.objlen", "json"), func() {
-			cmd1 := client.JSONSet(ctx, "objlen2", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-
-			cmd2 := client.JSONObjLen(ctx, "objlen2", "$..*")
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(HaveLen(7))
-			Expect(cmd2.Val()[0]).To(BeNil())
-			Expect(*cmd2.Val()[1]).To(Equal(int64(1)))
-		})
-
-		It("should JSONStrLen", Label("json.strlen", "json"), func() {
-			cmd1 := client.JSONSet(ctx, "strlen2", "$", `{"a": "alice", "b": "bob", "c": {"a": "alice", "b": "bob"}}`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-
-			cmd2 := client.JSONStrLen(ctx, "strlen2", "$..*")
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(HaveLen(5))
-			var tmp int64 = 20
-			Expect(cmd2.Val()[0]).To(BeAssignableToTypeOf(&tmp))
-			Expect(*cmd2.Val()[0]).To(Equal(int64(5)))
-			Expect(*cmd2.Val()[1]).To(Equal(int64(3)))
-			Expect(cmd2.Val()[2]).To(BeNil())
-			Expect(*cmd2.Val()[3]).To(Equal(int64(5)))
-			Expect(*cmd2.Val()[4]).To(Equal(int64(3)))
-		})
-
-		It("should JSONStrAppend", Label("json.strappend", "json"), func() {
-			cmd1, err := client.JSONSet(ctx, "strapp1", "$", `"foo"`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd1).To(Equal("OK"))
-			cmd2, err := client.JSONStrAppend(ctx, "strapp1", "$", `"bar"`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(*cmd2[0]).To(Equal(int64(6)))
-			cmd3, err := client.JSONGet(ctx, "strapp1", "$").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(cmd3).To(Equal(`["foobar"]`))
-		})
-
-		It("should JSONStrAppend and JSONStrLen with $", Label("json.strappend", "json.strlen", "json"), func() {
-			res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			intArrayResult, err := client.JSONStrAppend(ctx, "doc1", "$.nested1.a", `"baz"`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(*intArrayResult[0]).To(Equal(int64(8)))
-
-			res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(res).To(Equal("OK"))
-
-			intResult, err := client.JSONStrLen(ctx, "doc2", "$.nested1.a").Result()
-			Expect(err).NotTo(HaveOccurred())
-			Expect(*intResult[0]).To(Equal(int64(5)))
-		})
-
-		It("should JSONToggle", Label("json.toggle", "json"), func() {
-			cmd1 := client.JSONSet(ctx, "toggle1", "$", `[true]`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-
-			cmd2 := client.JSONToggle(ctx, "toggle1", "$[0]")
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(HaveLen(1))
-			Expect(*cmd2.Val()[0]).To(Equal(int64(0)))
-		})
-
-		It("should JSONType", Label("json.type", "json"), func() {
-			cmd1 := client.JSONSet(ctx, "type1", "$", `[true]`)
-			Expect(cmd1.Err()).NotTo(HaveOccurred())
-			Expect(cmd1.Val()).To(Equal("OK"))
-
-			cmd2 := client.JSONType(ctx, "type1", "$[0]")
-			Expect(cmd2.Err()).NotTo(HaveOccurred())
-			Expect(cmd2.Val()).To(HaveLen(1))
-			// RESP2 v RESP3
-			Expect(cmd2.Val()[0]).To(Or(Equal([]interface{}{"boolean"}), Equal("boolean")))
-		})
+	Context("when testing with RESP2 and RESP3", func() {
+		protocols := []int{2, 3}
+
+		for _, protocol := range protocols {
+			When("using protocol version", func() {
+				BeforeEach(func() {
+					client = setupRedisClient(protocol)
+				})
+
+				It("should perform complex JSON and RediSearch operations", func() {
+					jsonDoc := map[string]interface{}{
+						"person": map[string]interface{}{
+							"name":   "Alice",
+							"age":    30,
+							"status": true,
+							"address": map[string]interface{}{
+								"city":     "Wonderland",
+								"postcode": "12345",
+							},
+							"contacts": []map[string]interface{}{
+								{"type": "email", "value": "[email protected]"},
+								{"type": "phone", "value": "+123456789"},
+								{"type": "fax", "value": "+987654321"},
+							},
+							"friends": []map[string]interface{}{
+								{"name": "Bob", "age": 35, "status": true},
+								{"name": "Charlie", "age": 28, "status": false},
+							},
+						},
+						"settings": map[string]interface{}{
+							"notifications": map[string]interface{}{
+								"email":  true,
+								"sms":    false,
+								"alerts": []string{"low battery", "door open"},
+							},
+							"theme": "dark",
+						},
+					}
+
+					setCmd := client.JSONSet(ctx, "person:1", ".", jsonDoc)
+					Expect(setCmd.Err()).NotTo(HaveOccurred(), "JSON.SET failed")
+
+					getCmdRaw := client.JSONGet(ctx, "person:1", ".")
+					rawJSON, err := getCmdRaw.Result()
+					Expect(err).NotTo(HaveOccurred(), "JSON.GET (raw) failed")
+					GinkgoWriter.Printf("Raw JSON: %s\n", rawJSON)
+
+					getCmdExpanded := client.JSONGet(ctx, "person:1", ".")
+					expandedJSON, err := getCmdExpanded.Expanded()
+					Expect(err).NotTo(HaveOccurred(), "JSON.GET (expanded) failed")
+					GinkgoWriter.Printf("Expanded JSON: %+v\n", expandedJSON)
+
+					Expect(rawJSON).To(MatchJSON(jsonMustMarshal(expandedJSON)))
+
+					arrAppendCmd := client.JSONArrAppend(ctx, "person:1", "$.person.contacts", `{"type": "social", "value": "@alice_wonder"}`)
+					Expect(arrAppendCmd.Err()).NotTo(HaveOccurred(), "JSON.ARRAPPEND failed")
+					arrLenCmd := client.JSONArrLen(ctx, "person:1", "$.person.contacts")
+					arrLen, err := arrLenCmd.Result()
+					Expect(err).NotTo(HaveOccurred(), "JSON.ARRLEN failed")
+					Expect(arrLen).To(Equal([]int64{4}), "Array length mismatch after append")
+
+					arrInsertCmd := client.JSONArrInsert(ctx, "person:1", "$.person.friends", 1, `{"name": "Diana", "age": 25, "status": true}`)
+					Expect(arrInsertCmd.Err()).NotTo(HaveOccurred(), "JSON.ARRINSERT failed")
+
+					start := 0
+					stop := 1
+					arrTrimCmd := client.JSONArrTrimWithArgs(ctx, "person:1", "$.person.friends", &redis.JSONArrTrimArgs{Start: start, Stop: &stop})
+					Expect(arrTrimCmd.Err()).NotTo(HaveOccurred(), "JSON.ARRTRIM failed")
+
+					mergeData := map[string]interface{}{
+						"status":    false,
+						"nickname":  "WonderAlice",
+						"lastLogin": time.Now().Format(time.RFC3339),
+					}
+					mergeCmd := client.JSONMerge(ctx, "person:1", "$.person", jsonMustMarshal(mergeData))
+					Expect(mergeCmd.Err()).NotTo(HaveOccurred(), "JSON.MERGE failed")
+
+					typeCmd := client.JSONType(ctx, "person:1", "$.person.nickname")
+					nicknameType, err := typeCmd.Result()
+					Expect(err).NotTo(HaveOccurred(), "JSON.TYPE failed")
+					Expect(nicknameType[0]).To(Equal([]interface{}{"string"}), "JSON.TYPE mismatch for nickname")
+
+					createIndexCmd := client.Do(ctx, "FT.CREATE", "person_idx", "ON", "JSON",
+						"PREFIX", "1", "person:", "SCHEMA",
+						"$.person.name", "AS", "name", "TEXT",
+						"$.person.age", "AS", "age", "NUMERIC",
+						"$.person.address.city", "AS", "city", "TEXT",
+						"$.person.contacts[*].value", "AS", "contact_value", "TEXT",
+					)
+					Expect(createIndexCmd.Err()).NotTo(HaveOccurred(), "FT.CREATE failed")
+
+					searchCmd := client.FTSearchWithArgs(ctx, "person_idx", "@contact_value:(alice\\@example\\.com alice_wonder)", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "$.person.name"}, {FieldName: "$.person.age"}, {FieldName: "$.person.address.city"}}})
+					searchResult, err := searchCmd.Result()
+					Expect(err).NotTo(HaveOccurred(), "FT.SEARCH failed")
+					GinkgoWriter.Printf("Advanced Search result: %+v\n", searchResult)
+
+					incrCmd := client.JSONNumIncrBy(ctx, "person:1", "$.person.age", 5)
+					incrResult, err := incrCmd.Result()
+					Expect(err).NotTo(HaveOccurred(), "JSON.NUMINCRBY failed")
+					Expect(incrResult).To(Equal("[35]"), "Age increment mismatch")
+
+					delCmd := client.JSONDel(ctx, "person:1", "$.settings.notifications.email")
+					Expect(delCmd.Err()).NotTo(HaveOccurred(), "JSON.DEL failed")
+
+					typeCmd = client.JSONType(ctx, "person:1", "$.settings.notifications.email")
+					typeResult, err := typeCmd.Result()
+					Expect(err).ToNot(HaveOccurred())
+					Expect(typeResult[0]).To(BeEmpty(), "Expected JSON.TYPE to be empty for deleted field")
+				})
+			})
+		}
 	})
 })
+
+// Helper function to marshal data into JSON for comparisons
+func jsonMustMarshal(v interface{}) string {
+	bytes, err := json.Marshal(v)
+	Expect(err).NotTo(HaveOccurred())
+	return string(bytes)
+}
diff --git options.go options.go
index 6ed693a0b..53991dc9d 100644
--- options.go
+++ options.go
@@ -148,11 +148,23 @@ type Options struct {
 	// Enables read only queries on slave/follower nodes.
 	readOnly bool
 
-	// Disable set-lib on connect. Default is false.
+	// DisableIndentity - Disable set-lib on connect.
+	//
+	// default: false
+	//
+	// Deprecated: Use DisableIdentity instead.
 	DisableIndentity bool
 
+	// DisableIdentity is used to disable CLIENT SETINFO command on connect.
+	//
+	// default: false
+	DisableIdentity bool
+
 	// Add suffix to client name. Default is empty.
 	IdentitySuffix string
+
+	// UnstableResp3 enables Unstable mode for Redis Search module with RESP3.
+	UnstableResp3 bool
 }
 
 func (opt *Options) init() {
diff --git osscluster.go osscluster.go
index 73a9e2b74..f5a576842 100644
--- osscluster.go
+++ osscluster.go
@@ -86,10 +86,24 @@ type ClusterOptions struct {
 	ConnMaxIdleTime time.Duration
 	ConnMaxLifetime time.Duration
 
-	TLSConfig        *tls.Config
-	DisableIndentity bool // Disable set-lib on connect. Default is false.
+	TLSConfig *tls.Config
+
+	// DisableIndentity - Disable set-lib on connect.
+	//
+	// default: false
+	//
+	// Deprecated: Use DisableIdentity instead.
+	DisableIndentity bool
+
+	// DisableIdentity is used to disable CLIENT SETINFO command on connect.
+	//
+	// default: false
+	DisableIdentity bool
 
 	IdentitySuffix string // Add suffix to client name. Default is empty.
+
+	// UnstableResp3 enables Unstable mode for Redis Search module with RESP3.
+	UnstableResp3 bool
 }
 
 func (opt *ClusterOptions) init() {
@@ -296,7 +310,8 @@ func (opt *ClusterOptions) clientOptions() *Options {
 		MaxActiveConns:   opt.MaxActiveConns,
 		ConnMaxIdleTime:  opt.ConnMaxIdleTime,
 		ConnMaxLifetime:  opt.ConnMaxLifetime,
-		DisableIndentity: opt.DisableIndentity,
+		DisableIdentity:  opt.DisableIdentity,
+		DisableIndentity: opt.DisableIdentity,
 		IdentitySuffix:   opt.IdentitySuffix,
 		TLSConfig:        opt.TLSConfig,
 		// If ClusterSlots is populated, then we probably have an artificial
@@ -304,7 +319,8 @@ func (opt *ClusterOptions) clientOptions() *Options {
 		// much use for ClusterSlots config).  This means we cannot execute the
 		// READONLY command against that node -- setting readOnly to false in such
 		// situations in the options below will prevent that from happening.
-		readOnly: opt.ReadOnly && opt.ClusterSlots == nil,
+		readOnly:      opt.ReadOnly && opt.ClusterSlots == nil,
+		UnstableResp3: opt.UnstableResp3,
 	}
 }
 
@@ -465,9 +481,11 @@ func (c *clusterNodes) Addrs() ([]string, error) {
 	closed := c.closed //nolint:ifshort
 	if !closed {
 		if len(c.activeAddrs) > 0 {
-			addrs = c.activeAddrs
+			addrs = make([]string, len(c.activeAddrs))
+			copy(addrs, c.activeAddrs)
 		} else {
-			addrs = c.addrs
+			addrs = make([]string, len(c.addrs))
+			copy(addrs, c.addrs)
 		}
 	}
 	c.mu.RUnlock()
@@ -938,10 +956,13 @@ func (c *ClusterClient) Process(ctx context.Context, cmd Cmder) error {
 func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
 	slot := c.cmdSlot(ctx, cmd)
 	var node *clusterNode
+	var moved bool
 	var ask bool
 	var lastErr error
 	for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ {
-		if attempt > 0 {
+		// MOVED and ASK responses are not transient errors that require retry delay; they
+		// should be attempted immediately.
+		if attempt > 0 && !moved && !ask {
 			if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
 				return err
 			}
@@ -985,7 +1006,6 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
 			continue
 		}
 
-		var moved bool
 		var addr string
 		moved, ask, addr = isMovedError(lastErr)
 		if moved || ask {
diff --git osscluster_test.go osscluster_test.go
index 3d2f80711..9c3eaba35 100644
--- osscluster_test.go
+++ osscluster_test.go
@@ -653,6 +653,58 @@ var _ = Describe("ClusterClient", func() {
 			Expect(client.Close()).NotTo(HaveOccurred())
 		})
 
+		It("determines hash slots correctly for generic commands", func() {
+			opt := redisClusterOptions()
+			opt.MaxRedirects = -1
+			client := cluster.newClusterClient(ctx, opt)
+
+			err := client.Do(ctx, "GET", "A").Err()
+			Expect(err).To(Equal(redis.Nil))
+
+			err = client.Do(ctx, []byte("GET"), []byte("A")).Err()
+			Expect(err).To(Equal(redis.Nil))
+
+			Eventually(func() error {
+				return client.SwapNodes(ctx, "A")
+			}, 30*time.Second).ShouldNot(HaveOccurred())
+
+			err = client.Do(ctx, "GET", "A").Err()
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(ContainSubstring("MOVED"))
+
+			err = client.Do(ctx, []byte("GET"), []byte("A")).Err()
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(ContainSubstring("MOVED"))
+
+			Expect(client.Close()).NotTo(HaveOccurred())
+		})
+
+		It("follows node redirection immediately", func() {
+			// Configure retry backoffs far in excess of the expected duration of redirection
+			opt := redisClusterOptions()
+			opt.MinRetryBackoff = 10 * time.Minute
+			opt.MaxRetryBackoff = 20 * time.Minute
+			client := cluster.newClusterClient(ctx, opt)
+
+			Eventually(func() error {
+				return client.SwapNodes(ctx, "A")
+			}, 30*time.Second).ShouldNot(HaveOccurred())
+
+			// Note that this context sets a deadline more aggressive than the lowest possible bound
+			// of the retry backoff; this verifies that redirection completes immediately.
+			redirCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
+			defer cancel()
+
+			err := client.Set(redirCtx, "A", "VALUE", 0).Err()
+			Expect(err).NotTo(HaveOccurred())
+
+			v, err := client.Get(redirCtx, "A").Result()
+			Expect(err).NotTo(HaveOccurred())
+			Expect(v).To(Equal("VALUE"))
+
+			Expect(client.Close()).NotTo(HaveOccurred())
+		})
+
 		It("calls fn for every master node", func() {
 			for i := 0; i < 10; i++ {
 				Expect(client.Set(ctx, strconv.Itoa(i), "", 0).Err()).NotTo(HaveOccurred())
diff --git redis.go redis.go
index 527afb677..730430515 100644
--- redis.go
+++ redis.go
@@ -41,7 +41,7 @@ type (
 )
 
 type hooksMixin struct {
-	hooksMu *sync.Mutex
+	hooksMu *sync.RWMutex
 
 	slice   []Hook
 	initial hooks
@@ -49,7 +49,7 @@ type hooksMixin struct {
 }
 
 func (hs *hooksMixin) initHooks(hooks hooks) {
-	hs.hooksMu = new(sync.Mutex)
+	hs.hooksMu = new(sync.RWMutex)
 	hs.initial = hooks
 	hs.chain()
 }
@@ -151,7 +151,7 @@ func (hs *hooksMixin) clone() hooksMixin {
 	clone := *hs
 	l := len(clone.slice)
 	clone.slice = clone.slice[:l:l]
-	clone.hooksMu = new(sync.Mutex)
+	clone.hooksMu = new(sync.RWMutex)
 	return clone
 }
 
@@ -176,9 +176,14 @@ func (hs *hooksMixin) withProcessPipelineHook(
 }
 
 func (hs *hooksMixin) dialHook(ctx context.Context, network, addr string) (net.Conn, error) {
-	hs.hooksMu.Lock()
-	defer hs.hooksMu.Unlock()
-	return hs.current.dial(ctx, network, addr)
+	// Access to hs.current is guarded by a read-only lock since it may be mutated by AddHook(...)
+	// while this dialer is concurrently accessed by the background connection pool population
+	// routine when MinIdleConns > 0.
+	hs.hooksMu.RLock()
+	current := hs.current
+	hs.hooksMu.RUnlock()
+
+	return current.dial(ctx, network, addr)
 }
 
 func (hs *hooksMixin) processHook(ctx context.Context, cmd Cmder) error {
@@ -345,7 +350,7 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {
 		return err
 	}
 
-	if !c.opt.DisableIndentity {
+	if !c.opt.DisableIdentity && !c.opt.DisableIndentity {
 		libName := ""
 		libVer := Version()
 		if c.opt.IdentitySuffix != "" {
@@ -354,7 +359,11 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {
 		p := conn.Pipeline()
 		p.ClientSetInfo(ctx, WithLibraryName(libName))
 		p.ClientSetInfo(ctx, WithLibraryVersion(libVer))
-		_, _ = p.Exec(ctx)
+		// Handle network errors (e.g. timeouts) in CLIENT SETINFO to avoid
+		// out of order responses later on.
+		if _, err = p.Exec(ctx); err != nil && !isRedisError(err) {
+			return err
+		}
 	}
 
 	if c.opt.OnConnect != nil {
@@ -412,6 +421,19 @@ func (c *baseClient) process(ctx context.Context, cmd Cmder) error {
 	return lastErr
 }
 
+func (c *baseClient) assertUnstableCommand(cmd Cmder) bool {
+	switch cmd.(type) {
+	case *AggregateCmd, *FTInfoCmd, *FTSpellCheckCmd, *FTSearchCmd, *FTSynDumpCmd:
+		if c.opt.UnstableResp3 {
+			return true
+		} else {
+			panic("RESP3 responses for this command are disabled because they may still change. Please set the flag UnstableResp3 .  See the [README](https://github.com/redis/go-redis/blob/master/README.md) and the release notes for guidance.")
+		}
+	default:
+		return false
+	}
+}
+
 func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool, error) {
 	if attempt > 0 {
 		if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
@@ -427,8 +449,12 @@ func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool
 			atomic.StoreUint32(&retryTimeout, 1)
 			return err
 		}
-
-		if err := cn.WithReader(c.context(ctx), c.cmdTimeout(cmd), cmd.readReply); err != nil {
+		readReplyFunc := cmd.readReply
+		// Apply unstable RESP3 search module.
+		if c.opt.Protocol != 2 && c.assertUnstableCommand(cmd) {
+			readReplyFunc = cmd.readRawReply
+		}
+		if err := cn.WithReader(c.context(ctx), c.cmdTimeout(cmd), readReplyFunc); err != nil {
 			if cmd.readTimeout() == nil {
 				atomic.StoreUint32(&retryTimeout, 1)
 			} else {
diff --git redis_test.go redis_test.go
index ef2125452..04836a684 100644
--- redis_test.go
+++ redis_test.go
@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"sync"
 	"testing"
 	"time"
 
@@ -373,6 +374,13 @@ var _ = Describe("Client timeout", func() {
 	})
 
 	testTimeout := func() {
+		It("SETINFO timeouts", func() {
+			conn := client.Conn()
+			err := conn.Ping(ctx).Err()
+			Expect(err).To(HaveOccurred())
+			Expect(err.(net.Error).Timeout()).To(BeTrue())
+		})
+
 		It("Ping timeouts", func() {
 			err := client.Ping(ctx).Err()
 			Expect(err).To(HaveOccurred())
@@ -633,3 +641,67 @@ var _ = Describe("Hook with MinIdleConns", func() {
 		}))
 	})
 })
+
+var _ = Describe("Dialer connection timeouts", func() {
+	var client *redis.Client
+
+	const dialSimulatedDelay = 1 * time.Second
+
+	BeforeEach(func() {
+		options := redisOptions()
+		options.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
+			// Simulated slow dialer.
+			// Note that the following sleep is deliberately not context-aware.
+			time.Sleep(dialSimulatedDelay)
+			return net.Dial("tcp", options.Addr)
+		}
+		options.MinIdleConns = 1
+		client = redis.NewClient(options)
+	})
+
+	AfterEach(func() {
+		err := client.Close()
+		Expect(err).NotTo(HaveOccurred())
+	})
+
+	It("does not contend on connection dial for concurrent commands", func() {
+		var wg sync.WaitGroup
+
+		const concurrency = 10
+
+		durations := make(chan time.Duration, concurrency)
+		errs := make(chan error, concurrency)
+
+		start := time.Now()
+		wg.Add(concurrency)
+
+		for i := 0; i < concurrency; i++ {
+			go func() {
+				defer wg.Done()
+
+				start := time.Now()
+				err := client.Ping(ctx).Err()
+				durations <- time.Since(start)
+				errs <- err
+			}()
+		}
+
+		wg.Wait()
+		close(durations)
+		close(errs)
+
+		// All commands should eventually succeed, after acquiring a connection.
+		for err := range errs {
+			Expect(err).NotTo(HaveOccurred())
+		}
+
+		// Each individual command should complete within the simulated dial duration bound.
+		for duration := range durations {
+			Expect(duration).To(BeNumerically("<", 2*dialSimulatedDelay))
+		}
+
+		// Due to concurrent execution, the entire test suite should also complete within
+		// the same dial duration bound applied for individual commands.
+		Expect(time.Since(start)).To(BeNumerically("<", 2*dialSimulatedDelay))
+	})
+})
diff --git ring.go ring.go
index 4ae00542b..990a4c887 100644
--- ring.go
+++ ring.go
@@ -98,8 +98,19 @@ type RingOptions struct {
 	TLSConfig *tls.Config
 	Limiter   Limiter
 
+	// DisableIndentity - Disable set-lib on connect.
+	//
+	// default: false
+	//
+	// Deprecated: Use DisableIdentity instead.
 	DisableIndentity bool
-	IdentitySuffix   string
+
+	// DisableIdentity is used to disable CLIENT SETINFO command on connect.
+	//
+	// default: false
+	DisableIdentity bool
+	IdentitySuffix  string
+	UnstableResp3   bool
 }
 
 func (opt *RingOptions) init() {
@@ -166,8 +177,11 @@ func (opt *RingOptions) clientOptions() *Options {
 		TLSConfig: opt.TLSConfig,
 		Limiter:   opt.Limiter,
 
+		DisableIdentity:  opt.DisableIdentity,
 		DisableIndentity: opt.DisableIndentity,
-		IdentitySuffix:   opt.IdentitySuffix,
+
+		IdentitySuffix: opt.IdentitySuffix,
+		UnstableResp3:  opt.UnstableResp3,
 	}
 }
 
diff --git search_commands.go search_commands.go
index 8214a570b..9359a723e 100644
--- search_commands.go
+++ search_commands.go
@@ -16,7 +16,7 @@ type SearchCmdable interface {
 	FTAliasAdd(ctx context.Context, index string, alias string) *StatusCmd
 	FTAliasDel(ctx context.Context, alias string) *StatusCmd
 	FTAliasUpdate(ctx context.Context, index string, alias string) *StatusCmd
-	FTAlter(ctx context.Context, index string, skipInitalScan bool, definition []interface{}) *StatusCmd
+	FTAlter(ctx context.Context, index string, skipInitialScan bool, definition []interface{}) *StatusCmd
 	FTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd
 	FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd
 	FTCreate(ctx context.Context, index string, options *FTCreateOptions, schema ...*FieldSchema) *StatusCmd
@@ -57,7 +57,7 @@ type FTCreateOptions struct {
 	NoFields        bool
 	NoFreqs         bool
 	StopWords       []interface{}
-	SkipInitalScan  bool
+	SkipInitialScan bool
 }
 
 type FieldSchema struct {
@@ -70,11 +70,13 @@ type FieldSchema struct {
 	NoIndex           bool
 	PhoneticMatcher   string
 	Weight            float64
-	Seperator         string
+	Separator         string
 	CaseSensitive     bool
 	WithSuffixtrie    bool
 	VectorArgs        *FTVectorArgs
 	GeoShapeFieldType string
+	IndexEmpty        bool
+	IndexMissing      bool
 }
 
 type FTVectorArgs struct {
@@ -245,6 +247,8 @@ type FTAggregateOptions struct {
 	GroupBy           []FTAggregateGroupBy
 	SortBy            []FTAggregateSortBy
 	SortByMax         int
+	Scorer            string
+	AddScores         bool
 	Apply             []FTAggregateApply
 	LimitOffset       int
 	Limit             int
@@ -283,7 +287,7 @@ type FTSearchSortBy struct {
 type FTSearchOptions struct {
 	NoContent       bool
 	Verbatim        bool
-	NoStopWrods     bool
+	NoStopWords     bool
 	WithScores      bool
 	WithPayloads    bool
 	WithSortKeys    bool
@@ -481,6 +485,15 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery
 		if options.Verbatim {
 			queryArgs = append(queryArgs, "VERBATIM")
 		}
+
+		if options.Scorer != "" {
+			queryArgs = append(queryArgs, "SCORER", options.Scorer)
+		}
+
+		if options.AddScores {
+			queryArgs = append(queryArgs, "ADDSCORES")
+		}
+
 		if options.LoadAll && options.Load != nil {
 			panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive")
 		}
@@ -489,16 +502,29 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery
 		}
 		if options.Load != nil {
 			queryArgs = append(queryArgs, "LOAD", len(options.Load))
+			index, count := len(queryArgs)-1, 0
 			for _, load := range options.Load {
 				queryArgs = append(queryArgs, load.Field)
+				count++
 				if load.As != "" {
 					queryArgs = append(queryArgs, "AS", load.As)
+					count += 2
 				}
 			}
+			queryArgs[index] = count
 		}
+
 		if options.Timeout > 0 {
 			queryArgs = append(queryArgs, "TIMEOUT", options.Timeout)
 		}
+
+		for _, apply := range options.Apply {
+			queryArgs = append(queryArgs, "APPLY", apply.Field)
+			if apply.As != "" {
+				queryArgs = append(queryArgs, "AS", apply.As)
+			}
+		}
+
 		if options.GroupBy != nil {
 			for _, groupBy := range options.GroupBy {
 				queryArgs = append(queryArgs, "GROUPBY", len(groupBy.Fields))
@@ -540,17 +566,8 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery
 		if options.SortByMax > 0 {
 			queryArgs = append(queryArgs, "MAX", options.SortByMax)
 		}
-		for _, apply := range options.Apply {
-			queryArgs = append(queryArgs, "APPLY", apply.Field)
-			if apply.As != "" {
-				queryArgs = append(queryArgs, "AS", apply.As)
-			}
-		}
-		if options.LimitOffset > 0 {
-			queryArgs = append(queryArgs, "LIMIT", options.LimitOffset)
-		}
-		if options.Limit > 0 {
-			queryArgs = append(queryArgs, options.Limit)
+		if options.LimitOffset >= 0 && options.Limit > 0 {
+			queryArgs = append(queryArgs, "LIMIT", options.LimitOffset, options.Limit)
 		}
 		if options.Filter != "" {
 			queryArgs = append(queryArgs, "FILTER", options.Filter)
@@ -572,6 +589,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery
 				queryArgs = append(queryArgs, key, value)
 			}
 		}
+
 		if options.DialectVersion > 0 {
 			queryArgs = append(queryArgs, "DIALECT", options.DialectVersion)
 		}
@@ -636,6 +654,14 @@ func (cmd *AggregateCmd) Result() (*FTAggregateResult, error) {
 	return cmd.val, cmd.err
 }
 
+func (cmd *AggregateCmd) RawVal() interface{} {
+	return cmd.rawVal
+}
+
+func (cmd *AggregateCmd) RawResult() (interface{}, error) {
+	return cmd.rawVal, cmd.err
+}
+
 func (cmd *AggregateCmd) String() string {
 	return cmdString(cmd, cmd.val)
 }
@@ -643,12 +669,11 @@ func (cmd *AggregateCmd) String() string {
 func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) {
 	data, err := rd.ReadSlice()
 	if err != nil {
-		cmd.err = err
-		return nil
+		return err
 	}
 	cmd.val, err = ProcessAggregateResult(data)
 	if err != nil {
-		cmd.err = err
+		return err
 	}
 	return nil
 }
@@ -664,6 +689,12 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st
 		if options.Verbatim {
 			args = append(args, "VERBATIM")
 		}
+		if options.Scorer != "" {
+			args = append(args, "SCORER", options.Scorer)
+		}
+		if options.AddScores {
+			args = append(args, "ADDSCORES")
+		}
 		if options.LoadAll && options.Load != nil {
 			panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive")
 		}
@@ -672,16 +703,26 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st
 		}
 		if options.Load != nil {
 			args = append(args, "LOAD", len(options.Load))
+			index, count := len(args)-1, 0
 			for _, load := range options.Load {
 				args = append(args, load.Field)
+				count++
 				if load.As != "" {
 					args = append(args, "AS", load.As)
+					count += 2
 				}
 			}
+			args[index] = count
 		}
 		if options.Timeout > 0 {
 			args = append(args, "TIMEOUT", options.Timeout)
 		}
+		for _, apply := range options.Apply {
+			args = append(args, "APPLY", apply.Field)
+			if apply.As != "" {
+				args = append(args, "AS", apply.As)
+			}
+		}
 		if options.GroupBy != nil {
 			for _, groupBy := range options.GroupBy {
 				args = append(args, "GROUPBY", len(groupBy.Fields))
@@ -723,17 +764,8 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st
 		if options.SortByMax > 0 {
 			args = append(args, "MAX", options.SortByMax)
 		}
-		for _, apply := range options.Apply {
-			args = append(args, "APPLY", apply.Field)
-			if apply.As != "" {
-				args = append(args, "AS", apply.As)
-			}
-		}
-		if options.LimitOffset > 0 {
-			args = append(args, "LIMIT", options.LimitOffset)
-		}
-		if options.Limit > 0 {
-			args = append(args, options.Limit)
+		if options.LimitOffset >= 0 && options.Limit > 0 {
+			args = append(args, "LIMIT", options.LimitOffset, options.Limit)
 		}
 		if options.Filter != "" {
 			args = append(args, "FILTER", options.Filter)
@@ -798,13 +830,13 @@ func (c cmdable) FTAliasUpdate(ctx context.Context, index string, alias string)
 }
 
 // FTAlter - Alters the definition of an existing index.
-// The 'index' parameter specifies the index to alter, and the 'skipInitalScan' parameter specifies whether to skip the initial scan.
+// The 'index' parameter specifies the index to alter, and the 'skipInitialScan' parameter specifies whether to skip the initial scan.
 // The 'definition' parameter specifies the new definition for the index.
 // For more information, please refer to the Redis documentation:
 // [FT.ALTER]: (https://redis.io/commands/ft.alter/)
-func (c cmdable) FTAlter(ctx context.Context, index string, skipInitalScan bool, definition []interface{}) *StatusCmd {
+func (c cmdable) FTAlter(ctx context.Context, index string, skipInitialScan bool, definition []interface{}) *StatusCmd {
 	args := []interface{}{"FT.ALTER", index}
-	if skipInitalScan {
+	if skipInitialScan {
 		args = append(args, "SKIPINITIALSCAN")
 	}
 	args = append(args, "SCHEMA", "ADD")
@@ -897,7 +929,7 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp
 			args = append(args, "STOPWORDS", len(options.StopWords))
 			args = append(args, options.StopWords...)
 		}
-		if options.SkipInitalScan {
+		if options.SkipInitialScan {
 			args = append(args, "SKIPINITIALSCAN")
 		}
 	}
@@ -993,8 +1025,8 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp
 		if schema.Weight > 0 {
 			args = append(args, "WEIGHT", schema.Weight)
 		}
-		if schema.Seperator != "" {
-			args = append(args, "SEPERATOR", schema.Seperator)
+		if schema.Separator != "" {
+			args = append(args, "SEPARATOR", schema.Separator)
 		}
 		if schema.CaseSensitive {
 			args = append(args, "CASESENSITIVE")
@@ -1002,6 +1034,13 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp
 		if schema.WithSuffixtrie {
 			args = append(args, "WITHSUFFIXTRIE")
 		}
+		if schema.IndexEmpty {
+			args = append(args, "INDEXEMPTY")
+		}
+		if schema.IndexMissing {
+			args = append(args, "INDEXMISSING")
+
+		}
 	}
 	cmd := NewStatusCmd(ctx, args...)
 	_ = c(ctx, cmd)
@@ -1328,6 +1367,13 @@ func (cmd *FTInfoCmd) Val() FTInfoResult {
 	return cmd.val
 }
 
+func (cmd *FTInfoCmd) RawVal() interface{} {
+	return cmd.rawVal
+}
+
+func (cmd *FTInfoCmd) RawResult() (interface{}, error) {
+	return cmd.rawVal, cmd.err
+}
 func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) {
 	n, err := rd.ReadMapLen()
 	if err != nil {
@@ -1356,7 +1402,7 @@ func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) {
 	}
 	cmd.val, err = parseFTInfo(data)
 	if err != nil {
-		cmd.err = err
+		return err
 	}
 
 	return nil
@@ -1438,15 +1484,22 @@ func (cmd *FTSpellCheckCmd) Val() []SpellCheckResult {
 	return cmd.val
 }
 
+func (cmd *FTSpellCheckCmd) RawVal() interface{} {
+	return cmd.rawVal
+}
+
+func (cmd *FTSpellCheckCmd) RawResult() (interface{}, error) {
+	return cmd.rawVal, cmd.err
+}
+
 func (cmd *FTSpellCheckCmd) readReply(rd *proto.Reader) (err error) {
 	data, err := rd.ReadSlice()
 	if err != nil {
-		cmd.err = err
-		return nil
+		return err
 	}
 	cmd.val, err = parseFTSpellCheck(data)
 	if err != nil {
-		cmd.err = err
+		return err
 	}
 	return nil
 }
@@ -1619,22 +1672,30 @@ func (cmd *FTSearchCmd) Val() FTSearchResult {
 	return cmd.val
 }
 
+func (cmd *FTSearchCmd) RawVal() interface{} {
+	return cmd.rawVal
+}
+
+func (cmd *FTSearchCmd) RawResult() (interface{}, error) {
+	return cmd.rawVal, cmd.err
+}
+
 func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) {
 	data, err := rd.ReadSlice()
 	if err != nil {
-		cmd.err = err
-		return nil
+		return err
 	}
 	cmd.val, err = parseFTSearch(data, cmd.options.NoContent, cmd.options.WithScores, cmd.options.WithPayloads, cmd.options.WithSortKeys)
 	if err != nil {
-		cmd.err = err
+		return err
 	}
 	return nil
 }
 
 // FTSearch - Executes a search query on an index.
 // The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query.
-// For more information, please refer to the Redis documentation:
+// For more information, please refer to the Redis documentation about [FT.SEARCH].
+//
 // [FT.SEARCH]: (https://redis.io/commands/ft.search/)
 func (c cmdable) FTSearch(ctx context.Context, index string, query string) *FTSearchCmd {
 	args := []interface{}{"FT.SEARCH", index, query}
@@ -1645,6 +1706,12 @@ func (c cmdable) FTSearch(ctx context.Context, index string, query string) *FTSe
 
 type SearchQuery []interface{}
 
+// FTSearchQuery - Executes a search query on an index with additional options.
+// The 'index' parameter specifies the index to search, the 'query' parameter specifies the search query,
+// and the 'options' parameter specifies additional options for the search.
+// For more information, please refer to the Redis documentation about [FT.SEARCH].
+//
+// [FT.SEARCH]: (https://redis.io/commands/ft.search/)
 func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery {
 	queryArgs := []interface{}{query}
 	if options != nil {
@@ -1654,7 +1721,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery {
 		if options.Verbatim {
 			queryArgs = append(queryArgs, "VERBATIM")
 		}
-		if options.NoStopWrods {
+		if options.NoStopWords {
 			queryArgs = append(queryArgs, "NOSTOPWORDS")
 		}
 		if options.WithScores {
@@ -1735,7 +1802,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery {
 				}
 			}
 			if options.SortByWithCount {
-				queryArgs = append(queryArgs, "WITHCOUT")
+				queryArgs = append(queryArgs, "WITHCOUNT")
 			}
 		}
 		if options.LimitOffset >= 0 && options.Limit > 0 {
@@ -1757,7 +1824,8 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery {
 // FTSearchWithArgs - Executes a search query on an index with additional options.
 // The 'index' parameter specifies the index to search, the 'query' parameter specifies the search query,
 // and the 'options' parameter specifies additional options for the search.
-// For more information, please refer to the Redis documentation:
+// For more information, please refer to the Redis documentation about [FT.SEARCH].
+//
 // [FT.SEARCH]: (https://redis.io/commands/ft.search/)
 func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *FTSearchCmd {
 	args := []interface{}{"FT.SEARCH", index, query}
@@ -1768,7 +1836,7 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin
 		if options.Verbatim {
 			args = append(args, "VERBATIM")
 		}
-		if options.NoStopWrods {
+		if options.NoStopWords {
 			args = append(args, "NOSTOPWORDS")
 		}
 		if options.WithScores {
@@ -1849,7 +1917,7 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin
 				}
 			}
 			if options.SortByWithCount {
-				args = append(args, "WITHCOUT")
+				args = append(args, "WITHCOUNT")
 			}
 		}
 		if options.LimitOffset >= 0 && options.Limit > 0 {
@@ -1895,6 +1963,14 @@ func (cmd *FTSynDumpCmd) Result() ([]FTSynDumpResult, error) {
 	return cmd.val, cmd.err
 }
 
+func (cmd *FTSynDumpCmd) RawVal() interface{} {
+	return cmd.rawVal
+}
+
+func (cmd *FTSynDumpCmd) RawResult() (interface{}, error) {
+	return cmd.rawVal, cmd.err
+}
+
 func (cmd *FTSynDumpCmd) readReply(rd *proto.Reader) error {
 	termSynonymPairs, err := rd.ReadSlice()
 	if err != nil {
diff --git search_test.go search_test.go
index 60888ef5c..af98e5f7d 100644
--- search_test.go
+++ search_test.go
@@ -2,6 +2,8 @@ package redis_test
 
 import (
 	"context"
+	"fmt"
+	"strconv"
 	"time"
 
 	. "github.com/bsm/ginkgo/v2"
@@ -18,11 +20,13 @@ func WaitForIndexing(c *redis.Client, index string) {
 				return
 			}
 			time.Sleep(100 * time.Millisecond)
+		} else {
+			return
 		}
 	}
 }
 
-var _ = Describe("RediSearch commands", Label("search"), func() {
+var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
 	ctx := context.TODO()
 	var client *redis.Client
 
@@ -123,6 +127,13 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
 		Expect(res2.Docs[1].ID).To(BeEquivalentTo("doc2"))
 		Expect(res2.Docs[0].ID).To(BeEquivalentTo("doc3"))
 
+		res3, err := client.FTSearchWithArgs(ctx, "num", "foo", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}, SortByWithCount: true}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res3.Total).To(BeEquivalentTo(int64(3)))
+
+		res4, err := client.FTSearchWithArgs(ctx, "num", "notpresentf00", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}, SortByWithCount: true}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res4.Total).To(BeEquivalentTo(int64(0)))
 	})
 
 	It("should FTCreate and FTSearch example", Label("search", "ftcreate", "ftsearch"), func() {
@@ -130,7 +141,7 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
 		Expect(err).NotTo(HaveOccurred())
 		Expect(val).To(BeEquivalentTo("OK"))
 		WaitForIndexing(client, "txt")
-		client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch impements a search engine on top of redis")
+		client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch implements a search engine on top of redis")
 		res1, err := client.FTSearchWithArgs(ctx, "txt", "search engine", &redis.FTSearchOptions{NoContent: true, Verbatim: true, LimitOffset: 0, Limit: 5}).Result()
 		Expect(err).NotTo(HaveOccurred())
 		Expect(res1.Total).To(BeEquivalentTo(int64(1)))
@@ -258,6 +269,8 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
 		Expect(err).NotTo(HaveOccurred())
 		Expect(res1.Total).To(BeEquivalentTo(int64(1)))
 
+		_, err = client.FTSearch(ctx, "idx_not_exist", "only in the body").Result()
+		Expect(err).To(HaveOccurred())
 	})
 
 	It("should FTSpellCheck", Label("search", "ftcreate", "ftsearch", "ftspellcheck"), func() {
@@ -430,7 +443,7 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
 		WaitForIndexing(client, "idx1")
 
 		client.HSet(ctx, "search", "title", "RediSearch",
-			"body", "Redisearch impements a search engine on top of redis",
+			"body", "Redisearch implements a search engine on top of redis",
 			"parent", "redis",
 			"random_num", 10)
 		client.HSet(ctx, "ai", "title", "RedisAI",
@@ -559,6 +572,11 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
 		res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
 		Expect(err).NotTo(HaveOccurred())
 		Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("b"))
+
+		options = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t1"}}, Limit: 1, LimitOffset: 0}
+		res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("a"))
 	})
 
 	It("should FTAggregate load ", Label("search", "ftaggregate"), func() {
@@ -581,11 +599,118 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
 		Expect(err).NotTo(HaveOccurred())
 		Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world"))
 
+		options = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t2", As: "t2alias"}}}
+		res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Rows[0].Fields["t2alias"]).To(BeEquivalentTo("world"))
+
+		options = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t1"}, {Field: "t2", As: "t2alias"}}}
+		res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello"))
+		Expect(res.Rows[0].Fields["t2alias"]).To(BeEquivalentTo("world"))
+
 		options = &redis.FTAggregateOptions{LoadAll: true}
 		res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
 		Expect(err).NotTo(HaveOccurred())
 		Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello"))
 		Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world"))
+
+		_, err = client.FTAggregateWithArgs(ctx, "idx_not_exist", "*", &redis.FTAggregateOptions{}).Result()
+		Expect(err).To(HaveOccurred())
+	})
+
+	It("should FTAggregate with scorer and addscores", Label("search", "ftaggregate", "NonRedisEnterprise"), func() {
+		title := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: false}
+		description := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, Sortable: false}
+		val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{"product:"}}, title, description).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).To(BeEquivalentTo("OK"))
+		WaitForIndexing(client, "idx1")
+
+		client.HSet(ctx, "product:1", "title", "New Gaming Laptop", "description", "this is not a desktop")
+		client.HSet(ctx, "product:2", "title", "Super Old Not Gaming Laptop", "description", "this laptop is not a new laptop but it is a laptop")
+		client.HSet(ctx, "product:3", "title", "Office PC", "description", "office desktop pc")
+
+		options := &redis.FTAggregateOptions{
+			AddScores: true,
+			Scorer:    "BM25",
+			SortBy: []redis.FTAggregateSortBy{{
+				FieldName: "@__score",
+				Desc:      true,
+			}},
+		}
+
+		res, err := client.FTAggregateWithArgs(ctx, "idx1", "laptop", options).Result()
+		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)
+		Expect(err).NotTo(HaveOccurred())
+		score2, err := strconv.ParseFloat(fmt.Sprintf("%s", res.Rows[1].Fields["__score"]), 64)
+		Expect(err).NotTo(HaveOccurred())
+		Expect(score1).To(BeNumerically(">", score2))
+
+		optionsDM := &redis.FTAggregateOptions{
+			AddScores: true,
+			Scorer:    "DISMAX",
+			SortBy: []redis.FTAggregateSortBy{{
+				FieldName: "@__score",
+				Desc:      true,
+			}},
+		}
+
+		resDM, err := client.FTAggregateWithArgs(ctx, "idx1", "laptop", optionsDM).Result()
+		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)
+		Expect(err).NotTo(HaveOccurred())
+		score2DM, err := strconv.ParseFloat(fmt.Sprintf("%s", resDM.Rows[1].Fields["__score"]), 64)
+		Expect(err).NotTo(HaveOccurred())
+		Expect(score1DM).To(BeNumerically(">", score2DM))
+
+		Expect(score1DM).To(BeEquivalentTo(float64(4)))
+		Expect(score2DM).To(BeEquivalentTo(float64(1)))
+		Expect(score1).NotTo(BeEquivalentTo(score1DM))
+		Expect(score2).NotTo(BeEquivalentTo(score2DM))
+	})
+
+	It("should FTAggregate apply and groupby", Label("search", "ftaggregate"), func() {
+		text1 := &redis.FieldSchema{FieldName: "PrimaryKey", FieldType: redis.SearchFieldTypeText, Sortable: true}
+		num1 := &redis.FieldSchema{FieldName: "CreatedDateTimeUTC", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}
+		val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).To(BeEquivalentTo("OK"))
+		WaitForIndexing(client, "idx1")
+
+		// 6 feb
+		client.HSet(ctx, "doc1", "PrimaryKey", "9::362330", "CreatedDateTimeUTC", "1738823999")
+
+		// 12 feb
+		client.HSet(ctx, "doc2", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "1739342399")
+		client.HSet(ctx, "doc3", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "1739353199")
+
+		reducer := redis.FTAggregateReducer{Reducer: redis.SearchCount, As: "perDay"}
+
+		options := &redis.FTAggregateOptions{
+			Apply: []redis.FTAggregateApply{{Field: "floo,r(@CreatedDateTimeUTC /(60*60*24))", As: "TimestampAsDay"}},
+			GroupBy: []redis.FTAggregateGroupBy{{
+				Fields: []interface{}{"@TimestampAsDay"},
+				Reduce: []redis.FTAggregateReducer{reducer},
+			}},
+			SortBy: []redis.FTAggregateSortBy{{
+				FieldName: "@perDay",
+				Desc:      true,
+			}},
+		}
+
+		res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res).ToNot(BeNil())
+		Expect(len(res.Rows)).To(BeEquivalentTo(2))
+		Expect(res.Rows[0].Fields["perDay"]).To(BeEquivalentTo("2"))
+		Expect(res.Rows[1].Fields["perDay"]).To(BeEquivalentTo("1"))
 	})
 
 	It("should FTAggregate apply", Label("search", "ftaggregate"), func() {
@@ -632,14 +757,13 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
 			Expect(res.Rows[0].Fields["age"]).To(BeEquivalentTo("19"))
 			Expect(res.Rows[1].Fields["age"]).To(BeEquivalentTo("25"))
 		}
-
 	})
 
-	It("should FTSearch SkipInitalScan", Label("search", "ftsearch"), func() {
+	It("should FTSearch SkipInitialScan", Label("search", "ftsearch"), func() {
 		client.HSet(ctx, "doc1", "foo", "bar")
 
 		text1 := &redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}
-		val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{SkipInitalScan: true}, text1).Result()
+		val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{SkipInitialScan: true}, text1).Result()
 		Expect(err).NotTo(HaveOccurred())
 		Expect(val).To(BeEquivalentTo("OK"))
 		WaitForIndexing(client, "idx1")
@@ -1017,6 +1141,111 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
 		Expect(res.Attributes[0].WithSuffixtrie).To(BeTrue())
 	})
 
+	It("should test dialect 4", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() {
+		val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{
+			Prefix: []interface{}{"resource:"},
+		}, &redis.FieldSchema{
+			FieldName: "uuid",
+			FieldType: redis.SearchFieldTypeTag,
+		}, &redis.FieldSchema{
+			FieldName: "tags",
+			FieldType: redis.SearchFieldTypeTag,
+		}, &redis.FieldSchema{
+			FieldName: "description",
+			FieldType: redis.SearchFieldTypeText,
+		}, &redis.FieldSchema{
+			FieldName: "rating",
+			FieldType: redis.SearchFieldTypeNumeric,
+		}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).To(BeEquivalentTo("OK"))
+
+		client.HSet(ctx, "resource:1", map[string]interface{}{
+			"uuid":        "123e4567-e89b-12d3-a456-426614174000",
+			"tags":        "finance|crypto|$btc|blockchain",
+			"description": "Analysis of blockchain technologies & Bitcoin's potential.",
+			"rating":      5,
+		})
+		client.HSet(ctx, "resource:2", map[string]interface{}{
+			"uuid":        "987e6543-e21c-12d3-a456-426614174999",
+			"tags":        "health|well-being|fitness|new-year's-resolutions",
+			"description": "Health trends for the new year, including fitness regimes.",
+			"rating":      4,
+		})
+
+		res, err := client.FTSearchWithArgs(ctx, "idx1", "@uuid:{$uuid}",
+			&redis.FTSearchOptions{
+				DialectVersion: 2,
+				Params:         map[string]interface{}{"uuid": "123e4567-e89b-12d3-a456-426614174000"},
+			}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Total).To(BeEquivalentTo(int64(1)))
+		Expect(res.Docs[0].ID).To(BeEquivalentTo("resource:1"))
+
+		res, err = client.FTSearchWithArgs(ctx, "idx1", "@uuid:{$uuid}",
+			&redis.FTSearchOptions{
+				DialectVersion: 4,
+				Params:         map[string]interface{}{"uuid": "123e4567-e89b-12d3-a456-426614174000"},
+			}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Total).To(BeEquivalentTo(int64(1)))
+		Expect(res.Docs[0].ID).To(BeEquivalentTo("resource:1"))
+
+		client.HSet(ctx, "test:1", map[string]interface{}{
+			"uuid":  "3d3586fe-0416-4572-8ce",
+			"email": "[email protected]",
+			"num":   5,
+		})
+
+		// Create the index
+		ftCreateOptions := &redis.FTCreateOptions{
+			Prefix: []interface{}{"test:"},
+		}
+		schema := []*redis.FieldSchema{
+			{
+				FieldName: "uuid",
+				FieldType: redis.SearchFieldTypeTag,
+			},
+			{
+				FieldName: "email",
+				FieldType: redis.SearchFieldTypeTag,
+			},
+			{
+				FieldName: "num",
+				FieldType: redis.SearchFieldTypeNumeric,
+			},
+		}
+
+		val, err = client.FTCreate(ctx, "idx_hash", ftCreateOptions, schema...).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).To(Equal("OK"))
+		WaitForIndexing(client, "idx_hash")
+
+		ftSearchOptions := &redis.FTSearchOptions{
+			DialectVersion: 4,
+			Params: map[string]interface{}{
+				"uuid":  "3d3586fe-0416-4572-8ce",
+				"email": "[email protected]",
+			},
+		}
+
+		res, err = client.FTSearchWithArgs(ctx, "idx_hash", "@uuid:{$uuid}", ftSearchOptions).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Docs[0].ID).To(BeEquivalentTo("test:1"))
+		Expect(res.Docs[0].Fields["uuid"]).To(BeEquivalentTo("3d3586fe-0416-4572-8ce"))
+
+		res, err = client.FTSearchWithArgs(ctx, "idx_hash", "@email:{$email}", ftSearchOptions).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Docs[0].ID).To(BeEquivalentTo("test:1"))
+		Expect(res.Docs[0].Fields["email"]).To(BeEquivalentTo("[email protected]"))
+
+		ftSearchOptions.Params = map[string]interface{}{"num": 5}
+		res, err = client.FTSearchWithArgs(ctx, "idx_hash", "@num:[5]", ftSearchOptions).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Docs[0].ID).To(BeEquivalentTo("test:1"))
+		Expect(res.Docs[0].Fields["num"]).To(BeEquivalentTo("5"))
+	})
+
 	It("should FTCreate GeoShape", Label("search", "ftcreate", "ftsearch"), func() {
 		val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "geom", FieldType: redis.SearchFieldTypeGeoShape, GeoShapeFieldType: "FLAT"}).Result()
 		Expect(err).NotTo(HaveOccurred())
@@ -1043,8 +1272,185 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
 		Expect(err).NotTo(HaveOccurred())
 		Expect(res2.Total).To(BeEquivalentTo(int64(2)))
 	})
+
+	It("should create search index with FLOAT16 and BFLOAT16 vectors", Label("search", "ftcreate", "NonRedisEnterprise"), func() {
+		val, err := client.FTCreate(ctx, "index", &redis.FTCreateOptions{},
+			&redis.FieldSchema{FieldName: "float16", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: &redis.FTFlatOptions{Type: "FLOAT16", Dim: 768, DistanceMetric: "COSINE"}}},
+			&redis.FieldSchema{FieldName: "bfloat16", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: &redis.FTFlatOptions{Type: "BFLOAT16", Dim: 768, DistanceMetric: "COSINE"}}},
+		).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).To(BeEquivalentTo("OK"))
+		WaitForIndexing(client, "index")
+	})
+
+	It("should test geoshapes query intersects and disjoint", Label("NonRedisEnterprise"), func() {
+		_, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{
+			FieldName:         "g",
+			FieldType:         redis.SearchFieldTypeGeoShape,
+			GeoShapeFieldType: "FLAT",
+		}).Result()
+		Expect(err).NotTo(HaveOccurred())
+
+		client.HSet(ctx, "doc_point1", "g", "POINT (10 10)")
+		client.HSet(ctx, "doc_point2", "g", "POINT (50 50)")
+		client.HSet(ctx, "doc_polygon1", "g", "POLYGON ((20 20, 25 35, 35 25, 20 20))")
+		client.HSet(ctx, "doc_polygon2", "g", "POLYGON ((60 60, 65 75, 70 70, 65 55, 60 60))")
+
+		intersection, err := client.FTSearchWithArgs(ctx, "idx1", "@g:[intersects $shape]",
+			&redis.FTSearchOptions{
+				DialectVersion: 3,
+				Params:         map[string]interface{}{"shape": "POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))"},
+			}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		_assert_geosearch_result(&intersection, []string{"doc_point2", "doc_polygon1"})
+
+		disjunction, err := client.FTSearchWithArgs(ctx, "idx1", "@g:[disjoint $shape]",
+			&redis.FTSearchOptions{
+				DialectVersion: 3,
+				Params:         map[string]interface{}{"shape": "POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))"},
+			}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		_assert_geosearch_result(&disjunction, []string{"doc_point1", "doc_polygon2"})
+	})
+
+	It("should test geoshapes query contains and within", func() {
+		_, err := client.FTCreate(ctx, "idx2", &redis.FTCreateOptions{}, &redis.FieldSchema{
+			FieldName:         "g",
+			FieldType:         redis.SearchFieldTypeGeoShape,
+			GeoShapeFieldType: "FLAT",
+		}).Result()
+		Expect(err).NotTo(HaveOccurred())
+
+		client.HSet(ctx, "doc_point1", "g", "POINT (10 10)")
+		client.HSet(ctx, "doc_point2", "g", "POINT (50 50)")
+		client.HSet(ctx, "doc_polygon1", "g", "POLYGON ((20 20, 25 35, 35 25, 20 20))")
+		client.HSet(ctx, "doc_polygon2", "g", "POLYGON ((60 60, 65 75, 70 70, 65 55, 60 60))")
+
+		containsA, err := client.FTSearchWithArgs(ctx, "idx2", "@g:[contains $shape]",
+			&redis.FTSearchOptions{
+				DialectVersion: 3,
+				Params:         map[string]interface{}{"shape": "POINT(25 25)"},
+			}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		_assert_geosearch_result(&containsA, []string{"doc_polygon1"})
+
+		containsB, err := client.FTSearchWithArgs(ctx, "idx2", "@g:[contains $shape]",
+			&redis.FTSearchOptions{
+				DialectVersion: 3,
+				Params:         map[string]interface{}{"shape": "POLYGON((24 24, 24 26, 25 25, 24 24))"},
+			}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		_assert_geosearch_result(&containsB, []string{"doc_polygon1"})
+
+		within, err := client.FTSearchWithArgs(ctx, "idx2", "@g:[within $shape]",
+			&redis.FTSearchOptions{
+				DialectVersion: 3,
+				Params:         map[string]interface{}{"shape": "POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))"},
+			}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		_assert_geosearch_result(&within, []string{"doc_point2", "doc_polygon1"})
+	})
+
+	It("should search missing fields", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() {
+		val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{Prefix: []interface{}{"property:"}},
+			&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: true},
+			&redis.FieldSchema{FieldName: "features", FieldType: redis.SearchFieldTypeTag, IndexMissing: true},
+			&redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, IndexMissing: true}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).To(BeEquivalentTo("OK"))
+		WaitForIndexing(client, "idx1")
+
+		client.HSet(ctx, "property:1", map[string]interface{}{
+			"title":       "Luxury Villa in Malibu",
+			"features":    "pool,sea view,modern",
+			"description": "A stunning modern villa overlooking the Pacific Ocean.",
+		})
+
+		client.HSet(ctx, "property:2", map[string]interface{}{
+			"title":       "Downtown Flat",
+			"description": "Modern flat in central Paris with easy access to metro.",
+		})
+
+		client.HSet(ctx, "property:3", map[string]interface{}{
+			"title":    "Beachfront Bungalow",
+			"features": "beachfront,sun deck",
+		})
+
+		res, err := client.FTSearchWithArgs(ctx, "idx1", "ismissing(@features)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Docs[0].ID).To(BeEquivalentTo("property:2"))
+
+		res, err = client.FTSearchWithArgs(ctx, "idx1", "-ismissing(@features)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1"))
+		Expect(res.Docs[1].ID).To(BeEquivalentTo("property:3"))
+
+		res, err = client.FTSearchWithArgs(ctx, "idx1", "ismissing(@description)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Docs[0].ID).To(BeEquivalentTo("property:3"))
+
+		res, err = client.FTSearchWithArgs(ctx, "idx1", "-ismissing(@description)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1"))
+		Expect(res.Docs[1].ID).To(BeEquivalentTo("property:2"))
+	})
+
+	It("should search empty fields", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() {
+		val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{Prefix: []interface{}{"property:"}},
+			&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: true},
+			&redis.FieldSchema{FieldName: "features", FieldType: redis.SearchFieldTypeTag, IndexEmpty: true},
+			&redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, IndexEmpty: true}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).To(BeEquivalentTo("OK"))
+		WaitForIndexing(client, "idx1")
+
+		client.HSet(ctx, "property:1", map[string]interface{}{
+			"title":       "Luxury Villa in Malibu",
+			"features":    "pool,sea view,modern",
+			"description": "A stunning modern villa overlooking the Pacific Ocean.",
+		})
+
+		client.HSet(ctx, "property:2", map[string]interface{}{
+			"title":       "Downtown Flat",
+			"features":    "",
+			"description": "Modern flat in central Paris with easy access to metro.",
+		})
+
+		client.HSet(ctx, "property:3", map[string]interface{}{
+			"title":       "Beachfront Bungalow",
+			"features":    "beachfront,sun deck",
+			"description": "",
+		})
+
+		res, err := client.FTSearchWithArgs(ctx, "idx1", "@features:{\"\"}", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Docs[0].ID).To(BeEquivalentTo("property:2"))
+
+		res, err = client.FTSearchWithArgs(ctx, "idx1", "-@features:{\"\"}", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1"))
+		Expect(res.Docs[1].ID).To(BeEquivalentTo("property:3"))
+
+		res, err = client.FTSearchWithArgs(ctx, "idx1", "@description:''", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Docs[0].ID).To(BeEquivalentTo("property:3"))
+
+		res, err = client.FTSearchWithArgs(ctx, "idx1", "-@description:''", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1"))
+		Expect(res.Docs[1].ID).To(BeEquivalentTo("property:2"))
+	})
 })
 
+func _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []string) {
+	ids := make([]string, len(result.Docs))
+	for i, doc := range result.Docs {
+		ids[i] = doc.ID
+	}
+	Expect(ids).To(ConsistOf(expectedDocIDs))
+	Expect(result.Total).To(BeEquivalentTo(len(expectedDocIDs)))
+}
+
 // It("should FTProfile Search and Aggregate", Label("search", "ftprofile"), func() {
 // 	val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}).Result()
 // 	Expect(err).NotTo(HaveOccurred())
@@ -1134,3 +1540,189 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
 // 		Expect(results0["id"]).To(BeEquivalentTo("a"))
 // 		Expect(results0["extra_attributes"].(map[interface{}]interface{})["__v_score"]).To(BeEquivalentTo("0"))
 // 	})
+
+var _ = Describe("RediSearch commands Resp 3", Label("search"), func() {
+	ctx := context.TODO()
+	var client *redis.Client
+	var client2 *redis.Client
+
+	BeforeEach(func() {
+		client = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 3, UnstableResp3: true})
+		client2 = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 3})
+		Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
+	})
+
+	AfterEach(func() {
+		Expect(client.Close()).NotTo(HaveOccurred())
+	})
+
+	It("should handle FTAggregate with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftaggregate"), func() {
+		text1 := &redis.FieldSchema{FieldName: "PrimaryKey", FieldType: redis.SearchFieldTypeText, Sortable: true}
+		num1 := &redis.FieldSchema{FieldName: "CreatedDateTimeUTC", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}
+		val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).To(BeEquivalentTo("OK"))
+		WaitForIndexing(client, "idx1")
+
+		client.HSet(ctx, "doc1", "PrimaryKey", "9::362330", "CreatedDateTimeUTC", "637387878524969984")
+		client.HSet(ctx, "doc2", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "637387875859270016")
+
+		options := &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}}
+		res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).RawResult()
+		results := res.(map[interface{}]interface{})["results"].([]interface{})
+		Expect(results[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["CreatedDateTimeUTC"]).
+			To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
+		Expect(results[1].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["CreatedDateTimeUTC"]).
+			To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
+
+		rawVal := client.FTAggregateWithArgs(ctx, "idx1", "*", options).RawVal()
+		rawValResults := rawVal.(map[interface{}]interface{})["results"].([]interface{})
+		Expect(err).NotTo(HaveOccurred())
+		Expect(rawValResults[0]).To(Or(BeEquivalentTo(results[0]), BeEquivalentTo(results[1])))
+		Expect(rawValResults[1]).To(Or(BeEquivalentTo(results[0]), BeEquivalentTo(results[1])))
+
+		// Test with UnstableResp3 false
+		Expect(func() {
+			options = &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}}
+			rawRes, _ := client2.FTAggregateWithArgs(ctx, "idx1", "*", options).RawResult()
+			rawVal = client2.FTAggregateWithArgs(ctx, "idx1", "*", options).RawVal()
+			Expect(rawRes).To(BeNil())
+			Expect(rawVal).To(BeNil())
+		}).Should(Panic())
+
+	})
+
+	It("should handle FTInfo with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftinfo"), func() {
+		val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText, Sortable: true, NoStem: true}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).To(BeEquivalentTo("OK"))
+		WaitForIndexing(client, "idx1")
+
+		resInfo, err := client.FTInfo(ctx, "idx1").RawResult()
+		Expect(err).NotTo(HaveOccurred())
+		attributes := resInfo.(map[interface{}]interface{})["attributes"].([]interface{})
+		flags := attributes[0].(map[interface{}]interface{})["flags"].([]interface{})
+		Expect(flags).To(ConsistOf("SORTABLE", "NOSTEM"))
+
+		valInfo := client.FTInfo(ctx, "idx1").RawVal()
+		attributes = valInfo.(map[interface{}]interface{})["attributes"].([]interface{})
+		flags = attributes[0].(map[interface{}]interface{})["flags"].([]interface{})
+		Expect(flags).To(ConsistOf("SORTABLE", "NOSTEM"))
+
+		// Test with UnstableResp3 false
+		Expect(func() {
+			rawResInfo, _ := client2.FTInfo(ctx, "idx1").RawResult()
+			rawValInfo := client2.FTInfo(ctx, "idx1").RawVal()
+			Expect(rawResInfo).To(BeNil())
+			Expect(rawValInfo).To(BeNil())
+		}).Should(Panic())
+	})
+
+	It("should handle FTSpellCheck with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftspellcheck"), func() {
+		text1 := &redis.FieldSchema{FieldName: "f1", FieldType: redis.SearchFieldTypeText}
+		text2 := &redis.FieldSchema{FieldName: "f2", FieldType: redis.SearchFieldTypeText}
+		val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).To(BeEquivalentTo("OK"))
+		WaitForIndexing(client, "idx1")
+
+		client.HSet(ctx, "doc1", "f1", "some valid content", "f2", "this is sample text")
+		client.HSet(ctx, "doc2", "f1", "very important", "f2", "lorem ipsum")
+
+		resSpellCheck, err := client.FTSpellCheck(ctx, "idx1", "impornant").RawResult()
+		valSpellCheck := client.FTSpellCheck(ctx, "idx1", "impornant").RawVal()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(valSpellCheck).To(BeEquivalentTo(resSpellCheck))
+		results := resSpellCheck.(map[interface{}]interface{})["results"].(map[interface{}]interface{})
+		Expect(results["impornant"].([]interface{})[0].(map[interface{}]interface{})["important"]).To(BeEquivalentTo(0.5))
+
+		// Test with UnstableResp3 false
+		Expect(func() {
+			rawResSpellCheck, _ := client2.FTSpellCheck(ctx, "idx1", "impornant").RawResult()
+			rawValSpellCheck := client2.FTSpellCheck(ctx, "idx1", "impornant").RawVal()
+			Expect(rawResSpellCheck).To(BeNil())
+			Expect(rawValSpellCheck).To(BeNil())
+		}).Should(Panic())
+	})
+
+	It("should handle FTSearch with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftsearch"), func() {
+		val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{StopWords: []interface{}{"foo", "bar", "baz"}}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).To(BeEquivalentTo("OK"))
+		WaitForIndexing(client, "txt")
+		client.HSet(ctx, "doc1", "txt", "foo baz")
+		client.HSet(ctx, "doc2", "txt", "hello world")
+		res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{NoContent: true}).RawResult()
+		val1 := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{NoContent: true}).RawVal()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val1).To(BeEquivalentTo(res1))
+		totalResults := res1.(map[interface{}]interface{})["total_results"]
+		Expect(totalResults).To(BeEquivalentTo(int64(0)))
+		res2, err := client.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).RawResult()
+		Expect(err).NotTo(HaveOccurred())
+		totalResults2 := res2.(map[interface{}]interface{})["total_results"]
+		Expect(totalResults2).To(BeEquivalentTo(int64(1)))
+
+		// Test with UnstableResp3 false
+		Expect(func() {
+			rawRes2, _ := client2.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).RawResult()
+			rawVal2 := client2.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).RawVal()
+			Expect(rawRes2).To(BeNil())
+			Expect(rawVal2).To(BeNil())
+		}).Should(Panic())
+	})
+	It("should handle FTSynDump with Unstable RESP3 Search Module and without stability", Label("search", "ftsyndump"), func() {
+		text1 := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}
+		text2 := &redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}
+		val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true}, text1, text2).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).To(BeEquivalentTo("OK"))
+		WaitForIndexing(client, "idx1")
+
+		resSynUpdate, err := client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"boy", "child", "offspring"}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(resSynUpdate).To(BeEquivalentTo("OK"))
+
+		resSynUpdate, err = client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"baby", "child"}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(resSynUpdate).To(BeEquivalentTo("OK"))
+
+		resSynUpdate, err = client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"tree", "wood"}).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(resSynUpdate).To(BeEquivalentTo("OK"))
+
+		resSynDump, err := client.FTSynDump(ctx, "idx1").RawResult()
+		valSynDump := client.FTSynDump(ctx, "idx1").RawVal()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(valSynDump).To(BeEquivalentTo(resSynDump))
+		Expect(resSynDump.(map[interface{}]interface{})["baby"]).To(BeEquivalentTo([]interface{}{"id1"}))
+
+		// Test with UnstableResp3 false
+		Expect(func() {
+			rawResSynDump, _ := client2.FTSynDump(ctx, "idx1").RawResult()
+			rawValSynDump := client2.FTSynDump(ctx, "idx1").RawVal()
+			Expect(rawResSynDump).To(BeNil())
+			Expect(rawValSynDump).To(BeNil())
+		}).Should(Panic())
+	})
+
+	It("should test not affected Resp 3 Search method - FTExplain", Label("search", "ftexplain"), func() {
+		text1 := &redis.FieldSchema{FieldName: "f1", FieldType: redis.SearchFieldTypeText}
+		text2 := &redis.FieldSchema{FieldName: "f2", FieldType: redis.SearchFieldTypeText}
+		text3 := &redis.FieldSchema{FieldName: "f3", FieldType: redis.SearchFieldTypeText}
+		val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, text1, text2, text3).Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(val).To(BeEquivalentTo("OK"))
+		WaitForIndexing(client, "txt")
+		res1, err := client.FTExplain(ctx, "txt", "@f3:f3_val @f2:f2_val @f1:f1_val").Result()
+		Expect(err).NotTo(HaveOccurred())
+		Expect(res1).ToNot(BeEmpty())
+
+		// Test with UnstableResp3 false
+		Expect(func() {
+			res2, err := client2.FTExplain(ctx, "txt", "@f3:f3_val @f2:f2_val @f1:f1_val").Result()
+			Expect(err).NotTo(HaveOccurred())
+			Expect(res2).ToNot(BeEmpty())
+		}).ShouldNot(Panic())
+	})
+})
diff --git sentinel.go sentinel.go
index 188f88494..a4c9f53c4 100644
--- sentinel.go
+++ sentinel.go
@@ -80,8 +80,20 @@ type FailoverOptions struct {
 
 	TLSConfig *tls.Config
 
+	// DisableIndentity - Disable set-lib on connect.
+	//
+	// default: false
+	//
+	// Deprecated: Use DisableIdentity instead.
 	DisableIndentity bool
-	IdentitySuffix   string
+
+	// DisableIdentity is used to disable CLIENT SETINFO command on connect.
+	//
+	// default: false
+	DisableIdentity bool
+
+	IdentitySuffix string
+	UnstableResp3  bool
 }
 
 func (opt *FailoverOptions) clientOptions() *Options {
@@ -117,8 +129,11 @@ func (opt *FailoverOptions) clientOptions() *Options {
 
 		TLSConfig: opt.TLSConfig,
 
+		DisableIdentity:  opt.DisableIdentity,
 		DisableIndentity: opt.DisableIndentity,
-		IdentitySuffix:   opt.IdentitySuffix,
+
+		IdentitySuffix: opt.IdentitySuffix,
+		UnstableResp3:  opt.UnstableResp3,
 	}
 }
 
@@ -154,8 +169,11 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
 
 		TLSConfig: opt.TLSConfig,
 
+		DisableIdentity:  opt.DisableIdentity,
 		DisableIndentity: opt.DisableIndentity,
-		IdentitySuffix:   opt.IdentitySuffix,
+
+		IdentitySuffix: opt.IdentitySuffix,
+		UnstableResp3:  opt.UnstableResp3,
 	}
 }
 
@@ -194,8 +212,10 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
 
 		TLSConfig: opt.TLSConfig,
 
+		DisableIdentity:  opt.DisableIdentity,
 		DisableIndentity: opt.DisableIndentity,
-		IdentitySuffix:   opt.IdentitySuffix,
+
+		IdentitySuffix: opt.IdentitySuffix,
 	}
 }
 
diff --git universal.go universal.go
index 275bef3d6..483c81127 100644
--- universal.go
+++ universal.go
@@ -61,13 +61,25 @@ type UniversalOptions struct {
 	RouteByLatency bool
 	RouteRandomly  bool
 
-	// The sentinel master name.
-	// Only failover clients.
-
+	// MasterName is the sentinel master name.
+	// Only for failover clients.
 	MasterName string
 
+	// DisableIndentity - Disable set-lib on connect.
+	//
+	// default: false
+	//
+	// Deprecated: Use DisableIdentity instead.
 	DisableIndentity bool
-	IdentitySuffix   string
+
+	// DisableIdentity is used to disable CLIENT SETINFO command on connect.
+	//
+	// default: false
+	DisableIdentity bool
+
+	IdentitySuffix string
+	UnstableResp3  bool
+
 }
 
 // Cluster returns cluster options created from the universal options.
@@ -112,8 +124,10 @@ func (o *UniversalOptions) Cluster() *ClusterOptions {
 
 		TLSConfig: o.TLSConfig,
 
+		DisableIdentity:  o.DisableIdentity,
 		DisableIndentity: o.DisableIndentity,
 		IdentitySuffix:   o.IdentitySuffix,
+		UnstableResp3:    o.UnstableResp3,
 	}
 }
 
@@ -158,8 +172,10 @@ func (o *UniversalOptions) Failover() *FailoverOptions {
 
 		TLSConfig: o.TLSConfig,
 
+		DisableIdentity:  o.DisableIdentity,
 		DisableIndentity: o.DisableIndentity,
 		IdentitySuffix:   o.IdentitySuffix,
+		UnstableResp3:    o.UnstableResp3,
 	}
 }
 
@@ -201,8 +217,10 @@ func (o *UniversalOptions) Simple() *Options {
 
 		TLSConfig: o.TLSConfig,
 
+		DisableIdentity:  o.DisableIdentity,
 		DisableIn,dentity: o.DisableIndentity,
 		IdentitySuffix:   o.IdentitySuffix,
+		UnstableResp3:    o.UnstableResp3,
 	}
 }
 
diff --git universal_test.go universal_test.go
index 747c68acb..9328b4776 100644
--- universal_test.go
+++ universal_test.go
@@ -38,4 +38,26 @@ var _ = Describe("UniversalClient", func() {
 		})
 		Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred())
 	})
+
+	It("connect to clusters with UniversalClient and UnstableResp3", Label("NonRedisEnterprise"), func() {
+		client = redis.NewUniversalClient(&redis.UniversalOptions{
+			Addrs:         cluster.addrs(),
+			Protocol:      3,
+			UnstableResp3: true,
+		})
+		Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred())
+		a := func() { client.FTInfo(ctx, "all").Result() }
+		Expect(a).ToNot(Panic())
+	})
+
+	It("connect to clusters with ClusterClient and UnstableResp3", Label("NonRedisEnterprise"), func() {
+		client = redis.NewClusterClient(&redis.ClusterOptions{
+			Addrs:         cluster.addrs(),
+			Protocol:      3,
+			UnstableResp3: true,
+		})
+		Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred())
+		a := func() { client.FTInfo(ctx, "all").Result() }
+		Expect(a).ToNot(Panic())
+	})
 })
diff --git version.go version.go
index 2ea7df99a..a4832fc1e 100644
--- version.go
+++ version.go
@@ -2,5 +2,5 @@ package redis
 
 // Version is the current release version.
 func Version() string {
-	return "9.5.3"
+	return "9.7.3"
 }


Description

This PR includes several significant changes to the go-redis library, including bug fixes, new features, documentation improvements, and cleanup of deprecated code. The key motivations are improving stability, adding RESP3 support for Redis Search commands, and fixing connection handling.

Possible Issues

  1. Breaking change in connection identity verification where DisableIndentity (with typo) is deprecated in favor of DisableIdentity
  2. New UnstableResp3 flag requirement for certain Redis Search commands could break existing code if not properly migrated

Security Hotspots

  1. TLS connection handling changes in connCheck function - removal of extra TLS unwrapping could affect TLS verification behavior
  2. Identity verification changes could potentially impact client identification and authentication flows
Changes

Changes

  1. Version & Documentation:
  • Updated version from 9.5.3 to 9.7.3
  • Added RESP3 documentation and unstable warning for RediSearch commands
  • Added retraction notices for accidentally released versions
  • Added code coverage badge and reporting
  1. Core Features:
  • Added RESP3 support for unstable Redis Search commands
  • Fixed connection identity verification (DisableIdentity vs DisableIndentity)
  • Added UnstableResp3 flag for handling unstable RESP3 features
  1. Bug Fixes:
  • Fixed immediacy of MOVED/ASK redirection in cluster mode
  • Fixed connection check behavior for TLS connections
  • Fixed race condition in hook handling
  • Fixed array copy issues in cluster node addressing
  1. Testing:
  • Added extensive tests for RESP3 search commands
  • Added tests for bitmap, JSON, and list operations
  • Added test coverage for new features and edge cases
sequenceDiagram
    participant Client
    participant Redis
    participant Search
    
    Client->>Redis: Connect with RESP3
    alt UnstableResp3 Enabled
        Client->>Search: Execute Search Command
        Search-->>Client: Raw RESP3 Response
    else UnstableResp3 Disabled
        Client->>Search: Execute Search Command
        Search-->>Client: Panic (Unstable API)
    end
    
    alt MOVED/ASK Response
        Redis-->>Client: Redirect Response
        Note over Client: Immediate Retry
        Client->>Redis: Retry Command
    end
    
    alt Identity Check
        Client->>Redis: CLIENT SETINFO
        Note over Redis: Check DisableIdentity
        Redis-->>Client: OK/Error
    end

github-actions[bot] avatar Apr 03 '25 02:04 github-actions[bot]