diff --git a/command.go b/command.go index 59ba08969..7dd86f2b9 100644 --- a/command.go +++ b/command.go @@ -3787,6 +3787,65 @@ func (cmd *MapStringStringSliceCmd) readReply(rd *proto.Reader) error { return nil } +// ----------------------------------------------------------------------- +// MapStringInterfaceCmd represents a command that returns a map of strings to interface{}. +type MapMapStringInterfaceCmd struct { + baseCmd + val map[string]interface{} +} + +func NewMapMapStringInterfaceCmd(ctx context.Context, args ...interface{}) *MapMapStringInterfaceCmd { + return &MapMapStringInterfaceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *MapMapStringInterfaceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *MapMapStringInterfaceCmd) SetVal(val map[string]interface{}) { + cmd.val = val +} + +func (cmd *MapMapStringInterfaceCmd) Result() (map[string]interface{}, error) { + return cmd.val, cmd.err +} + +func (cmd *MapMapStringInterfaceCmd) Val() map[string]interface{} { + return cmd.val +} + +func (cmd *MapMapStringInterfaceCmd) readReply(rd *proto.Reader) (err error) { + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + + data := make(map[string]interface{}, n/2) + for i := 0; i < n; i += 2 { + _, err := rd.ReadArrayLen() + if err != nil { + err = cmd.err + } + key, err := rd.ReadString() + if err != nil { + err = cmd.err + } + value, err := rd.ReadString() + if err != nil { + err = cmd.err + } + data[key] = value + } + + cmd.val = data + return nil +} + //----------------------------------------------------------------------- type MapStringInterfaceSliceCmd struct { diff --git a/internal/util.go b/internal/util.go index 235a91afa..cc1bff24e 100644 --- a/internal/util.go +++ b/internal/util.go @@ -3,6 +3,7 @@ package internal import ( "context" "net" + "strconv" "strings" "time" @@ -81,3 +82,47 @@ func GetAddr(addr string) string { } return net.JoinHostPort(addr[:ind], addr[ind+1:]) } + +func ToInteger(val interface{}) int { + switch v := val.(type) { + case int: + return v + case int64: + return int(v) + case string: + i, _ := strconv.Atoi(v) + return i + default: + return 0 + } +} + +func ToFloat(val interface{}) float64 { + switch v := val.(type) { + case float64: + return v + case string: + f, _ := strconv.ParseFloat(v, 64) + return f + default: + return 0.0 + } +} + +func ToString(val interface{}) string { + if str, ok := val.(string); ok { + return str + } + return "" +} + +func ToStringSlice(val interface{}) []string { + if arr, ok := val.([]interface{}); ok { + result := make([]string, len(arr)) + for i, v := range arr { + result[i] = ToString(v) + } + return result + } + return nil +} diff --git a/search_commands.go b/search_commands.go index c49460ae0..8dfed4944 100644 --- a/search_commands.go +++ b/search_commands.go @@ -2,35 +2,39 @@ package redis import ( "context" + "fmt" + "strconv" + + "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/proto" ) type SearchCmdable interface { FT_List(ctx context.Context) *StringSliceCmd FTAggregate(ctx context.Context, index string, query string) *MapStringInterfaceCmd - FTAggregateWithArgs(ctx context.Context, index string, query string, options *FTAggregateOptions) *MapStringInterfaceCmd + FTAggregateWithArgs(ctx context.Context, index string, query string, options *FTAggregateOptions) *AggregateCmd 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 - FTConfigGet(ctx context.Context, option string) *MapStringInterfaceCmd + 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 FTCursorDel(ctx context.Context, index string, cursorId int) *StatusCmd FTCursorRead(ctx context.Context, index string, cursorId int, count int) *MapStringInterfaceCmd - FTDictAdd(ctx context.Context, dict string, term []interface{}) *IntCmd - FTDictDel(ctx context.Context, dict string, term []interface{}) *IntCmd + FTDictAdd(ctx context.Context, dict string, term ...interface{}) *IntCmd + FTDictDel(ctx context.Context, dict string, term ...interface{}) *IntCmd FTDictDump(ctx context.Context, dict string) *StringSliceCmd FTDropIndex(ctx context.Context, index string) *StatusCmd FTDropIndexWithArgs(ctx context.Context, index string, options *FTDropIndexOptions) *StatusCmd FTExplain(ctx context.Context, index string, query string) *StringCmd FTExplainWithArgs(ctx context.Context, index string, query string, options *FTExplainOptions) *StringCmd - FTInfo(ctx context.Context, index string) *MapStringInterfaceCmd - FTProfile(ctx context.Context, index string, limited bool, query interface{}) *MapStringInterfaceCmd - FTSpellCheck(ctx context.Context, index string, query string) *MapStringInterfaceCmd - FTSpellCheckWithArgs(ctx context.Context, index string, query string, options *FTSpellCheckOptions) *MapStringInterfaceCmd - FTSearch(ctx context.Context, index string, query string) *Cmd - FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *Cmd - FTSynDump(ctx context.Context, index string) *MapStringSliceInterfaceCmd + FTInfo(ctx context.Context, index string) *FTInfoCmd + FTSpellCheck(ctx context.Context, index string, query string) *FTSpellCheckCmd + FTSpellCheckWithArgs(ctx context.Context, index string, query string, options *FTSpellCheckOptions) *FTSpellCheckCmd + FTSearch(ctx context.Context, index string, query string) *FTSearchCmd + FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *FTSearchCmd + FTSynDump(ctx context.Context, index string) *FTSynDumpCmd FTSynUpdate(ctx context.Context, index string, synGroupId interface{}, terms []interface{}) *StatusCmd FTSynUpdateWithArgs(ctx context.Context, index string, synGroupId interface{}, options *FTSynUpdateOptions, terms []interface{}) *StatusCmd FTTagVals(ctx context.Context, index string, field string) *StringSliceCmd @@ -107,12 +111,6 @@ type SpellCheckTerms struct { Dictionary string } -type FTSpellCheckOptions struct { - Distance int - Terms SpellCheckTerms - Dialect int -} - type FTExplainOptions struct { Dialect string } @@ -310,6 +308,153 @@ type FTSearchOptions struct { DialectVersion int } +type FTSynDumpResult struct { + Term string + Synonyms []string +} + +type FTSynDumpCmd struct { + baseCmd + val []FTSynDumpResult +} + +type FTAggregateResult struct { + Total int + Rows []AggregateRow +} + +type AggregateRow struct { + Fields map[string]interface{} +} + +type AggregateCmd struct { + baseCmd + val *FTAggregateResult +} + +type FTInfoResult struct { + IndexErrors IndexErrors + Attributes []FTAttribute + BytesPerRecordAvg string + Cleaning int + CursorStats CursorStats + DialectStats map[string]int + DocTableSizeMB float64 + FieldStatistics []FieldStatistic + GCStats GCStats + GeoshapesSzMB float64 + HashIndexingFailures int + IndexDefinition IndexDefinition + IndexName string + IndexOptions []string + Indexing int + InvertedSzMB float64 + KeyTableSizeMB float64 + MaxDocID int + NumDocs int + NumRecords int + NumTerms int + NumberOfUses int + OffsetBitsPerRecordAvg string + OffsetVectorsSzMB float64 + OffsetsPerTermAvg string + PercentIndexed float64 + RecordsPerDocAvg string + SortableValuesSizeMB float64 + TagOverheadSzMB float64 + TextOverheadSzMB float64 + TotalIndexMemorySzMB float64 + TotalIndexingTime int + TotalInvertedIndexBlocks int + VectorIndexSzMB float64 +} + +type IndexErrors struct { + IndexingFailures int + LastIndexingError string + LastIndexingErrorKey string +} + +type FTAttribute struct { + Identifier string + Attribute string + Type string + Weight float64 + Sortable bool + NoStem bool + NoIndex bool + UNF bool + PhoneticMatcher string + CaseSensitive bool + WithSuffixtrie bool +} + +type CursorStats struct { + GlobalIdle int + GlobalTotal int + IndexCapacity int + IndexTotal int +} + +type FieldStatistic struct { + Identifier string + Attribute string + IndexErrors IndexErrors +} + +type GCStats struct { + BytesCollected int + TotalMsRun int + TotalCycles int + AverageCycleTimeMs string + LastRunTimeMs int + GCNumericTreesMissed int + GCBlocksDenied int +} + +type IndexDefinition struct { + KeyType string + Prefixes []string + DefaultScore float64 +} + +type FTSpellCheckOptions struct { + Distance int + Terms *FTSpellCheckTerms + Dialect int +} + +type FTSpellCheckTerms struct { + Inclusion string // Either "INCLUDE" or "EXCLUDE" + Dictionary string + Terms []interface{} +} + +type SpellCheckResult struct { + Term string + Suggestions []SpellCheckSuggestion +} + +type SpellCheckSuggestion struct { + Score float64 + Suggestion string +} + +type FTSearchResult struct { + Total int + Docs []Document +} + +type Document struct { + ID string + Score *float64 + Payload *string + SortKey *string + Fields map[string]string +} + +type AggregateQuery []interface{} + // FT_List - Lists all the existing indexes in the database. // For more information, please refer to the Redis documentation: // [FT._LIST]: (https://redis.io/commands/ft._list/) @@ -330,8 +475,6 @@ func (c cmdable) FTAggregate(ctx context.Context, index string, query string) *M return cmd } -type AggregateQuery []interface{} - func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery { queryArgs := []interface{}{query} if options != nil { @@ -436,12 +579,86 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery return queryArgs } +func ProcessAggregateResult(data []interface{}) (*FTAggregateResult, error) { + if len(data) == 0 { + return nil, fmt.Errorf("no data returned") + } + + total, ok := data[0].(int64) + if !ok { + return nil, fmt.Errorf("invalid total format") + } + + rows := make([]AggregateRow, 0, len(data)-1) + for _, row := range data[1:] { + fields, ok := row.([]interface{}) + if !ok { + return nil, fmt.Errorf("invalid row format") + } + + rowMap := make(map[string]interface{}) + for i := 0; i < len(fields); i += 2 { + key, ok := fields[i].(string) + if !ok { + return nil, fmt.Errorf("invalid field key format") + } + value := fields[i+1] + rowMap[key] = value + } + rows = append(rows, AggregateRow{Fields: rowMap}) + } + + result := &FTAggregateResult{ + Total: int(total), + Rows: rows, + } + return result, nil +} + +func NewAggregateCmd(ctx context.Context, args ...interface{}) *AggregateCmd { + return &AggregateCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *AggregateCmd) SetVal(val *FTAggregateResult) { + cmd.val = val +} + +func (cmd *AggregateCmd) Val() *FTAggregateResult { + return cmd.val +} + +func (cmd *AggregateCmd) Result() (*FTAggregateResult, error) { + return cmd.val, cmd.err +} + +func (cmd *AggregateCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) { + data, err := rd.ReadSlice() + if err != nil { + cmd.err = err + return nil + } + cmd.val, err = ProcessAggregateResult(data) + if err != nil { + cmd.err = err + } + return nil +} + // FTAggregateWithArgs - Performs a search query on an index and applies a series of aggregate transformations to the result. // The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query. // This function also allows for specifying additional options such as: Verbatim, LoadAll, Load, Timeout, GroupBy, SortBy, SortByMax, Apply, LimitOffset, Limit, Filter, WithCursor, Params, and DialectVersion. // For more information, please refer to the Redis documentation: // [FT.AGGREGATE]: (https://redis.io/commands/ft.aggregate/) -func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query string, options *FTAggregateOptions) *MapStringInterfaceCmd { +func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query string, options *FTAggregateOptions) *AggregateCmd { args := []interface{}{"FT.AGGREGATE", index, query} if options != nil { if options.Verbatim { @@ -543,7 +760,7 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st } } - cmd := NewMapStringInterfaceCmd(ctx, args...) + cmd := NewAggregateCmd(ctx, args...) _ = c(ctx, cmd) return cmd } @@ -601,8 +818,8 @@ func (c cmdable) FTAlter(ctx context.Context, index string, skipInitalScan bool, // The 'option' parameter specifies the configuration parameter to retrieve. // For more information, please refer to the Redis documentation: // [FT.CONFIG GET]: (https://redis.io/commands/ft.config-get/) -func (c cmdable) FTConfigGet(ctx context.Context, option string) *MapStringInterfaceCmd { - cmd := NewMapStringInterfaceCmd(ctx, "FT.CONFIG", "GET", option) +func (c cmdable) FTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd { + cmd := NewMapMapStringInterfaceCmd(ctx, "FT.CONFIG", "GET", option) _ = c(ctx, cmd) return cmd } @@ -819,7 +1036,7 @@ func (c cmdable) FTCursorRead(ctx context.Context, index string, cursorId int, c // The 'dict' parameter specifies the dictionary to which to add the terms, and the 'term' parameter specifies the terms to add. // For more information, please refer to the Redis documentation: // [FT.DICTADD]: (https://redis.io/commands/ft.dictadd/) -func (c cmdable) FTDictAdd(ctx context.Context, dict string, term []interface{}) *IntCmd { +func (c cmdable) FTDictAdd(ctx context.Context, dict string, term ...interface{}) *IntCmd { args := []interface{}{"FT.DICTADD", dict} args = append(args, term...) cmd := NewIntCmd(ctx, args...) @@ -831,7 +1048,7 @@ func (c cmdable) FTDictAdd(ctx context.Context, dict string, term []interface{}) // The 'dict' parameter specifies the dictionary from which to delete the terms, and the 'term' parameter specifies the terms to delete. // For more information, please refer to the Redis documentation: // [FT.DICTDEL]: (https://redis.io/commands/ft.dictdel/) -func (c cmdable) FTDictDel(ctx context.Context, dict string, term []interface{}) *IntCmd { +func (c cmdable) FTDictDel(ctx context.Context, dict string, term ...interface{}) *IntCmd { args := []interface{}{"FT.DICTDEL", dict} args = append(args, term...) cmd := NewIntCmd(ctx, args...) @@ -906,45 +1123,229 @@ func (c cmdable) FTExplainCli(ctx context.Context, key, path string) error { panic("not implemented") } -// FTInfo - Retrieves information about an index. -// The 'index' parameter specifies the index to retrieve information about. -// For more information, please refer to the Redis documentation: -// [FT.INFO]: (https://redis.io/commands/ft.info/) -func (c cmdable) FTInfo(ctx context.Context, index string) *MapStringInterfaceCmd { - cmd := NewMapStringInterfaceCmd(ctx, "FT.INFO", index) - _ = c(ctx, cmd) - return cmd +func parseFTInfo(data map[string]interface{}) (FTInfoResult, error) { + var ftInfo FTInfoResult + // Manually parse each field from the map + if indexErrors, ok := data["Index Errors"].([]interface{}); ok { + ftInfo.IndexErrors = IndexErrors{ + IndexingFailures: internal.ToInteger(indexErrors[1]), + LastIndexingError: internal.ToString(indexErrors[3]), + LastIndexingErrorKey: internal.ToString(indexErrors[5]), + } + } + + if attributes, ok := data["attributes"].([]interface{}); ok { + for _, attr := range attributes { + if attrMap, ok := attr.([]interface{}); ok { + att := FTAttribute{} + for i := 0; i < len(attrMap); i++ { + if internal.ToLower(internal.ToString(attrMap[i])) == "attribute" { + att.Attribute = internal.ToString(attrMap[i+1]) + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "identifier" { + att.Identifier = internal.ToString(attrMap[i+1]) + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "type" { + att.Type = internal.ToString(attrMap[i+1]) + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "weight" { + att.Weight = internal.ToFloat(attrMap[i+1]) + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "nostem" { + att.NoStem = true + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "sortable" { + att.Sortable = true + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "noindex" { + att.NoIndex = true + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "unf" { + att.UNF = true + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "phonetic" { + att.PhoneticMatcher = internal.ToString(attrMap[i+1]) + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "case_sensitive" { + att.CaseSensitive = true + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "withsuffixtrie" { + att.WithSuffixtrie = true + continue + } + + } + ftInfo.Attributes = append(ftInfo.Attributes, att) + } + } + } + + ftInfo.BytesPerRecordAvg = internal.ToString(data["bytes_per_record_avg"]) + ftInfo.Cleaning = internal.ToInteger(data["cleaning"]) + + if cursorStats, ok := data["cursor_stats"].([]interface{}); ok { + ftInfo.CursorStats = CursorStats{ + GlobalIdle: internal.ToInteger(cursorStats[1]), + GlobalTotal: internal.ToInteger(cursorStats[3]), + IndexCapacity: internal.ToInteger(cursorStats[5]), + IndexTotal: internal.ToInteger(cursorStats[7]), + } + } + + if dialectStats, ok := data["dialect_stats"].([]interface{}); ok { + ftInfo.DialectStats = make(map[string]int) + for i := 0; i < len(dialectStats); i += 2 { + ftInfo.DialectStats[internal.ToString(dialectStats[i])] = internal.ToInteger(dialectStats[i+1]) + } + } + + ftInfo.DocTableSizeMB = internal.ToFloat(data["doc_table_size_mb"]) + + if fieldStats, ok := data["field statistics"].([]interface{}); ok { + for _, stat := range fieldStats { + if statMap, ok := stat.([]interface{}); ok { + ftInfo.FieldStatistics = append(ftInfo.FieldStatistics, FieldStatistic{ + Identifier: internal.ToString(statMap[1]), + Attribute: internal.ToString(statMap[3]), + IndexErrors: IndexErrors{ + IndexingFailures: internal.ToInteger(statMap[5].([]interface{})[1]), + LastIndexingError: internal.ToString(statMap[5].([]interface{})[3]), + LastIndexingErrorKey: internal.ToString(statMap[5].([]interface{})[5]), + }, + }) + } + } + } + + if gcStats, ok := data["gc_stats"].([]interface{}); ok { + ftInfo.GCStats = GCStats{ + BytesCollected: internal.ToInteger(gcStats[1]), + TotalMsRun: internal.ToInteger(gcStats[3]), + TotalCycles: internal.ToInteger(gcStats[5]), + AverageCycleTimeMs: internal.ToString(gcStats[7]), + LastRunTimeMs: internal.ToInteger(gcStats[9]), + GCNumericTreesMissed: internal.ToInteger(gcStats[11]), + GCBlocksDenied: internal.ToInteger(gcStats[13]), + } + } + + ftInfo.GeoshapesSzMB = internal.ToFloat(data["geoshapes_sz_mb"]) + ftInfo.HashIndexingFailures = internal.ToInteger(data["hash_indexing_failures"]) + + if indexDef, ok := data["index_definition"].([]interface{}); ok { + ftInfo.IndexDefinition = IndexDefinition{ + KeyType: internal.ToString(indexDef[1]), + Prefixes: internal.ToStringSlice(indexDef[3]), + DefaultScore: internal.ToFloat(indexDef[5]), + } + } + + ftInfo.IndexName = internal.ToString(data["index_name"]) + ftInfo.IndexOptions = internal.ToStringSlice(data["index_options"].([]interface{})) + ftInfo.Indexing = internal.ToInteger(data["indexing"]) + ftInfo.InvertedSzMB = internal.ToFloat(data["inverted_sz_mb"]) + ftInfo.KeyTableSizeMB = internal.ToFloat(data["key_table_size_mb"]) + ftInfo.MaxDocID = internal.ToInteger(data["max_doc_id"]) + ftInfo.NumDocs = internal.ToInteger(data["num_docs"]) + ftInfo.NumRecords = internal.ToInteger(data["num_records"]) + ftInfo.NumTerms = internal.ToInteger(data["num_terms"]) + ftInfo.NumberOfUses = internal.ToInteger(data["number_of_uses"]) + ftInfo.OffsetBitsPerRecordAvg = internal.ToString(data["offset_bits_per_record_avg"]) + ftInfo.OffsetVectorsSzMB = internal.ToFloat(data["offset_vectors_sz_mb"]) + ftInfo.OffsetsPerTermAvg = internal.ToString(data["offsets_per_term_avg"]) + ftInfo.PercentIndexed = internal.ToFloat(data["percent_indexed"]) + ftInfo.RecordsPerDocAvg = internal.ToString(data["records_per_doc_avg"]) + ftInfo.SortableValuesSizeMB = internal.ToFloat(data["sortable_values_size_mb"]) + ftInfo.TagOverheadSzMB = internal.ToFloat(data["tag_overhead_sz_mb"]) + ftInfo.TextOverheadSzMB = internal.ToFloat(data["text_overhead_sz_mb"]) + ftInfo.TotalIndexMemorySzMB = internal.ToFloat(data["total_index_memory_sz_mb"]) + ftInfo.TotalIndexingTime = internal.ToInteger(data["total_indexing_time"]) + ftInfo.TotalInvertedIndexBlocks = internal.ToInteger(data["total_inverted_index_blocks"]) + ftInfo.VectorIndexSzMB = internal.ToFloat(data["vector_index_sz_mb"]) + + return ftInfo, nil } -// FTProfileSearch - Executes a search query and returns a profile of how the query was processed. -// The 'index' parameter specifies the index to search, the 'limited' parameter specifies whether to limit the results, -// and the 'query' parameter specifies the search / aggreagte query. Please notice that you must either pass a SearchQuery or an AggregateQuery. -// For more information, please refer to the Redis documentation: -// [FT.PROFILE SEARCH]: (https://redis.io/commands/ft.profile/) -func (c cmdable) FTProfile(ctx context.Context, index string, limited bool, query interface{}) *MapStringInterfaceCmd { - queryType := "" - var argsQuery []interface{} - - switch v := query.(type) { - case AggregateQuery: - queryType = "AGGREGATE" - argsQuery = v - case SearchQuery: - queryType = "SEARCH" - argsQuery = v - default: - panic("FT.PROFILE: query must be either AggregateQuery or SearchQuery") +type FTInfoCmd struct { + baseCmd + val FTInfoResult +} + +func newFTInfoCmd(ctx context.Context, args ...interface{}) *FTInfoCmd { + return &FTInfoCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, } +} + +func (cmd *FTInfoCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *FTInfoCmd) SetVal(val FTInfoResult) { + cmd.val = val +} - args := []interface{}{"FT.PROFILE", index, queryType} +func (cmd *FTInfoCmd) Result() (FTInfoResult, error) { + return cmd.val, cmd.err +} - if limited { - args = append(args, "LIMITED") +func (cmd *FTInfoCmd) Val() FTInfoResult { + return cmd.val +} + +func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) { + n, err := rd.ReadMapLen() + if err != nil { + return err } - args = append(args, "QUERY") - args = append(args, argsQuery...) - cmd := NewMapStringInterfaceCmd(ctx, args...) + data := make(map[string]interface{}, n) + for i := 0; i < n; i++ { + k, err := rd.ReadString() + if err != nil { + return err + } + v, err := rd.ReadReply() + if err != nil { + if err == Nil { + data[k] = Nil + continue + } + if err, ok := err.(proto.RedisError); ok { + data[k] = err + continue + } + return err + } + data[k] = v + } + cmd.val, err = parseFTInfo(data) + if err != nil { + cmd.err = err + } + + return nil +} + +// FTInfo - Retrieves information about an index. +// The 'index' parameter specifies the index to retrieve information about. +// For more information, please refer to the Redis documentation: +// [FT.INFO]: (https://redis.io/commands/ft.info/) +func (c cmdable) FTInfo(ctx context.Context, index string) *FTInfoCmd { + cmd := newFTInfoCmd(ctx, "FT.INFO", index) _ = c(ctx, cmd) return cmd } @@ -954,8 +1355,9 @@ func (c cmdable) FTProfile(ctx context.Context, index string, limited bool, quer // https://redis.io/docs/interact/search-and-query/advanced-concepts/spellcheck/ // For more information, please refer to the Redis documentation: // [FT.SPELLCHECK]: (https://redis.io/commands/ft.spellcheck/) -func (c cmdable) FTSpellCheck(ctx context.Context, index string, query string) *MapStringInterfaceCmd { - cmd := NewMapStringInterfaceCmd(ctx, "FT.SPELLCHECK", index, query) +func (c cmdable) FTSpellCheck(ctx context.Context, index string, query string) *FTSpellCheckCmd { + args := []interface{}{"FT.SPELLCHECK", index, query} + cmd := newFTSpellCheckCmd(ctx, args...) _ = c(ctx, cmd) return cmd } @@ -965,43 +1367,256 @@ func (c cmdable) FTSpellCheck(ctx context.Context, index string, query string) * // https://redis.io/docs/interact/search-and-query/advanced-concepts/spellcheck/ // For more information, please refer to the Redis documentation: // [FT.SPELLCHECK]: (https://redis.io/commands/ft.spellcheck/) -func (c cmdable) FTSpellCheckWithArgs(ctx context.Context, index string, query string, options *FTSpellCheckOptions) *MapStringInterfaceCmd { +func (c cmdable) FTSpellCheckWithArgs(ctx context.Context, index string, query string, options *FTSpellCheckOptions) *FTSpellCheckCmd { args := []interface{}{"FT.SPELLCHECK", index, query} if options != nil { - if options.Distance > 4 { - panic("FT.SPELLCHECK: DISTANCE must be between 0 and 4") - } if options.Distance > 0 { args = append(args, "DISTANCE", options.Distance) } - if options.Terms.Include && options.Terms.Exclude { - panic("FT.SPELLCHECK: INCLUDE and EXCLUDE are mutually exclusive") - } - if options.Terms.Include { - args = append(args, "TERMS", "INCLUDE") - } - if options.Terms.Exclude { - args = append(args, "TERMS", "EXCLUDE") - } - if options.Terms.Dictionary != "" { - args = append(args, options.Terms.Dictionary) + if options.Terms != nil { + args = append(args, "TERMS", options.Terms.Inclusion, options.Terms.Dictionary) + args = append(args, options.Terms.Terms...) } if options.Dialect > 0 { args = append(args, "DIALECT", options.Dialect) } } - cmd := NewMapStringInterfaceCmd(ctx, args...) + cmd := newFTSpellCheckCmd(ctx, args...) _ = c(ctx, cmd) return cmd } +type FTSpellCheckCmd struct { + baseCmd + val []SpellCheckResult +} + +func newFTSpellCheckCmd(ctx context.Context, args ...interface{}) *FTSpellCheckCmd { + return &FTSpellCheckCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *FTSpellCheckCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *FTSpellCheckCmd) SetVal(val []SpellCheckResult) { + cmd.val = val +} + +func (cmd *FTSpellCheckCmd) Result() ([]SpellCheckResult, error) { + return cmd.val, cmd.err +} + +func (cmd *FTSpellCheckCmd) Val() []SpellCheckResult { + return cmd.val +} + +func (cmd *FTSpellCheckCmd) readReply(rd *proto.Reader) (err error) { + data, err := rd.ReadSlice() + if err != nil { + cmd.err = err + return nil + } + cmd.val, err = parseFTSpellCheck(data) + if err != nil { + cmd.err = err + } + return nil +} + +func parseFTSpellCheck(data []interface{}) ([]SpellCheckResult, error) { + results := make([]SpellCheckResult, 0, len(data)) + + for _, termData := range data { + termInfo, ok := termData.([]interface{}) + if !ok || len(termInfo) != 3 { + return nil, fmt.Errorf("invalid term format") + } + + term, ok := termInfo[1].(string) + if !ok { + return nil, fmt.Errorf("invalid term format") + } + + suggestionsData, ok := termInfo[2].([]interface{}) + if !ok { + return nil, fmt.Errorf("invalid suggestions format") + } + + suggestions := make([]SpellCheckSuggestion, 0, len(suggestionsData)) + for _, suggestionData := range suggestionsData { + suggestionInfo, ok := suggestionData.([]interface{}) + if !ok || len(suggestionInfo) != 2 { + return nil, fmt.Errorf("invalid suggestion format") + } + + scoreStr, ok := suggestionInfo[0].(string) + if !ok { + return nil, fmt.Errorf("invalid suggestion score format") + } + score, err := strconv.ParseFloat(scoreStr, 64) + if err != nil { + return nil, fmt.Errorf("invalid suggestion score value") + } + + suggestion, ok := suggestionInfo[1].(string) + if !ok { + return nil, fmt.Errorf("invalid suggestion format") + } + + suggestions = append(suggestions, SpellCheckSuggestion{ + Score: score, + Suggestion: suggestion, + }) + } + + results = append(results, SpellCheckResult{ + Term: term, + Suggestions: suggestions, + }) + } + + return results, nil +} + +func parseFTSearch(data []interface{}, noContent, withScores, withPayloads, withSortKeys bool) (FTSearchResult, error) { + if len(data) < 1 { + return FTSearchResult{}, fmt.Errorf("unexpected search result format") + } + + total, ok := data[0].(int64) + if !ok { + return FTSearchResult{}, fmt.Errorf("invalid total results format") + } + + var results []Document + for i := 1; i < len(data); { + docID, ok := data[i].(string) + if !ok { + return FTSearchResult{}, fmt.Errorf("invalid document ID format") + } + + doc := Document{ + ID: docID, + Fields: make(map[string]string), + } + i++ + + if noContent { + results = append(results, doc) + continue + } + + if withScores && i < len(data) { + if scoreStr, ok := data[i].(string); ok { + score, err := strconv.ParseFloat(scoreStr, 64) + if err != nil { + return FTSearchResult{}, fmt.Errorf("invalid score format") + } + doc.Score = &score + i++ + } + } + + if withPayloads && i < len(data) { + if payload, ok := data[i].(string); ok { + doc.Payload = &payload + i++ + } + } + + if withSortKeys && i < len(data) { + if sortKey, ok := data[i].(string); ok { + doc.SortKey = &sortKey + i++ + } + } + + if i < len(data) { + fields, ok := data[i].([]interface{}) + if !ok { + return FTSearchResult{}, fmt.Errorf("invalid document fields format") + } + + for j := 0; j < len(fields); j += 2 { + key, ok := fields[j].(string) + if !ok { + return FTSearchResult{}, fmt.Errorf("invalid field key format") + } + value, ok := fields[j+1].(string) + if !ok { + return FTSearchResult{}, fmt.Errorf("invalid field value format") + } + doc.Fields[key] = value + } + i++ + } + + results = append(results, doc) + } + return FTSearchResult{ + Total: int(total), + Docs: results, + }, nil +} + +type FTSearchCmd struct { + baseCmd + val FTSearchResult + options *FTSearchOptions +} + +func newFTSearchCmd(ctx context.Context, options *FTSearchOptions, args ...interface{}) *FTSearchCmd { + return &FTSearchCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + options: options, + } +} + +func (cmd *FTSearchCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *FTSearchCmd) SetVal(val FTSearchResult) { + cmd.val = val +} + +func (cmd *FTSearchCmd) Result() (FTSearchResult, error) { + return cmd.val, cmd.err +} + +func (cmd *FTSearchCmd) Val() FTSearchResult { + return cmd.val +} + +func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) { + data, err := rd.ReadSlice() + if err != nil { + cmd.err = err + return nil + } + cmd.val, err = parseFTSearch(data, cmd.options.NoContent, cmd.options.WithScores, cmd.options.WithPayloads, cmd.options.WithSortKeys) + if err != nil { + cmd.err = 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: // [FT.SEARCH]: (https://redis.io/commands/ft.search/) -func (c cmdable) FTSearch(ctx context.Context, index string, query string) *Cmd { +func (c cmdable) FTSearch(ctx context.Context, index string, query string) *FTSearchCmd { args := []interface{}{"FT.SEARCH", index, query} - cmd := NewCmd(ctx, args...) + cmd := newFTSearchCmd(ctx, &FTSearchOptions{}, args...) _ = c(ctx, cmd) return cmd } @@ -1122,7 +1737,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { // and the 'options' parameter specifies additional options for the search. // For more information, please refer to the Redis documentation: // [FT.SEARCH]: (https://redis.io/commands/ft.search/) -func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *Cmd { +func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *FTSearchCmd { args := []interface{}{"FT.SEARCH", index, query} if options != nil { if options.NoContent { @@ -1228,17 +1843,79 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin args = append(args, "DIALECT", options.DialectVersion) } } - cmd := NewCmd(ctx, args...) + cmd := newFTSearchCmd(ctx, options, args...) _ = c(ctx, cmd) return cmd } -// FTSynDump - Dumps the synonyms data structure. +func NewFTSynDumpCmd(ctx context.Context, args ...interface{}) *FTSynDumpCmd { + return &FTSynDumpCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *FTSynDumpCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *FTSynDumpCmd) SetVal(val []FTSynDumpResult) { + cmd.val = val +} + +func (cmd *FTSynDumpCmd) Val() []FTSynDumpResult { + return cmd.val +} + +func (cmd *FTSynDumpCmd) Result() ([]FTSynDumpResult, error) { + return cmd.val, cmd.err +} + +func (cmd *FTSynDumpCmd) readReply(rd *proto.Reader) error { + termSynonymPairs, err := rd.ReadSlice() + if err != nil { + return err + } + + var results []FTSynDumpResult + for i := 0; i < len(termSynonymPairs); i += 2 { + term, ok := termSynonymPairs[i].(string) + if !ok { + return fmt.Errorf("invalid term format") + } + + synonyms, ok := termSynonymPairs[i+1].([]interface{}) + if !ok { + return fmt.Errorf("invalid synonyms format") + } + + synonymList := make([]string, len(synonyms)) + for j, syn := range synonyms { + synonym, ok := syn.(string) + if !ok { + return fmt.Errorf("invalid synonym format") + } + synonymList[j] = synonym + } + + results = append(results, FTSynDumpResult{ + Term: term, + Synonyms: synonymList, + }) + } + + cmd.val = results + return nil +} + +// FTSynDump - Dumps the contents of a synonym group. // The 'index' parameter specifies the index to dump. // For more information, please refer to the Redis documentation: // [FT.SYNDUMP]: (https://redis.io/commands/ft.syndump/) -func (c cmdable) FTSynDump(ctx context.Context, index string) *MapStringSliceInterfaceCmd { - cmd := NewMapStringSliceInterfaceCmd(ctx, "FT.SYNDUMP", index) +func (c cmdable) FTSynDump(ctx context.Context, index string) *FTSynDumpCmd { + cmd := NewFTSynDumpCmd(ctx, "FT.SYNDUMP", index) _ = c(ctx, cmd) return cmd } @@ -1279,3 +1956,215 @@ func (c cmdable) FTTagVals(ctx context.Context, index string, field string) *Str _ = c(ctx, cmd) return cmd } + +// type FTProfileResult struct { +// Results []interface{} +// Profile ProfileDetails +// } + +// type ProfileDetails struct { +// TotalProfileTime string +// ParsingTime string +// PipelineCreationTime string +// Warning string +// IteratorsProfile []IteratorProfile +// ResultProcessorsProfile []ResultProcessorProfile +// } + +// type IteratorProfile struct { +// Type string +// QueryType string +// Time interface{} +// Counter int +// Term string +// Size int +// ChildIterators []IteratorProfile +// } + +// type ResultProcessorProfile struct { +// Type string +// Time interface{} +// Counter int +// } + +// func parseFTProfileResult(data []interface{}) (FTProfileResult, error) { +// var result FTProfileResult +// if len(data) < 2 { +// return result, fmt.Errorf("unexpected data length") +// } + +// // Parse results +// result.Results = data[0].([]interface{}) + +// // Parse profile details +// profileData := data[1].([]interface{}) +// profileDetails := ProfileDetails{} +// for i := 0; i < len(profileData); i += 2 { +// switch profileData[i].(string) { +// case "Total profile time": +// profileDetails.TotalProfileTime = profileData[i+1].(string) +// case "Parsing time": +// profileDetails.ParsingTime = profileData[i+1].(string) +// case "Pipeline creation time": +// profileDetails.PipelineCreationTime = profileData[i+1].(string) +// case "Warning": +// profileDetails.Warning = profileData[i+1].(string) +// case "Iterators profile": +// profileDetails.IteratorsProfile = parseIteratorsProfile(profileData[i+1].([]interface{})) +// case "Result processors profile": +// profileDetails.ResultProcessorsProfile = parseResultProcessorsProfile(profileData[i+1].([]interface{})) +// } +// } + +// result.Profile = profileDetails +// return result, nil +// } + +// func parseIteratorsProfile(data []interface{}) []IteratorProfile { +// var iterators []IteratorProfile +// for _, item := range data { +// profile := item.([]interface{}) +// iterator := IteratorProfile{} +// for i := 0; i < len(profile); i += 2 { +// switch profile[i].(string) { +// case "Type": +// iterator.Type = profile[i+1].(string) +// case "Query type": +// iterator.QueryType = profile[i+1].(string) +// case "Time": +// iterator.Time = profile[i+1] +// case "Counter": +// iterator.Counter = int(profile[i+1].(int64)) +// case "Term": +// iterator.Term = profile[i+1].(string) +// case "Size": +// iterator.Size = int(profile[i+1].(int64)) +// case "Child iterators": +// iterator.ChildIterators = parseChildIteratorsProfile(profile[i+1].([]interface{})) +// } +// } +// iterators = append(iterators, iterator) +// } +// return iterators +// } + +// func parseChildIteratorsProfile(data []interface{}) []IteratorProfile { +// var iterators []IteratorProfile +// for _, item := range data { +// profile := item.([]interface{}) +// iterator := IteratorProfile{} +// for i := 0; i < len(profile); i += 2 { +// switch profile[i].(string) { +// case "Type": +// iterator.Type = profile[i+1].(string) +// case "Query type": +// iterator.QueryType = profile[i+1].(string) +// case "Time": +// iterator.Time = profile[i+1] +// case "Counter": +// iterator.Counter = int(profile[i+1].(int64)) +// case "Term": +// iterator.Term = profile[i+1].(string) +// case "Size": +// iterator.Size = int(profile[i+1].(int64)) +// } +// } +// iterators = append(iterators, iterator) +// } +// return iterators +// } + +// func parseResultProcessorsProfile(data []interface{}) []ResultProcessorProfile { +// var processors []ResultProcessorProfile +// for _, item := range data { +// profile := item.([]interface{}) +// processor := ResultProcessorProfile{} +// for i := 0; i < len(profile); i += 2 { +// switch profile[i].(string) { +// case "Type": +// processor.Type = profile[i+1].(string) +// case "Time": +// processor.Time = profile[i+1] +// case "Counter": +// processor.Counter = int(profile[i+1].(int64)) +// } +// } +// processors = append(processors, processor) +// } +// return processors +// } + +// func NewFTProfileCmd(ctx context.Context, args ...interface{}) *FTProfileCmd { +// return &FTProfileCmd{ +// baseCmd: baseCmd{ +// ctx: ctx, +// args: args, +// }, +// } +// } + +// type FTProfileCmd struct { +// baseCmd +// val FTProfileResult +// } + +// func (cmd *FTProfileCmd) String() string { +// return cmdString(cmd, cmd.val) +// } + +// func (cmd *FTProfileCmd) SetVal(val FTProfileResult) { +// cmd.val = val +// } + +// func (cmd *FTProfileCmd) Result() (FTProfileResult, error) { +// return cmd.val, cmd.err +// } + +// func (cmd *FTProfileCmd) Val() FTProfileResult { +// return cmd.val +// } + +// func (cmd *FTProfileCmd) readReply(rd *proto.Reader) (err error) { +// data, err := rd.ReadSlice() +// if err != nil { +// return err +// } +// cmd.val, err = parseFTProfileResult(data) +// if err != nil { +// cmd.err = err +// } +// return nil +// } + +// // FTProfile - Executes a search query and returns a profile of how the query was processed. +// // The 'index' parameter specifies the index to search, the 'limited' parameter specifies whether to limit the results, +// // and the 'query' parameter specifies the search / aggreagte query. Please notice that you must either pass a SearchQuery or an AggregateQuery. +// // For more information, please refer to the Redis documentation: +// // [FT.PROFILE]: (https://redis.io/commands/ft.profile/) +// func (c cmdable) FTProfile(ctx context.Context, index string, limited bool, query interface{}) *FTProfileCmd { +// queryType := "" +// var argsQuery []interface{} + +// switch v := query.(type) { +// case AggregateQuery: +// queryType = "AGGREGATE" +// argsQuery = v +// case SearchQuery: +// queryType = "SEARCH" +// argsQuery = v +// default: +// panic("FT.PROFILE: query must be either AggregateQuery or SearchQuery") +// } + +// args := []interface{}{"FT.PROFILE", index, queryType} + +// if limited { +// args = append(args, "LIMITED") +// } +// args = append(args, "QUERY") +// args = append(args, argsQuery...) + +// cmd := NewFTProfileCmd(ctx, args...) +// _ = c(ctx, cmd) +// return cmd +// } diff --git a/search_test.go b/search_test.go index 9bf1e9a37..6a3c345bb 100644 --- a/search_test.go +++ b/search_test.go @@ -13,10 +13,12 @@ func WaitForIndexing(c *redis.Client, index string) { for { res, err := c.FTInfo(context.Background(), index).Result() Expect(err).NotTo(HaveOccurred()) - if res["indexing"].(string) == "0" { - return + if c.Options().Protocol == 2 { + if res.Indexing == 0 { + return + } + time.Sleep(100 * time.Millisecond) } - time.Sleep(100 * time.Millisecond) } } @@ -41,16 +43,16 @@ var _ = Describe("RediSearch commands", Label("search"), func() { client.HSet(ctx, "doc1", "txt", "foo baz") client.HSet(ctx, "doc2", "txt", "foo bar") res, err := client.FTSearchWithArgs(ctx, "txt", "foo ~bar", &redis.FTSearchOptions{WithScores: true}).Result() + Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(nil)) - // searchResult := res.(map[interface{}]interface{}) - // Expect(searchResult["total_results"]).To(BeEquivalentTo(int64(2))) - // Expect(searchResult["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc2")) - // Expect(searchResult["results"].([]interface{})[0].(map[interface{}]interface{})["score"]).To(BeEquivalentTo(float64(3.0))) - // Expect(searchResult["results"].([]interface{})[1].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc1")) + Expect(res.Total).To(BeEquivalentTo(int64(2))) + for _, doc := range res.Docs { + Expect(*doc.Score).To(BeNumerically(">", 0)) + Expect(doc.ID).To(Or(Equal("doc1"), Equal("doc2"))) + } }) - It("should FTCreate and FTSearch stopwords ", Label("search", "ftcreate", "ftsearch"), func() { + It("should FTCreate and FTSearch stopwords", 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")) @@ -59,16 +61,13 @@ var _ = Describe("RediSearch commands", Label("search"), func() { client.HSet(ctx, "doc2", "txt", "hello world") res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{NoContent: true}).Result() Expect(err).NotTo(HaveOccurred()) - searchResult1 := res1.(map[interface{}]interface{}) - Expect(searchResult1["total_results"]).To(BeEquivalentTo(int64(0))) + Expect(res1.Total).To(BeEquivalentTo(int64(0))) res2, err := client.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).Result() Expect(err).NotTo(HaveOccurred()) - searchResult2 := res2.(map[interface{}]interface{}) - Expect(searchResult2["total_results"]).To(BeEquivalentTo(int64(1))) - + Expect(res2.Total).To(BeEquivalentTo(int64(1))) }) - It("should FTCreate and FTSearch filters ", Label("search", "ftcreate", "ftsearch"), func() { + It("should FTCreate and FTSearch filters", Label("search", "ftcreate", "ftsearch"), func() { val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}, &redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric}, &redis.FieldSchema{FieldName: "loc", FieldType: redis.SearchFieldTypeGeo}).Result() Expect(err).NotTo(HaveOccurred()) Expect(val).To(BeEquivalentTo("OK")) @@ -77,33 +76,29 @@ var _ = Describe("RediSearch commands", Label("search"), func() { client.HSet(ctx, "doc2", "txt", "foo baz", "num", 2, "loc", "-0.1,51.2") res1, err := client.FTSearchWithArgs(ctx, "txt", "foo", &redis.FTSearchOptions{Filters: []redis.FTSearchFilter{{FieldName: "num", Min: 0, Max: 2}}, NoContent: true}).Result() Expect(err).NotTo(HaveOccurred()) - searchResult1 := res1.(map[interface{}]interface{}) - Expect(searchResult1["total_results"]).To(BeEquivalentTo(int64(1))) - Expect(searchResult1["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc2")) + Expect(res1.Total).To(BeEquivalentTo(int64(1))) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc2")) res2, err := client.FTSearchWithArgs(ctx, "txt", "foo", &redis.FTSearchOptions{Filters: []redis.FTSearchFilter{{FieldName: "num", Min: 0, Max: "+inf"}}, NoContent: true}).Result() Expect(err).NotTo(HaveOccurred()) - searchResult2 := res2.(map[interface{}]interface{}) - Expect(searchResult2["total_results"]).To(BeEquivalentTo(int64(2))) - Expect(searchResult2["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc1")) + Expect(res2.Total).To(BeEquivalentTo(int64(2))) + Expect(res2.Docs[0].ID).To(BeEquivalentTo("doc1")) // Test Geo filter geoFilter1 := redis.FTSearchGeoFilter{FieldName: "loc", Longitude: -0.44, Latitude: 51.45, Radius: 10, Unit: "km"} geoFilter2 := redis.FTSearchGeoFilter{FieldName: "loc", Longitude: -0.44, Latitude: 51.45, Radius: 100, Unit: "km"} res3, err := client.FTSearchWithArgs(ctx, "txt", "foo", &redis.FTSearchOptions{GeoFilter: []redis.FTSearchGeoFilter{geoFilter1}, NoContent: true}).Result() Expect(err).NotTo(HaveOccurred()) - searchResult3 := res3.(map[interface{}]interface{}) - Expect(searchResult3["total_results"]).To(BeEquivalentTo(int64(1))) - Expect(searchResult3["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc1")) + Expect(res3.Total).To(BeEquivalentTo(int64(1))) + Expect(res3.Docs[0].ID).To(BeEquivalentTo("doc1")) res4, err := client.FTSearchWithArgs(ctx, "txt", "foo", &redis.FTSearchOptions{GeoFilter: []redis.FTSearchGeoFilter{geoFilter2}, NoContent: true}).Result() Expect(err).NotTo(HaveOccurred()) - searchResult4 := res4.(map[interface{}]interface{}) - Expect(searchResult4["total_results"]).To(BeEquivalentTo(int64(2))) - docs := []interface{}{searchResult4["results"].([]interface{})[0].(map[interface{}]interface{})["id"], searchResult4["results"].([]interface{})[1].(map[interface{}]interface{})["id"]} + Expect(res4.Total).To(BeEquivalentTo(int64(2))) + docs := []interface{}{res4.Docs[0].ID, res4.Docs[1].ID} Expect(docs).To(ContainElement("doc1")) Expect(docs).To(ContainElement("doc2")) }) - It("should FTCreate and FTSearch sortby ", Label("search", "ftcreate", "ftsearch"), func() { + It("should FTCreate and FTSearch sortby", Label("search", "ftcreate", "ftsearch"), func() { val, err := client.FTCreate(ctx, "num", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}, &redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}).Result() Expect(err).NotTo(HaveOccurred()) Expect(val).To(BeEquivalentTo("OK")) @@ -116,23 +111,21 @@ var _ = Describe("RediSearch commands", Label("search"), func() { sortBy2 := redis.FTSearchSortBy{FieldName: "num", Desc: true} res1, err := client.FTSearchWithArgs(ctx, "num", "foo", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy1}}).Result() Expect(err).NotTo(HaveOccurred()) - searchResult1 := res1.(map[interface{}]interface{}) - Expect(searchResult1["total_results"]).To(BeEquivalentTo(int64(3))) - Expect(searchResult1["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc1")) - Expect(searchResult1["results"].([]interface{})[1].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc2")) - Expect(searchResult1["results"].([]interface{})[2].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc3")) + Expect(res1.Total).To(BeEquivalentTo(int64(3))) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc1")) + Expect(res1.Docs[1].ID).To(BeEquivalentTo("doc2")) + Expect(res1.Docs[2].ID).To(BeEquivalentTo("doc3")) res2, err := client.FTSearchWithArgs(ctx, "num", "foo", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}}).Result() Expect(err).NotTo(HaveOccurred()) - searchResult2 := res2.(map[interface{}]interface{}) - Expect(searchResult2["total_results"]).To(BeEquivalentTo(int64(3))) - Expect(searchResult2["results"].([]interface{})[2].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc1")) - Expect(searchResult2["results"].([]interface{})[1].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc2")) - Expect(searchResult2["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc3")) + Expect(res2.Total).To(BeEquivalentTo(int64(3))) + Expect(res2.Docs[2].ID).To(BeEquivalentTo("doc1")) + Expect(res2.Docs[1].ID).To(BeEquivalentTo("doc2")) + Expect(res2.Docs[0].ID).To(BeEquivalentTo("doc3")) }) - It("should FTCreate and FTSearch example ", Label("search", "ftcreate", "ftsearch"), func() { + It("should FTCreate and FTSearch example", Label("search", "ftcreate", "ftsearch"), func() { val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Weight: 5}, &redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}).Result() Expect(err).NotTo(HaveOccurred()) Expect(val).To(BeEquivalentTo("OK")) @@ -140,12 +133,11 @@ var _ = Describe("RediSearch commands", Label("search"), func() { client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch impements 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()) - searchResult1 := res1.(map[interface{}]interface{}) - Expect(searchResult1).ToNot(BeEmpty()) + Expect(res1.Total).To(BeEquivalentTo(int64(1))) }) - It("should FTCreate NoIndex ", Label("search", "ftcreate", "ftsearch"), func() { + It("should FTCreate NoIndex", Label("search", "ftcreate", "ftsearch"), func() { text1 := &redis.FieldSchema{FieldName: "field", FieldType: redis.SearchFieldTypeText} text2 := &redis.FieldSchema{FieldName: "text", FieldType: redis.SearchFieldTypeText, NoIndex: true, Sortable: true} num := &redis.FieldSchema{FieldName: "numeric", FieldType: redis.SearchFieldTypeNumeric, NoIndex: true, Sortable: true} @@ -159,31 +151,31 @@ var _ = Describe("RediSearch commands", Label("search"), func() { client.HSet(ctx, "doc2", "field", "aab", "text", "2", "numeric", 2, "geo", "2,2", "tag", "2") res1, err := client.FTSearch(ctx, "idx", "@text:aa*").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res1.(map[interface{}]interface{})["total_results"]).To(BeEquivalentTo(int64(0))) + Expect(res1.Total).To(BeEquivalentTo(int64(0))) res2, err := client.FTSearch(ctx, "idx", "@field:aa*").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res2.(map[interface{}]interface{})["total_results"]).To(BeEquivalentTo(int64(2))) + Expect(res2.Total).To(BeEquivalentTo(int64(2))) res3, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "text", Desc: true}}}).Result() Expect(err).NotTo(HaveOccurred()) - Expect(res3.(map[interface{}]interface{})["total_results"]).To(BeEquivalentTo(int64(2))) - Expect(res3.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc2")) + Expect(res3.Total).To(BeEquivalentTo(int64(2))) + Expect(res3.Docs[0].ID).To(BeEquivalentTo("doc2")) res4, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "text", Asc: true}}}).Result() Expect(err).NotTo(HaveOccurred()) - Expect(res4.(map[interface{}]interface{})["total_results"]).To(BeEquivalentTo(int64(2))) - Expect(res4.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc1")) + Expect(res4.Total).To(BeEquivalentTo(int64(2))) + Expect(res4.Docs[0].ID).To(BeEquivalentTo("doc1")) res5, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "numeric", Asc: true}}}).Result() Expect(err).NotTo(HaveOccurred()) - Expect(res5.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc1")) + Expect(res5.Docs[0].ID).To(BeEquivalentTo("doc1")) res6, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "geo", Asc: true}}}).Result() Expect(err).NotTo(HaveOccurred()) - Expect(res6.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc1")) + Expect(res6.Docs[0].ID).To(BeEquivalentTo("doc1")) res7, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "tag", Asc: true}}}).Result() Expect(err).NotTo(HaveOccurred()) - Expect(res7.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc1")) + Expect(res7.Docs[0].ID).To(BeEquivalentTo("doc1")) }) - It("should FTExplain ", Label("search", "ftexplain"), func() { + It("should 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} @@ -197,7 +189,7 @@ var _ = Describe("RediSearch commands", Label("search"), func() { }) - It("should FTAlias ", Label("search", "ftexplain"), func() { + It("should FTAlias", Label("search", "ftexplain"), func() { text1 := &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText} text2 := &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText} val1, err := client.FTCreate(ctx, "testAlias", &redis.FTCreateOptions{Prefix: []interface{}{"index1:"}}, text1).Result() @@ -214,7 +206,7 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res1, err := client.FTSearch(ctx, "testAlias", "*").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res1.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("index1:lonestar")) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("index1:lonestar")) aliasAddRes, err := client.FTAliasAdd(ctx, "testAlias", "mj23").Result() Expect(err).NotTo(HaveOccurred()) @@ -222,7 +214,7 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res1, err = client.FTSearch(ctx, "mj23", "*").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res1.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("index1:lonestar")) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("index1:lonestar")) aliasUpdateRes, err := client.FTAliasUpdate(ctx, "testAlias2", "kb24").Result() Expect(err).NotTo(HaveOccurred()) @@ -230,7 +222,7 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res3, err := client.FTSearch(ctx, "kb24", "*").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res3.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("index2:yogurt")) + Expect(res3.Docs[0].ID).To(BeEquivalentTo("index2:yogurt")) aliasDelRes, err := client.FTAliasDel(ctx, "mj23").Result() Expect(err).NotTo(HaveOccurred()) @@ -246,11 +238,12 @@ var _ = Describe("RediSearch commands", Label("search"), func() { resInfo, err := client.FTInfo(ctx, "idx1").Result() Expect(err).NotTo(HaveOccurred()) - Expect(resInfo["attributes"].([]interface{})[0].(map[interface{}]interface{})["flags"]).To(ContainElements("SORTABLE", "NOSTEM")) + Expect(resInfo.Attributes[0].Sortable).To(BeTrue()) + Expect(resInfo.Attributes[0].NoStem).To(BeTrue()) }) - It("should FTAlter ", Label("search", "ftcreate", "ftsearch", "ftalter"), func() { + It("should FTAlter", Label("search", "ftcreate", "ftsearch", "ftalter"), func() { val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result() Expect(err).NotTo(HaveOccurred()) Expect(val).To(BeEquivalentTo("OK")) @@ -263,7 +256,7 @@ var _ = Describe("RediSearch commands", Label("search"), func() { client.HSet(ctx, "doc1", "title", "MyTitle", "body", "Some content only in the body") res1, err := client.FTSearch(ctx, "idx1", "only in the body").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res1.(map[interface{}]interface{})["total_results"]).To(BeEquivalentTo(int64(1))) + Expect(res1.Total).To(BeEquivalentTo(int64(1))) }) @@ -280,44 +273,41 @@ var _ = Describe("RediSearch commands", Label("search"), func() { resSpellCheck, err := client.FTSpellCheck(ctx, "idx1", "impornant").Result() Expect(err).NotTo(HaveOccurred()) - res := resSpellCheck["results"].(map[interface{}]interface{})["impornant"].([]interface{})[0].(map[interface{}]interface{}) - Expect("important").To(BeKeyOf(res)) + Expect(resSpellCheck[0].Suggestions[0].Suggestion).To(BeEquivalentTo("important")) resSpellCheck2, err := client.FTSpellCheck(ctx, "idx1", "contnt").Result() Expect(err).NotTo(HaveOccurred()) - res2 := resSpellCheck2["results"].(map[interface{}]interface{})["contnt"].([]interface{})[0].(map[interface{}]interface{}) - Expect("content").To(BeKeyOf(res2)) + Expect(resSpellCheck2[0].Suggestions[0].Suggestion).To(BeEquivalentTo("content")) // test spellcheck with Levenshtein distance resSpellCheck3, err := client.FTSpellCheck(ctx, "idx1", "vlis").Result() Expect(err).NotTo(HaveOccurred()) - Expect(resSpellCheck3["results"]).To(BeEquivalentTo(map[interface{}]interface{}{"vlis": []interface{}{}})) + Expect(resSpellCheck3[0].Term).To(BeEquivalentTo("vlis")) resSpellCheck4, err := client.FTSpellCheckWithArgs(ctx, "idx1", "vlis", &redis.FTSpellCheckOptions{Distance: 2}).Result() Expect(err).NotTo(HaveOccurred()) - Expect("valid").To(BeKeyOf(resSpellCheck4["results"].(map[interface{}]interface{})["vlis"].([]interface{})[0].(map[interface{}]interface{}))) + Expect(resSpellCheck4[0].Suggestions[0].Suggestion).To(BeEquivalentTo("valid")) // test spellcheck include - dict := []interface{}{"lore", "lorem", "lorm"} - resDictAdd, err := client.FTDictAdd(ctx, "dict", dict).Result() + resDictAdd, err := client.FTDictAdd(ctx, "dict", "lore", "lorem", "lorm").Result() Expect(err).NotTo(HaveOccurred()) Expect(resDictAdd).To(BeEquivalentTo(3)) - terms := redis.SpellCheckTerms{Include: true, Dictionary: "dict"} + terms := &redis.FTSpellCheckTerms{Inclusion: "INCLUDE", Dictionary: "dict"} resSpellCheck5, err := client.FTSpellCheckWithArgs(ctx, "idx1", "lorm", &redis.FTSpellCheckOptions{Terms: terms}).Result() Expect(err).NotTo(HaveOccurred()) - lorm := resSpellCheck5["results"].(map[interface{}]interface{})["lorm"].([]interface{}) + lorm := resSpellCheck5[0].Suggestions Expect(len(lorm)).To(BeEquivalentTo(3)) - Expect(lorm[0].(map[interface{}]interface{})["lorem"]).To(BeEquivalentTo(0.5)) - Expect(lorm[1].(map[interface{}]interface{})["lore"]).To(BeEquivalentTo(0)) - Expect(lorm[2].(map[interface{}]interface{})["lorm"]).To(BeEquivalentTo(0)) + Expect(lorm[0].Score).To(BeEquivalentTo(0.5)) + Expect(lorm[1].Score).To(BeEquivalentTo(0)) + Expect(lorm[2].Score).To(BeEquivalentTo(0)) - terms2 := redis.SpellCheckTerms{Exclude: true, Dictionary: "dict"} + terms2 := &redis.FTSpellCheckTerms{Inclusion: "EXCLUDE", Dictionary: "dict"} resSpellCheck6, err := client.FTSpellCheckWithArgs(ctx, "idx1", "lorm", &redis.FTSpellCheckOptions{Terms: terms2}).Result() Expect(err).NotTo(HaveOccurred()) - Expect(resSpellCheck6["results"]).To(BeEmpty()) + Expect(resSpellCheck6).To(BeEmpty()) }) - It("should FTDict opreations ", Label("search", "ftdictdump", "ftdictdel", "ftdictadd"), func() { + It("should FTDict opreations", Label("search", "ftdictdump", "ftdictdel", "ftdictadd"), 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() @@ -325,12 +315,11 @@ var _ = Describe("RediSearch commands", Label("search"), func() { Expect(val).To(BeEquivalentTo("OK")) WaitForIndexing(client, "idx1") - dict := []interface{}{"item1", "item2", "item3"} - resDictAdd, err := client.FTDictAdd(ctx, "custom_dict", dict).Result() + resDictAdd, err := client.FTDictAdd(ctx, "custom_dict", "item1", "item2", "item3").Result() Expect(err).NotTo(HaveOccurred()) Expect(resDictAdd).To(BeEquivalentTo(3)) - resDictDel, err := client.FTDictDel(ctx, "custom_dict", []interface{}{"item2"}).Result() + resDictDel, err := client.FTDictDel(ctx, "custom_dict", "item2").Result() Expect(err).NotTo(HaveOccurred()) Expect(resDictDel).To(BeEquivalentTo(1)) @@ -338,12 +327,12 @@ var _ = Describe("RediSearch commands", Label("search"), func() { Expect(err).NotTo(HaveOccurred()) Expect(resDictDump).To(BeEquivalentTo([]string{"item1", "item3"})) - resDictDel2, err := client.FTDictDel(ctx, "custom_dict", []interface{}{"item1", "item3"}).Result() + resDictDel2, err := client.FTDictDel(ctx, "custom_dict", "item1", "item3").Result() Expect(err).NotTo(HaveOccurred()) Expect(resDictDel2).To(BeEquivalentTo(2)) }) - It("should FTSearch phonetic matcher ", Label("search", "ftsearch"), func() { + It("should FTSearch phonetic matcher", Label("search", "ftsearch"), func() { text1 := &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() Expect(err).NotTo(HaveOccurred()) @@ -355,9 +344,8 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res1, err := client.FTSearch(ctx, "idx1", "Jon").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res1.(map[interface{}]interface{})["total_results"]).To(BeEquivalentTo(int64(1))) - name := res1.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["name"] - Expect(name).To(BeEquivalentTo("Jon")) + Expect(res1.Total).To(BeEquivalentTo(int64(1))) + Expect(res1.Docs[0].Fields["name"]).To(BeEquivalentTo("Jon")) client.FlushDB(ctx) text2 := &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText, PhoneticMatcher: "dm:en"} @@ -371,11 +359,8 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res2, err := client.FTSearch(ctx, "idx1", "Jon").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res2.(map[interface{}]interface{})["total_results"]).To(BeEquivalentTo(int64(2))) - results2 := res2.(map[interface{}]interface{})["results"].([]interface{}) - n1 := results2[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["name"] - n2 := results2[1].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["name"] - names := []interface{}{n1, n2} + Expect(res2.Total).To(BeEquivalentTo(int64(2))) + names := []interface{}{res2.Docs[0].Fields["name"], res2.Docs[1].Fields["name"]} Expect(names).To(ContainElement("Jon")) Expect(names).To(ContainElement("John")) }) @@ -392,38 +377,31 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res, err := client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true}).Result() Expect(err).NotTo(HaveOccurred()) - result := res.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["score"] - Expect(result).To(BeEquivalentTo(1)) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1))) res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "TFIDF"}).Result() Expect(err).NotTo(HaveOccurred()) - result = res.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["score"] - Expect(result).To(BeEquivalentTo(1)) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1))) res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "TFIDF.DOCNORM"}).Result() Expect(err).NotTo(HaveOccurred()) - result = res.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["score"] - Expect(result).To(BeEquivalentTo(0.14285714285714285)) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(0.14285714285714285)) res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "BM25"}).Result() Expect(err).NotTo(HaveOccurred()) - result = res.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["score"] - Expect(result).To(BeEquivalentTo(0.22471909420069797)) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(0.22471909420069797)) res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "DISMAX"}).Result() Expect(err).NotTo(HaveOccurred()) - result = res.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["score"] - Expect(result).To(BeEquivalentTo(2)) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(2))) res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "DOCSCORE"}).Result() Expect(err).NotTo(HaveOccurred()) - result = res.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["score"] - Expect(result).To(BeEquivalentTo(1)) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1))) res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "HAMMING"}).Result() Expect(err).NotTo(HaveOccurred()) - result = res.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{})["score"] - Expect(result).To(BeEquivalentTo(0)) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(0))) }) It("should FTConfigSet and FTConfigGet ", Label("search", "ftconfigget", "ftconfigset"), func() { @@ -468,94 +446,86 @@ var _ = Describe("RediSearch commands", Label("search"), func() { options := &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} res, err := client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr := res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr["parent"]).To(BeEquivalentTo("redis")) - Expect(extraAttr["__generated_aliascount"]).To(BeEquivalentTo("3")) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliascount"]).To(BeEquivalentTo("3")) reducer = redis.FTAggregateReducer{Reducer: redis.SearchCountDistinct, Args: []interface{}{"@title"}} options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr["parent"]).To(BeEquivalentTo("redis")) - Expect(extraAttr["__generated_aliascount_distincttitle"]).To(BeEquivalentTo("3")) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliascount_distincttitle"]).To(BeEquivalentTo("3")) reducer = redis.FTAggregateReducer{Reducer: redis.SearchSum, Args: []interface{}{"@random_num"}} options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr["parent"]).To(BeEquivalentTo("redis")) - Expect(extraAttr["__generated_aliassumrandom_num"]).To(BeEquivalentTo("21")) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliassumrandom_num"]).To(BeEquivalentTo("21")) reducer = redis.FTAggregateReducer{Reducer: redis.SearchMin, Args: []interface{}{"@random_num"}} options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr["parent"]).To(BeEquivalentTo("redis")) - Expect(extraAttr["__generated_aliasminrandom_num"]).To(BeEquivalentTo("3")) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliasminrandom_num"]).To(BeEquivalentTo("3")) reducer = redis.FTAggregateReducer{Reducer: redis.SearchMax, Args: []interface{}{"@random_num"}} options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr["parent"]).To(BeEquivalentTo("redis")) - Expect(extraAttr["__generated_aliasmaxrandom_num"]).To(BeEquivalentTo("10")) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliasmaxrandom_num"]).To(BeEquivalentTo("10")) reducer = redis.FTAggregateReducer{Reducer: redis.SearchAvg, Args: []interface{}{"@random_num"}} options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr["parent"]).To(BeEquivalentTo("redis")) - Expect(extraAttr["__generated_aliasavgrandom_num"]).To(BeEquivalentTo("7")) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliasavgrandom_num"]).To(BeEquivalentTo("7")) reducer = redis.FTAggregateReducer{Reducer: redis.SearchStdDev, Args: []interface{}{"@random_num"}} options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr["parent"]).To(BeEquivalentTo("redis")) - Expect(extraAttr["__generated_aliasstddevrandom_num"]).To(BeEquivalentTo("3.60555127546")) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliasstddevrandom_num"]).To(BeEquivalentTo("3.60555127546")) reducer = redis.FTAggregateReducer{Reducer: redis.SearchQuantile, Args: []interface{}{"@random_num", 0.5}} options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr["parent"]).To(BeEquivalentTo("redis")) - Expect(extraAttr["__generated_aliasquantilerandom_num,0.5"]).To(BeEquivalentTo("8")) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliasquantilerandom_num,0.5"]).To(BeEquivalentTo("8")) reducer = redis.FTAggregateReducer{Reducer: redis.SearchToList, Args: []interface{}{"@title"}} options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr["parent"]).To(BeEquivalentTo("redis")) - Expect(extraAttr["__generated_aliastolisttitle"].([]interface{})).To(ContainElements("RediSearch", "RedisAI", "RedisJson")) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliastolisttitle"]).To(ContainElements("RediSearch", "RedisAI", "RedisJson")) reducer = redis.FTAggregateReducer{Reducer: redis.SearchFirstValue, Args: []interface{}{"@title"}, As: "first"} options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr["parent"]).To(BeEquivalentTo("redis")) - Expect(extraAttr["first"]).To(BeEquivalentTo("RediSearch")) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["first"]).To(BeEquivalentTo("RediSearch")) reducer = redis.FTAggregateReducer{Reducer: redis.SearchRandomSample, Args: []interface{}{"@title", 2}, As: "random"} options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr["parent"]).To(BeEquivalentTo("redis")) - Expect(len(extraAttr["random"].([]interface{}))).To(BeEquivalentTo(2)) - Expect(extraAttr["random"].([]interface{})[0]).To(BeElementOf([]string{"RediSearch", "RedisAI", "RedisJson"})) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["random"]).To(Or( + ContainElement("RediSearch"), + ContainElement("RedisAI"), + ContainElement("RedisJson"), + )) }) - It("should FTAggregate sort and limit ", Label("search", "ftaggregate"), func() { + It("should FTAggregate sort and limit", Label("search", "ftaggregate"), func() { text1 := &redis.FieldSchema{FieldName: "t1", FieldType: redis.SearchFieldTypeText} text2 := &redis.FieldSchema{FieldName: "t2", FieldType: redis.SearchFieldTypeText} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result() @@ -569,32 +539,26 @@ var _ = Describe("RediSearch commands", Label("search"), func() { options := &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t2", Asc: true}, {FieldName: "@t1", Desc: true}}} res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr0 := res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - extraAttr1 := res["results"].([]interface{})[1].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr0).To(BeEquivalentTo(map[interface{}]interface{}{"t1": "b", "t2": "a"})) - Expect(extraAttr1).To(BeEquivalentTo(map[interface{}]interface{}{"t1": "a", "t2": "b"})) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("b")) + Expect(res.Rows[1].Fields["t1"]).To(BeEquivalentTo("a")) + Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("a")) + Expect(res.Rows[1].Fields["t2"]).To(BeEquivalentTo("b")) options = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t1"}}} res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr0 = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - extraAttr1 = res["results"].([]interface{})[1].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr0).To(BeEquivalentTo(map[interface{}]interface{}{"t1": "a"})) - Expect(extraAttr1).To(BeEquivalentTo(map[interface{}]interface{}{"t1": "b"})) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("a")) + Expect(res.Rows[1].Fields["t1"]).To(BeEquivalentTo("b")) options = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t1"}}, SortByMax: 1} res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() Expect(err).NotTo(HaveOccurred()) - results := res["results"].([]interface{}) - Expect(len(results)).To(BeEquivalentTo(1)) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("a")) options = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t1"}}, Limit: 1, LimitOffset: 1} res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() Expect(err).NotTo(HaveOccurred()) - results = res["results"].([]interface{}) - extraAttr0 = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(len(results)).To(BeEquivalentTo(1)) - Expect(extraAttr0).To(BeEquivalentTo(map[interface{}]interface{}{"t1": "b"})) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("b")) }) It("should FTAggregate load ", Label("search", "ftaggregate"), func() { @@ -610,20 +574,18 @@ var _ = Describe("RediSearch commands", Label("search"), func() { options := &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t1"}}} res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr0 := res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr0).To(BeEquivalentTo(map[interface{}]interface{}{"t1": "hello"})) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello")) options = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t2"}}} res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr0 = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr0).To(BeEquivalentTo(map[interface{}]interface{}{"t2": "world"})) + Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world")) options = &redis.FTAggregateOptions{LoadAll: true} res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr0 = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr0).To(BeEquivalentTo(map[interface{}]interface{}{"t2": "world", "t1": "hello"})) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello")) + Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world")) }) It("should FTAggregate apply", Label("search", "ftaggregate"), func() { @@ -640,9 +602,8 @@ var _ = Describe("RediSearch commands", Label("search"), func() { options := &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}} res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() Expect(err).NotTo(HaveOccurred()) - extraAttr0 := res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - extraAttr1 := res["results"].([]interface{})[1].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect([]interface{}{extraAttr0["CreatedDateTimeUTC"], extraAttr1["CreatedDateTimeUTC"]}).To(ContainElements("6373878785249699840", "6373878758592700416")) + Expect(res.Rows[0].Fields["CreatedDateTimeUTC"]).To(BeEquivalentTo("6373878785249699840")) + Expect(res.Rows[1].Fields["CreatedDateTimeUTC"]).To(BeEquivalentTo("6373878758592700416")) }) @@ -661,18 +622,15 @@ var _ = Describe("RediSearch commands", Label("search"), func() { options := &redis.FTAggregateOptions{Filter: "@name=='foo' && @age < 20", DialectVersion: dlc} res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() Expect(err).NotTo(HaveOccurred()) - Expect(len(res["results"].([]interface{}))).To(BeEquivalentTo(1)) - extraAttr0 := res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr0).To(BeEquivalentTo(map[interface{}]interface{}{"name": "foo", "age": "19"})) + Expect(res.Total).To(BeEquivalentTo(2)) + Expect(res.Rows[0].Fields["name"]).To(BeEquivalentTo("foo")) options = &redis.FTAggregateOptions{Filter: "@age > 15", DialectVersion: dlc, SortBy: []redis.FTAggregateSortBy{{FieldName: "@age"}}} res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() Expect(err).NotTo(HaveOccurred()) - Expect(len(res["results"].([]interface{}))).To(BeEquivalentTo(2)) - extraAttr0 = res["results"].([]interface{})[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - extraAttr1 := res["results"].([]interface{})[1].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{}) - Expect(extraAttr0).To(BeEquivalentTo(map[interface{}]interface{}{"age": "19"})) - Expect(extraAttr1).To(BeEquivalentTo(map[interface{}]interface{}{"age": "25"})) + Expect(res.Total).To(BeEquivalentTo(2)) + Expect(res.Rows[0].Fields["age"]).To(BeEquivalentTo("19")) + Expect(res.Rows[1].Fields["age"]).To(BeEquivalentTo("25")) } }) @@ -688,7 +646,7 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res, err := client.FTSearch(ctx, "idx1", "@foo:bar").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res.(map[interface{}]interface{})["total_results"]).To(BeEquivalentTo(int64(0))) + Expect(res.Total).To(BeEquivalentTo(int64(0))) }) It("should FTCreate json", Label("search", "ftcreate"), func() { @@ -704,11 +662,9 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res, err := client.FTSearch(ctx, "idx1", "henry").Result() Expect(err).NotTo(HaveOccurred()) - totalResults := res.(map[interface{}]interface{})["total_results"] - Expect(totalResults).To(BeEquivalentTo(1)) - results0 := res.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{}) - Expect(results0["id"]).To(BeEquivalentTo("king:1")) - Expect(results0["extra_attributes"].(map[interface{}]interface{})["$"]).To(BeEquivalentTo(`{"name":"henry"}`)) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("king:1")) + Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"name":"henry"}`)) }) It("should FTCreate json fields as names", Label("search", "ftcreate"), func() { @@ -724,12 +680,10 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res, err := client.FTSearchWithArgs(ctx, "idx1", "Jon", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "name"}, {FieldName: "just_a_number"}}}).Result() Expect(err).NotTo(HaveOccurred()) - results := res.(map[interface{}]interface{})["results"].([]interface{}) - Expect(len(results)).To(BeEquivalentTo(1)) - results0 := results[0].(map[interface{}]interface{}) - Expect(results0["id"]).To(BeEquivalentTo("doc:1")) - Expect(results0["extra_attributes"].(map[interface{}]interface{})["name"]).To(BeEquivalentTo("Jon")) - Expect(results0["extra_attributes"].(map[interface{}]interface{})["just_a_number"]).To(BeEquivalentTo("25")) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1")) + Expect(res.Docs[0].Fields["name"]).To(BeEquivalentTo("Jon")) + Expect(res.Docs[0].Fields["just_a_number"]).To(BeEquivalentTo("25")) }) It("should FTCreate CaseSensitive", Label("search", "ftcreate"), func() { @@ -745,16 +699,13 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res, err := client.FTSearch(ctx, "idx1", "@t:{HELLO}").Result() Expect(err).NotTo(HaveOccurred()) - results := res.(map[interface{}]interface{})["results"].([]interface{}) - Expect(len(results)).To(BeEquivalentTo(2)) - results0 := results[0].(map[interface{}]interface{}) - results1 := results[1].(map[interface{}]interface{}) - Expect(results0["id"]).To(BeEquivalentTo("1")) - Expect(results1["id"]).To(BeEquivalentTo("2")) + Expect(res.Total).To(BeEquivalentTo(2)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("1")) + Expect(res.Docs[1].ID).To(BeEquivalentTo("2")) - res, err = client.FTDropIndex(ctx, "idx1").Result() + resDrop, err := client.FTDropIndex(ctx, "idx1").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo("OK")) + Expect(resDrop).To(BeEquivalentTo("OK")) tag2 := &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeTag, CaseSensitive: true} val, err = client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, tag2).Result() @@ -764,10 +715,8 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res, err = client.FTSearch(ctx, "idx1", "@t:{HELLO}").Result() Expect(err).NotTo(HaveOccurred()) - results = res.(map[interface{}]interface{})["results"].([]interface{}) - Expect(len(results)).To(BeEquivalentTo(1)) - results0 = results[0].(map[interface{}]interface{}) - Expect(results0["id"]).To(BeEquivalentTo("1")) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("1")) }) @@ -785,19 +734,15 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res, err := client.FTSearchWithArgs(ctx, "idx1", "*", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "$.t", As: "txt"}}}).Result() Expect(err).NotTo(HaveOccurred()) - results := res.(map[interface{}]interface{})["results"].([]interface{}) - Expect(len(results)).To(BeEquivalentTo(1)) - results0 := results[0].(map[interface{}]interface{}) - Expect(results0["id"]).To(BeEquivalentTo("doc:1")) - Expect(results0["extra_attributes"].(map[interface{}]interface{})["txt"]).To(BeEquivalentTo("riceratops")) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1")) + Expect(res.Docs[0].Fields["txt"]).To(BeEquivalentTo("riceratops")) res, err = client.FTSearchWithArgs(ctx, "idx1", "*", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "$.t2", As: "txt"}}}).Result() Expect(err).NotTo(HaveOccurred()) - results = res.(map[interface{}]interface{})["results"].([]interface{}) - Expect(len(results)).To(BeEquivalentTo(1)) - results0 = results[0].(map[interface{}]interface{}) - Expect(results0["id"]).To(BeEquivalentTo("doc:1")) - Expect(results0["extra_attributes"].(map[interface{}]interface{})["txt"]).To(BeEquivalentTo("telmatosaurus")) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1")) + Expect(res.Docs[0].Fields["txt"]).To(BeEquivalentTo("telmatosaurus")) }) It("should FTSynUpdate", Label("search", "ftsynupdate"), func() { @@ -821,10 +766,9 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res, err := client.FTSearchWithArgs(ctx, "idx1", "child", &redis.FTSearchOptions{Expander: "SYNONYM"}).Result() Expect(err).NotTo(HaveOccurred()) - results0 := res.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{}) - Expect(results0["id"]).To(BeEquivalentTo("doc2")) - Expect(results0["extra_attributes"].(map[interface{}]interface{})["title"]).To(BeEquivalentTo("he is another baby")) - Expect(results0["extra_attributes"].(map[interface{}]interface{})["body"]).To(BeEquivalentTo("another test")) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc2")) + Expect(res.Docs[0].Fields["title"]).To(BeEquivalentTo("he is another baby")) + Expect(res.Docs[0].Fields["body"]).To(BeEquivalentTo("another test")) }) It("should FTSynDump", Label("search", "ftsyndump"), func() { @@ -850,13 +794,19 @@ var _ = Describe("RediSearch commands", Label("search"), func() { resSynDump, err := client.FTSynDump(ctx, "idx1").Result() Expect(err).NotTo(HaveOccurred()) - Expect(resSynDump).To(BeEquivalentTo(map[string][]interface{}{ - "offspring": {"id1"}, - "baby": {"id1"}, - "wood": {"id1"}, - "boy": {"id1"}, - "tree": {"id1"}, - "child": {"id1", "id1"}})) + Expect(resSynDump[0].Term).To(BeEquivalentTo("baby")) + Expect(resSynDump[0].Synonyms).To(BeEquivalentTo([]string{"id1"})) + Expect(resSynDump[1].Term).To(BeEquivalentTo("wood")) + Expect(resSynDump[1].Synonyms).To(BeEquivalentTo([]string{"id1"})) + Expect(resSynDump[2].Term).To(BeEquivalentTo("boy")) + Expect(resSynDump[2].Synonyms).To(BeEquivalentTo([]string{"id1"})) + Expect(resSynDump[3].Term).To(BeEquivalentTo("tree")) + Expect(resSynDump[3].Synonyms).To(BeEquivalentTo([]string{"id1"})) + Expect(resSynDump[4].Term).To(BeEquivalentTo("child")) + Expect(resSynDump[4].Synonyms).To(BeEquivalentTo([]string{"id1"})) + Expect(resSynDump[5].Term).To(BeEquivalentTo("offspring")) + Expect(resSynDump[5].Synonyms).To(BeEquivalentTo([]string{"id1"})) + }) It("should FTCreate json with alias", Label("search", "ftcreate"), func() { @@ -873,19 +823,15 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res, err := client.FTSearch(ctx, "idx1", "@name:henry").Result() Expect(err).NotTo(HaveOccurred()) - totalResults := res.(map[interface{}]interface{})["total_results"] - Expect(totalResults).To(BeEquivalentTo(1)) - results0 := res.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{}) - Expect(results0["id"]).To(BeEquivalentTo("king:1")) - Expect(results0["extra_attributes"].(map[interface{}]interface{})["$"]).To(BeEquivalentTo(`{"name":"henry","num":42}`)) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("king:1")) + Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"name":"henry","num":42}`)) res, err = client.FTSearch(ctx, "idx1", "@num:[0 10]").Result() Expect(err).NotTo(HaveOccurred()) - totalResults = res.(map[interface{}]interface{})["total_results"] - Expect(totalResults).To(BeEquivalentTo(1)) - results0 = res.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{}) - Expect(results0["id"]).To(BeEquivalentTo("king:2")) - Expect(results0["extra_attributes"].(map[interface{}]interface{})["$"]).To(BeEquivalentTo(`{"name":"james","num":3.14}`)) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("king:2")) + Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"name":"james","num":3.14}`)) }) It("should FTCreate json with multipath", Label("search", "ftcreate"), func() { @@ -900,11 +846,9 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res, err := client.FTSearch(ctx, "idx1", "@name:{england}").Result() Expect(err).NotTo(HaveOccurred()) - totalResults := res.(map[interface{}]interface{})["total_results"] - Expect(totalResults).To(BeEquivalentTo(1)) - results0 := res.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{}) - Expect(results0["id"]).To(BeEquivalentTo("king:1")) - Expect(results0["extra_attributes"].(map[interface{}]interface{})["$"]).To(BeEquivalentTo(`{"name":"henry","country":{"name":"england"}}`)) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("king:1")) + Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"name":"henry","country":{"name":"england"}}`)) }) It("should FTCreate json with jsonpath", Label("search", "ftcreate"), func() { @@ -920,114 +864,20 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res, err := client.FTSearch(ctx, "idx1", "@name:RediSearch").Result() Expect(err).NotTo(HaveOccurred()) - totalResults := res.(map[interface{}]interface{})["total_results"] - Expect(totalResults).To(BeEquivalentTo(1)) - results0 := res.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{}) - Expect(results0["id"]).To(BeEquivalentTo("doc:1")) - Expect(results0["extra_attributes"].(map[interface{}]interface{})["$"]).To(BeEquivalentTo(`{"prod:name":"RediSearch"}`)) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1")) + Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"prod:name":"RediSearch"}`)) res, err = client.FTSearch(ctx, "idx1", "@name_unsupported:RediSearch").Result() Expect(err).NotTo(HaveOccurred()) - totalResults = res.(map[interface{}]interface{})["total_results"] - Expect(totalResults).To(BeEquivalentTo(1)) + Expect(res.Total).To(BeEquivalentTo(1)) res, err = client.FTSearchWithArgs(ctx, "idx1", "@name:RediSearch", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "name"}}}).Result() Expect(err).NotTo(HaveOccurred()) - totalResults = res.(map[interface{}]interface{})["total_results"] - Expect(totalResults).To(BeEquivalentTo(1)) - results0 = res.(map[interface{}]interface{})["results"].([]interface{})[0].(map[interface{}]interface{}) - Expect(results0["id"]).To(BeEquivalentTo("doc:1")) - Expect(results0["extra_attributes"].(map[interface{}]interface{})["name"]).To(BeEquivalentTo("RediSearch")) - - }) - - 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()) - Expect(val).To(BeEquivalentTo("OK")) - WaitForIndexing(client, "idx1") - - client.HSet(ctx, "1", "t", "hello") - client.HSet(ctx, "2", "t", "world") - - // FTProfile Search - query := redis.FTSearchQuery("hello|world", &redis.FTSearchOptions{NoContent: true}) - res1, err := client.FTProfile(ctx, "idx1", false, query).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(res1["results"].([]interface{}))).To(BeEquivalentTo(2)) - resProfile := res1["profile"].(map[interface{}]interface{}) - Expect(resProfile["Parsing time"].(float64) < 0.5).To(BeTrue()) - iterProfile0 := resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) - Expect(iterProfile0["Counter"]).To(BeEquivalentTo(2.0)) - Expect(iterProfile0["Type"]).To(BeEquivalentTo("UNION")) - - // FTProfile Aggregate - aggQuery := redis.FTAggregateQuery("*", &redis.FTAggregateOptions{ - Load: []redis.FTAggregateLoad{{Field: "t"}}, - Apply: []redis.FTAggregateApply{{Field: "startswith(@t, 'hel')", As: "prefix"}}}) - res2, err := client.FTProfile(ctx, "idx1", false, aggQuery).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(res2["results"].([]interface{}))).To(BeEquivalentTo(2)) - resProfile = res2["profile"].(map[interface{}]interface{}) - iterProfile0 = resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) - Expect(iterProfile0["Counter"]).To(BeEquivalentTo(2)) - Expect(iterProfile0["Type"]).To(BeEquivalentTo("WILDCARD")) - }) - - It("should FTProfile Search Limited", Label("search", "ftprofile"), func() { - val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - WaitForIndexing(client, "idx1") - - client.HSet(ctx, "1", "t", "hello") - client.HSet(ctx, "2", "t", "hell") - client.HSet(ctx, "3", "t", "help") - client.HSet(ctx, "4", "t", "helowa") - - // FTProfile Search - query := redis.FTSearchQuery("%hell% hel*", &redis.FTSearchOptions{}) - res1, err := client.FTProfile(ctx, "idx1", true, query).Result() - Expect(err).NotTo(HaveOccurred()) - resProfile := res1["profile"].(map[interface{}]interface{}) - iterProfile0 := resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) - Expect(iterProfile0["Type"]).To(BeEquivalentTo("INTERSECT")) - Expect(len(res1["results"].([]interface{}))).To(BeEquivalentTo(3)) - Expect(iterProfile0["Child iterators"].([]interface{})[0].(map[interface{}]interface{})["Child iterators"]).To(BeEquivalentTo("The number of iterators in the union is 3")) - Expect(iterProfile0["Child iterators"].([]interface{})[1].(map[interface{}]interface{})["Child iterators"]).To(BeEquivalentTo("The number of iterators in the union is 4")) - }) - - It("should FTProfile Search query params", Label("search", "ftprofile"), func() { - hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"} - val, err := client.FTCreate(ctx, "idx1", - &redis.FTCreateOptions{}, - &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - WaitForIndexing(client, "idx1") - - client.HSet(ctx, "a", "v", "aaaaaaaa") - client.HSet(ctx, "b", "v", "aaaabaaa") - client.HSet(ctx, "c", "v", "aaaaabaa") + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1")) + Expect(res.Docs[0].Fields["name"]).To(BeEquivalentTo("RediSearch")) - // FTProfile Search - searchOptions := &redis.FTSearchOptions{ - Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, - SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, - DialectVersion: 2, - Params: map[string]interface{}{"vec": "aaaaaaaa"}, - } - query := redis.FTSearchQuery("*=>[KNN 2 @v $vec]", searchOptions) - res1, err := client.FTProfile(ctx, "idx1", false, query).Result() - Expect(err).NotTo(HaveOccurred()) - resProfile := res1["profile"].(map[interface{}]interface{}) - iterProfile0 := resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) - Expect(iterProfile0["Counter"]).To(BeEquivalentTo(2)) - Expect(iterProfile0["Type"]).To(BeEquivalentTo(redis.SearchFieldTypeVector.String())) - Expect(res1["total_results"]).To(BeEquivalentTo(2)) - results0 := res1["results"].([]interface{})[0].(map[interface{}]interface{}) - Expect(results0["id"]).To(BeEquivalentTo("a")) - Expect(results0["extra_attributes"].(map[interface{}]interface{})["__v_score"]).To(BeEquivalentTo("0")) }) It("should FTCreate VECTOR", Label("search", "ftcreate"), func() { @@ -1051,10 +901,8 @@ var _ = Describe("RediSearch commands", Label("search"), func() { } res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec]", searchOptions).Result() Expect(err).NotTo(HaveOccurred()) - res1 := res.(map[interface{}]interface{}) - results0 := res1["results"].([]interface{})[0].(map[interface{}]interface{}) - Expect(results0["id"]).To(BeEquivalentTo("a")) - Expect(results0["extra_attributes"].(map[interface{}]interface{})["__v_score"]).To(BeEquivalentTo("0")) + Expect(res.Docs[0].ID).To(BeEquivalentTo("a")) + Expect(res.Docs[0].Fields["__v_score"]).To(BeEquivalentTo("0")) }) It("should FTCreate and FTSearch text params", Label("search", "ftcreate", "ftsearch"), func() { @@ -1069,10 +917,9 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res1, err := client.FTSearchWithArgs(ctx, "idx1", "@name:($name1 | $name2 )", &redis.FTSearchOptions{Params: map[string]interface{}{"name1": "Alice", "name2": "Bob"}, DialectVersion: 2}).Result() Expect(err).NotTo(HaveOccurred()) - searchResult1 := res1.(map[interface{}]interface{}) - Expect(searchResult1["total_results"]).To(BeEquivalentTo(int64(2))) - Expect(searchResult1["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc1")) - Expect(searchResult1["results"].([]interface{})[1].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc2")) + Expect(res1.Total).To(BeEquivalentTo(int64(2))) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc1")) + Expect(res1.Docs[1].ID).To(BeEquivalentTo("doc2")) }) @@ -1088,10 +935,9 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res1, err := client.FTSearchWithArgs(ctx, "idx1", "@numval:[$min $max]", &redis.FTSearchOptions{Params: map[string]interface{}{"min": 101, "max": 102}, DialectVersion: 2}).Result() Expect(err).NotTo(HaveOccurred()) - searchResult1 := res1.(map[interface{}]interface{}) - Expect(searchResult1["total_results"]).To(BeEquivalentTo(int64(2))) - Expect(searchResult1["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc1")) - Expect(searchResult1["results"].([]interface{})[1].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc2")) + Expect(res1.Total).To(BeEquivalentTo(int64(2))) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc1")) + Expect(res1.Docs[1].ID).To(BeEquivalentTo("doc2")) }) @@ -1107,11 +953,10 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res1, err := client.FTSearchWithArgs(ctx, "idx1", "@g:[$lon $lat $radius $units]", &redis.FTSearchOptions{Params: map[string]interface{}{"lat": "34.95126", "lon": "29.69465", "radius": 1000, "units": "km"}, DialectVersion: 2}).Result() Expect(err).NotTo(HaveOccurred()) - searchResult1 := res1.(map[interface{}]interface{}) - Expect(searchResult1["total_results"]).To(BeEquivalentTo(int64(3))) - Expect(searchResult1["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc1")) - Expect(searchResult1["results"].([]interface{})[1].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc2")) - Expect(searchResult1["results"].([]interface{})[2].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("doc3")) + Expect(res1.Total).To(BeEquivalentTo(int64(3))) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc1")) + Expect(res1.Docs[1].ID).To(BeEquivalentTo("doc2")) + Expect(res1.Docs[2].ID).To(BeEquivalentTo("doc3")) }) @@ -1141,8 +986,7 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res, err := client.FTInfo(ctx, "idx1").Result() Expect(err).NotTo(HaveOccurred()) - resAttr0 := res["attributes"].([]interface{})[0].(map[interface{}]interface{}) - Expect(resAttr0["flags"]).To(BeEmpty()) + Expect(res.Attributes[0].Attribute).To(BeEquivalentTo("txt")) resDrop, err := client.FTDropIndex(ctx, "idx1").Result() Expect(err).NotTo(HaveOccurred()) @@ -1156,8 +1000,7 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res, err = client.FTInfo(ctx, "idx1").Result() Expect(err).NotTo(HaveOccurred()) - resAttr0 = res["attributes"].([]interface{})[0].(map[interface{}]interface{}) - Expect(resAttr0["flags"].([]interface{})).To(ContainElement("WITHSUFFIXTRIE")) + Expect(res.Attributes[0].WithSuffixtrie).To(BeTrue()) resDrop, err = client.FTDropIndex(ctx, "idx1").Result() Expect(err).NotTo(HaveOccurred()) @@ -1171,8 +1014,7 @@ var _ = Describe("RediSearch commands", Label("search"), func() { res, err = client.FTInfo(ctx, "idx1").Result() Expect(err).NotTo(HaveOccurred()) - resAttr0 = res["attributes"].([]interface{})[0].(map[interface{}]interface{}) - Expect(resAttr0["flags"].([]interface{})).To(ContainElement("WITHSUFFIXTRIE")) + Expect(res.Attributes[0].WithSuffixtrie).To(BeTrue()) }) It("should FTCreate GeoShape", Label("search", "ftcreate", "ftsearch"), func() { @@ -1190,9 +1032,8 @@ var _ = Describe("RediSearch commands", Label("search"), func() { Params: map[string]interface{}{"poly": "POLYGON((0 0, 0 150, 150 150, 150 0, 0 0))"}, }).Result() Expect(err).NotTo(HaveOccurred()) - searchResult1 := res1.(map[interface{}]interface{}) - Expect(searchResult1["total_results"]).To(BeEquivalentTo(int64(1))) - Expect(searchResult1["results"].([]interface{})[0].(map[interface{}]interface{})["id"]).To(BeEquivalentTo("small")) + Expect(res1.Total).To(BeEquivalentTo(int64(1))) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("small")) res2, err := client.FTSearchWithArgs(ctx, "idx1", "@geom:[CONTAINS $poly]", &redis.FTSearchOptions{ @@ -1200,7 +1041,96 @@ var _ = Describe("RediSearch commands", Label("search"), func() { Params: map[string]interface{}{"poly": "POLYGON((2 2, 2 50, 50 50, 50 2, 2 2))"}, }).Result() Expect(err).NotTo(HaveOccurred()) - searchResult2 := res2.(map[interface{}]interface{}) - Expect(searchResult2["total_results"]).To(BeEquivalentTo(int64(2))) + Expect(res2.Total).To(BeEquivalentTo(int64(2))) }) }) + +// 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()) +// Expect(val).To(BeEquivalentTo("OK")) +// WaitForIndexing(client, "idx1") + +// client.HSet(ctx, "1", "t", "hello") +// client.HSet(ctx, "2", "t", "world") + +// // FTProfile Search +// query := redis.FTSearchQuery("hello|world", &redis.FTSearchOptions{NoContent: true}) +// res1, err := client.FTProfile(ctx, "idx1", false, query).Result() +// Expect(err).NotTo(HaveOccurred()) +// panic(res1) +// Expect(len(res1["results"].([]interface{}))).To(BeEquivalentTo(3)) +// resProfile := res1["profile"].(map[interface{}]interface{}) +// Expect(resProfile["Parsing time"].(float64) < 0.5).To(BeTrue()) +// iterProfile0 := resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) +// Expect(iterProfile0["Counter"]).To(BeEquivalentTo(2.0)) +// Expect(iterProfile0["Type"]).To(BeEquivalentTo("UNION")) + +// // FTProfile Aggregate +// aggQuery := redis.FTAggregateQuery("*", &redis.FTAggregateOptions{ +// Load: []redis.FTAggregateLoad{{Field: "t"}}, +// Apply: []redis.FTAggregateApply{{Field: "startswith(@t, 'hel')", As: "prefix"}}}) +// res2, err := client.FTProfile(ctx, "idx1", false, aggQuery).Result() +// Expect(err).NotTo(HaveOccurred()) +// Expect(len(res2["results"].([]interface{}))).To(BeEquivalentTo(2)) +// resProfile = res2["profile"].(map[interface{}]interface{}) +// iterProfile0 = resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) +// Expect(iterProfile0["Counter"]).To(BeEquivalentTo(2)) +// Expect(iterProfile0["Type"]).To(BeEquivalentTo("WILDCARD")) +// }) + +// It("should FTProfile Search Limited", Label("search", "ftprofile"), func() { +// val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}).Result() +// Expect(err).NotTo(HaveOccurred()) +// Expect(val).To(BeEquivalentTo("OK")) +// WaitForIndexing(client, "idx1") + +// client.HSet(ctx, "1", "t", "hello") +// client.HSet(ctx, "2", "t", "hell") +// client.HSet(ctx, "3", "t", "help") +// client.HSet(ctx, "4", "t", "helowa") + +// // FTProfile Search +// query := redis.FTSearchQuery("%hell% hel*", &redis.FTSearchOptions{}) +// res1, err := client.FTProfile(ctx, "idx1", true, query).Result() +// Expect(err).NotTo(HaveOccurred()) +// resProfile := res1["profile"].(map[interface{}]interface{}) +// iterProfile0 := resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) +// Expect(iterProfile0["Type"]).To(BeEquivalentTo("INTERSECT")) +// Expect(len(res1["results"].([]interface{}))).To(BeEquivalentTo(3)) +// Expect(iterProfile0["Child iterators"].([]interface{})[0].(map[interface{}]interface{})["Child iterators"]).To(BeEquivalentTo("The number of iterators in the union is 3")) +// Expect(iterProfile0["Child iterators"].([]interface{})[1].(map[interface{}]interface{})["Child iterators"]).To(BeEquivalentTo("The number of iterators in the union is 4")) +// }) + +// It("should FTProfile Search query params", Label("search", "ftprofile"), func() { +// hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"} +// val, err := client.FTCreate(ctx, "idx1", +// &redis.FTCreateOptions{}, +// &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result() +// Expect(err).NotTo(HaveOccurred()) +// Expect(val).To(BeEquivalentTo("OK")) +// WaitForIndexing(client, "idx1") + +// client.HSet(ctx, "a", "v", "aaaaaaaa") +// client.HSet(ctx, "b", "v", "aaaabaaa") +// client.HSet(ctx, "c", "v", "aaaaabaa") + +// // FTProfile Search +// searchOptions := &redis.FTSearchOptions{ +// Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, +// SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, +// DialectVersion: 2, +// Params: map[string]interface{}{"vec": "aaaaaaaa"}, +// } +// query := redis.FTSearchQuery("*=>[KNN 2 @v $vec]", searchOptions) +// res1, err := client.FTProfile(ctx, "idx1", false, query).Result() +// Expect(err).NotTo(HaveOccurred()) +// resProfile := res1["profile"].(map[interface{}]interface{}) +// iterProfile0 := resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) +// Expect(iterProfile0["Counter"]).To(BeEquivalentTo(2)) +// Expect(iterProfile0["Type"]).To(BeEquivalentTo(redis.SearchFieldTypeVector.String())) +// Expect(res1["total_results"]).To(BeEquivalentTo(2)) +// results0 := res1["results"].([]interface{})[0].(map[interface{}]interface{}) +// Expect(results0["id"]).To(BeEquivalentTo("a")) +// Expect(results0["extra_attributes"].(map[interface{}]interface{})["__v_score"]).To(BeEquivalentTo("0")) +// })