mirror of
https://github.com/redis/go-redis.git
synced 2026-10-22 19:49:00 +03:00
feat(search): parser resp3 responses for search (#3741)
* parse resp3 responses * backwards compatibility * doctests and format * fix parsing * remove unused functions * debug test * debug test x2 peek directly * debug test * remove debug and skip re build * fix typo * add tests and fix parsers for resp3 * add comment for the unreliable Total in RESP3 for aggregate * fix build * remove shadowing rawVal * fix strange redis search float,int,string behaviour handling * mark UnstableResp3 as Deprecated --------- Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com>
This commit is contained in:
1
.github/workflows/test-redis-enterprise.yml
vendored
1
.github/workflows/test-redis-enterprise.yml
vendored
@@ -10,6 +10,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: false # Temporarily disabled, should be enabled when we have a current and stable RE build
|
||||
name: build
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
|
||||
@@ -199,11 +199,11 @@ func ExampleClient_geoindex() {
|
||||
// OK
|
||||
// OK
|
||||
// OK
|
||||
// {1 [{product:46885 <nil> <nil> <nil> map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}] <nil>}]}
|
||||
// {1 [{product:46885 <nil> <nil> <nil> map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}] <nil>}] []}
|
||||
// OK
|
||||
// OK
|
||||
// OK
|
||||
// OK
|
||||
// OK
|
||||
// {1 [{shape:4 <nil> <nil> <nil> map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]] <nil>}]}
|
||||
// {1 [{shape:4 <nil> <nil> <nil> map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]] <nil>}] []}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ func ExampleClient_search_json() {
|
||||
// STEP_END
|
||||
|
||||
// Output:
|
||||
// {1 [{user:3 <nil> <nil> <nil> map[$:{"age":35,"city":"Tel Aviv","email":"paul.zamir@example.com","name":"Paul Zamir"}] <nil>}]}
|
||||
// {1 [{user:3 <nil> <nil> <nil> map[$:{"age":35,"city":"Tel Aviv","email":"paul.zamir@example.com","name":"Paul Zamir"}] <nil>}] []}
|
||||
// London
|
||||
// Tel Aviv
|
||||
// 0
|
||||
@@ -329,5 +329,5 @@ func ExampleClient_search_hash() {
|
||||
// STEP_END
|
||||
|
||||
// Output:
|
||||
// {1 [{huser:3 <nil> <nil> <nil> map[age:35 city:Tel Aviv email:paul.zamir@example.com name:Paul Zamir] <nil>}]}
|
||||
// {1 [{huser:3 <nil> <nil> <nil> map[age:35 city:Tel Aviv email:paul.zamir@example.com name:Paul Zamir] <nil>}] []}
|
||||
}
|
||||
|
||||
@@ -257,6 +257,6 @@ func ExampleClient_search_qs() {
|
||||
|
||||
// Output:
|
||||
// Documents found: 10
|
||||
// {1 [{bicycle:0 <nil> <nil> <nil> map[$:{"brand":"Velorim","condition":"new","description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","model":"Jigger","price":270}] <nil>}]}
|
||||
// {1 [{bicycle:4 <nil> <nil> <nil> map[$:{"brand":"Noka Bikes","condition":"used","description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","model":"Kahuna","price":3200}] <nil>}]}
|
||||
// {1 [{bicycle:0 <nil> <nil> <nil> map[$:{"brand":"Velorim","condition":"new","description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","model":"Jigger","price":270}] <nil>}] []}
|
||||
// {1 [{bicycle:4 <nil> <nil> <nil> map[$:{"brand":"Noka Bikes","condition":"used","description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","model":"Kahuna","price":3200}] <nil>}] []}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func main() {
|
||||
|
||||
// Example 2: TLS with CA certificate verification
|
||||
fmt.Println("\nExample 2: TLS with CA certificate verification")
|
||||
|
||||
|
||||
// Load CA certificate
|
||||
caCert, err := os.ReadFile("path/to/ca.crt")
|
||||
if err != nil {
|
||||
@@ -58,7 +58,7 @@ func main() {
|
||||
|
||||
// Example 3: TLS with client certificate (mutual TLS)
|
||||
fmt.Println("\nExample 3: TLS with client certificate (mutual TLS)")
|
||||
|
||||
|
||||
// Load CA certificate
|
||||
caCert, err = os.ReadFile("path/to/ca.crt")
|
||||
if err != nil {
|
||||
@@ -92,18 +92,18 @@ func main() {
|
||||
|
||||
// Example 4: Using rediss:// URL scheme
|
||||
fmt.Println("\nExample 4: Using rediss:// URL scheme")
|
||||
|
||||
|
||||
opt, err := redis.ParseURL("rediss://localhost:6666")
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to parse URL: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Add InsecureSkipVerify for testing with self-signed certs
|
||||
opt.TLSConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
|
||||
client4 := redis.NewClient(opt)
|
||||
defer client4.Close()
|
||||
|
||||
@@ -121,4 +121,3 @@ func main() {
|
||||
fmt.Println("The certificate's CN (Common Name) field will be used as the Redis username")
|
||||
fmt.Println("See tls_cert_auth_test.go for a complete working example")
|
||||
}
|
||||
|
||||
|
||||
@@ -86,8 +86,8 @@ func TestManager_Listener_NilConn(t *testing.T) {
|
||||
// Mock pooler for testing
|
||||
type mockPooler struct{}
|
||||
|
||||
func (m *mockPooler) NewConn(ctx context.Context) (*pool.Conn, error) { return nil, nil }
|
||||
func (m *mockPooler) CloseConn(context.Context, *pool.Conn, string, string) error { return nil }
|
||||
func (m *mockPooler) NewConn(ctx context.Context) (*pool.Conn, error) { return nil, nil }
|
||||
func (m *mockPooler) CloseConn(context.Context, *pool.Conn, string, string) error { return nil }
|
||||
func (m *mockPooler) Get(ctx context.Context) (*pool.Conn, error) { return nil, nil }
|
||||
func (m *mockPooler) Put(ctx context.Context, conn *pool.Conn) {}
|
||||
func (m *mockPooler) Remove(ctx context.Context, conn *pool.Conn, reason error) {}
|
||||
|
||||
@@ -2,6 +2,7 @@ package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -10,6 +11,29 @@ import (
|
||||
"github.com/redis/go-redis/v9/internal/util"
|
||||
)
|
||||
|
||||
// String representations of special float values.
|
||||
// Values are lowercase for consistency with Redis RESP2 protocol responses.
|
||||
const (
|
||||
NaN = "nan" // Not a Number
|
||||
Inf = "inf" // Positive infinity
|
||||
NInf = "-inf" // Negative infinity
|
||||
)
|
||||
|
||||
// FormatFloat formats a float64 to string, normalizing special values
|
||||
// (NaN, Inf) to lowercase for consistency with Redis RESP2 protocol.
|
||||
func FormatFloat(f float64) string {
|
||||
switch {
|
||||
case math.IsNaN(f):
|
||||
return NaN
|
||||
case math.IsInf(f, 1):
|
||||
return Inf
|
||||
case math.IsInf(f, -1):
|
||||
return NInf
|
||||
default:
|
||||
return strconv.FormatFloat(f, 'f', -1, 64)
|
||||
}
|
||||
}
|
||||
|
||||
func Sleep(ctx context.Context, dur time.Duration) error {
|
||||
t := time.NewTimer(dur)
|
||||
defer t.Stop()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"math"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -106,3 +107,29 @@ func BenchmarkReplaceSpacesUseBuilder(b *testing.B) {
|
||||
_ = ReplaceSpacesUseBuilder(version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatFloat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input float64
|
||||
expected string
|
||||
}{
|
||||
{"NaN", math.NaN(), NaN},
|
||||
{"positive infinity", math.Inf(1), Inf},
|
||||
{"negative infinity", math.Inf(-1), NInf},
|
||||
{"zero", 0, "0"},
|
||||
{"positive integer", 42, "42"},
|
||||
{"negative integer", -42, "-42"},
|
||||
{"positive float", 3.14159, "3.14159"},
|
||||
{"negative float", -3.14159, "-3.14159"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := FormatFloat(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("FormatFloat(%v) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package redis_test
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
. "github.com/bsm/ginkgo/v2"
|
||||
@@ -846,12 +847,12 @@ var _ = Describe("Go-Redis Advanced JSON and RediSearch Tests", func() {
|
||||
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)
|
||||
fmt.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)
|
||||
fmt.Printf("Expanded JSON: %+v\n", expandedJSON)
|
||||
|
||||
Expect(rawJSON).To(MatchJSON(jsonMustMarshal(expandedJSON)))
|
||||
|
||||
@@ -897,7 +898,7 @@ var _ = Describe("Go-Redis Advanced JSON and RediSearch Tests", func() {
|
||||
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)
|
||||
fmt.Printf("Advanced Search result: %+v\n", searchResult)
|
||||
|
||||
incrCmd := client.JSONNumIncrBy(ctx, "person:1", "$.person.age", 5)
|
||||
incrResult, err := incrCmd.Result()
|
||||
|
||||
@@ -292,6 +292,10 @@ type Options struct {
|
||||
|
||||
// UnstableResp3 enables Unstable mode for Redis Search module with RESP3.
|
||||
// When unstable mode is enabled, the client will use RESP3 protocol and only be able to use RawResult
|
||||
//
|
||||
// Deprecated: All RediSearch commands now have stable RESP3 parsing and this
|
||||
// flag is a no-op. It is kept for backwards compatibility and will be removed
|
||||
// in a future release.
|
||||
UnstableResp3 bool
|
||||
|
||||
// Push notifications are always enabled for RESP3 connections (Protocol: 3)
|
||||
|
||||
@@ -164,6 +164,10 @@ type ClusterOptions struct {
|
||||
IdentitySuffix string // Add suffix to client name. Default is empty.
|
||||
|
||||
// UnstableResp3 enables Unstable mode for Redis Search module with RESP3.
|
||||
//
|
||||
// Deprecated: All RediSearch commands now have stable RESP3 parsing and this
|
||||
// flag is a no-op. It is kept for backwards compatibility and will be removed
|
||||
// in a future release.
|
||||
UnstableResp3 bool
|
||||
|
||||
// PushNotificationProcessor is the processor for handling push notifications.
|
||||
|
||||
21
redis.go
21
redis.go
@@ -26,6 +26,14 @@ type Scanner = hscan.Scanner
|
||||
// Nil reply returned by Redis when key does not exist.
|
||||
const Nil = proto.Nil
|
||||
|
||||
// String representations of special float values.
|
||||
// Values are lowercase for consistency with Redis RESP2 protocol responses.
|
||||
const (
|
||||
NaN = internal.NaN // Not a Number
|
||||
Inf = internal.Inf // Positive infinity
|
||||
NInf = internal.NInf // Negative infinity
|
||||
)
|
||||
|
||||
// SetLogger set custom log
|
||||
// Use with VoidLogger to disable logging.
|
||||
// If logger is nil, the call is ignored and the existing logger is kept.
|
||||
@@ -936,16 +944,9 @@ func classifyCommandError(err error) (errorType, statusCode string, isInternal b
|
||||
}
|
||||
|
||||
func (c *baseClient) assertUnstableCommand(cmd Cmder) (bool, error) {
|
||||
switch cmd.(type) {
|
||||
case *AggregateCmd, *FTInfoCmd, *FTSpellCheckCmd, *FTSearchCmd, *FTSynDumpCmd:
|
||||
if c.opt.UnstableResp3 {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, fmt.Errorf("RESP3 responses for this command are disabled because they may still change. Please set the flag UnstableResp3. See the README and the release notes for guidance")
|
||||
}
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
// All search commands (FTSearchCmd, AggregateCmd, FTInfoCmd, FTSpellCheckCmd, FTSynDumpCmd)
|
||||
// now have stable RESP3 parsing. No commands require the UnstableResp3 flag anymore.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool, *pool.Conn, error) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
372
search_resp_test.go
Normal file
372
search_resp_test.go
Normal file
@@ -0,0 +1,372 @@
|
||||
package redis_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// TestSearchCommandsRESP2AndRESP3Equivalence tests that search commands
|
||||
// return equivalent results for both RESP2 and RESP3 protocols.
|
||||
func TestSearchCommandsRESP2AndRESP3Equivalence(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// RESP2 client
|
||||
client2 := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Protocol: 2,
|
||||
})
|
||||
defer client2.Close()
|
||||
|
||||
// RESP3 client
|
||||
client3 := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Protocol: 3,
|
||||
})
|
||||
defer client3.Close()
|
||||
|
||||
// Check connection
|
||||
if err := client2.Ping(ctx).Err(); err != nil {
|
||||
t.Skipf("Redis not available: %v", err)
|
||||
}
|
||||
|
||||
// Clean up before test
|
||||
client2.FTDropIndex(ctx, "test-idx")
|
||||
client2.Del(ctx, "doc1", "doc2", "doc3")
|
||||
|
||||
t.Run("FTInfo", func(t *testing.T) {
|
||||
// Create index
|
||||
_, err := client2.FTCreate(ctx, "test-idx",
|
||||
&redis.FTCreateOptions{},
|
||||
&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: true},
|
||||
&redis.FieldSchema{FieldName: "score", FieldType: redis.SearchFieldTypeNumeric, Sortable: true},
|
||||
).Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTCreate failed: %v", err)
|
||||
}
|
||||
defer client2.FTDropIndex(ctx, "test-idx")
|
||||
|
||||
// Add some documents
|
||||
client2.HSet(ctx, "doc1", "title", "hello world", "score", 100)
|
||||
client2.HSet(ctx, "doc2", "title", "foo bar", "score", 200)
|
||||
|
||||
// Wait for indexing
|
||||
waitForIndexing(t, client2, "test-idx", 2)
|
||||
|
||||
// Get FTInfo from both protocols
|
||||
info2, err := client2.FTInfo(ctx, "test-idx").Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTInfo RESP2 failed: %v", err)
|
||||
}
|
||||
|
||||
info3, err := client3.FTInfo(ctx, "test-idx").Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTInfo RESP3 failed: %v", err)
|
||||
}
|
||||
|
||||
// Compare key fields
|
||||
if info2.IndexName != info3.IndexName {
|
||||
t.Errorf("IndexName mismatch: RESP2=%q, RESP3=%q", info2.IndexName, info3.IndexName)
|
||||
}
|
||||
if info2.NumDocs != info3.NumDocs {
|
||||
t.Errorf("NumDocs mismatch: RESP2=%d, RESP3=%d", info2.NumDocs, info3.NumDocs)
|
||||
}
|
||||
if len(info2.Attributes) != len(info3.Attributes) {
|
||||
t.Errorf("Attributes length mismatch: RESP2=%d, RESP3=%d", len(info2.Attributes), len(info3.Attributes))
|
||||
}
|
||||
|
||||
// Check IndexDefinition
|
||||
if info2.IndexDefinition.KeyType != info3.IndexDefinition.KeyType {
|
||||
t.Errorf("IndexDefinition.KeyType mismatch: RESP2=%q, RESP3=%q",
|
||||
info2.IndexDefinition.KeyType, info3.IndexDefinition.KeyType)
|
||||
}
|
||||
if info2.IndexDefinition.DefaultScore != info3.IndexDefinition.DefaultScore {
|
||||
t.Errorf("IndexDefinition.DefaultScore mismatch: RESP2=%v, RESP3=%v",
|
||||
info2.IndexDefinition.DefaultScore, info3.IndexDefinition.DefaultScore)
|
||||
}
|
||||
|
||||
// Check CursorStats
|
||||
if info2.CursorStats != info3.CursorStats {
|
||||
t.Errorf("CursorStats mismatch: RESP2=%+v, RESP3=%+v", info2.CursorStats, info3.CursorStats)
|
||||
}
|
||||
|
||||
// Check IndexErrors
|
||||
if info2.IndexErrors != info3.IndexErrors {
|
||||
t.Errorf("IndexErrors mismatch: RESP2=%+v, RESP3=%+v", info2.IndexErrors, info3.IndexErrors)
|
||||
}
|
||||
|
||||
// Check DialectStats
|
||||
if len(info2.DialectStats) != len(info3.DialectStats) {
|
||||
t.Errorf("DialectStats length mismatch: RESP2=%d, RESP3=%d", len(info2.DialectStats), len(info3.DialectStats))
|
||||
}
|
||||
for k, v := range info2.DialectStats {
|
||||
if info3.DialectStats[k] != v {
|
||||
t.Errorf("DialectStats[%s] mismatch: RESP2=%d, RESP3=%d", k, v, info3.DialectStats[k])
|
||||
}
|
||||
}
|
||||
|
||||
// Check GCStats
|
||||
if info2.GCStats != info3.GCStats {
|
||||
t.Errorf("GCStats mismatch: RESP2=%+v, RESP3=%+v", info2.GCStats, info3.GCStats)
|
||||
}
|
||||
|
||||
// The four *_avg fields are emitted by RediSearch via REPLY_KVNUM
|
||||
// (RedisModule_ReplyWithDouble): bulk strings in RESP2, native doubles
|
||||
// in RESP3. The FTInfoResult fields are typed as string, so the RESP3
|
||||
// path must stringify the double; if it does not the field is "" and
|
||||
// the data is silently lost. Assert both that the RESP3 value is
|
||||
// non-empty and that it matches the RESP2 value.
|
||||
avgFields := []struct {
|
||||
name string
|
||||
resp2Val string
|
||||
resp3Val string
|
||||
}{
|
||||
{"BytesPerRecordAvg", info2.BytesPerRecordAvg, info3.BytesPerRecordAvg},
|
||||
{"RecordsPerDocAvg", info2.RecordsPerDocAvg, info3.RecordsPerDocAvg},
|
||||
{"OffsetsPerTermAvg", info2.OffsetsPerTermAvg, info3.OffsetsPerTermAvg},
|
||||
{"OffsetBitsPerRecordAvg", info2.OffsetBitsPerRecordAvg, info3.OffsetBitsPerRecordAvg},
|
||||
}
|
||||
for _, f := range avgFields {
|
||||
if f.resp3Val == "" {
|
||||
t.Errorf("%s is empty under RESP3 (RESP2=%q) - silent data loss", f.name, f.resp2Val)
|
||||
}
|
||||
if f.resp2Val != f.resp3Val {
|
||||
t.Errorf("%s mismatch: RESP2=%q, RESP3=%q", f.name, f.resp2Val, f.resp3Val)
|
||||
}
|
||||
}
|
||||
|
||||
// total_indexing_time is also emitted via REPLY_KVNUM (double) but the
|
||||
// FTInfoResult field is typed as int. Without explicit float-to-int
|
||||
// handling in the RESP3 parser, this field is silently 0.
|
||||
if info2.TotalIndexingTime != info3.TotalIndexingTime {
|
||||
t.Errorf("TotalIndexingTime mismatch: RESP2=%d, RESP3=%d",
|
||||
info2.TotalIndexingTime, info3.TotalIndexingTime)
|
||||
}
|
||||
|
||||
// All gc_stats numeric fields are emitted via REPLY_KVNUM in RediSearch
|
||||
// (see src/fork_gc/fork_gc.c statsCb) but typed as int in GCStats.
|
||||
// Compare each individually so a regression surfaces with a precise message.
|
||||
gcFields := []struct {
|
||||
name string
|
||||
resp2, resp3 int
|
||||
}{
|
||||
{"BytesCollected", info2.GCStats.BytesCollected, info3.GCStats.BytesCollected},
|
||||
{"TotalMsRun", info2.GCStats.TotalMsRun, info3.GCStats.TotalMsRun},
|
||||
{"TotalCycles", info2.GCStats.TotalCycles, info3.GCStats.TotalCycles},
|
||||
{"LastRunTimeMs", info2.GCStats.LastRunTimeMs, info3.GCStats.LastRunTimeMs},
|
||||
{"GCNumericTreesMissed", info2.GCStats.GCNumericTreesMissed, info3.GCStats.GCNumericTreesMissed},
|
||||
{"GCBlocksDenied", info2.GCStats.GCBlocksDenied, info3.GCStats.GCBlocksDenied},
|
||||
}
|
||||
for _, f := range gcFields {
|
||||
if f.resp2 != f.resp3 {
|
||||
t.Errorf("GCStats.%s mismatch: RESP2=%d, RESP3=%d", f.name, f.resp2, f.resp3)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FTSearch", func(t *testing.T) {
|
||||
// Create index
|
||||
_, err := client2.FTCreate(ctx, "test-idx",
|
||||
&redis.FTCreateOptions{},
|
||||
&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText},
|
||||
&redis.FieldSchema{FieldName: "score", FieldType: redis.SearchFieldTypeNumeric},
|
||||
).Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTCreate failed: %v", err)
|
||||
}
|
||||
defer client2.FTDropIndex(ctx, "test-idx")
|
||||
|
||||
// Add documents
|
||||
client2.HSet(ctx, "doc1", "title", "hello world", "score", 100)
|
||||
client2.HSet(ctx, "doc2", "title", "hello redis", "score", 200)
|
||||
client2.HSet(ctx, "doc3", "title", "goodbye world", "score", 300)
|
||||
|
||||
// Wait for indexing
|
||||
waitForIndexing(t, client2, "test-idx", 3)
|
||||
|
||||
// Search from both protocols
|
||||
result2, err := client2.FTSearch(ctx, "test-idx", "hello").Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTSearch RESP2 failed: %v", err)
|
||||
}
|
||||
|
||||
result3, err := client3.FTSearch(ctx, "test-idx", "hello").Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTSearch RESP3 failed: %v", err)
|
||||
}
|
||||
|
||||
// Compare results
|
||||
if result2.Total != result3.Total {
|
||||
t.Errorf("Total mismatch: RESP2=%d, RESP3=%d", result2.Total, result3.Total)
|
||||
}
|
||||
if len(result2.Docs) != len(result3.Docs) {
|
||||
t.Errorf("Docs length mismatch: RESP2=%d, RESP3=%d", len(result2.Docs), len(result3.Docs))
|
||||
}
|
||||
|
||||
t.Logf("RESP2 result: Total=%d, Docs=%d", result2.Total, len(result2.Docs))
|
||||
t.Logf("RESP3 result: Total=%d, Docs=%d", result3.Total, len(result3.Docs))
|
||||
|
||||
for i, doc := range result2.Docs {
|
||||
t.Logf("RESP2 Doc[%d]: ID=%s, Fields=%v", i, doc.ID, doc.Fields)
|
||||
}
|
||||
for i, doc := range result3.Docs {
|
||||
t.Logf("RESP3 Doc[%d]: ID=%s, Fields=%v", i, doc.ID, doc.Fields)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FTAggregate", func(t *testing.T) {
|
||||
// Create index
|
||||
_, err := client2.FTCreate(ctx, "test-idx",
|
||||
&redis.FTCreateOptions{},
|
||||
&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText},
|
||||
&redis.FieldSchema{FieldName: "score", FieldType: redis.SearchFieldTypeNumeric, Sortable: true},
|
||||
).Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTCreate failed: %v", err)
|
||||
}
|
||||
defer client2.FTDropIndex(ctx, "test-idx")
|
||||
|
||||
// Add documents
|
||||
client2.HSet(ctx, "doc1", "title", "hello world", "score", 100)
|
||||
client2.HSet(ctx, "doc2", "title", "hello redis", "score", 200)
|
||||
|
||||
// Wait for indexing
|
||||
waitForIndexing(t, client2, "test-idx", 2)
|
||||
|
||||
// Aggregate from both protocols
|
||||
options := &redis.FTAggregateOptions{
|
||||
Load: []redis.FTAggregateLoad{{Field: "@score"}},
|
||||
}
|
||||
|
||||
// Test that RawVal() is populated
|
||||
cmd2 := client2.FTAggregateWithArgs(ctx, "test-idx", "*", options)
|
||||
if cmd2.RawVal() == nil {
|
||||
t.Error("RESP2 RawVal() should not be nil")
|
||||
}
|
||||
cmd3 := client3.FTAggregateWithArgs(ctx, "test-idx", "*", options)
|
||||
if cmd3.RawVal() == nil {
|
||||
t.Error("RESP3 RawVal() should not be nil")
|
||||
}
|
||||
|
||||
result2, err := cmd2.Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTAggregate RESP2 failed: %v", err)
|
||||
}
|
||||
|
||||
result3, err := cmd3.Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTAggregate RESP3 failed: %v", err)
|
||||
}
|
||||
|
||||
// Compare results
|
||||
// Note: FT.AGGREGATE total_results is unreliable from the server in RESP3
|
||||
// It should only match when both are 0 (no results)
|
||||
if result2.Total == 0 && result3.Total != 0 {
|
||||
t.Errorf("Total mismatch when RESP2 is 0: RESP2=%d, RESP3=%d", result2.Total, result3.Total)
|
||||
}
|
||||
if len(result2.Rows) != len(result3.Rows) {
|
||||
t.Errorf("Rows length mismatch: RESP2=%d, RESP3=%d", len(result2.Rows), len(result3.Rows))
|
||||
}
|
||||
|
||||
t.Logf("RESP2 result: Total=%d, Rows=%d", result2.Total, len(result2.Rows))
|
||||
t.Logf("RESP3 result: Total=%d, Rows=%d (note: total_results is unreliable in RESP3)", result3.Total, len(result3.Rows))
|
||||
|
||||
for i, row := range result2.Rows {
|
||||
t.Logf("RESP2 Row[%d]: Fields=%v", i, row.Fields)
|
||||
}
|
||||
for i, row := range result3.Rows {
|
||||
t.Logf("RESP3 Row[%d]: Fields=%v", i, row.Fields)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FTSpellCheck", func(t *testing.T) {
|
||||
// Create index
|
||||
_, err := client2.FTCreate(ctx, "test-idx",
|
||||
&redis.FTCreateOptions{},
|
||||
&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText},
|
||||
).Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTCreate failed: %v", err)
|
||||
}
|
||||
defer client2.FTDropIndex(ctx, "test-idx")
|
||||
|
||||
// Add documents
|
||||
client2.HSet(ctx, "doc1", "title", "hello world")
|
||||
client2.HSet(ctx, "doc2", "title", "hello redis")
|
||||
|
||||
// Wait for indexing
|
||||
waitForIndexing(t, client2, "test-idx", 2)
|
||||
|
||||
// SpellCheck from both protocols
|
||||
result2, err := client2.FTSpellCheck(ctx, "test-idx", "helo").Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTSpellCheck RESP2 failed: %v", err)
|
||||
}
|
||||
|
||||
result3, err := client3.FTSpellCheck(ctx, "test-idx", "helo").Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTSpellCheck RESP3 failed: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("RESP2 result: %+v", result2)
|
||||
t.Logf("RESP3 result: %+v", result3)
|
||||
|
||||
// Compare results
|
||||
if len(result2) != len(result3) {
|
||||
t.Errorf("Results length mismatch: RESP2=%d, RESP3=%d", len(result2), len(result3))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FTSynDump", func(t *testing.T) {
|
||||
// Create index
|
||||
_, err := client2.FTCreate(ctx, "test-idx",
|
||||
&redis.FTCreateOptions{},
|
||||
&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText},
|
||||
).Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTCreate failed: %v", err)
|
||||
}
|
||||
defer client2.FTDropIndex(ctx, "test-idx")
|
||||
|
||||
// Add synonyms
|
||||
_, err = client2.FTSynUpdate(ctx, "test-idx", "group1", []interface{}{"hello", "hi", "hey"}).Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTSynUpdate failed: %v", err)
|
||||
}
|
||||
|
||||
// SynDump from both protocols
|
||||
result2, err := client2.FTSynDump(ctx, "test-idx").Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTSynDump RESP2 failed: %v", err)
|
||||
}
|
||||
|
||||
result3, err := client3.FTSynDump(ctx, "test-idx").Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTSynDump RESP3 failed: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("RESP2 result: %+v", result2)
|
||||
t.Logf("RESP3 result: %+v", result3)
|
||||
|
||||
// Compare results
|
||||
if len(result2) != len(result3) {
|
||||
t.Errorf("Results length mismatch: RESP2=%d, RESP3=%d", len(result2), len(result3))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func waitForIndexing(t *testing.T, client *redis.Client, index string, expectedDocs int) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
for i := 0; i < 100; i++ {
|
||||
info, err := client.FTInfo(ctx, index).Result()
|
||||
if err != nil {
|
||||
t.Fatalf("FTInfo failed: %v", err)
|
||||
}
|
||||
if info.NumDocs >= expectedDocs {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("Timeout waiting for indexing")
|
||||
}
|
||||
155
search_test.go
155
search_test.go
@@ -4401,7 +4401,7 @@ var _ = Describe("RediSearch commands Resp 3", Label("search"), func() {
|
||||
Expect(client.Close()).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should handle FTAggregate with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftaggregate"), func() {
|
||||
It("should handle FTAggregate with RESP3", 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()
|
||||
@@ -4411,40 +4411,45 @@ var _ = Describe("RediSearch commands Resp 3", Label("search"), func() {
|
||||
|
||||
client.HSet(ctx, "doc1", "PrimaryKey", "9::362330", "CreatedDateTimeUTC", "637387878524969984")
|
||||
client.HSet(ctx, "doc2", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "637387875859270016")
|
||||
WaitForIndexing(client, "idx1")
|
||||
|
||||
options := &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}}
|
||||
res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).RawResult()
|
||||
|
||||
// Test with UnstableResp3 true - both RawResult and Result should work
|
||||
cmd := client.FTAggregateWithArgs(ctx, "idx1", "*", options)
|
||||
res, err := cmd.RawResult()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
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{})
|
||||
// Test Result() also works with UnstableResp3 true
|
||||
result, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
|
||||
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])))
|
||||
Expect(result.Total).To(BeEquivalentTo(2))
|
||||
Expect(result.Rows).To(HaveLen(2))
|
||||
Expect(result.Rows[0].Fields["CreatedDateTimeUTC"]).To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
|
||||
Expect(result.Rows[1].Fields["CreatedDateTimeUTC"]).To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
|
||||
|
||||
// Test with UnstableResp3 false - should return error instead of panic
|
||||
// Test with UnstableResp3 false - should also work with stable RESP3 parsing
|
||||
options = &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}}
|
||||
rawRes, err := client2.FTAggregateWithArgs(ctx, "idx1", "*", options).RawResult()
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("RESP3 responses for this command are disabled"))
|
||||
Expect(rawRes).To(BeNil())
|
||||
|
||||
rawVal = client2.FTAggregateWithArgs(ctx, "idx1", "*", options).RawVal()
|
||||
Expect(client2.FTAggregateWithArgs(ctx, "idx1", "*", options).Err()).To(HaveOccurred())
|
||||
Expect(rawVal).To(BeNil())
|
||||
|
||||
result2, err := client2.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result2.Total).To(BeEquivalentTo(2))
|
||||
Expect(result2.Rows).To(HaveLen(2))
|
||||
Expect(result2.Rows[0].Fields["CreatedDateTimeUTC"]).To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
|
||||
Expect(result2.Rows[1].Fields["CreatedDateTimeUTC"]).To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
|
||||
})
|
||||
|
||||
It("should handle FTInfo with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftinfo"), func() {
|
||||
It("should handle FTInfo with RESP3", 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")
|
||||
|
||||
// Test with UnstableResp3 true - both RawResult and Result should work
|
||||
resInfo, err := client.FTInfo(ctx, "idx1").RawResult()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
attributes := resInfo.(map[interface{}]interface{})["attributes"].([]interface{})
|
||||
@@ -4456,18 +4461,22 @@ var _ = Describe("RediSearch commands Resp 3", Label("search"), func() {
|
||||
flags = attributes[0].(map[interface{}]interface{})["flags"].([]interface{})
|
||||
Expect(flags).To(ConsistOf("SORTABLE", "NOSTEM"))
|
||||
|
||||
// Test with UnstableResp3 false - should return error instead of panic
|
||||
rawResInfo, err := client2.FTInfo(ctx, "idx1").RawResult()
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("RESP3 responses for this command are disabled"))
|
||||
Expect(rawResInfo).To(BeNil())
|
||||
// Test Result() also works with UnstableResp3 true
|
||||
result, err := client.FTInfo(ctx, "idx1").Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result.IndexName).To(BeEquivalentTo("idx1"))
|
||||
Expect(result.Attributes).To(HaveLen(1))
|
||||
Expect(result.Attributes[0].Attribute).To(BeEquivalentTo("txt"))
|
||||
|
||||
rawValInfo := client2.FTInfo(ctx, "idx1").RawVal()
|
||||
Expect(client2.FTInfo(ctx, "idx1").Err()).To(HaveOccurred())
|
||||
Expect(rawValInfo).To(BeNil())
|
||||
// Test with UnstableResp3 false - should also work with stable RESP3 parsing
|
||||
result2, err := client2.FTInfo(ctx, "idx1").Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result2.IndexName).To(BeEquivalentTo("idx1"))
|
||||
Expect(result2.Attributes).To(HaveLen(1))
|
||||
Expect(result2.Attributes[0].Attribute).To(BeEquivalentTo("txt"))
|
||||
})
|
||||
|
||||
It("should handle FTSpellCheck with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftspellcheck"), func() {
|
||||
It("should handle FTSpellCheck with RESP3", 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()
|
||||
@@ -4478,6 +4487,7 @@ var _ = Describe("RediSearch commands Resp 3", Label("search"), func() {
|
||||
client.HSet(ctx, "doc1", "f1", "some valid content", "f2", "this is sample text")
|
||||
client.HSet(ctx, "doc2", "f1", "very important", "f2", "lorem ipsum")
|
||||
|
||||
// Test with UnstableResp3 true - both RawResult and Result should work
|
||||
resSpellCheck, err := client.FTSpellCheck(ctx, "idx1", "impornant").RawResult()
|
||||
valSpellCheck := client.FTSpellCheck(ctx, "idx1", "impornant").RawVal()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@@ -4485,24 +4495,34 @@ var _ = Describe("RediSearch commands Resp 3", Label("search"), func() {
|
||||
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 - should return error instead of panic
|
||||
rawResSpellCheck, err := client2.FTSpellCheck(ctx, "idx1", "impornant").RawResult()
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("RESP3 responses for this command are disabled"))
|
||||
Expect(rawResSpellCheck).To(BeNil())
|
||||
// Test Result() also works with UnstableResp3 true
|
||||
result, err := client.FTSpellCheck(ctx, "idx1", "impornant").Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result).To(HaveLen(1))
|
||||
Expect(result[0].Term).To(BeEquivalentTo("impornant"))
|
||||
Expect(result[0].Suggestions).To(HaveLen(1))
|
||||
Expect(result[0].Suggestions[0].Suggestion).To(BeEquivalentTo("important"))
|
||||
Expect(result[0].Suggestions[0].Score).To(BeEquivalentTo(0.5))
|
||||
|
||||
rawValSpellCheck := client2.FTSpellCheck(ctx, "idx1", "impornant").RawVal()
|
||||
Expect(client2.FTSpellCheck(ctx, "idx1", "impornant").Err()).To(HaveOccurred())
|
||||
Expect(rawValSpellCheck).To(BeNil())
|
||||
// Test with UnstableResp3 false - should also work with stable RESP3 parsing
|
||||
result2, err := client2.FTSpellCheck(ctx, "idx1", "impornant").Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result2).To(HaveLen(1))
|
||||
Expect(result2[0].Term).To(BeEquivalentTo("impornant"))
|
||||
Expect(result2[0].Suggestions).To(HaveLen(1))
|
||||
Expect(result2[0].Suggestions[0].Suggestion).To(BeEquivalentTo("important"))
|
||||
Expect(result2[0].Suggestions[0].Score).To(BeEquivalentTo(0.5))
|
||||
})
|
||||
|
||||
It("should handle FTSearch with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftsearch"), func() {
|
||||
It("should handle FTSearch with RESP3", 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")
|
||||
|
||||
// Test with UnstableResp3 true - both RawResult and Result should work
|
||||
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())
|
||||
@@ -4514,17 +4534,29 @@ var _ = Describe("RediSearch commands Resp 3", Label("search"), func() {
|
||||
totalResults2 := res2.(map[interface{}]interface{})["total_results"]
|
||||
Expect(totalResults2).To(BeEquivalentTo(int64(1)))
|
||||
|
||||
// Test with UnstableResp3 false - should return error instead of panic
|
||||
rawRes2, err := client2.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).RawResult()
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("RESP3 responses for this command are disabled"))
|
||||
Expect(rawRes2).To(BeNil())
|
||||
// Test Result() also works with UnstableResp3 true
|
||||
result, err := client.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result.Total).To(BeEquivalentTo(1))
|
||||
Expect(result.Docs).To(HaveLen(1))
|
||||
Expect(result.Docs[0].ID).To(BeEquivalentTo("doc2"))
|
||||
|
||||
rawVal2 := client2.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).RawVal()
|
||||
Expect(client2.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).Err()).To(HaveOccurred())
|
||||
Expect(rawVal2).To(BeNil())
|
||||
// Test with UnstableResp3 false - should also work with stable RESP3 parsing
|
||||
result2, err := client2.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result2.Total).To(BeEquivalentTo(1))
|
||||
Expect(result2.Docs).To(HaveLen(1))
|
||||
Expect(result2.Docs[0].ID).To(BeEquivalentTo("doc2"))
|
||||
|
||||
// Test with content
|
||||
result3, err := client2.FTSearchWithArgs(ctx, "txt", "hello world", &redis.FTSearchOptions{}).Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result3.Total).To(BeEquivalentTo(1))
|
||||
Expect(result3.Docs).To(HaveLen(1))
|
||||
Expect(result3.Docs[0].ID).To(BeEquivalentTo("doc2"))
|
||||
Expect(result3.Docs[0].Fields["txt"]).To(BeEquivalentTo("hello world"))
|
||||
})
|
||||
It("should handle FTSynDump with Unstable RESP3 Search Module and without stability", Label("search", "ftsyndump"), func() {
|
||||
It("should handle FTSynDump with RESP3", 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()
|
||||
@@ -4544,21 +4576,40 @@ var _ = Describe("RediSearch commands Resp 3", Label("search"), func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(resSynUpdate).To(BeEquivalentTo("OK"))
|
||||
|
||||
// Test with UnstableResp3 true - both RawResult and Result should work
|
||||
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 - should return error instead of panic
|
||||
rawResSynDump, err := client2.FTSynDump(ctx, "idx1").RawResult()
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("RESP3 responses for this command are disabled"))
|
||||
Expect(rawResSynDump).To(BeNil())
|
||||
// Test Result() also works with UnstableResp3 true
|
||||
result, err := client.FTSynDump(ctx, "idx1").Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
// Find the "baby" term in results
|
||||
var foundBaby bool
|
||||
for _, r := range result {
|
||||
if r.Term == "baby" {
|
||||
foundBaby = true
|
||||
Expect(r.Synonyms).To(ContainElement("id1"))
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(foundBaby).To(BeTrue())
|
||||
|
||||
rawValSynDump := client2.FTSynDump(ctx, "idx1").RawVal()
|
||||
Expect(client2.FTSynDump(ctx, "idx1").Err()).To(HaveOccurred())
|
||||
Expect(rawValSynDump).To(BeNil())
|
||||
// Test with UnstableResp3 false - should also work with stable RESP3 parsing
|
||||
result2, err := client2.FTSynDump(ctx, "idx1").Result()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
// Find the "baby" term in results
|
||||
foundBaby = false
|
||||
for _, r := range result2 {
|
||||
if r.Term == "baby" {
|
||||
foundBaby = true
|
||||
Expect(r.Synonyms).To(ContainElement("id1"))
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(foundBaby).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should test not affected Resp 3 Search method - FTExplain", Label("search", "ftexplain"), func() {
|
||||
|
||||
@@ -160,6 +160,9 @@ type FailoverOptions struct {
|
||||
// Only applies to failover cluster clients. Default is 15 seconds.
|
||||
FailingTimeoutSeconds int
|
||||
|
||||
// Deprecated: All RediSearch commands now have stable RESP3 parsing and this
|
||||
// flag is a no-op. It is kept for backwards compatibility and will be removed
|
||||
// in a future release.
|
||||
UnstableResp3 bool
|
||||
|
||||
// PushNotificationProcessor is the processor for handling push notifications.
|
||||
|
||||
@@ -280,4 +280,3 @@ var _ = Describe("TLS Cluster", Label("NonRedisEnterprise"), func() {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -74,4 +74,3 @@ func TestTLSRedissURL(t *testing.T) {
|
||||
|
||||
t.Log("✅ TLS rediss:// URL test passed")
|
||||
}
|
||||
|
||||
|
||||
25
tls_test.go
25
tls_test.go
@@ -33,9 +33,9 @@ func loadTLSConfig(certDir string) (*tls.Config, error) {
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
RootCAs: caCertPool,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ServerName: "localhost",
|
||||
RootCAs: caCertPool,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ServerName: "localhost",
|
||||
InsecureSkipVerify: true,
|
||||
}, nil
|
||||
}
|
||||
@@ -247,11 +247,11 @@ var _ = Describe("TLS", Label("NonRedisEnterprise"), func() {
|
||||
It("should respect MinVersion setting", func() {
|
||||
ctx := context.Background()
|
||||
tlsConfigWithMinVersion := &tls.Config{
|
||||
RootCAs: tlsConfig.RootCAs,
|
||||
Certificates: tlsConfig.Certificates,
|
||||
ServerName: "localhost",
|
||||
InsecureSkipVerify: true,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
RootCAs: tlsConfig.RootCAs,
|
||||
Certificates: tlsConfig.Certificates,
|
||||
ServerName: "localhost",
|
||||
InsecureSkipVerify: true,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
client := redis.NewClient(&redis.Options{
|
||||
@@ -268,10 +268,10 @@ var _ = Describe("TLS", Label("NonRedisEnterprise"), func() {
|
||||
It("should work with custom cipher suites", func() {
|
||||
ctx := context.Background()
|
||||
tlsConfigWithCiphers := &tls.Config{
|
||||
RootCAs: tlsConfig.RootCAs,
|
||||
Certificates: tlsConfig.Certificates,
|
||||
ServerName: "localhost",
|
||||
InsecureSkipVerify: true,
|
||||
RootCAs: tlsConfig.RootCAs,
|
||||
Certificates: tlsConfig.Certificates,
|
||||
ServerName: "localhost",
|
||||
InsecureSkipVerify: true,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
@@ -290,4 +290,3 @@ var _ = Describe("TLS", Label("NonRedisEnterprise"), func() {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -137,6 +137,9 @@ type UniversalOptions struct {
|
||||
// Only applies to cluster clients. Default is 15 seconds.
|
||||
FailingTimeoutSeconds int
|
||||
|
||||
// Deprecated: All RediSearch commands now have stable RESP3 parsing and this
|
||||
// flag is a no-op. It is kept for backwards compatibility and will be removed
|
||||
// in a future release.
|
||||
UnstableResp3 bool
|
||||
|
||||
// PushNotificationProcessor is the processor for handling push notifications.
|
||||
|
||||
Reference in New Issue
Block a user