...

Source file src/github.com/redis/go-redis/v9/search_commands.go

Documentation: github.com/redis/go-redis/v9

     1  package redis
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strconv"
     7  
     8  	"github.com/redis/go-redis/v9/internal"
     9  	"github.com/redis/go-redis/v9/internal/proto"
    10  )
    11  
    12  type SearchCmdable interface {
    13  	FT_List(ctx context.Context) *StringSliceCmd
    14  	FTAggregate(ctx context.Context, index string, query string) *MapStringInterfaceCmd
    15  	FTAggregateWithArgs(ctx context.Context, index string, query string, options *FTAggregateOptions) *AggregateCmd
    16  	FTAliasAdd(ctx context.Context, index string, alias string) *StatusCmd
    17  	FTAliasDel(ctx context.Context, alias string) *StatusCmd
    18  	FTAliasUpdate(ctx context.Context, index string, alias string) *StatusCmd
    19  	FTAlter(ctx context.Context, index string, skipInitialScan bool, definition []interface{}) *StatusCmd
    20  	FTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd
    21  	FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd
    22  	FTCreate(ctx context.Context, index string, options *FTCreateOptions, schema ...*FieldSchema) *StatusCmd
    23  	FTCursorDel(ctx context.Context, index string, cursorId int) *StatusCmd
    24  	FTCursorRead(ctx context.Context, index string, cursorId int, count int) *MapStringInterfaceCmd
    25  	FTDictAdd(ctx context.Context, dict string, term ...interface{}) *IntCmd
    26  	FTDictDel(ctx context.Context, dict string, term ...interface{}) *IntCmd
    27  	FTDictDump(ctx context.Context, dict string) *StringSliceCmd
    28  	FTDropIndex(ctx context.Context, index string) *StatusCmd
    29  	FTDropIndexWithArgs(ctx context.Context, index string, options *FTDropIndexOptions) *StatusCmd
    30  	FTExplain(ctx context.Context, index string, query string) *StringCmd
    31  	FTExplainWithArgs(ctx context.Context, index string, query string, options *FTExplainOptions) *StringCmd
    32  	FTInfo(ctx context.Context, index string) *FTInfoCmd
    33  	FTSpellCheck(ctx context.Context, index string, query string) *FTSpellCheckCmd
    34  	FTSpellCheckWithArgs(ctx context.Context, index string, query string, options *FTSpellCheckOptions) *FTSpellCheckCmd
    35  	FTSearch(ctx context.Context, index string, query string) *FTSearchCmd
    36  	FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *FTSearchCmd
    37  	FTSynDump(ctx context.Context, index string) *FTSynDumpCmd
    38  	FTSynUpdate(ctx context.Context, index string, synGroupId interface{}, terms []interface{}) *StatusCmd
    39  	FTSynUpdateWithArgs(ctx context.Context, index string, synGroupId interface{}, options *FTSynUpdateOptions, terms []interface{}) *StatusCmd
    40  	FTTagVals(ctx context.Context, index string, field string) *StringSliceCmd
    41  }
    42  
    43  type FTCreateOptions struct {
    44  	OnHash          bool
    45  	OnJSON          bool
    46  	Prefix          []interface{}
    47  	Filter          string
    48  	DefaultLanguage string
    49  	LanguageField   string
    50  	Score           float64
    51  	ScoreField      string
    52  	PayloadField    string
    53  	MaxTextFields   int
    54  	NoOffsets       bool
    55  	Temporary       int
    56  	NoHL            bool
    57  	NoFields        bool
    58  	NoFreqs         bool
    59  	StopWords       []interface{}
    60  	SkipInitialScan bool
    61  }
    62  
    63  type FieldSchema struct {
    64  	FieldName         string
    65  	As                string
    66  	FieldType         SearchFieldType
    67  	Sortable          bool
    68  	UNF               bool
    69  	NoStem            bool
    70  	NoIndex           bool
    71  	PhoneticMatcher   string
    72  	Weight            float64
    73  	Separator         string
    74  	CaseSensitive     bool
    75  	WithSuffixtrie    bool
    76  	VectorArgs        *FTVectorArgs
    77  	GeoShapeFieldType string
    78  	IndexEmpty        bool
    79  	IndexMissing      bool
    80  }
    81  
    82  type FTVectorArgs struct {
    83  	FlatOptions   *FTFlatOptions
    84  	HNSWOptions   *FTHNSWOptions
    85  	VamanaOptions *FTVamanaOptions
    86  }
    87  
    88  type FTFlatOptions struct {
    89  	Type            string
    90  	Dim             int
    91  	DistanceMetric  string
    92  	InitialCapacity int
    93  	BlockSize       int
    94  }
    95  
    96  type FTHNSWOptions struct {
    97  	Type                   string
    98  	Dim                    int
    99  	DistanceMetric         string
   100  	InitialCapacity        int
   101  	MaxEdgesPerNode        int
   102  	MaxAllowedEdgesPerNode int
   103  	EFRunTime              int
   104  	Epsilon                float64
   105  }
   106  
   107  type FTVamanaOptions struct {
   108  	Type                   string
   109  	Dim                    int
   110  	DistanceMetric         string
   111  	Compression            string
   112  	ConstructionWindowSize int
   113  	GraphMaxDegree         int
   114  	SearchWindowSize       int
   115  	Epsilon                float64
   116  	TrainingThreshold      int
   117  	ReduceDim              int
   118  }
   119  
   120  type FTDropIndexOptions struct {
   121  	DeleteDocs bool
   122  }
   123  
   124  type SpellCheckTerms struct {
   125  	Include    bool
   126  	Exclude    bool
   127  	Dictionary string
   128  }
   129  
   130  type FTExplainOptions struct {
   131  	// Dialect 1,3 and 4 are deprecated since redis 8.0
   132  	Dialect string
   133  }
   134  
   135  type FTSynUpdateOptions struct {
   136  	SkipInitialScan bool
   137  }
   138  
   139  type SearchAggregator int
   140  
   141  const (
   142  	SearchInvalid = SearchAggregator(iota)
   143  	SearchAvg
   144  	SearchSum
   145  	SearchMin
   146  	SearchMax
   147  	SearchCount
   148  	SearchCountDistinct
   149  	SearchCountDistinctish
   150  	SearchStdDev
   151  	SearchQuantile
   152  	SearchToList
   153  	SearchFirstValue
   154  	SearchRandomSample
   155  )
   156  
   157  func (a SearchAggregator) String() string {
   158  	switch a {
   159  	case SearchInvalid:
   160  		return ""
   161  	case SearchAvg:
   162  		return "AVG"
   163  	case SearchSum:
   164  		return "SUM"
   165  	case SearchMin:
   166  		return "MIN"
   167  	case SearchMax:
   168  		return "MAX"
   169  	case SearchCount:
   170  		return "COUNT"
   171  	case SearchCountDistinct:
   172  		return "COUNT_DISTINCT"
   173  	case SearchCountDistinctish:
   174  		return "COUNT_DISTINCTISH"
   175  	case SearchStdDev:
   176  		return "STDDEV"
   177  	case SearchQuantile:
   178  		return "QUANTILE"
   179  	case SearchToList:
   180  		return "TOLIST"
   181  	case SearchFirstValue:
   182  		return "FIRST_VALUE"
   183  	case SearchRandomSample:
   184  		return "RANDOM_SAMPLE"
   185  	default:
   186  		return ""
   187  	}
   188  }
   189  
   190  type SearchFieldType int
   191  
   192  const (
   193  	SearchFieldTypeInvalid = SearchFieldType(iota)
   194  	SearchFieldTypeNumeric
   195  	SearchFieldTypeTag
   196  	SearchFieldTypeText
   197  	SearchFieldTypeGeo
   198  	SearchFieldTypeVector
   199  	SearchFieldTypeGeoShape
   200  )
   201  
   202  func (t SearchFieldType) String() string {
   203  	switch t {
   204  	case SearchFieldTypeInvalid:
   205  		return ""
   206  	case SearchFieldTypeNumeric:
   207  		return "NUMERIC"
   208  	case SearchFieldTypeTag:
   209  		return "TAG"
   210  	case SearchFieldTypeText:
   211  		return "TEXT"
   212  	case SearchFieldTypeGeo:
   213  		return "GEO"
   214  	case SearchFieldTypeVector:
   215  		return "VECTOR"
   216  	case SearchFieldTypeGeoShape:
   217  		return "GEOSHAPE"
   218  	default:
   219  		return "TEXT"
   220  	}
   221  }
   222  
   223  // Each AggregateReducer have different args.
   224  // Please follow https://redis.io/docs/interact/search-and-query/search/aggregations/#supported-groupby-reducers for more information.
   225  type FTAggregateReducer struct {
   226  	Reducer SearchAggregator
   227  	Args    []interface{}
   228  	As      string
   229  }
   230  
   231  type FTAggregateGroupBy struct {
   232  	Fields []interface{}
   233  	Reduce []FTAggregateReducer
   234  }
   235  
   236  type FTAggregateSortBy struct {
   237  	FieldName string
   238  	Asc       bool
   239  	Desc      bool
   240  }
   241  
   242  type FTAggregateApply struct {
   243  	Field string
   244  	As    string
   245  }
   246  
   247  type FTAggregateLoad struct {
   248  	Field string
   249  	As    string
   250  }
   251  
   252  type FTAggregateWithCursor struct {
   253  	Count   int
   254  	MaxIdle int
   255  }
   256  
   257  type FTAggregateOptions struct {
   258  	Verbatim  bool
   259  	LoadAll   bool
   260  	Load      []FTAggregateLoad
   261  	Timeout   int
   262  	GroupBy   []FTAggregateGroupBy
   263  	SortBy    []FTAggregateSortBy
   264  	SortByMax int
   265  	// Scorer is used to set scoring function, if not set passed, a default will be used.
   266  	// The default scorer depends on the Redis version:
   267  	// - `BM25` for Redis >= 8
   268  	// - `TFIDF` for Redis < 8
   269  	Scorer string
   270  	// AddScores is available in Redis CE 8
   271  	AddScores         bool
   272  	Apply             []FTAggregateApply
   273  	LimitOffset       int
   274  	Limit             int
   275  	Filter            string
   276  	WithCursor        bool
   277  	WithCursorOptions *FTAggregateWithCursor
   278  	Params            map[string]interface{}
   279  	// Dialect 1,3 and 4 are deprecated since redis 8.0
   280  	DialectVersion int
   281  }
   282  
   283  type FTSearchFilter struct {
   284  	FieldName interface{}
   285  	Min       interface{}
   286  	Max       interface{}
   287  }
   288  
   289  type FTSearchGeoFilter struct {
   290  	FieldName string
   291  	Longitude float64
   292  	Latitude  float64
   293  	Radius    float64
   294  	Unit      string
   295  }
   296  
   297  type FTSearchReturn struct {
   298  	FieldName string
   299  	As        string
   300  }
   301  
   302  type FTSearchSortBy struct {
   303  	FieldName string
   304  	Asc       bool
   305  	Desc      bool
   306  }
   307  
   308  // FTSearchOptions hold options that can be passed to the FT.SEARCH command.
   309  // More information about the options can be found
   310  // in the documentation for FT.SEARCH https://redis.io/docs/latest/commands/ft.search/
   311  type FTSearchOptions struct {
   312  	NoContent    bool
   313  	Verbatim     bool
   314  	NoStopWords  bool
   315  	WithScores   bool
   316  	WithPayloads bool
   317  	WithSortKeys bool
   318  	Filters      []FTSearchFilter
   319  	GeoFilter    []FTSearchGeoFilter
   320  	InKeys       []interface{}
   321  	InFields     []interface{}
   322  	Return       []FTSearchReturn
   323  	Slop         int
   324  	Timeout      int
   325  	InOrder      bool
   326  	Language     string
   327  	Expander     string
   328  	// Scorer is used to set scoring function, if not set passed, a default will be used.
   329  	// The default scorer depends on the Redis version:
   330  	// - `BM25` for Redis >= 8
   331  	// - `TFIDF` for Redis < 8
   332  	Scorer          string
   333  	ExplainScore    bool
   334  	Payload         string
   335  	SortBy          []FTSearchSortBy
   336  	SortByWithCount bool
   337  	LimitOffset     int
   338  	Limit           int
   339  	// CountOnly sets LIMIT 0 0 to get the count - number of documents in the result set without actually returning the result set.
   340  	// When using this option, the Limit and LimitOffset options are ignored.
   341  	CountOnly bool
   342  	Params    map[string]interface{}
   343  	// Dialect 1,3 and 4 are deprecated since redis 8.0
   344  	DialectVersion int
   345  }
   346  
   347  type FTSynDumpResult struct {
   348  	Term     string
   349  	Synonyms []string
   350  }
   351  
   352  type FTSynDumpCmd struct {
   353  	baseCmd
   354  	val []FTSynDumpResult
   355  }
   356  
   357  type FTAggregateResult struct {
   358  	Total int
   359  	Rows  []AggregateRow
   360  }
   361  
   362  type AggregateRow struct {
   363  	Fields map[string]interface{}
   364  }
   365  
   366  type AggregateCmd struct {
   367  	baseCmd
   368  	val *FTAggregateResult
   369  }
   370  
   371  type FTInfoResult struct {
   372  	IndexErrors              IndexErrors
   373  	Attributes               []FTAttribute
   374  	BytesPerRecordAvg        string
   375  	Cleaning                 int
   376  	CursorStats              CursorStats
   377  	DialectStats             map[string]int
   378  	DocTableSizeMB           float64
   379  	FieldStatistics          []FieldStatistic
   380  	GCStats                  GCStats
   381  	GeoshapesSzMB            float64
   382  	HashIndexingFailures     int
   383  	IndexDefinition          IndexDefinition
   384  	IndexName                string
   385  	IndexOptions             []string
   386  	Indexing                 int
   387  	InvertedSzMB             float64
   388  	KeyTableSizeMB           float64
   389  	MaxDocID                 int
   390  	NumDocs                  int
   391  	NumRecords               int
   392  	NumTerms                 int
   393  	NumberOfUses             int
   394  	OffsetBitsPerRecordAvg   string
   395  	OffsetVectorsSzMB        float64
   396  	OffsetsPerTermAvg        string
   397  	PercentIndexed           float64
   398  	RecordsPerDocAvg         string
   399  	SortableValuesSizeMB     float64
   400  	TagOverheadSzMB          float64
   401  	TextOverheadSzMB         float64
   402  	TotalIndexMemorySzMB     float64
   403  	TotalIndexingTime        int
   404  	TotalInvertedIndexBlocks int
   405  	VectorIndexSzMB          float64
   406  }
   407  
   408  type IndexErrors struct {
   409  	IndexingFailures     int
   410  	LastIndexingError    string
   411  	LastIndexingErrorKey string
   412  }
   413  
   414  type FTAttribute struct {
   415  	Identifier      string
   416  	Attribute       string
   417  	Type            string
   418  	Weight          float64
   419  	Sortable        bool
   420  	NoStem          bool
   421  	NoIndex         bool
   422  	UNF             bool
   423  	PhoneticMatcher string
   424  	CaseSensitive   bool
   425  	WithSuffixtrie  bool
   426  }
   427  
   428  type CursorStats struct {
   429  	GlobalIdle    int
   430  	GlobalTotal   int
   431  	IndexCapacity int
   432  	IndexTotal    int
   433  }
   434  
   435  type FieldStatistic struct {
   436  	Identifier  string
   437  	Attribute   string
   438  	IndexErrors IndexErrors
   439  }
   440  
   441  type GCStats struct {
   442  	BytesCollected       int
   443  	TotalMsRun           int
   444  	TotalCycles          int
   445  	AverageCycleTimeMs   string
   446  	LastRunTimeMs        int
   447  	GCNumericTreesMissed int
   448  	GCBlocksDenied       int
   449  }
   450  
   451  type IndexDefinition struct {
   452  	KeyType      string
   453  	Prefixes     []string
   454  	DefaultScore float64
   455  }
   456  
   457  type FTSpellCheckOptions struct {
   458  	Distance int
   459  	Terms    *FTSpellCheckTerms
   460  	// Dialect 1,3 and 4 are deprecated since redis 8.0
   461  	Dialect int
   462  }
   463  
   464  type FTSpellCheckTerms struct {
   465  	Inclusion  string // Either "INCLUDE" or "EXCLUDE"
   466  	Dictionary string
   467  	Terms      []interface{}
   468  }
   469  
   470  type SpellCheckResult struct {
   471  	Term        string
   472  	Suggestions []SpellCheckSuggestion
   473  }
   474  
   475  type SpellCheckSuggestion struct {
   476  	Score      float64
   477  	Suggestion string
   478  }
   479  
   480  type FTSearchResult struct {
   481  	Total int
   482  	Docs  []Document
   483  }
   484  
   485  type Document struct {
   486  	ID      string
   487  	Score   *float64
   488  	Payload *string
   489  	SortKey *string
   490  	Fields  map[string]string
   491  	Error   error
   492  }
   493  
   494  type AggregateQuery []interface{}
   495  
   496  // FT_List - Lists all the existing indexes in the database.
   497  // For more information, please refer to the Redis documentation:
   498  // [FT._LIST]: (https://redis.io/commands/ft._list/)
   499  func (c cmdable) FT_List(ctx context.Context) *StringSliceCmd {
   500  	cmd := NewStringSliceCmd(ctx, "FT._LIST")
   501  	_ = c(ctx, cmd)
   502  	return cmd
   503  }
   504  
   505  // FTAggregate - Performs a search query on an index and applies a series of aggregate transformations to the result.
   506  // The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query.
   507  // For more information, please refer to the Redis documentation:
   508  // [FT.AGGREGATE]: (https://redis.io/commands/ft.aggregate/)
   509  func (c cmdable) FTAggregate(ctx context.Context, index string, query string) *MapStringInterfaceCmd {
   510  	args := []interface{}{"FT.AGGREGATE", index, query}
   511  	cmd := NewMapStringInterfaceCmd(ctx, args...)
   512  	_ = c(ctx, cmd)
   513  	return cmd
   514  }
   515  
   516  func FTAggregateQuery(query string, options *FTAggregateOptions) (AggregateQuery, error) {
   517  	queryArgs := []interface{}{query}
   518  	if options != nil {
   519  		if options.Verbatim {
   520  			queryArgs = append(queryArgs, "VERBATIM")
   521  		}
   522  
   523  		if options.Scorer != "" {
   524  			queryArgs = append(queryArgs, "SCORER", options.Scorer)
   525  		}
   526  
   527  		if options.AddScores {
   528  			queryArgs = append(queryArgs, "ADDSCORES")
   529  		}
   530  
   531  		if options.LoadAll && options.Load != nil {
   532  			return nil, fmt.Errorf("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive")
   533  		}
   534  		if options.LoadAll {
   535  			queryArgs = append(queryArgs, "LOAD", "*")
   536  		}
   537  		if options.Load != nil {
   538  			queryArgs = append(queryArgs, "LOAD", len(options.Load))
   539  			index, count := len(queryArgs)-1, 0
   540  			for _, load := range options.Load {
   541  				queryArgs = append(queryArgs, load.Field)
   542  				count++
   543  				if load.As != "" {
   544  					queryArgs = append(queryArgs, "AS", load.As)
   545  					count += 2
   546  				}
   547  			}
   548  			queryArgs[index] = count
   549  		}
   550  
   551  		if options.Timeout > 0 {
   552  			queryArgs = append(queryArgs, "TIMEOUT", options.Timeout)
   553  		}
   554  
   555  		for _, apply := range options.Apply {
   556  			queryArgs = append(queryArgs, "APPLY", apply.Field)
   557  			if apply.As != "" {
   558  				queryArgs = append(queryArgs, "AS", apply.As)
   559  			}
   560  		}
   561  
   562  		if options.GroupBy != nil {
   563  			for _, groupBy := range options.GroupBy {
   564  				queryArgs = append(queryArgs, "GROUPBY", len(groupBy.Fields))
   565  				queryArgs = append(queryArgs, groupBy.Fields...)
   566  
   567  				for _, reducer := range groupBy.Reduce {
   568  					queryArgs = append(queryArgs, "REDUCE")
   569  					queryArgs = append(queryArgs, reducer.Reducer.String())
   570  					if reducer.Args != nil {
   571  						queryArgs = append(queryArgs, len(reducer.Args))
   572  						queryArgs = append(queryArgs, reducer.Args...)
   573  					} else {
   574  						queryArgs = append(queryArgs, 0)
   575  					}
   576  					if reducer.As != "" {
   577  						queryArgs = append(queryArgs, "AS", reducer.As)
   578  					}
   579  				}
   580  			}
   581  		}
   582  		if options.SortBy != nil {
   583  			queryArgs = append(queryArgs, "SORTBY")
   584  			sortByOptions := []interface{}{}
   585  			for _, sortBy := range options.SortBy {
   586  				sortByOptions = append(sortByOptions, sortBy.FieldName)
   587  				if sortBy.Asc && sortBy.Desc {
   588  					return nil, fmt.Errorf("FT.AGGREGATE: ASC and DESC are mutually exclusive")
   589  				}
   590  				if sortBy.Asc {
   591  					sortByOptions = append(sortByOptions, "ASC")
   592  				}
   593  				if sortBy.Desc {
   594  					sortByOptions = append(sortByOptions, "DESC")
   595  				}
   596  			}
   597  			queryArgs = append(queryArgs, len(sortByOptions))
   598  			queryArgs = append(queryArgs, sortByOptions...)
   599  		}
   600  		if options.SortByMax > 0 {
   601  			queryArgs = append(queryArgs, "MAX", options.SortByMax)
   602  		}
   603  		if options.LimitOffset >= 0 && options.Limit > 0 {
   604  			queryArgs = append(queryArgs, "LIMIT", options.LimitOffset, options.Limit)
   605  		}
   606  		if options.Filter != "" {
   607  			queryArgs = append(queryArgs, "FILTER", options.Filter)
   608  		}
   609  		if options.WithCursor {
   610  			queryArgs = append(queryArgs, "WITHCURSOR")
   611  			if options.WithCursorOptions != nil {
   612  				if options.WithCursorOptions.Count > 0 {
   613  					queryArgs = append(queryArgs, "COUNT", options.WithCursorOptions.Count)
   614  				}
   615  				if options.WithCursorOptions.MaxIdle > 0 {
   616  					queryArgs = append(queryArgs, "MAXIDLE", options.WithCursorOptions.MaxIdle)
   617  				}
   618  			}
   619  		}
   620  		if options.Params != nil {
   621  			queryArgs = append(queryArgs, "PARAMS", len(options.Params)*2)
   622  			for key, value := range options.Params {
   623  				queryArgs = append(queryArgs, key, value)
   624  			}
   625  		}
   626  
   627  		if options.DialectVersion > 0 {
   628  			queryArgs = append(queryArgs, "DIALECT", options.DialectVersion)
   629  		} else {
   630  			queryArgs = append(queryArgs, "DIALECT", 2)
   631  		}
   632  	}
   633  	return queryArgs, nil
   634  }
   635  
   636  func ProcessAggregateResult(data []interface{}) (*FTAggregateResult, error) {
   637  	if len(data) == 0 {
   638  		return nil, fmt.Errorf("no data returned")
   639  	}
   640  
   641  	total, ok := data[0].(int64)
   642  	if !ok {
   643  		return nil, fmt.Errorf("invalid total format")
   644  	}
   645  
   646  	rows := make([]AggregateRow, 0, len(data)-1)
   647  	for _, row := range data[1:] {
   648  		fields, ok := row.([]interface{})
   649  		if !ok {
   650  			return nil, fmt.Errorf("invalid row format")
   651  		}
   652  
   653  		rowMap := make(map[string]interface{})
   654  		for i := 0; i < len(fields); i += 2 {
   655  			key, ok := fields[i].(string)
   656  			if !ok {
   657  				return nil, fmt.Errorf("invalid field key format")
   658  			}
   659  			value := fields[i+1]
   660  			rowMap[key] = value
   661  		}
   662  		rows = append(rows, AggregateRow{Fields: rowMap})
   663  	}
   664  
   665  	result := &FTAggregateResult{
   666  		Total: int(total),
   667  		Rows:  rows,
   668  	}
   669  	return result, nil
   670  }
   671  
   672  func NewAggregateCmd(ctx context.Context, args ...interface{}) *AggregateCmd {
   673  	return &AggregateCmd{
   674  		baseCmd: baseCmd{
   675  			ctx:  ctx,
   676  			args: args,
   677  		},
   678  	}
   679  }
   680  
   681  func (cmd *AggregateCmd) SetVal(val *FTAggregateResult) {
   682  	cmd.val = val
   683  }
   684  
   685  func (cmd *AggregateCmd) Val() *FTAggregateResult {
   686  	return cmd.val
   687  }
   688  
   689  func (cmd *AggregateCmd) Result() (*FTAggregateResult, error) {
   690  	return cmd.val, cmd.err
   691  }
   692  
   693  func (cmd *AggregateCmd) RawVal() interface{} {
   694  	return cmd.rawVal
   695  }
   696  
   697  func (cmd *AggregateCmd) RawResult() (interface{}, error) {
   698  	return cmd.rawVal, cmd.err
   699  }
   700  
   701  func (cmd *AggregateCmd) String() string {
   702  	return cmdString(cmd, cmd.val)
   703  }
   704  
   705  func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) {
   706  	data, err := rd.ReadSlice()
   707  	if err != nil {
   708  		return err
   709  	}
   710  	cmd.val, err = ProcessAggregateResult(data)
   711  	if err != nil {
   712  		return err
   713  	}
   714  	return nil
   715  }
   716  
   717  // FTAggregateWithArgs - Performs a search query on an index and applies a series of aggregate transformations to the result.
   718  // The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query.
   719  // 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.
   720  // For more information, please refer to the Redis documentation:
   721  // [FT.AGGREGATE]: (https://redis.io/commands/ft.aggregate/)
   722  func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query string, options *FTAggregateOptions) *AggregateCmd {
   723  	args := []interface{}{"FT.AGGREGATE", index, query}
   724  	if options != nil {
   725  		if options.Verbatim {
   726  			args = append(args, "VERBATIM")
   727  		}
   728  		if options.Scorer != "" {
   729  			args = append(args, "SCORER", options.Scorer)
   730  		}
   731  		if options.AddScores {
   732  			args = append(args, "ADDSCORES")
   733  		}
   734  		if options.LoadAll && options.Load != nil {
   735  			cmd := NewAggregateCmd(ctx, args...)
   736  			cmd.SetErr(fmt.Errorf("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive"))
   737  			return cmd
   738  		}
   739  		if options.LoadAll {
   740  			args = append(args, "LOAD", "*")
   741  		}
   742  		if options.Load != nil {
   743  			args = append(args, "LOAD", len(options.Load))
   744  			index, count := len(args)-1, 0
   745  			for _, load := range options.Load {
   746  				args = append(args, load.Field)
   747  				count++
   748  				if load.As != "" {
   749  					args = append(args, "AS", load.As)
   750  					count += 2
   751  				}
   752  			}
   753  			args[index] = count
   754  		}
   755  		if options.Timeout > 0 {
   756  			args = append(args, "TIMEOUT", options.Timeout)
   757  		}
   758  		for _, apply := range options.Apply {
   759  			args = append(args, "APPLY", apply.Field)
   760  			if apply.As != "" {
   761  				args = append(args, "AS", apply.As)
   762  			}
   763  		}
   764  		if options.GroupBy != nil {
   765  			for _, groupBy := range options.GroupBy {
   766  				args = append(args, "GROUPBY", len(groupBy.Fields))
   767  				args = append(args, groupBy.Fields...)
   768  
   769  				for _, reducer := range groupBy.Reduce {
   770  					args = append(args, "REDUCE")
   771  					args = append(args, reducer.Reducer.String())
   772  					if reducer.Args != nil {
   773  						args = append(args, len(reducer.Args))
   774  						args = append(args, reducer.Args...)
   775  					} else {
   776  						args = append(args, 0)
   777  					}
   778  					if reducer.As != "" {
   779  						args = append(args, "AS", reducer.As)
   780  					}
   781  				}
   782  			}
   783  		}
   784  		if options.SortBy != nil {
   785  			args = append(args, "SORTBY")
   786  			sortByOptions := []interface{}{}
   787  			for _, sortBy := range options.SortBy {
   788  				sortByOptions = append(sortByOptions, sortBy.FieldName)
   789  				if sortBy.Asc && sortBy.Desc {
   790  					cmd := NewAggregateCmd(ctx, args...)
   791  					cmd.SetErr(fmt.Errorf("FT.AGGREGATE: ASC and DESC are mutually exclusive"))
   792  					return cmd
   793  				}
   794  				if sortBy.Asc {
   795  					sortByOptions = append(sortByOptions, "ASC")
   796  				}
   797  				if sortBy.Desc {
   798  					sortByOptions = append(sortByOptions, "DESC")
   799  				}
   800  			}
   801  			args = append(args, len(sortByOptions))
   802  			args = append(args, sortByOptions...)
   803  		}
   804  		if options.SortByMax > 0 {
   805  			args = append(args, "MAX", options.SortByMax)
   806  		}
   807  		if options.LimitOffset >= 0 && options.Limit > 0 {
   808  			args = append(args, "LIMIT", options.LimitOffset, options.Limit)
   809  		}
   810  		if options.Filter != "" {
   811  			args = append(args, "FILTER", options.Filter)
   812  		}
   813  		if options.WithCursor {
   814  			args = append(args, "WITHCURSOR")
   815  			if options.WithCursorOptions != nil {
   816  				if options.WithCursorOptions.Count > 0 {
   817  					args = append(args, "COUNT", options.WithCursorOptions.Count)
   818  				}
   819  				if options.WithCursorOptions.MaxIdle > 0 {
   820  					args = append(args, "MAXIDLE", options.WithCursorOptions.MaxIdle)
   821  				}
   822  			}
   823  		}
   824  		if options.Params != nil {
   825  			args = append(args, "PARAMS", len(options.Params)*2)
   826  			for key, value := range options.Params {
   827  				args = append(args, key, value)
   828  			}
   829  		}
   830  		if options.DialectVersion > 0 {
   831  			args = append(args, "DIALECT", options.DialectVersion)
   832  		} else {
   833  			args = append(args, "DIALECT", 2)
   834  		}
   835  	}
   836  
   837  	cmd := NewAggregateCmd(ctx, args...)
   838  	_ = c(ctx, cmd)
   839  	return cmd
   840  }
   841  
   842  // FTAliasAdd - Adds an alias to an index.
   843  // The 'index' parameter specifies the index to which the alias is added, and the 'alias' parameter specifies the alias.
   844  // For more information, please refer to the Redis documentation:
   845  // [FT.ALIASADD]: (https://redis.io/commands/ft.aliasadd/)
   846  func (c cmdable) FTAliasAdd(ctx context.Context, index string, alias string) *StatusCmd {
   847  	args := []interface{}{"FT.ALIASADD", alias, index}
   848  	cmd := NewStatusCmd(ctx, args...)
   849  	_ = c(ctx, cmd)
   850  	return cmd
   851  }
   852  
   853  // FTAliasDel - Removes an alias from an index.
   854  // The 'alias' parameter specifies the alias to be removed.
   855  // For more information, please refer to the Redis documentation:
   856  // [FT.ALIASDEL]: (https://redis.io/commands/ft.aliasdel/)
   857  func (c cmdable) FTAliasDel(ctx context.Context, alias string) *StatusCmd {
   858  	cmd := NewStatusCmd(ctx, "FT.ALIASDEL", alias)
   859  	_ = c(ctx, cmd)
   860  	return cmd
   861  }
   862  
   863  // FTAliasUpdate - Updates an alias to an index.
   864  // The 'index' parameter specifies the index to which the alias is updated, and the 'alias' parameter specifies the alias.
   865  // If the alias already exists for a different index, it updates the alias to point to the specified index instead.
   866  // For more information, please refer to the Redis documentation:
   867  // [FT.ALIASUPDATE]: (https://redis.io/commands/ft.aliasupdate/)
   868  func (c cmdable) FTAliasUpdate(ctx context.Context, index string, alias string) *StatusCmd {
   869  	cmd := NewStatusCmd(ctx, "FT.ALIASUPDATE", alias, index)
   870  	_ = c(ctx, cmd)
   871  	return cmd
   872  }
   873  
   874  // FTAlter - Alters the definition of an existing index.
   875  // The 'index' parameter specifies the index to alter, and the 'skipInitialScan' parameter specifies whether to skip the initial scan.
   876  // The 'definition' parameter specifies the new definition for the index.
   877  // For more information, please refer to the Redis documentation:
   878  // [FT.ALTER]: (https://redis.io/commands/ft.alter/)
   879  func (c cmdable) FTAlter(ctx context.Context, index string, skipInitialScan bool, definition []interface{}) *StatusCmd {
   880  	args := []interface{}{"FT.ALTER", index}
   881  	if skipInitialScan {
   882  		args = append(args, "SKIPINITIALSCAN")
   883  	}
   884  	args = append(args, "SCHEMA", "ADD")
   885  	args = append(args, definition...)
   886  	cmd := NewStatusCmd(ctx, args...)
   887  	_ = c(ctx, cmd)
   888  	return cmd
   889  }
   890  
   891  // Retrieves the value of a RediSearch configuration parameter.
   892  // The 'option' parameter specifies the configuration parameter to retrieve.
   893  // For more information, please refer to the Redis [FT.CONFIG GET] documentation.
   894  //
   895  // Deprecated: FTConfigGet is deprecated in Redis 8.
   896  // All configuration will be done with the CONFIG GET command.
   897  // For more information check [Client.ConfigGet] and [CONFIG GET Documentation]
   898  //
   899  // [CONFIG GET Documentation]: https://redis.io/commands/config-get/
   900  // [FT.CONFIG GET]: https://redis.io/commands/ft.config-get/
   901  func (c cmdable) FTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd {
   902  	cmd := NewMapMapStringInterfaceCmd(ctx, "FT.CONFIG", "GET", option)
   903  	_ = c(ctx, cmd)
   904  	return cmd
   905  }
   906  
   907  // Sets the value of a RediSearch configuration parameter.
   908  // The 'option' parameter specifies the configuration parameter to set, and the 'value' parameter specifies the new value.
   909  // For more information, please refer to the Redis [FT.CONFIG SET] documentation.
   910  //
   911  // Deprecated: FTConfigSet is deprecated in Redis 8.
   912  // All configuration will be done with the CONFIG SET command.
   913  // For more information check [Client.ConfigSet] and [CONFIG SET Documentation]
   914  //
   915  // [CONFIG SET Documentation]: https://redis.io/commands/config-set/
   916  // [FT.CONFIG SET]: https://redis.io/commands/ft.config-set/
   917  func (c cmdable) FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd {
   918  	cmd := NewStatusCmd(ctx, "FT.CONFIG", "SET", option, value)
   919  	_ = c(ctx, cmd)
   920  	return cmd
   921  }
   922  
   923  // FTCreate - Creates a new index with the given options and schema.
   924  // The 'index' parameter specifies the name of the index to create.
   925  // The 'options' parameter specifies various options for the index, such as:
   926  // whether to index hashes or JSONs, prefixes, filters, default language, score, score field, payload field, etc.
   927  // The 'schema' parameter specifies the schema for the index, which includes the field name, field type, etc.
   928  // For more information, please refer to the Redis documentation:
   929  // [FT.CREATE]: (https://redis.io/commands/ft.create/)
   930  func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOptions, schema ...*FieldSchema) *StatusCmd {
   931  	args := []interface{}{"FT.CREATE", index}
   932  	if options != nil {
   933  		if options.OnHash && !options.OnJSON {
   934  			args = append(args, "ON", "HASH")
   935  		}
   936  		if options.OnJSON && !options.OnHash {
   937  			args = append(args, "ON", "JSON")
   938  		}
   939  		if options.OnHash && options.OnJSON {
   940  			cmd := NewStatusCmd(ctx, args...)
   941  			cmd.SetErr(fmt.Errorf("FT.CREATE: ON HASH and ON JSON are mutually exclusive"))
   942  			return cmd
   943  		}
   944  		if options.Prefix != nil {
   945  			args = append(args, "PREFIX", len(options.Prefix))
   946  			args = append(args, options.Prefix...)
   947  		}
   948  		if options.Filter != "" {
   949  			args = append(args, "FILTER", options.Filter)
   950  		}
   951  		if options.DefaultLanguage != "" {
   952  			args = append(args, "LANGUAGE", options.DefaultLanguage)
   953  		}
   954  		if options.LanguageField != "" {
   955  			args = append(args, "LANGUAGE_FIELD", options.LanguageField)
   956  		}
   957  		if options.Score > 0 {
   958  			args = append(args, "SCORE", options.Score)
   959  		}
   960  		if options.ScoreField != "" {
   961  			args = append(args, "SCORE_FIELD", options.ScoreField)
   962  		}
   963  		if options.PayloadField != "" {
   964  			args = append(args, "PAYLOAD_FIELD", options.PayloadField)
   965  		}
   966  		if options.MaxTextFields > 0 {
   967  			args = append(args, "MAXTEXTFIELDS", options.MaxTextFields)
   968  		}
   969  		if options.NoOffsets {
   970  			args = append(args, "NOOFFSETS")
   971  		}
   972  		if options.Temporary > 0 {
   973  			args = append(args, "TEMPORARY", options.Temporary)
   974  		}
   975  		if options.NoHL {
   976  			args = append(args, "NOHL")
   977  		}
   978  		if options.NoFields {
   979  			args = append(args, "NOFIELDS")
   980  		}
   981  		if options.NoFreqs {
   982  			args = append(args, "NOFREQS")
   983  		}
   984  		if options.StopWords != nil {
   985  			args = append(args, "STOPWORDS", len(options.StopWords))
   986  			args = append(args, options.StopWords...)
   987  		}
   988  		if options.SkipInitialScan {
   989  			args = append(args, "SKIPINITIALSCAN")
   990  		}
   991  	}
   992  	if schema == nil {
   993  		cmd := NewStatusCmd(ctx, args...)
   994  		cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA is required"))
   995  		return cmd
   996  	}
   997  	args = append(args, "SCHEMA")
   998  	for _, schema := range schema {
   999  		if schema.FieldName == "" || schema.FieldType == SearchFieldTypeInvalid {
  1000  			cmd := NewStatusCmd(ctx, args...)
  1001  			cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA FieldName and FieldType are required"))
  1002  			return cmd
  1003  		}
  1004  		args = append(args, schema.FieldName)
  1005  		if schema.As != "" {
  1006  			args = append(args, "AS", schema.As)
  1007  		}
  1008  		args = append(args, schema.FieldType.String())
  1009  		if schema.VectorArgs != nil {
  1010  			if schema.FieldType != SearchFieldTypeVector {
  1011  				cmd := NewStatusCmd(ctx, args...)
  1012  				cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA FieldType VECTOR is required for VectorArgs"))
  1013  				return cmd
  1014  			}
  1015  			// Check mutual exclusivity of vector options
  1016  			optionCount := 0
  1017  			if schema.VectorArgs.FlatOptions != nil {
  1018  				optionCount++
  1019  			}
  1020  			if schema.VectorArgs.HNSWOptions != nil {
  1021  				optionCount++
  1022  			}
  1023  			if schema.VectorArgs.VamanaOptions != nil {
  1024  				optionCount++
  1025  			}
  1026  			if optionCount != 1 {
  1027  				cmd := NewStatusCmd(ctx, args...)
  1028  				cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions"))
  1029  				return cmd
  1030  			}
  1031  			if schema.VectorArgs.FlatOptions != nil {
  1032  				args = append(args, "FLAT")
  1033  				if schema.VectorArgs.FlatOptions.Type == "" || schema.VectorArgs.FlatOptions.Dim == 0 || schema.VectorArgs.FlatOptions.DistanceMetric == "" {
  1034  					cmd := NewStatusCmd(ctx, args...)
  1035  					cmd.SetErr(fmt.Errorf("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR FLAT"))
  1036  					return cmd
  1037  				}
  1038  				flatArgs := []interface{}{
  1039  					"TYPE", schema.VectorArgs.FlatOptions.Type,
  1040  					"DIM", schema.VectorArgs.FlatOptions.Dim,
  1041  					"DISTANCE_METRIC", schema.VectorArgs.FlatOptions.DistanceMetric,
  1042  				}
  1043  				if schema.VectorArgs.FlatOptions.InitialCapacity > 0 {
  1044  					flatArgs = append(flatArgs, "INITIAL_CAP", schema.VectorArgs.FlatOptions.InitialCapacity)
  1045  				}
  1046  				if schema.VectorArgs.FlatOptions.BlockSize > 0 {
  1047  					flatArgs = append(flatArgs, "BLOCK_SIZE", schema.VectorArgs.FlatOptions.BlockSize)
  1048  				}
  1049  				args = append(args, len(flatArgs))
  1050  				args = append(args, flatArgs...)
  1051  			}
  1052  			if schema.VectorArgs.HNSWOptions != nil {
  1053  				args = append(args, "HNSW")
  1054  				if schema.VectorArgs.HNSWOptions.Type == "" || schema.VectorArgs.HNSWOptions.Dim == 0 || schema.VectorArgs.HNSWOptions.DistanceMetric == "" {
  1055  					cmd := NewStatusCmd(ctx, args...)
  1056  					cmd.SetErr(fmt.Errorf("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR HNSW"))
  1057  					return cmd
  1058  				}
  1059  				hnswArgs := []interface{}{
  1060  					"TYPE", schema.VectorArgs.HNSWOptions.Type,
  1061  					"DIM", schema.VectorArgs.HNSWOptions.Dim,
  1062  					"DISTANCE_METRIC", schema.VectorArgs.HNSWOptions.DistanceMetric,
  1063  				}
  1064  				if schema.VectorArgs.HNSWOptions.InitialCapacity > 0 {
  1065  					hnswArgs = append(hnswArgs, "INITIAL_CAP", schema.VectorArgs.HNSWOptions.InitialCapacity)
  1066  				}
  1067  				if schema.VectorArgs.HNSWOptions.MaxEdgesPerNode > 0 {
  1068  					hnswArgs = append(hnswArgs, "M", schema.VectorArgs.HNSWOptions.MaxEdgesPerNode)
  1069  				}
  1070  				if schema.VectorArgs.HNSWOptions.MaxAllowedEdgesPerNode > 0 {
  1071  					hnswArgs = append(hnswArgs, "EF_CONSTRUCTION", schema.VectorArgs.HNSWOptions.MaxAllowedEdgesPerNode)
  1072  				}
  1073  				if schema.VectorArgs.HNSWOptions.EFRunTime > 0 {
  1074  					hnswArgs = append(hnswArgs, "EF_RUNTIME", schema.VectorArgs.HNSWOptions.EFRunTime)
  1075  				}
  1076  				if schema.VectorArgs.HNSWOptions.Epsilon > 0 {
  1077  					hnswArgs = append(hnswArgs, "EPSILON", schema.VectorArgs.HNSWOptions.Epsilon)
  1078  				}
  1079  				args = append(args, len(hnswArgs))
  1080  				args = append(args, hnswArgs...)
  1081  			}
  1082  			if schema.VectorArgs.VamanaOptions != nil {
  1083  				args = append(args, "SVS-VAMANA")
  1084  				if schema.VectorArgs.VamanaOptions.Type == "" || schema.VectorArgs.VamanaOptions.Dim == 0 || schema.VectorArgs.VamanaOptions.DistanceMetric == "" {
  1085  					cmd := NewStatusCmd(ctx, args...)
  1086  					cmd.SetErr(fmt.Errorf("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR VAMANA"))
  1087  					return cmd
  1088  				}
  1089  				vamanaArgs := []interface{}{
  1090  					"TYPE", schema.VectorArgs.VamanaOptions.Type,
  1091  					"DIM", schema.VectorArgs.VamanaOptions.Dim,
  1092  					"DISTANCE_METRIC", schema.VectorArgs.VamanaOptions.DistanceMetric,
  1093  				}
  1094  				if schema.VectorArgs.VamanaOptions.Compression != "" {
  1095  					vamanaArgs = append(vamanaArgs, "COMPRESSION", schema.VectorArgs.VamanaOptions.Compression)
  1096  				}
  1097  				if schema.VectorArgs.VamanaOptions.ConstructionWindowSize > 0 {
  1098  					vamanaArgs = append(vamanaArgs, "CONSTRUCTION_WINDOW_SIZE", schema.VectorArgs.VamanaOptions.ConstructionWindowSize)
  1099  				}
  1100  				if schema.VectorArgs.VamanaOptions.GraphMaxDegree > 0 {
  1101  					vamanaArgs = append(vamanaArgs, "GRAPH_MAX_DEGREE", schema.VectorArgs.VamanaOptions.GraphMaxDegree)
  1102  				}
  1103  				if schema.VectorArgs.VamanaOptions.SearchWindowSize > 0 {
  1104  					vamanaArgs = append(vamanaArgs, "SEARCH_WINDOW_SIZE", schema.VectorArgs.VamanaOptions.SearchWindowSize)
  1105  				}
  1106  				if schema.VectorArgs.VamanaOptions.Epsilon > 0 {
  1107  					vamanaArgs = append(vamanaArgs, "EPSILON", schema.VectorArgs.VamanaOptions.Epsilon)
  1108  				}
  1109  				if schema.VectorArgs.VamanaOptions.TrainingThreshold > 0 {
  1110  					vamanaArgs = append(vamanaArgs, "TRAINING_THRESHOLD", schema.VectorArgs.VamanaOptions.TrainingThreshold)
  1111  				}
  1112  				if schema.VectorArgs.VamanaOptions.ReduceDim > 0 {
  1113  					vamanaArgs = append(vamanaArgs, "REDUCE", schema.VectorArgs.VamanaOptions.ReduceDim)
  1114  				}
  1115  				args = append(args, len(vamanaArgs))
  1116  				args = append(args, vamanaArgs...)
  1117  			}
  1118  		}
  1119  		if schema.GeoShapeFieldType != "" {
  1120  			if schema.FieldType != SearchFieldTypeGeoShape {
  1121  				cmd := NewStatusCmd(ctx, args...)
  1122  				cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA FieldType GEOSHAPE is required for GeoShapeFieldType"))
  1123  				return cmd
  1124  			}
  1125  			args = append(args, schema.GeoShapeFieldType)
  1126  		}
  1127  		if schema.NoStem {
  1128  			args = append(args, "NOSTEM")
  1129  		}
  1130  		if schema.Sortable {
  1131  			args = append(args, "SORTABLE")
  1132  		}
  1133  		if schema.UNF {
  1134  			args = append(args, "UNF")
  1135  		}
  1136  		if schema.NoIndex {
  1137  			args = append(args, "NOINDEX")
  1138  		}
  1139  		if schema.PhoneticMatcher != "" {
  1140  			args = append(args, "PHONETIC", schema.PhoneticMatcher)
  1141  		}
  1142  		if schema.Weight > 0 {
  1143  			args = append(args, "WEIGHT", schema.Weight)
  1144  		}
  1145  		if schema.Separator != "" {
  1146  			args = append(args, "SEPARATOR", schema.Separator)
  1147  		}
  1148  		if schema.CaseSensitive {
  1149  			args = append(args, "CASESENSITIVE")
  1150  		}
  1151  		if schema.WithSuffixtrie {
  1152  			args = append(args, "WITHSUFFIXTRIE")
  1153  		}
  1154  		if schema.IndexEmpty {
  1155  			args = append(args, "INDEXEMPTY")
  1156  		}
  1157  		if schema.IndexMissing {
  1158  			args = append(args, "INDEXMISSING")
  1159  
  1160  		}
  1161  	}
  1162  	cmd := NewStatusCmd(ctx, args...)
  1163  	_ = c(ctx, cmd)
  1164  	return cmd
  1165  }
  1166  
  1167  // FTCursorDel - Deletes a cursor from an existing index.
  1168  // The 'index' parameter specifies the index from which to delete the cursor, and the 'cursorId' parameter specifies the ID of the cursor to delete.
  1169  // For more information, please refer to the Redis documentation:
  1170  // [FT.CURSOR DEL]: (https://redis.io/commands/ft.cursor-del/)
  1171  func (c cmdable) FTCursorDel(ctx context.Context, index string, cursorId int) *StatusCmd {
  1172  	cmd := NewStatusCmd(ctx, "FT.CURSOR", "DEL", index, cursorId)
  1173  	_ = c(ctx, cmd)
  1174  	return cmd
  1175  }
  1176  
  1177  // FTCursorRead - Reads the next results from an existing cursor.
  1178  // The 'index' parameter specifies the index from which to read the cursor, the 'cursorId' parameter specifies the ID of the cursor to read, and the 'count' parameter specifies the number of results to read.
  1179  // For more information, please refer to the Redis documentation:
  1180  // [FT.CURSOR READ]: (https://redis.io/commands/ft.cursor-read/)
  1181  func (c cmdable) FTCursorRead(ctx context.Context, index string, cursorId int, count int) *MapStringInterfaceCmd {
  1182  	args := []interface{}{"FT.CURSOR", "READ", index, cursorId}
  1183  	if count > 0 {
  1184  		args = append(args, "COUNT", count)
  1185  	}
  1186  	cmd := NewMapStringInterfaceCmd(ctx, args...)
  1187  	_ = c(ctx, cmd)
  1188  	return cmd
  1189  }
  1190  
  1191  // FTDictAdd - Adds terms to a dictionary.
  1192  // The 'dict' parameter specifies the dictionary to which to add the terms, and the 'term' parameter specifies the terms to add.
  1193  // For more information, please refer to the Redis documentation:
  1194  // [FT.DICTADD]: (https://redis.io/commands/ft.dictadd/)
  1195  func (c cmdable) FTDictAdd(ctx context.Context, dict string, term ...interface{}) *IntCmd {
  1196  	args := []interface{}{"FT.DICTADD", dict}
  1197  	args = append(args, term...)
  1198  	cmd := NewIntCmd(ctx, args...)
  1199  	_ = c(ctx, cmd)
  1200  	return cmd
  1201  }
  1202  
  1203  // FTDictDel - Deletes terms from a dictionary.
  1204  // The 'dict' parameter specifies the dictionary from which to delete the terms, and the 'term' parameter specifies the terms to delete.
  1205  // For more information, please refer to the Redis documentation:
  1206  // [FT.DICTDEL]: (https://redis.io/commands/ft.dictdel/)
  1207  func (c cmdable) FTDictDel(ctx context.Context, dict string, term ...interface{}) *IntCmd {
  1208  	args := []interface{}{"FT.DICTDEL", dict}
  1209  	args = append(args, term...)
  1210  	cmd := NewIntCmd(ctx, args...)
  1211  	_ = c(ctx, cmd)
  1212  	return cmd
  1213  }
  1214  
  1215  // FTDictDump - Returns all terms in the specified dictionary.
  1216  // The 'dict' parameter specifies the dictionary from which to return the terms.
  1217  // For more information, please refer to the Redis documentation:
  1218  // [FT.DICTDUMP]: (https://redis.io/commands/ft.dictdump/)
  1219  func (c cmdable) FTDictDump(ctx context.Context, dict string) *StringSliceCmd {
  1220  	cmd := NewStringSliceCmd(ctx, "FT.DICTDUMP", dict)
  1221  	_ = c(ctx, cmd)
  1222  	return cmd
  1223  }
  1224  
  1225  // FTDropIndex - Deletes an index.
  1226  // The 'index' parameter specifies the index to delete.
  1227  // For more information, please refer to the Redis documentation:
  1228  // [FT.DROPINDEX]: (https://redis.io/commands/ft.dropindex/)
  1229  func (c cmdable) FTDropIndex(ctx context.Context, index string) *StatusCmd {
  1230  	args := []interface{}{"FT.DROPINDEX", index}
  1231  	cmd := NewStatusCmd(ctx, args...)
  1232  	_ = c(ctx, cmd)
  1233  	return cmd
  1234  }
  1235  
  1236  // FTDropIndexWithArgs - Deletes an index with options.
  1237  // The 'index' parameter specifies the index to delete, and the 'options' parameter specifies the DeleteDocs option for docs deletion.
  1238  // For more information, please refer to the Redis documentation:
  1239  // [FT.DROPINDEX]: (https://redis.io/commands/ft.dropindex/)
  1240  func (c cmdable) FTDropIndexWithArgs(ctx context.Context, index string, options *FTDropIndexOptions) *StatusCmd {
  1241  	args := []interface{}{"FT.DROPINDEX", index}
  1242  	if options != nil {
  1243  		if options.DeleteDocs {
  1244  			args = append(args, "DD")
  1245  		}
  1246  	}
  1247  	cmd := NewStatusCmd(ctx, args...)
  1248  	_ = c(ctx, cmd)
  1249  	return cmd
  1250  }
  1251  
  1252  // FTExplain - Returns the execution plan for a complex query.
  1253  // The 'index' parameter specifies the index to query, and the 'query' parameter specifies the query string.
  1254  // For more information, please refer to the Redis documentation:
  1255  // [FT.EXPLAIN]: (https://redis.io/commands/ft.explain/)
  1256  func (c cmdable) FTExplain(ctx context.Context, index string, query string) *StringCmd {
  1257  	cmd := NewStringCmd(ctx, "FT.EXPLAIN", index, query)
  1258  	_ = c(ctx, cmd)
  1259  	return cmd
  1260  }
  1261  
  1262  // FTExplainWithArgs - Returns the execution plan for a complex query with options.
  1263  // The 'index' parameter specifies the index to query, the 'query' parameter specifies the query string, and the 'options' parameter specifies the Dialect for the query.
  1264  // For more information, please refer to the Redis documentation:
  1265  // [FT.EXPLAIN]: (https://redis.io/commands/ft.explain/)
  1266  func (c cmdable) FTExplainWithArgs(ctx context.Context, index string, query string, options *FTExplainOptions) *StringCmd {
  1267  	args := []interface{}{"FT.EXPLAIN", index, query}
  1268  	if options.Dialect != "" {
  1269  		args = append(args, "DIALECT", options.Dialect)
  1270  	} else {
  1271  		args = append(args, "DIALECT", 2)
  1272  	}
  1273  	cmd := NewStringCmd(ctx, args...)
  1274  	_ = c(ctx, cmd)
  1275  	return cmd
  1276  }
  1277  
  1278  // FTExplainCli - Returns the execution plan for a complex query. [Not Implemented]
  1279  // For more information, see https://redis.io/commands/ft.explaincli/
  1280  func (c cmdable) FTExplainCli(ctx context.Context, key, path string) error {
  1281  	return fmt.Errorf("FTExplainCli is not implemented")
  1282  }
  1283  
  1284  func parseFTInfo(data map[string]interface{}) (FTInfoResult, error) {
  1285  	var ftInfo FTInfoResult
  1286  	// Manually parse each field from the map
  1287  	if indexErrors, ok := data["Index Errors"].([]interface{}); ok {
  1288  		ftInfo.IndexErrors = IndexErrors{
  1289  			IndexingFailures:     internal.ToInteger(indexErrors[1]),
  1290  			LastIndexingError:    internal.ToString(indexErrors[3]),
  1291  			LastIndexingErrorKey: internal.ToString(indexErrors[5]),
  1292  		}
  1293  	}
  1294  
  1295  	if attributes, ok := data["attributes"].([]interface{}); ok {
  1296  		for _, attr := range attributes {
  1297  			if attrMap, ok := attr.([]interface{}); ok {
  1298  				att := FTAttribute{}
  1299  				for i := 0; i < len(attrMap); i++ {
  1300  					if internal.ToLower(internal.ToString(attrMap[i])) == "attribute" {
  1301  						att.Attribute = internal.ToString(attrMap[i+1])
  1302  						continue
  1303  					}
  1304  					if internal.ToLower(internal.ToString(attrMap[i])) == "identifier" {
  1305  						att.Identifier = internal.ToString(attrMap[i+1])
  1306  						continue
  1307  					}
  1308  					if internal.ToLower(internal.ToString(attrMap[i])) == "type" {
  1309  						att.Type = internal.ToString(attrMap[i+1])
  1310  						continue
  1311  					}
  1312  					if internal.ToLower(internal.ToString(attrMap[i])) == "weight" {
  1313  						att.Weight = internal.ToFloat(attrMap[i+1])
  1314  						continue
  1315  					}
  1316  					if internal.ToLower(internal.ToString(attrMap[i])) == "nostem" {
  1317  						att.NoStem = true
  1318  						continue
  1319  					}
  1320  					if internal.ToLower(internal.ToString(attrMap[i])) == "sortable" {
  1321  						att.Sortable = true
  1322  						continue
  1323  					}
  1324  					if internal.ToLower(internal.ToString(attrMap[i])) == "noindex" {
  1325  						att.NoIndex = true
  1326  						continue
  1327  					}
  1328  					if internal.ToLower(internal.ToString(attrMap[i])) == "unf" {
  1329  						att.UNF = true
  1330  						continue
  1331  					}
  1332  					if internal.ToLower(internal.ToString(attrMap[i])) == "phonetic" {
  1333  						att.PhoneticMatcher = internal.ToString(attrMap[i+1])
  1334  						continue
  1335  					}
  1336  					if internal.ToLower(internal.ToString(attrMap[i])) == "case_sensitive" {
  1337  						att.CaseSensitive = true
  1338  						continue
  1339  					}
  1340  					if internal.ToLower(internal.ToString(attrMap[i])) == "withsuffixtrie" {
  1341  						att.WithSuffixtrie = true
  1342  						continue
  1343  					}
  1344  
  1345  				}
  1346  				ftInfo.Attributes = append(ftInfo.Attributes, att)
  1347  			}
  1348  		}
  1349  	}
  1350  
  1351  	ftInfo.BytesPerRecordAvg = internal.ToString(data["bytes_per_record_avg"])
  1352  	ftInfo.Cleaning = internal.ToInteger(data["cleaning"])
  1353  
  1354  	if cursorStats, ok := data["cursor_stats"].([]interface{}); ok {
  1355  		ftInfo.CursorStats = CursorStats{
  1356  			GlobalIdle:    internal.ToInteger(cursorStats[1]),
  1357  			GlobalTotal:   internal.ToInteger(cursorStats[3]),
  1358  			IndexCapacity: internal.ToInteger(cursorStats[5]),
  1359  			IndexTotal:    internal.ToInteger(cursorStats[7]),
  1360  		}
  1361  	}
  1362  
  1363  	if dialectStats, ok := data["dialect_stats"].([]interface{}); ok {
  1364  		ftInfo.DialectStats = make(map[string]int)
  1365  		for i := 0; i < len(dialectStats); i += 2 {
  1366  			ftInfo.DialectStats[internal.ToString(dialectStats[i])] = internal.ToInteger(dialectStats[i+1])
  1367  		}
  1368  	}
  1369  
  1370  	ftInfo.DocTableSizeMB = internal.ToFloat(data["doc_table_size_mb"])
  1371  
  1372  	if fieldStats, ok := data["field statistics"].([]interface{}); ok {
  1373  		for _, stat := range fieldStats {
  1374  			if statMap, ok := stat.([]interface{}); ok {
  1375  				ftInfo.FieldStatistics = append(ftInfo.FieldStatistics, FieldStatistic{
  1376  					Identifier: internal.ToString(statMap[1]),
  1377  					Attribute:  internal.ToString(statMap[3]),
  1378  					IndexErrors: IndexErrors{
  1379  						IndexingFailures:     internal.ToInteger(statMap[5].([]interface{})[1]),
  1380  						LastIndexingError:    internal.ToString(statMap[5].([]interface{})[3]),
  1381  						LastIndexingErrorKey: internal.ToString(statMap[5].([]interface{})[5]),
  1382  					},
  1383  				})
  1384  			}
  1385  		}
  1386  	}
  1387  
  1388  	if gcStats, ok := data["gc_stats"].([]interface{}); ok {
  1389  		ftInfo.GCStats = GCStats{}
  1390  		for i := 0; i < len(gcStats); i += 2 {
  1391  			if internal.ToLower(internal.ToString(gcStats[i])) == "bytes_collected" {
  1392  				ftInfo.GCStats.BytesCollected = internal.ToInteger(gcStats[i+1])
  1393  				continue
  1394  			}
  1395  			if internal.ToLower(internal.ToString(gcStats[i])) == "total_ms_run" {
  1396  				ftInfo.GCStats.TotalMsRun = internal.ToInteger(gcStats[i+1])
  1397  				continue
  1398  			}
  1399  			if internal.ToLower(internal.ToString(gcStats[i])) == "total_cycles" {
  1400  				ftInfo.GCStats.TotalCycles = internal.ToInteger(gcStats[i+1])
  1401  				continue
  1402  			}
  1403  			if internal.ToLower(internal.ToString(gcStats[i])) == "average_cycle_time_ms" {
  1404  				ftInfo.GCStats.AverageCycleTimeMs = internal.ToString(gcStats[i+1])
  1405  				continue
  1406  			}
  1407  			if internal.ToLower(internal.ToString(gcStats[i])) == "last_run_time_ms" {
  1408  				ftInfo.GCStats.LastRunTimeMs = internal.ToInteger(gcStats[i+1])
  1409  				continue
  1410  			}
  1411  			if internal.ToLower(internal.ToString(gcStats[i])) == "gc_numeric_trees_missed" {
  1412  				ftInfo.GCStats.GCNumericTreesMissed = internal.ToInteger(gcStats[i+1])
  1413  				continue
  1414  			}
  1415  			if internal.ToLower(internal.ToString(gcStats[i])) == "gc_blocks_denied" {
  1416  				ftInfo.GCStats.GCBlocksDenied = internal.ToInteger(gcStats[i+1])
  1417  				continue
  1418  			}
  1419  		}
  1420  	}
  1421  
  1422  	ftInfo.GeoshapesSzMB = internal.ToFloat(data["geoshapes_sz_mb"])
  1423  	ftInfo.HashIndexingFailures = internal.ToInteger(data["hash_indexing_failures"])
  1424  
  1425  	if indexDef, ok := data["index_definition"].([]interface{}); ok {
  1426  		ftInfo.IndexDefinition = IndexDefinition{
  1427  			KeyType:      internal.ToString(indexDef[1]),
  1428  			Prefixes:     internal.ToStringSlice(indexDef[3]),
  1429  			DefaultScore: internal.ToFloat(indexDef[5]),
  1430  		}
  1431  	}
  1432  
  1433  	ftInfo.IndexName = internal.ToString(data["index_name"])
  1434  	ftInfo.IndexOptions = internal.ToStringSlice(data["index_options"].([]interface{}))
  1435  	ftInfo.Indexing = internal.ToInteger(data["indexing"])
  1436  	ftInfo.InvertedSzMB = internal.ToFloat(data["inverted_sz_mb"])
  1437  	ftInfo.KeyTableSizeMB = internal.ToFloat(data["key_table_size_mb"])
  1438  	ftInfo.MaxDocID = internal.ToInteger(data["max_doc_id"])
  1439  	ftInfo.NumDocs = internal.ToInteger(data["num_docs"])
  1440  	ftInfo.NumRecords = internal.ToInteger(data["num_records"])
  1441  	ftInfo.NumTerms = internal.ToInteger(data["num_terms"])
  1442  	ftInfo.NumberOfUses = internal.ToInteger(data["number_of_uses"])
  1443  	ftInfo.OffsetBitsPerRecordAvg = internal.ToString(data["offset_bits_per_record_avg"])
  1444  	ftInfo.OffsetVectorsSzMB = internal.ToFloat(data["offset_vectors_sz_mb"])
  1445  	ftInfo.OffsetsPerTermAvg = internal.ToString(data["offsets_per_term_avg"])
  1446  	ftInfo.PercentIndexed = internal.ToFloat(data["percent_indexed"])
  1447  	ftInfo.RecordsPerDocAvg = internal.ToString(data["records_per_doc_avg"])
  1448  	ftInfo.SortableValuesSizeMB = internal.ToFloat(data["sortable_values_size_mb"])
  1449  	ftInfo.TagOverheadSzMB = internal.ToFloat(data["tag_overhead_sz_mb"])
  1450  	ftInfo.TextOverheadSzMB = internal.ToFloat(data["text_overhead_sz_mb"])
  1451  	ftInfo.TotalIndexMemorySzMB = internal.ToFloat(data["total_index_memory_sz_mb"])
  1452  	ftInfo.TotalIndexingTime = internal.ToInteger(data["total_indexing_time"])
  1453  	ftInfo.TotalInvertedIndexBlocks = internal.ToInteger(data["total_inverted_index_blocks"])
  1454  	ftInfo.VectorIndexSzMB = internal.ToFloat(data["vector_index_sz_mb"])
  1455  
  1456  	return ftInfo, nil
  1457  }
  1458  
  1459  type FTInfoCmd struct {
  1460  	baseCmd
  1461  	val FTInfoResult
  1462  }
  1463  
  1464  func newFTInfoCmd(ctx context.Context, args ...interface{}) *FTInfoCmd {
  1465  	return &FTInfoCmd{
  1466  		baseCmd: baseCmd{
  1467  			ctx:  ctx,
  1468  			args: args,
  1469  		},
  1470  	}
  1471  }
  1472  
  1473  func (cmd *FTInfoCmd) String() string {
  1474  	return cmdString(cmd, cmd.val)
  1475  }
  1476  
  1477  func (cmd *FTInfoCmd) SetVal(val FTInfoResult) {
  1478  	cmd.val = val
  1479  }
  1480  
  1481  func (cmd *FTInfoCmd) Result() (FTInfoResult, error) {
  1482  	return cmd.val, cmd.err
  1483  }
  1484  
  1485  func (cmd *FTInfoCmd) Val() FTInfoResult {
  1486  	return cmd.val
  1487  }
  1488  
  1489  func (cmd *FTInfoCmd) RawVal() interface{} {
  1490  	return cmd.rawVal
  1491  }
  1492  
  1493  func (cmd *FTInfoCmd) RawResult() (interface{}, error) {
  1494  	return cmd.rawVal, cmd.err
  1495  }
  1496  func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) {
  1497  	n, err := rd.ReadMapLen()
  1498  	if err != nil {
  1499  		return err
  1500  	}
  1501  
  1502  	data := make(map[string]interface{}, n)
  1503  	for i := 0; i < n; i++ {
  1504  		k, err := rd.ReadString()
  1505  		if err != nil {
  1506  			return err
  1507  		}
  1508  		v, err := rd.ReadReply()
  1509  		if err != nil {
  1510  			if err == Nil {
  1511  				data[k] = Nil
  1512  				continue
  1513  			}
  1514  			if err, ok := err.(proto.RedisError); ok {
  1515  				data[k] = err
  1516  				continue
  1517  			}
  1518  			return err
  1519  		}
  1520  		data[k] = v
  1521  	}
  1522  	cmd.val, err = parseFTInfo(data)
  1523  	if err != nil {
  1524  		return err
  1525  	}
  1526  
  1527  	return nil
  1528  }
  1529  
  1530  // FTInfo - Retrieves information about an index.
  1531  // The 'index' parameter specifies the index to retrieve information about.
  1532  // For more information, please refer to the Redis documentation:
  1533  // [FT.INFO]: (https://redis.io/commands/ft.info/)
  1534  func (c cmdable) FTInfo(ctx context.Context, index string) *FTInfoCmd {
  1535  	cmd := newFTInfoCmd(ctx, "FT.INFO", index)
  1536  	_ = c(ctx, cmd)
  1537  	return cmd
  1538  }
  1539  
  1540  // FTSpellCheck - Checks a query string for spelling errors.
  1541  // For more details about spellcheck query please follow:
  1542  // https://redis.io/docs/interact/search-and-query/advanced-concepts/spellcheck/
  1543  // For more information, please refer to the Redis documentation:
  1544  // [FT.SPELLCHECK]: (https://redis.io/commands/ft.spellcheck/)
  1545  func (c cmdable) FTSpellCheck(ctx context.Context, index string, query string) *FTSpellCheckCmd {
  1546  	args := []interface{}{"FT.SPELLCHECK", index, query}
  1547  	cmd := newFTSpellCheckCmd(ctx, args...)
  1548  	_ = c(ctx, cmd)
  1549  	return cmd
  1550  }
  1551  
  1552  // FTSpellCheckWithArgs - Checks a query string for spelling errors with additional options.
  1553  // For more details about spellcheck query please follow:
  1554  // https://redis.io/docs/interact/search-and-query/advanced-concepts/spellcheck/
  1555  // For more information, please refer to the Redis documentation:
  1556  // [FT.SPELLCHECK]: (https://redis.io/commands/ft.spellcheck/)
  1557  func (c cmdable) FTSpellCheckWithArgs(ctx context.Context, index string, query string, options *FTSpellCheckOptions) *FTSpellCheckCmd {
  1558  	args := []interface{}{"FT.SPELLCHECK", index, query}
  1559  	if options != nil {
  1560  		if options.Distance > 0 {
  1561  			args = append(args, "DISTANCE", options.Distance)
  1562  		}
  1563  		if options.Terms != nil {
  1564  			args = append(args, "TERMS", options.Terms.Inclusion, options.Terms.Dictionary)
  1565  			args = append(args, options.Terms.Terms...)
  1566  		}
  1567  		if options.Dialect > 0 {
  1568  			args = append(args, "DIALECT", options.Dialect)
  1569  		} else {
  1570  			args = append(args, "DIALECT", 2)
  1571  		}
  1572  	}
  1573  	cmd := newFTSpellCheckCmd(ctx, args...)
  1574  	_ = c(ctx, cmd)
  1575  	return cmd
  1576  }
  1577  
  1578  type FTSpellCheckCmd struct {
  1579  	baseCmd
  1580  	val []SpellCheckResult
  1581  }
  1582  
  1583  func newFTSpellCheckCmd(ctx context.Context, args ...interface{}) *FTSpellCheckCmd {
  1584  	return &FTSpellCheckCmd{
  1585  		baseCmd: baseCmd{
  1586  			ctx:  ctx,
  1587  			args: args,
  1588  		},
  1589  	}
  1590  }
  1591  
  1592  func (cmd *FTSpellCheckCmd) String() string {
  1593  	return cmdString(cmd, cmd.val)
  1594  }
  1595  
  1596  func (cmd *FTSpellCheckCmd) SetVal(val []SpellCheckResult) {
  1597  	cmd.val = val
  1598  }
  1599  
  1600  func (cmd *FTSpellCheckCmd) Result() ([]SpellCheckResult, error) {
  1601  	return cmd.val, cmd.err
  1602  }
  1603  
  1604  func (cmd *FTSpellCheckCmd) Val() []SpellCheckResult {
  1605  	return cmd.val
  1606  }
  1607  
  1608  func (cmd *FTSpellCheckCmd) RawVal() interface{} {
  1609  	return cmd.rawVal
  1610  }
  1611  
  1612  func (cmd *FTSpellCheckCmd) RawResult() (interface{}, error) {
  1613  	return cmd.rawVal, cmd.err
  1614  }
  1615  
  1616  func (cmd *FTSpellCheckCmd) readReply(rd *proto.Reader) (err error) {
  1617  	data, err := rd.ReadSlice()
  1618  	if err != nil {
  1619  		return err
  1620  	}
  1621  	cmd.val, err = parseFTSpellCheck(data)
  1622  	if err != nil {
  1623  		return err
  1624  	}
  1625  	return nil
  1626  }
  1627  
  1628  func parseFTSpellCheck(data []interface{}) ([]SpellCheckResult, error) {
  1629  	results := make([]SpellCheckResult, 0, len(data))
  1630  
  1631  	for _, termData := range data {
  1632  		termInfo, ok := termData.([]interface{})
  1633  		if !ok || len(termInfo) != 3 {
  1634  			return nil, fmt.Errorf("invalid term format")
  1635  		}
  1636  
  1637  		term, ok := termInfo[1].(string)
  1638  		if !ok {
  1639  			return nil, fmt.Errorf("invalid term format")
  1640  		}
  1641  
  1642  		suggestionsData, ok := termInfo[2].([]interface{})
  1643  		if !ok {
  1644  			return nil, fmt.Errorf("invalid suggestions format")
  1645  		}
  1646  
  1647  		suggestions := make([]SpellCheckSuggestion, 0, len(suggestionsData))
  1648  		for _, suggestionData := range suggestionsData {
  1649  			suggestionInfo, ok := suggestionData.([]interface{})
  1650  			if !ok || len(suggestionInfo) != 2 {
  1651  				return nil, fmt.Errorf("invalid suggestion format")
  1652  			}
  1653  
  1654  			scoreStr, ok := suggestionInfo[0].(string)
  1655  			if !ok {
  1656  				return nil, fmt.Errorf("invalid suggestion score format")
  1657  			}
  1658  			score, err := strconv.ParseFloat(scoreStr, 64)
  1659  			if err != nil {
  1660  				return nil, fmt.Errorf("invalid suggestion score value")
  1661  			}
  1662  
  1663  			suggestion, ok := suggestionInfo[1].(string)
  1664  			if !ok {
  1665  				return nil, fmt.Errorf("invalid suggestion format")
  1666  			}
  1667  
  1668  			suggestions = append(suggestions, SpellCheckSuggestion{
  1669  				Score:      score,
  1670  				Suggestion: suggestion,
  1671  			})
  1672  		}
  1673  
  1674  		results = append(results, SpellCheckResult{
  1675  			Term:        term,
  1676  			Suggestions: suggestions,
  1677  		})
  1678  	}
  1679  
  1680  	return results, nil
  1681  }
  1682  
  1683  func parseFTSearch(data []interface{}, noContent, withScores, withPayloads, withSortKeys bool) (FTSearchResult, error) {
  1684  	if len(data) < 1 {
  1685  		return FTSearchResult{}, fmt.Errorf("unexpected search result format")
  1686  	}
  1687  
  1688  	total, ok := data[0].(int64)
  1689  	if !ok {
  1690  		return FTSearchResult{}, fmt.Errorf("invalid total results format")
  1691  	}
  1692  
  1693  	var results []Document
  1694  	for i := 1; i < len(data); {
  1695  		docID, ok := data[i].(string)
  1696  		if !ok {
  1697  			return FTSearchResult{}, fmt.Errorf("invalid document ID format")
  1698  		}
  1699  
  1700  		doc := Document{
  1701  			ID:     docID,
  1702  			Fields: make(map[string]string),
  1703  		}
  1704  		i++
  1705  
  1706  		if noContent {
  1707  			results = append(results, doc)
  1708  			continue
  1709  		}
  1710  
  1711  		if withScores && i < len(data) {
  1712  			if scoreStr, ok := data[i].(string); ok {
  1713  				score, err := strconv.ParseFloat(scoreStr, 64)
  1714  				if err != nil {
  1715  					return FTSearchResult{}, fmt.Errorf("invalid score format")
  1716  				}
  1717  				doc.Score = &score
  1718  				i++
  1719  			}
  1720  		}
  1721  
  1722  		if withPayloads && i < len(data) {
  1723  			if payload, ok := data[i].(string); ok {
  1724  				doc.Payload = &payload
  1725  				i++
  1726  			}
  1727  		}
  1728  
  1729  		if withSortKeys && i < len(data) {
  1730  			if sortKey, ok := data[i].(string); ok {
  1731  				doc.SortKey = &sortKey
  1732  				i++
  1733  			}
  1734  		}
  1735  
  1736  		if i < len(data) {
  1737  			fields, ok := data[i].([]interface{})
  1738  			if !ok {
  1739  				if data[i] == proto.Nil || data[i] == nil {
  1740  					doc.Error = proto.Nil
  1741  					doc.Fields = map[string]string{}
  1742  					fields = []interface{}{}
  1743  				} else {
  1744  					return FTSearchResult{}, fmt.Errorf("invalid document fields format")
  1745  				}
  1746  			}
  1747  
  1748  			for j := 0; j < len(fields); j += 2 {
  1749  				key, ok := fields[j].(string)
  1750  				if !ok {
  1751  					return FTSearchResult{}, fmt.Errorf("invalid field key format")
  1752  				}
  1753  				value, ok := fields[j+1].(string)
  1754  				if !ok {
  1755  					return FTSearchResult{}, fmt.Errorf("invalid field value format")
  1756  				}
  1757  				doc.Fields[key] = value
  1758  			}
  1759  			i++
  1760  		}
  1761  
  1762  		results = append(results, doc)
  1763  	}
  1764  	return FTSearchResult{
  1765  		Total: int(total),
  1766  		Docs:  results,
  1767  	}, nil
  1768  }
  1769  
  1770  type FTSearchCmd struct {
  1771  	baseCmd
  1772  	val     FTSearchResult
  1773  	options *FTSearchOptions
  1774  }
  1775  
  1776  func newFTSearchCmd(ctx context.Context, options *FTSearchOptions, args ...interface{}) *FTSearchCmd {
  1777  	return &FTSearchCmd{
  1778  		baseCmd: baseCmd{
  1779  			ctx:  ctx,
  1780  			args: args,
  1781  		},
  1782  		options: options,
  1783  	}
  1784  }
  1785  
  1786  func (cmd *FTSearchCmd) String() string {
  1787  	return cmdString(cmd, cmd.val)
  1788  }
  1789  
  1790  func (cmd *FTSearchCmd) SetVal(val FTSearchResult) {
  1791  	cmd.val = val
  1792  }
  1793  
  1794  func (cmd *FTSearchCmd) Result() (FTSearchResult, error) {
  1795  	return cmd.val, cmd.err
  1796  }
  1797  
  1798  func (cmd *FTSearchCmd) Val() FTSearchResult {
  1799  	return cmd.val
  1800  }
  1801  
  1802  func (cmd *FTSearchCmd) RawVal() interface{} {
  1803  	return cmd.rawVal
  1804  }
  1805  
  1806  func (cmd *FTSearchCmd) RawResult() (interface{}, error) {
  1807  	return cmd.rawVal, cmd.err
  1808  }
  1809  
  1810  func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) {
  1811  	data, err := rd.ReadSlice()
  1812  	if err != nil {
  1813  		return err
  1814  	}
  1815  	cmd.val, err = parseFTSearch(data, cmd.options.NoContent, cmd.options.WithScores, cmd.options.WithPayloads, cmd.options.WithSortKeys)
  1816  	if err != nil {
  1817  		return err
  1818  	}
  1819  	return nil
  1820  }
  1821  
  1822  // FTSearch - Executes a search query on an index.
  1823  // The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query.
  1824  // For more information, please refer to the Redis documentation about [FT.SEARCH].
  1825  //
  1826  // [FT.SEARCH]: (https://redis.io/commands/ft.search/)
  1827  func (c cmdable) FTSearch(ctx context.Context, index string, query string) *FTSearchCmd {
  1828  	args := []interface{}{"FT.SEARCH", index, query}
  1829  	cmd := newFTSearchCmd(ctx, &FTSearchOptions{}, args...)
  1830  	_ = c(ctx, cmd)
  1831  	return cmd
  1832  }
  1833  
  1834  type SearchQuery []interface{}
  1835  
  1836  // FTSearchQuery - Executes a search query on an index with additional options.
  1837  // The 'index' parameter specifies the index to search, the 'query' parameter specifies the search query,
  1838  // and the 'options' parameter specifies additional options for the search.
  1839  // For more information, please refer to the Redis documentation about [FT.SEARCH].
  1840  //
  1841  // [FT.SEARCH]: (https://redis.io/commands/ft.search/)
  1842  func FTSearchQuery(query string, options *FTSearchOptions) (SearchQuery, error) {
  1843  	queryArgs := []interface{}{query}
  1844  	if options != nil {
  1845  		if options.NoContent {
  1846  			queryArgs = append(queryArgs, "NOCONTENT")
  1847  		}
  1848  		if options.Verbatim {
  1849  			queryArgs = append(queryArgs, "VERBATIM")
  1850  		}
  1851  		if options.NoStopWords {
  1852  			queryArgs = append(queryArgs, "NOSTOPWORDS")
  1853  		}
  1854  		if options.WithScores {
  1855  			queryArgs = append(queryArgs, "WITHSCORES")
  1856  		}
  1857  		if options.WithPayloads {
  1858  			queryArgs = append(queryArgs, "WITHPAYLOADS")
  1859  		}
  1860  		if options.WithSortKeys {
  1861  			queryArgs = append(queryArgs, "WITHSORTKEYS")
  1862  		}
  1863  		if options.Filters != nil {
  1864  			for _, filter := range options.Filters {
  1865  				queryArgs = append(queryArgs, "FILTER", filter.FieldName, filter.Min, filter.Max)
  1866  			}
  1867  		}
  1868  		if options.GeoFilter != nil {
  1869  			for _, geoFilter := range options.GeoFilter {
  1870  				queryArgs = append(queryArgs, "GEOFILTER", geoFilter.FieldName, geoFilter.Longitude, geoFilter.Latitude, geoFilter.Radius, geoFilter.Unit)
  1871  			}
  1872  		}
  1873  		if options.InKeys != nil {
  1874  			queryArgs = append(queryArgs, "INKEYS", len(options.InKeys))
  1875  			queryArgs = append(queryArgs, options.InKeys...)
  1876  		}
  1877  		if options.InFields != nil {
  1878  			queryArgs = append(queryArgs, "INFIELDS", len(options.InFields))
  1879  			queryArgs = append(queryArgs, options.InFields...)
  1880  		}
  1881  		if options.Return != nil {
  1882  			queryArgs = append(queryArgs, "RETURN")
  1883  			queryArgsReturn := []interface{}{}
  1884  			for _, ret := range options.Return {
  1885  				queryArgsReturn = append(queryArgsReturn, ret.FieldName)
  1886  				if ret.As != "" {
  1887  					queryArgsReturn = append(queryArgsReturn, "AS", ret.As)
  1888  				}
  1889  			}
  1890  			queryArgs = append(queryArgs, len(queryArgsReturn))
  1891  			queryArgs = append(queryArgs, queryArgsReturn...)
  1892  		}
  1893  		if options.Slop > 0 {
  1894  			queryArgs = append(queryArgs, "SLOP", options.Slop)
  1895  		}
  1896  		if options.Timeout > 0 {
  1897  			queryArgs = append(queryArgs, "TIMEOUT", options.Timeout)
  1898  		}
  1899  		if options.InOrder {
  1900  			queryArgs = append(queryArgs, "INORDER")
  1901  		}
  1902  		if options.Language != "" {
  1903  			queryArgs = append(queryArgs, "LANGUAGE", options.Language)
  1904  		}
  1905  		if options.Expander != "" {
  1906  			queryArgs = append(queryArgs, "EXPANDER", options.Expander)
  1907  		}
  1908  		if options.Scorer != "" {
  1909  			queryArgs = append(queryArgs, "SCORER", options.Scorer)
  1910  		}
  1911  		if options.ExplainScore {
  1912  			queryArgs = append(queryArgs, "EXPLAINSCORE")
  1913  		}
  1914  		if options.Payload != "" {
  1915  			queryArgs = append(queryArgs, "PAYLOAD", options.Payload)
  1916  		}
  1917  		if options.SortBy != nil {
  1918  			queryArgs = append(queryArgs, "SORTBY")
  1919  			for _, sortBy := range options.SortBy {
  1920  				queryArgs = append(queryArgs, sortBy.FieldName)
  1921  				if sortBy.Asc && sortBy.Desc {
  1922  					return nil, fmt.Errorf("FT.SEARCH: ASC and DESC are mutually exclusive")
  1923  				}
  1924  				if sortBy.Asc {
  1925  					queryArgs = append(queryArgs, "ASC")
  1926  				}
  1927  				if sortBy.Desc {
  1928  					queryArgs = append(queryArgs, "DESC")
  1929  				}
  1930  			}
  1931  			if options.SortByWithCount {
  1932  				queryArgs = append(queryArgs, "WITHCOUNT")
  1933  			}
  1934  		}
  1935  		if options.LimitOffset >= 0 && options.Limit > 0 {
  1936  			queryArgs = append(queryArgs, "LIMIT", options.LimitOffset, options.Limit)
  1937  		}
  1938  		if options.Params != nil {
  1939  			queryArgs = append(queryArgs, "PARAMS", len(options.Params)*2)
  1940  			for key, value := range options.Params {
  1941  				queryArgs = append(queryArgs, key, value)
  1942  			}
  1943  		}
  1944  		if options.DialectVersion > 0 {
  1945  			queryArgs = append(queryArgs, "DIALECT", options.DialectVersion)
  1946  		} else {
  1947  			queryArgs = append(queryArgs, "DIALECT", 2)
  1948  		}
  1949  	}
  1950  	return queryArgs, nil
  1951  }
  1952  
  1953  // FTSearchWithArgs - Executes a search query on an index with additional options.
  1954  // The 'index' parameter specifies the index to search, the 'query' parameter specifies the search query,
  1955  // and the 'options' parameter specifies additional options for the search.
  1956  // For more information, please refer to the Redis documentation about [FT.SEARCH].
  1957  //
  1958  // [FT.SEARCH]: (https://redis.io/commands/ft.search/)
  1959  func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *FTSearchCmd {
  1960  	args := []interface{}{"FT.SEARCH", index, query}
  1961  	if options != nil {
  1962  		if options.NoContent {
  1963  			args = append(args, "NOCONTENT")
  1964  		}
  1965  		if options.Verbatim {
  1966  			args = append(args, "VERBATIM")
  1967  		}
  1968  		if options.NoStopWords {
  1969  			args = append(args, "NOSTOPWORDS")
  1970  		}
  1971  		if options.WithScores {
  1972  			args = append(args, "WITHSCORES")
  1973  		}
  1974  		if options.WithPayloads {
  1975  			args = append(args, "WITHPAYLOADS")
  1976  		}
  1977  		if options.WithSortKeys {
  1978  			args = append(args, "WITHSORTKEYS")
  1979  		}
  1980  		if options.Filters != nil {
  1981  			for _, filter := range options.Filters {
  1982  				args = append(args, "FILTER", filter.FieldName, filter.Min, filter.Max)
  1983  			}
  1984  		}
  1985  		if options.GeoFilter != nil {
  1986  			for _, geoFilter := range options.GeoFilter {
  1987  				args = append(args, "GEOFILTER", geoFilter.FieldName, geoFilter.Longitude, geoFilter.Latitude, geoFilter.Radius, geoFilter.Unit)
  1988  			}
  1989  		}
  1990  		if options.InKeys != nil {
  1991  			args = append(args, "INKEYS", len(options.InKeys))
  1992  			args = append(args, options.InKeys...)
  1993  		}
  1994  		if options.InFields != nil {
  1995  			args = append(args, "INFIELDS", len(options.InFields))
  1996  			args = append(args, options.InFields...)
  1997  		}
  1998  		if options.Return != nil {
  1999  			args = append(args, "RETURN")
  2000  			argsReturn := []interface{}{}
  2001  			for _, ret := range options.Return {
  2002  				argsReturn = append(argsReturn, ret.FieldName)
  2003  				if ret.As != "" {
  2004  					argsReturn = append(argsReturn, "AS", ret.As)
  2005  				}
  2006  			}
  2007  			args = append(args, len(argsReturn))
  2008  			args = append(args, argsReturn...)
  2009  		}
  2010  		if options.Slop > 0 {
  2011  			args = append(args, "SLOP", options.Slop)
  2012  		}
  2013  		if options.Timeout > 0 {
  2014  			args = append(args, "TIMEOUT", options.Timeout)
  2015  		}
  2016  		if options.InOrder {
  2017  			args = append(args, "INORDER")
  2018  		}
  2019  		if options.Language != "" {
  2020  			args = append(args, "LANGUAGE", options.Language)
  2021  		}
  2022  		if options.Expander != "" {
  2023  			args = append(args, "EXPANDER", options.Expander)
  2024  		}
  2025  		if options.Scorer != "" {
  2026  			args = append(args, "SCORER", options.Scorer)
  2027  		}
  2028  		if options.ExplainScore {
  2029  			args = append(args, "EXPLAINSCORE")
  2030  		}
  2031  		if options.Payload != "" {
  2032  			args = append(args, "PAYLOAD", options.Payload)
  2033  		}
  2034  		if options.SortBy != nil {
  2035  			args = append(args, "SORTBY")
  2036  			for _, sortBy := range options.SortBy {
  2037  				args = append(args, sortBy.FieldName)
  2038  				if sortBy.Asc && sortBy.Desc {
  2039  					cmd := newFTSearchCmd(ctx, options, args...)
  2040  					cmd.SetErr(fmt.Errorf("FT.SEARCH: ASC and DESC are mutually exclusive"))
  2041  					return cmd
  2042  				}
  2043  				if sortBy.Asc {
  2044  					args = append(args, "ASC")
  2045  				}
  2046  				if sortBy.Desc {
  2047  					args = append(args, "DESC")
  2048  				}
  2049  			}
  2050  			if options.SortByWithCount {
  2051  				args = append(args, "WITHCOUNT")
  2052  			}
  2053  		}
  2054  		if options.CountOnly {
  2055  			args = append(args, "LIMIT", 0, 0)
  2056  		} else {
  2057  			if options.LimitOffset >= 0 && options.Limit > 0 || options.LimitOffset > 0 && options.Limit == 0 {
  2058  				args = append(args, "LIMIT", options.LimitOffset, options.Limit)
  2059  			}
  2060  		}
  2061  		if options.Params != nil {
  2062  			args = append(args, "PARAMS", len(options.Params)*2)
  2063  			for key, value := range options.Params {
  2064  				args = append(args, key, value)
  2065  			}
  2066  		}
  2067  		if options.DialectVersion > 0 {
  2068  			args = append(args, "DIALECT", options.DialectVersion)
  2069  		} else {
  2070  			args = append(args, "DIALECT", 2)
  2071  		}
  2072  	}
  2073  	cmd := newFTSearchCmd(ctx, options, args...)
  2074  	_ = c(ctx, cmd)
  2075  	return cmd
  2076  }
  2077  
  2078  func NewFTSynDumpCmd(ctx context.Context, args ...interface{}) *FTSynDumpCmd {
  2079  	return &FTSynDumpCmd{
  2080  		baseCmd: baseCmd{
  2081  			ctx:  ctx,
  2082  			args: args,
  2083  		},
  2084  	}
  2085  }
  2086  
  2087  func (cmd *FTSynDumpCmd) String() string {
  2088  	return cmdString(cmd, cmd.val)
  2089  }
  2090  
  2091  func (cmd *FTSynDumpCmd) SetVal(val []FTSynDumpResult) {
  2092  	cmd.val = val
  2093  }
  2094  
  2095  func (cmd *FTSynDumpCmd) Val() []FTSynDumpResult {
  2096  	return cmd.val
  2097  }
  2098  
  2099  func (cmd *FTSynDumpCmd) Result() ([]FTSynDumpResult, error) {
  2100  	return cmd.val, cmd.err
  2101  }
  2102  
  2103  func (cmd *FTSynDumpCmd) RawVal() interface{} {
  2104  	return cmd.rawVal
  2105  }
  2106  
  2107  func (cmd *FTSynDumpCmd) RawResult() (interface{}, error) {
  2108  	return cmd.rawVal, cmd.err
  2109  }
  2110  
  2111  func (cmd *FTSynDumpCmd) readReply(rd *proto.Reader) error {
  2112  	termSynonymPairs, err := rd.ReadSlice()
  2113  	if err != nil {
  2114  		return err
  2115  	}
  2116  
  2117  	var results []FTSynDumpResult
  2118  	for i := 0; i < len(termSynonymPairs); i += 2 {
  2119  		term, ok := termSynonymPairs[i].(string)
  2120  		if !ok {
  2121  			return fmt.Errorf("invalid term format")
  2122  		}
  2123  
  2124  		synonyms, ok := termSynonymPairs[i+1].([]interface{})
  2125  		if !ok {
  2126  			return fmt.Errorf("invalid synonyms format")
  2127  		}
  2128  
  2129  		synonymList := make([]string, len(synonyms))
  2130  		for j, syn := range synonyms {
  2131  			synonym, ok := syn.(string)
  2132  			if !ok {
  2133  				return fmt.Errorf("invalid synonym format")
  2134  			}
  2135  			synonymList[j] = synonym
  2136  		}
  2137  
  2138  		results = append(results, FTSynDumpResult{
  2139  			Term:     term,
  2140  			Synonyms: synonymList,
  2141  		})
  2142  	}
  2143  
  2144  	cmd.val = results
  2145  	return nil
  2146  }
  2147  
  2148  // FTSynDump - Dumps the contents of a synonym group.
  2149  // The 'index' parameter specifies the index to dump.
  2150  // For more information, please refer to the Redis documentation:
  2151  // [FT.SYNDUMP]: (https://redis.io/commands/ft.syndump/)
  2152  func (c cmdable) FTSynDump(ctx context.Context, index string) *FTSynDumpCmd {
  2153  	cmd := NewFTSynDumpCmd(ctx, "FT.SYNDUMP", index)
  2154  	_ = c(ctx, cmd)
  2155  	return cmd
  2156  }
  2157  
  2158  // FTSynUpdate - Creates or updates a synonym group with additional terms.
  2159  // The 'index' parameter specifies the index to update, the 'synGroupId' parameter specifies the synonym group id, and the 'terms' parameter specifies the additional terms.
  2160  // For more information, please refer to the Redis documentation:
  2161  // [FT.SYNUPDATE]: (https://redis.io/commands/ft.synupdate/)
  2162  func (c cmdable) FTSynUpdate(ctx context.Context, index string, synGroupId interface{}, terms []interface{}) *StatusCmd {
  2163  	args := []interface{}{"FT.SYNUPDATE", index, synGroupId}
  2164  	args = append(args, terms...)
  2165  	cmd := NewStatusCmd(ctx, args...)
  2166  	_ = c(ctx, cmd)
  2167  	return cmd
  2168  }
  2169  
  2170  // FTSynUpdateWithArgs - Creates or updates a synonym group with additional terms and options.
  2171  // The 'index' parameter specifies the index to update, the 'synGroupId' parameter specifies the synonym group id, the 'options' parameter specifies additional options for the update, and the 'terms' parameter specifies the additional terms.
  2172  // For more information, please refer to the Redis documentation:
  2173  // [FT.SYNUPDATE]: (https://redis.io/commands/ft.synupdate/)
  2174  func (c cmdable) FTSynUpdateWithArgs(ctx context.Context, index string, synGroupId interface{}, options *FTSynUpdateOptions, terms []interface{}) *StatusCmd {
  2175  	args := []interface{}{"FT.SYNUPDATE", index, synGroupId}
  2176  	if options.SkipInitialScan {
  2177  		args = append(args, "SKIPINITIALSCAN")
  2178  	}
  2179  	args = append(args, terms...)
  2180  	cmd := NewStatusCmd(ctx, args...)
  2181  	_ = c(ctx, cmd)
  2182  	return cmd
  2183  }
  2184  
  2185  // FTTagVals - Returns all distinct values indexed in a tag field.
  2186  // The 'index' parameter specifies the index to check, and the 'field' parameter specifies the tag field to retrieve values from.
  2187  // For more information, please refer to the Redis documentation:
  2188  // [FT.TAGVALS]: (https://redis.io/commands/ft.tagvals/)
  2189  func (c cmdable) FTTagVals(ctx context.Context, index string, field string) *StringSliceCmd {
  2190  	cmd := NewStringSliceCmd(ctx, "FT.TAGVALS", index, field)
  2191  	_ = c(ctx, cmd)
  2192  	return cmd
  2193  }
  2194  

View as plain text