diff --git a/commands.go b/commands.go index 842cc23a..08d65475 100644 --- a/commands.go +++ b/commands.go @@ -221,6 +221,7 @@ type Cmdable interface { ProbabilisticCmdable PubSubCmdable ScriptingFunctionsCmdable + SearchCmdable SetCmdable SortedSetCmdable StringCmdable diff --git a/redis_search.go b/redis_search.go deleted file mode 100644 index 93caf048..00000000 --- a/redis_search.go +++ /dev/null @@ -1,858 +0,0 @@ -package redis - -import ( - "context" -) - -type SearchCmdable interface { -} - -type FTCreateOptions struct { - OnHash bool - OnJSON bool - Prefix []interface{} - Filter string - DefaultLanguage string - LanguageField string - Score float64 - ScoreField string - PayloadField string - MaxTextFields int - NoOffsets bool - Temporary int - NoHL bool - NoFields bool - NoFreqs bool - StopWords []interface{} - SkipInitalScan bool -} - -type SearchSchema struct { - Identifier string - Attribute string - AttributeType string - Sortable bool - UNF bool - NoStem bool - NoIndex bool - PhoneticMatcher string - Weight float64 - Seperator string - CaseSensitive bool - WithSuffix bool -} -type FTDropIndexOptions struct { - DeleteDocs bool -} - -type SpellCheckTerms struct { - Include bool - Exclude bool - Dictionary string -} - -type FTSpellCheckOptions struct { - Distance int - Terms SpellCheckTerms - Dialect int -} - -type FTExplainOptions struct { - Dialect string -} - -type FTSynUpdateOptions struct { - SkipInitialScan bool -} - -type SearchAggregator int - -const ( - SearchInvalid = SearchAggregator(iota) - SearchAvg - SearchSum - SearchMin - SearchMax - SearchCount - SearchCountDistinct - SearchCountDistinctish - SearchStdDev - SearchQuantile - SearchToList - SearchFirstValue - SearchRandomSample -) - -func (a SearchAggregator) String() string { - switch a { - case SearchInvalid: - return "" - case SearchAvg: - return "AVG" - case SearchSum: - return "SUM" - case SearchMin: - return "MIN" - case SearchMax: - return "MAX" - case SearchCount: - return "COUNT" - case SearchCountDistinct: - return "COUNT_DISTINCT" - case SearchCountDistinctish: - return "COUNT_DISTINCTISH" - case SearchStdDev: - return "STDDEV" - case SearchQuantile: - return "QUANTILE" - case SearchToList: - return "TOLIST" - case SearchFirstValue: - return "FIRST_VALUE" - case SearchRandomSample: - return "RANDOM_SAMPLE" - default: - return "" - } -} - -// Each AggregateReducer have different args. -// Please follow https://redis.io/docs/interact/search-and-query/search/aggregations/#supported-groupby-reducers for more information. -type FTAggregateReducer struct { - Reducer SearchAggregator - Args []interface{} - As string -} - -type FTAggregateGroupBy struct { - Fields []interface{} - Reduce []FTAggregateReducer -} - -type FTAggregateSortBy struct { - FieldName string - Asc bool - Desc bool -} - -type FTAggregateApply struct { - Field string - As string -} - -type FTAggregateLoad struct { - Field string - As string -} - -type FTAggregateWithCursor struct { - Count int - MaxIdle int -} - -type FTAggregateOptions struct { - Verbatim bool - LoadAll bool - Load []FTAggregateLoad - Timeout int - GroupBy []FTAggregateGroupBy - SortBy []FTAggregateSortBy - SortByMax int - Apply []FTAggregateApply - LimitOffset int - Limit int - Filter string - WithCursor bool - WithCursorOptions *FTAggregateWithCursor - Params map[string]interface{} - DialectVersion int -} - -type FTSearchFilter struct { - FieldName interface{} - Min interface{} - Max interface{} -} - -type FTSearchGeoFilter struct { - FieldName string - Longitude float64 - Latitude float64 - Radius float64 - Unit string -} - -type FTSearchReturn struct { - FieldName string - As string -} - -type FTSearchSortBy struct { - FieldName string - Asc bool - Desc bool -} - -type FTSearchOptions struct { - NoContent bool - Verbatim bool - NoStopWrods bool - WithScores bool - WithPayloads bool - WithSortKeys bool - Filters []FTSearchFilter - GeoFilter []FTSearchGeoFilter - InKeys []interface{} - InFields []interface{} - Return []FTSearchReturn - Slop int - Timeout int - InOrder bool - Language string - Expander string - Scorer string - ExplainScore bool - Payload string - SortBy []FTSearchSortBy - SortByWithCount bool - LimitOffset int - Limit int - Params map[string]interface{} - DialectVersion int -} - -func (c cmdable) FT_List(ctx context.Context) *StringSliceCmd { - cmd := NewStringSliceCmd(ctx, "FT._LIST") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTAggregate(ctx context.Context, index string, query string) *MapStringInterfaceCmd { - args := []interface{}{"FT.AGGREGATE", index, query} - cmd := NewMapStringInterfaceCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query string, options *FTAggregateOptions) *MapStringInterfaceCmd { - args := []interface{}{"FT.AGGREGATE", index, query} - if options != nil { - if options.Verbatim { - args = append(args, "VERBATIM") - } - if options.LoadAll && options.Load != nil { - panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") - } - if options.LoadAll { - args = append(args, "LOAD", "*") - } - if options.Load != nil { - args = append(args, "LOAD", len(options.Load)) - for _, load := range options.Load { - args = append(args, load.Field) - if load.As != "" { - args = append(args, "AS", load.As) - } - } - } - if options.Timeout > 0 { - args = append(args, "TIMEOUT", options.Timeout) - } - if options.GroupBy != nil { - for _, groupBy := range options.GroupBy { - args = append(args, "GROUPBY", len(groupBy.Fields)) - args = append(args, groupBy.Fields...) - - for _, reducer := range groupBy.Reduce { - args = append(args, "REDUCE") - args = append(args, reducer.Reducer.String()) - if reducer.Args != nil { - args = append(args, len(reducer.Args)) - args = append(args, reducer.Args...) - } else { - args = append(args, 0) - } - if reducer.As != "" { - args = append(args, "AS", reducer.As) - } - } - } - } - if options.SortBy != nil { - args = append(args, "SORTBY") - sortByOptions := []interface{}{} - for _, sortBy := range options.SortBy { - sortByOptions = append(sortByOptions, sortBy.FieldName) - if sortBy.Asc && sortBy.Desc { - panic("FT.AGGREGATE: ASC and DESC are mutually exclusive") - } - if sortBy.Asc { - sortByOptions = append(sortByOptions, "ASC") - } - if sortBy.Desc { - sortByOptions = append(sortByOptions, "DESC") - } - } - args = append(args, len(sortByOptions)) - args = append(args, sortByOptions...) - } - if options.SortByMax > 0 { - args = append(args, "MAX", options.SortByMax) - } - if options.Apply != nil { - args = append(args, "APPLY", len(options.Apply)) - for _, apply := range options.Apply { - args = append(args, apply.Field) - if apply.As != "" { - args = append(args, "AS", apply.As) - } - } - } - if options.LimitOffset > 0 { - args = append(args, "LIMIT", options.LimitOffset) - } - if options.Limit > 0 { - args = append(args, options.Limit) - } - if options.Filter != "" { - args = append(args, "FILTER", options.Filter) - } - if options.WithCursor { - args = append(args, "WITHCURSOR") - if options.WithCursorOptions != nil { - if options.WithCursorOptions.Count > 0 { - args = append(args, "COUNT", options.WithCursorOptions.Count) - } - if options.WithCursorOptions.MaxIdle > 0 { - args = append(args, "MAXIDLE", options.WithCursorOptions.MaxIdle) - } - } - } - if options.Params != nil { - args = append(args, "PARAMS", len(options.Params)*2) - for key, value := range options.Params { - args = append(args, key, value) - } - } - if options.DialectVersion > 0 { - args = append(args, "DIALECT", options.DialectVersion) - } - } - - cmd := NewMapStringInterfaceCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTAliasAdd(ctx context.Context, index string, alias string) *StatusCmd { - args := []interface{}{"FT.ALIASADD", alias, index} - cmd := NewStatusCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTAliasDel(ctx context.Context, alias string) *StatusCmd { - cmd := NewStatusCmd(ctx, "FT.ALIASDEL", alias) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTAliasUpdate(ctx context.Context, index string, alias string) *StatusCmd { - cmd := NewStatusCmd(ctx, "FT.ALIASUPDATE", alias, index) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTAlter(ctx context.Context, index string, skipInitalScan bool, definition []interface{}) *StatusCmd { - args := []interface{}{"FT.ALTER", index} - if skipInitalScan { - args = append(args, "SKIPINITIALSCAN") - } - args = append(args, "SCHEMA", "ADD") - args = append(args, definition...) - cmd := NewStatusCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTConfigGet(ctx context.Context, option string) *MapStringInterfaceCmd { - cmd := NewMapStringInterfaceCmd(ctx, "FT.CONFIG", "GET", option) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd { - cmd := NewStatusCmd(ctx, "FT.CONFIG", "SET", option, value) - _ = c(ctx, cmd) - return cmd -} - -// TODO Fix schema for loop -func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOptions, schema ...*SearchSchema) *StatusCmd { - args := []interface{}{"FT.CREATE", index} - if options != nil { - if options.OnHash && !options.OnJSON { - args = append(args, "ON", "HASH") - } - if options.OnJSON && !options.OnHash { - args = append(args, "ON", "JSON") - } - if options.OnHash && options.OnJSON { - panic("FT.CREATE: ON HASH and ON JSON are mutually exclusive") - } - if options.Prefix != nil { - args = append(args, "PREFIX", len(options.Prefix)) - args = append(args, options.Prefix...) - } - if options.Filter != "" { - args = append(args, "FILTER", options.Filter) - } - if options.DefaultLanguage != "" { - args = append(args, "LANGUAGE", options.DefaultLanguage) - } - if options.LanguageField != "" { - args = append(args, "LANGUAGE_FIELD", options.LanguageField) - } - if options.Score > 0 { - args = append(args, "SCORE", options.Score) - } - if options.ScoreField != "" { - args = append(args, "SCORE_FIELD", options.ScoreField) - } - if options.PayloadField != "" { - args = append(args, "PAYLOAD_FIELD", options.PayloadField) - } - if options.MaxTextFields > 0 { - args = append(args, "MAXTEXTFIELDS", options.MaxTextFields) - } - if options.NoOffsets { - args = append(args, "NOOFFSETS") - } - if options.Temporary > 0 { - args = append(args, "TEMPORARY", options.Temporary) - } - if options.NoHL { - args = append(args, "NOHL") - } - if options.NoFields { - args = append(args, "NOFIELDS") - } - if options.NoFreqs { - args = append(args, "NOFREQS") - } - if options.StopWords != nil { - args = append(args, "STOPWORDS", len(options.StopWords)) - args = append(args, options.StopWords...) - } - if options.SkipInitalScan { - args = append(args, "SKIPINITIALSCAN") - } - } - if schema == nil { - panic("FT.CREATE: SCHEMA is required") - } - args = append(args, "SCHEMA") - for _, schema := range schema { - if schema.Identifier == "" || schema.AttributeType == "" { - panic("FT.CREATE: SCHEMA IDENTIFIER and ATTRIBUTE_TYPE are required") - } - args = append(args, schema.Identifier) - if schema.Attribute != "" { - args = append(args, "AS", schema.Attribute) - } - args = append(args, schema.AttributeType) - if schema.NoStem { - args = append(args, "NOSTEM") - } - if schema.Sortable { - args = append(args, "SORTABLE") - } - if schema.UNF { - args = append(args, "UNF") - } - if schema.NoIndex { - args = append(args, "NOINDEX") - } - if schema.PhoneticMatcher != "" { - args = append(args, "PHONETIC", schema.PhoneticMatcher) - } - if schema.Weight > 0 { - args = append(args, "WEIGHT", schema.Weight) - } - if schema.Seperator != "" { - args = append(args, "SEPERATOR", schema.Seperator) - } - if schema.CaseSensitive { - args = append(args, "CASESENSITIVE") - } - if schema.WithSuffix { - args = append(args, "WITHSUFFIX") - } - } - cmd := NewStatusCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTCursorDel(ctx context.Context, index string, cursorId int) *StatusCmd { - cmd := NewStatusCmd(ctx, "FT.CURSOR", "DEL", index, cursorId) - _ = c(ctx, cmd) - return cmd -} - -// TODO TEST IT -func (c cmdable) FTCursorRead(ctx context.Context, index string, cursorId int, count int) *MapStringInterfaceCmd { - args := []interface{}{"FT.CURSOR", "READ", index, cursorId} - if count > 0 { - args = append(args, "COUNT", count) - } - cmd := NewMapStringInterfaceCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -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...) - _ = c(ctx, cmd) - return cmd -} - -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...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTDictDump(ctx context.Context, dict string) *StringSliceCmd { - cmd := NewStringSliceCmd(ctx, "FT.DICTDUMP", dict) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTDropIndex(ctx context.Context, index string) *StatusCmd { - args := []interface{}{"FT.DROPINDEX", index} - cmd := NewStatusCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTDropIndexWithArgs(ctx context.Context, index string, options *FTDropIndexOptions) *StatusCmd { - args := []interface{}{"FT.DROPINDEX", index} - if options != nil { - if options.DeleteDocs { - args = append(args, "DD") - } - } - cmd := NewStatusCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTExplain(ctx context.Context, index string, query string) *StringCmd { - cmd := NewStringCmd(ctx, "FT.EXPLAIN", index, query) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTExplainWithArgs(ctx context.Context, index string, query string, options *FTExplainOptions) *StringCmd { - args := []interface{}{"FT.EXPLAIN", index, query} - if options.Dialect != "" { - args = append(args, "DIALECT", options.Dialect) - } - cmd := NewStringCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTInfo(ctx context.Context, index string) *MapStringInterfaceCmd { - cmd := NewMapStringInterfaceCmd(ctx, "FT.INFO", index) - _ = c(ctx, cmd) - return cmd -} - -// TODO - When ft search is ready -// func (c cmdable) FTProfileSearch(ctx context.Context, index string, limited bool, query string) *StringCmd { -// args := []interface{}{"FT.PROFILE", index, "SEARCH"} -// if limited { -// args = append(args, "LIMITED") -// } -// args = append(args, "QUERY", query) - -// cmd := NewStringCmd(ctx, args...) -// _ = c(ctx, cmd) -// return cmd -// } - -// func (c cmdable) FTProfileAggregate(ctx context.Context, index string, limited bool, query string) *FTProfileAggregateCmd { -// args := []interface{}{"FT.PROFILE", index, "AGGREGATE"} -// if limited { -// args = append(args, "LIMITED") -// } -// args = append(args, "QUERY", query) - -// cmd := newFTProfileAggregateCmd(ctx, args...) -// _ = c(ctx, cmd) -// return cmd -// } - -// type FTProfileAggregateResult struct { -// aggregateResult MapStringInterfaceCmd -// profileResult KeyValueSliceCmd -// } - -// type FTProfileAggregateCmd struct { -// baseCmd - -// val FTProfileAggregateResult -// } - -// func newFTProfileAggregateCmd(ctx context.Context, args ...interface{}) *FTProfileAggregateCmd { -// return &FTProfileAggregateCmd{ -// baseCmd: baseCmd{ -// ctx: ctx, -// args: args, -// }, -// } -// } - -// func (cmd *FTProfileAggregateCmd) String() string { -// return cmdString(cmd, cmd.val) -// } - -// func (cmd *FTProfileAggregateCmd) SetVal(val FTProfileAggregateResult) { -// cmd.val = val -// } - -// func (cmd *FTProfileAggregateCmd) Result() (FTProfileAggregateResult, error) { -// return cmd.val, cmd.err -// } - -// func (cmd *FTProfileAggregateCmd) Val() FTProfileAggregateResult { -// return cmd.val -// } - -// func (cmd *FTProfileAggregateCmd) readReply(rd *proto.Reader) (err error) { -// _, err = rd.ReadArrayLen() -// if err != nil { -// return err -// } -// _, err = rd.ReadArrayLen() -// if err != nil { -// return err -// } -// cmd.val = FTProfileAggregateResult{} -// status, err := rd.ReadInt() -// if err != nil { -// return err -// } -// cmd.val.aggregateResult.Status = status -// nn, err := rd.ReadArrayLen() -// if err != nil { -// return err -// } -// cmd.val.aggregateResult.Fields = make(map[string]string, nn/2) -// for i := 0; i < nn; i++ { -// key, err := rd.ReadString() -// if err != nil { -// return err -// } - -// value, err := rd.ReadString() -// if err != nil { -// return err -// } - -// cmd.val.aggregateResult.Fields[key] = value -// } -// cmd.val.profileResult = *NewKeyValueSliceCmd(cmd.ctx, cmd.args...) -// return nil - -// } - -// For more details about spellcheck query please follow: -// https://redis.io/docs/interact/search-and-query/advanced-concepts/spellcheck/ -func (c cmdable) FTSpellCheck(ctx context.Context, index string, query string) *MapStringInterfaceCmd { - cmd := NewMapStringInterfaceCmd(ctx, "FT.SPELLCHECK", index, query) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTSpellCheckWithArgs(ctx context.Context, index string, query string, options *FTSpellCheckOptions) *MapStringInterfaceCmd { - 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.Dialect > 0 { - args = append(args, "DIALECT", options.Dialect) - } - } - cmd := NewMapStringInterfaceCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTSearch(ctx context.Context, index string, query string) *Cmd { - args := []interface{}{"FT.SEARCH", index, query} - cmd := NewCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *Cmd { - args := []interface{}{"FT.SEARCH", index, query} - if options != nil { - if options.NoContent { - args = append(args, "NOCONTENT") - } - if options.Verbatim { - args = append(args, "VERBATIM") - } - if options.NoStopWrods { - args = append(args, "NOSTOPWORDS") - } - if options.WithScores { - args = append(args, "WITHSCORES") - } - if options.WithPayloads { - args = append(args, "WITHPAYLOADS") - } - if options.WithSortKeys { - args = append(args, "WITHSORTKEYS") - } - if options.Filters != nil { - for _, filter := range options.Filters { - args = append(args, "FILTER", filter.FieldName, filter.Min, filter.Max) - } - } - if options.GeoFilter != nil { - for _, geoFilter := range options.GeoFilter { - args = append(args, "GEOFILTER", geoFilter.FieldName, geoFilter.Longitude, geoFilter.Latitude, geoFilter.Radius, geoFilter.Unit) - } - } - if options.InKeys != nil { - args = append(args, "INKEYS", len(options.InKeys)) - args = append(args, options.InKeys...) - } - if options.InFields != nil { - args = append(args, "INFIELDS", len(options.InFields)) - args = append(args, options.InFields...) - } - if options.Return != nil { - args = append(args, "RETURN", len(options.Return)) - for _, ret := range options.Return { - args = append(args, ret.FieldName) - if ret.As != "" { - args = append(args, "AS", ret.As) - } - } - } - if options.Slop > 0 { - args = append(args, "SLOP", options.Slop) - } - if options.Timeout > 0 { - args = append(args, "TIMEOUT", options.Timeout) - } - if options.InOrder { - args = append(args, "INORDER") - } - if options.Language != "" { - args = append(args, "LANGUAGE", options.Language) - } - if options.Expander != "" { - args = append(args, "EXPANDER", options.Expander) - } - if options.Scorer != "" { - args = append(args, "SCORER", options.Scorer) - } - if options.ExplainScore { - args = append(args, "EXPLAINSCORE") - } - if options.Payload != "" { - args = append(args, "PAYLOAD", options.Payload) - } - if options.SortBy != nil { - args = append(args, "SORTBY") - for _, sortBy := range options.SortBy { - args = append(args, sortBy.FieldName) - if sortBy.Asc && sortBy.Desc { - panic("FT.SEARCH: ASC and DESC are mutually exclusive") - } - if sortBy.Asc { - args = append(args, "ASC") - } - if sortBy.Desc { - args = append(args, "DESC") - } - } - if options.SortByWithCount { - args = append(args, "WITHCOUT") - } - } - if options.LimitOffset >= 0 && options.Limit > 0 { - args = append(args, "LIMIT", options.LimitOffset, options.Limit) - } - if options.Params != nil { - args = append(args, "PARAMS", len(options.Params)*2) - for key, value := range options.Params { - args = append(args, key, value) - } - } - if options.DialectVersion > 0 { - args = append(args, "DIALECT", options.DialectVersion) - } - } - cmd := NewCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTSynDump(ctx context.Context, index string) *StringSliceCmd { - cmd := NewStringSliceCmd(ctx, "FT.SYNDUMP", index) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTSynUpdate(ctx context.Context, index string, synGroupId interface{}, terms []interface{}) *StatusCmd { - args := []interface{}{"FT.SYNUPDATE", index, synGroupId} - args = append(args, terms...) - cmd := NewStatusCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTSynUpdateWithArgs(ctx context.Context, index string, synGroupId interface{}, options *FTSynUpdateOptions, terms []interface{}) *StatusCmd { - args := []interface{}{"FT.SYNUPDATE", index, synGroupId} - if options.SkipInitialScan { - args = append(args, "SKIPINITIALSCAN") - } - args = append(args, terms...) - cmd := NewStatusCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) FTTagVals(ctx context.Context, index string, field string) *StringSliceCmd { - cmd := NewStringSliceCmd(ctx, "FT.TAGVALS", index, field) - _ = c(ctx, cmd) - return cmd -} diff --git a/redis_search_test.go b/redis_search_test.go deleted file mode 100644 index 994bbbb6..00000000 --- a/redis_search_test.go +++ /dev/null @@ -1,567 +0,0 @@ -package redis_test - -import ( - "context" - - . "github.com/bsm/ginkgo/v2" - . "github.com/bsm/gomega" - "github.com/redis/go-redis/v9" -) - -var _ = Describe("RediSearch commands", Label("search"), func() { - ctx := context.TODO() - var client *redis.Client - - BeforeEach(func() { - client = redis.NewClient(&redis.Options{Addr: ":6379"}) - Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - Expect(client.Close()).NotTo(HaveOccurred()) - }) - - It("should FTCreate and FTSearch WithScores ", Label("search", "ftcreate", "ftsearch"), func() { - val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.SearchSchema{Identifier: "txt", AttributeType: "TEXT"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - 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()) - 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")) - }) - - 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.SearchSchema{Identifier: "txt", AttributeType: "TEXT"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - client.HSet(ctx, "doc1", "txt", "foo baz") - client.HSet(ctx, "doc2", "txt", "hello world") - res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{NoContent: true}).Result() - Expect(err).NotTo(HaveOccurred()) - searchResult1 := res1.(map[interface{}]interface{}) - Expect(searchResult1["total_results"]).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))) - - }) - - It("should FTCreate and FTSearch filters ", Label("search", "ftcreate", "ftsearch"), func() { - val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.SearchSchema{Identifier: "txt", AttributeType: "TEXT"}, &redis.SearchSchema{Identifier: "num", AttributeType: "NUMERIC"}, &redis.SearchSchema{Identifier: "loc", AttributeType: "GEO"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - client.HSet(ctx, "doc1", "txt", "foo bar", "num", 3.141, "loc", "-0.441,51.458") - 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")) - 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")) - // 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")) - 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(docs).To(ContainElement("doc1")) - Expect(docs).To(ContainElement("doc2")) - - }) - - It("should FTCreate and FTSearch sortby ", Label("search", "ftcreate", "ftsearch"), func() { - val, err := client.FTCreate(ctx, "num", &redis.FTCreateOptions{}, &redis.SearchSchema{Identifier: "txt", AttributeType: "TEXT"}, &redis.SearchSchema{Identifier: "num", AttributeType: "NUMERIC", Sortable: true}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - client.HSet(ctx, "doc1", "txt", "foo bar", "num", 1) - client.HSet(ctx, "doc2", "txt", "foo baz", "num", 2) - client.HSet(ctx, "doc3", "txt", "foo qux", "num", 3) - - sortBy1 := redis.FTSearchSortBy{FieldName: "num", Asc: true} - 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")) - - 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")) - - }) - - It("should FTCreate and FTSearch example ", Label("search", "ftcreate", "ftsearch"), func() { - val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.SearchSchema{Identifier: "title", AttributeType: "TEXT", Weight: 5}, &redis.SearchSchema{Identifier: "body", AttributeType: "TEXT"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - 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()) - - }) - - It("should FTCreate NoIndex ", Label("search", "ftcreate", "ftsearch"), func() { - text1 := &redis.SearchSchema{Identifier: "field", AttributeType: "TEXT"} - text2 := &redis.SearchSchema{Identifier: "text", AttributeType: "TEXT", NoIndex: true, Sortable: true} - num := &redis.SearchSchema{Identifier: "numeric", AttributeType: "NUMERIC", NoIndex: true, Sortable: true} - geo := &redis.SearchSchema{Identifier: "geo", AttributeType: "GEO", NoIndex: true, Sortable: true} - tag := &redis.SearchSchema{Identifier: "tag", AttributeType: "TAG", NoIndex: true, Sortable: true} - val, err := client.FTCreate(ctx, "idx", &redis.FTCreateOptions{}, text1, text2, num, geo, tag).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - client.HSet(ctx, "doc1", "field", "aaa", "text", "1", "numeric", 1, "geo", "1,1", "tag", "1") - 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))) - res2, err := client.FTSearch(ctx, "idx", "@field:aa*").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res2.(map[interface{}]interface{})["total_results"]).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")) - 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")) - 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")) - 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")) - 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")) - - }) - - It("should FTExplain ", Label("search", "ftexplain"), func() { - text1 := &redis.SearchSchema{Identifier: "f1", AttributeType: "TEXT"} - text2 := &redis.SearchSchema{Identifier: "f2", AttributeType: "TEXT"} - text3 := &redis.SearchSchema{Identifier: "f3", AttributeType: "TEXT"} - val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, text1, text2, text3).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - res1, err := client.FTExplain(ctx, "txt", "@f3:f3_val @f2:f2_val @f1:f1_val").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res1).ToNot(BeEmpty()) - - }) - - It("should FTAlias ", Label("search", "ftexplain"), func() { - text1 := &redis.SearchSchema{Identifier: "name", AttributeType: "TEXT"} - text2 := &redis.SearchSchema{Identifier: "name", AttributeType: "TEXT"} - val1, err := client.FTCreate(ctx, "testAlias", &redis.FTCreateOptions{Prefix: []interface{}{"index1:"}}, text1).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val1).To(BeEquivalentTo("OK")) - val2, err := client.FTCreate(ctx, "testAlias2", &redis.FTCreateOptions{Prefix: []interface{}{"index2:"}}, text2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val2).To(BeEquivalentTo("OK")) - - client.HSet(ctx, "index1:lonestar", "name", "lonestar") - client.HSet(ctx, "index2:yogurt", "name", "yogurt") - - 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")) - - aliasAddRes, err := client.FTAliasAdd(ctx, "testAlias", "mj23").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(aliasAddRes).To(BeEquivalentTo("OK")) - - 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")) - - aliasUpdateRes, err := client.FTAliasUpdate(ctx, "testAlias2", "kb24").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(aliasUpdateRes).To(BeEquivalentTo("OK")) - - 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")) - - aliasDelRes, err := client.FTAliasDel(ctx, "mj23").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(aliasDelRes).To(BeEquivalentTo("OK")) - - }) - - It("should FTCreate and FTSearch textfield, sortable and nostem ", Label("search", "ftcreate", "ftsearch"), func() { - val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.SearchSchema{Identifier: "txt", AttributeType: "TEXT", Sortable: true, NoStem: true}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - - resInfo, err := client.FTInfo(ctx, "idx1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resInfo["attributes"].([]interface{})[0].(map[interface{}]interface{})["flags"]).To(ContainElements("SORTABLE", "NOSTEM")) - - }) - - It("should FTAlter ", Label("search", "ftcreate", "ftsearch", "ftalter"), func() { - val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.SearchSchema{Identifier: "txt", AttributeType: "TEXT"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - - resAlter, err := client.FTAlter(ctx, "idx1", false, []interface{}{"body", "TEXT"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resAlter).To(BeEquivalentTo("OK")) - - 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))) - - }) - - It("should FTSpellCheck", Label("search", "ftcreate", "ftsearch", "ftspellcheck"), func() { - text1 := &redis.SearchSchema{Identifier: "f1", AttributeType: "TEXT"} - text2 := &redis.SearchSchema{Identifier: "f2", AttributeType: "TEXT"} - val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - - client.HSet(ctx, "doc1", "f1", "some valid content", "f2", "this is sample text") - client.HSet(ctx, "doc2", "f1", "very important", "f2", "lorem ipsum") - - resSpellCheck, err := client.FTSpellCheck(ctx, "idx1", "impornant").Result() - Expect(err).NotTo(HaveOccurred()) - res := resSpellCheck["results"].(map[interface{}]interface{})["impornant"].([]interface{})[0].(map[interface{}]interface{}) - Expect("important").To(BeKeyOf(res)) - - 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)) - - // 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{}{}})) - - 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{}))) - - // test spellcheck include - dict := []interface{}{"lore", "lorem", "lorm"} - resDictAdd, err := client.FTDictAdd(ctx, "dict", dict).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resDictAdd).To(BeEquivalentTo(3)) - terms := redis.SpellCheckTerms{Include: true, 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{}) - 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)) - - terms2 := redis.SpellCheckTerms{Exclude: true, Dictionary: "dict"} - resSpellCheck6, err := client.FTSpellCheckWithArgs(ctx, "idx1", "lorm", &redis.FTSpellCheckOptions{Terms: terms2}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resSpellCheck6["results"]).To(BeEmpty()) - }) - - It("should FTDict opreations ", Label("search", "ftdictdump", "ftdictdel", "ftdictadd"), func() { - text1 := &redis.SearchSchema{Identifier: "f1", AttributeType: "TEXT"} - text2 := &redis.SearchSchema{Identifier: "f2", AttributeType: "TEXT"} - val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - - dict := []interface{}{"item1", "item2", "item3"} - resDictAdd, err := client.FTDictAdd(ctx, "custom_dict", dict).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resDictAdd).To(BeEquivalentTo(3)) - - resDictDel, err := client.FTDictDel(ctx, "custom_dict", []interface{}{"item2"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resDictDel).To(BeEquivalentTo(1)) - - resDictDump, err := client.FTDictDump(ctx, "custom_dict").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resDictDump).To(BeEquivalentTo([]string{"item1", "item3"})) - - resDictDel2, err := client.FTDictDel(ctx, "custom_dict", []interface{}{"item1", "item3"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resDictDel2).To(BeEquivalentTo(2)) - }) - - It("should FTSearch phonetic matcher ", Label("search", "ftsearch"), func() { - text1 := &redis.SearchSchema{Identifier: "name", AttributeType: "TEXT"} - val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - - client.HSet(ctx, "doc1", "name", "Jon") - client.HSet(ctx, "doc2", "name", "John") - - 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")) - - client.FlushDB(ctx) - text2 := &redis.SearchSchema{Identifier: "name", AttributeType: "TEXT", PhoneticMatcher: "dm:en"} - val2, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val2).To(BeEquivalentTo("OK")) - - client.HSet(ctx, "doc1", "name", "Jon") - client.HSet(ctx, "doc2", "name", "John") - - 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(names).To(ContainElement("Jon")) - Expect(names).To(ContainElement("John")) - }) - - It("should FTSearch WithScores", Label("search", "ftsearch"), func() { - text1 := &redis.SearchSchema{Identifier: "description", AttributeType: "TEXT"} - val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - - client.HSet(ctx, "doc1", "description", "The quick brown fox jumps over the lazy dog") - client.HSet(ctx, "doc2", "description", "Quick alice was beginning to get very tired of sitting by her quick sister on the bank, and of having nothing to do.") - - 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)) - - 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)) - - 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.1111111111111111)) - - 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.17699114465425977)) - - 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)) - - 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)) - - 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)) - }) - - It("should FTConfigSet and FTConfigGet ", Label("search", "ftconfigget", "ftconfigset"), func() { - val, err := client.FTConfigSet(ctx, "TIMEOUT", "100").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - - res, err := client.FTConfigGet(ctx, "*").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res["TIMEOUT"]).To(BeEquivalentTo("100")) - - res, err = client.FTConfigGet(ctx, "TIMEOUT").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(map[string]interface{}{"TIMEOUT": "100"})) - - }) - - It("should FTAggregate GroupBy ", Label("search", "ftaggregate"), func() { - text1 := &redis.SearchSchema{Identifier: "title", AttributeType: "TEXT"} - text2 := &redis.SearchSchema{Identifier: "body", AttributeType: "TEXT"} - text3 := &redis.SearchSchema{Identifier: "parent", AttributeType: "TEXT"} - num := &redis.SearchSchema{Identifier: "random_num", AttributeType: "NUMERIC"} - val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2, text3, num).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - - client.HSet(ctx, "search", "title", "RediSearch", - "body", "Redisearch impements a search engine on top of redis", - "parent", "redis", - "random_num", 10) - client.HSet(ctx, "ai", "title", "RedisAI", - "body", "RedisAI executes Deep Learning/Machine Learning models and managing their data.", - "parent", "redis", - "random_num", 3) - client.HSet(ctx, "json", "title", "RedisJson", - "body", "RedisJSON implements ECMA-404 The JSON Data Interchange Standard as a native data type.", - "parent", "redis", - "random_num", 8) - - reducer := redis.FTAggregateReducer{Reducer: redis.SearchCount} - 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")) - - 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")) - - 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")) - - 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")) - - 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")) - - 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")) - - 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")) - - 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")) - - 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")) - - 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")) - - 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"})) - - }) - It("should FTAggregate sort and limit ", Label("search", "ftaggregate"), func() { - text1 := &redis.SearchSchema{Identifier: "t1", AttributeType: "TEXT"} - text2 := &redis.SearchSchema{Identifier: "t2", AttributeType: "TEXT"} - val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(val).To(BeEquivalentTo("OK")) - - client.HSet(ctx, "doc1", "t1", "a", "t2", "b") - client.HSet(ctx, "doc2", "t1", "b", "t2", "a") - - 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"})) - - 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"})) - - 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)) - - 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"})) - }) -}) diff --git a/search_commands.go b/search_commands.go new file mode 100644 index 00000000..a8251655 --- /dev/null +++ b/search_commands.go @@ -0,0 +1,1215 @@ +package redis + +import ( + "context" + "strings" +) + +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 + 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 + FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd + FTCreate(ctx context.Context, index string, options *FTCreateOptions, schema ...*SearchSchema) *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 + 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 + FTProfileSearch(ctx context.Context, index string, limited bool, query SearchQuery) *MapStringInterfaceCmd + FTProfileAggregate(ctx context.Context, index string, limited bool, query AggregateQuery) *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 + 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 +} + +type FTCreateOptions struct { + OnHash bool + OnJSON bool + Prefix []interface{} + Filter string + DefaultLanguage string + LanguageField string + Score float64 + ScoreField string + PayloadField string + MaxTextFields int + NoOffsets bool + Temporary int + NoHL bool + NoFields bool + NoFreqs bool + StopWords []interface{} + SkipInitalScan bool +} + +type SearchSchema struct { + FieldName string + As string + FieldType string + Sortable bool + UNF bool + NoStem bool + NoIndex bool + PhoneticMatcher string + Weight float64 + Seperator string + CaseSensitive bool + WithSuffixtrie bool + VectorArgs *FTVectorArgs + GeoShapeFieldType string +} + +type FTVectorArgs struct { + FlatOptions *FTFlatOptions + HNSWOptions *FTHNSWOptions +} + +type FTFlatOptions struct { + Type string + Dim int + DistanceMetric string + InitialCapacity int + BlockSize int +} + +type FTHNSWOptions struct { + Type string + Dim int + DistanceMetric string + InitialCapacity int + MaxEdgesPerNode int + MaxAllowedEdgesPerNode int + EFRunTime int + Epsilon float64 +} + +type FTDropIndexOptions struct { + DeleteDocs bool +} + +type SpellCheckTerms struct { + Include bool + Exclude bool + Dictionary string +} + +type FTSpellCheckOptions struct { + Distance int + Terms SpellCheckTerms + Dialect int +} + +type FTExplainOptions struct { + Dialect string +} + +type FTSynUpdateOptions struct { + SkipInitialScan bool +} + +type SearchAggregator int + +const ( + SearchInvalid = SearchAggregator(iota) + SearchAvg + SearchSum + SearchMin + SearchMax + SearchCount + SearchCountDistinct + SearchCountDistinctish + SearchStdDev + SearchQuantile + SearchToList + SearchFirstValue + SearchRandomSample +) + +func (a SearchAggregator) String() string { + switch a { + case SearchInvalid: + return "" + case SearchAvg: + return "AVG" + case SearchSum: + return "SUM" + case SearchMin: + return "MIN" + case SearchMax: + return "MAX" + case SearchCount: + return "COUNT" + case SearchCountDistinct: + return "COUNT_DISTINCT" + case SearchCountDistinctish: + return "COUNT_DISTINCTISH" + case SearchStdDev: + return "STDDEV" + case SearchQuantile: + return "QUANTILE" + case SearchToList: + return "TOLIST" + case SearchFirstValue: + return "FIRST_VALUE" + case SearchRandomSample: + return "RANDOM_SAMPLE" + default: + return "" + } +} + +// Each AggregateReducer have different args. +// Please follow https://redis.io/docs/interact/search-and-query/search/aggregations/#supported-groupby-reducers for more information. +type FTAggregateReducer struct { + Reducer SearchAggregator + Args []interface{} + As string +} + +type FTAggregateGroupBy struct { + Fields []interface{} + Reduce []FTAggregateReducer +} + +type FTAggregateSortBy struct { + FieldName string + Asc bool + Desc bool +} + +type FTAggregateApply struct { + Field string + As string +} + +type FTAggregateLoad struct { + Field string + As string +} + +type FTAggregateWithCursor struct { + Count int + MaxIdle int +} + +type FTAggregateOptions struct { + Verbatim bool + LoadAll bool + Load []FTAggregateLoad + Timeout int + GroupBy []FTAggregateGroupBy + SortBy []FTAggregateSortBy + SortByMax int + Apply []FTAggregateApply + LimitOffset int + Limit int + Filter string + WithCursor bool + WithCursorOptions *FTAggregateWithCursor + Params map[string]interface{} + DialectVersion int +} + +type FTSearchFilter struct { + FieldName interface{} + Min interface{} + Max interface{} +} + +type FTSearchGeoFilter struct { + FieldName string + Longitude float64 + Latitude float64 + Radius float64 + Unit string +} + +type FTSearchReturn struct { + FieldName string + As string +} + +type FTSearchSortBy struct { + FieldName string + Asc bool + Desc bool +} + +type FTSearchOptions struct { + NoContent bool + Verbatim bool + NoStopWrods bool + WithScores bool + WithPayloads bool + WithSortKeys bool + Filters []FTSearchFilter + GeoFilter []FTSearchGeoFilter + InKeys []interface{} + InFields []interface{} + Return []FTSearchReturn + Slop int + Timeout int + InOrder bool + Language string + Expander string + Scorer string + ExplainScore bool + Payload string + SortBy []FTSearchSortBy + SortByWithCount bool + LimitOffset int + Limit int + Params map[string]interface{} + DialectVersion int +} + +// 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/) +func (c cmdable) FT_List(ctx context.Context) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "FT._LIST") + _ = c(ctx, cmd) + return cmd +} + +// FTAggregate - 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. +// For more information, please refer to the Redis documentation: [FT.AGGREGATE](https://redis.io/commands/ft.aggregate/) +func (c cmdable) FTAggregate(ctx context.Context, index string, query string) *MapStringInterfaceCmd { + args := []interface{}{"FT.AGGREGATE", index, query} + cmd := NewMapStringInterfaceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +type AggregateQuery []interface{} + +func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery { + queryArgs := []interface{}{query} + if options != nil { + if options.Verbatim { + queryArgs = append(queryArgs, "VERBATIM") + } + if options.LoadAll && options.Load != nil { + panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") + } + if options.LoadAll { + queryArgs = append(queryArgs, "LOAD", "*") + } + if options.Load != nil { + queryArgs = append(queryArgs, "LOAD", len(options.Load)) + for _, load := range options.Load { + queryArgs = append(queryArgs, load.Field) + if load.As != "" { + queryArgs = append(queryArgs, "AS", load.As) + } + } + } + if options.Timeout > 0 { + queryArgs = append(queryArgs, "TIMEOUT", options.Timeout) + } + if options.GroupBy != nil { + for _, groupBy := range options.GroupBy { + queryArgs = append(queryArgs, "GROUPBY", len(groupBy.Fields)) + queryArgs = append(queryArgs, groupBy.Fields...) + + for _, reducer := range groupBy.Reduce { + queryArgs = append(queryArgs, "REDUCE") + queryArgs = append(queryArgs, reducer.Reducer.String()) + if reducer.Args != nil { + queryArgs = append(queryArgs, len(reducer.Args)) + queryArgs = append(queryArgs, reducer.Args...) + } else { + queryArgs = append(queryArgs, 0) + } + if reducer.As != "" { + queryArgs = append(queryArgs, "AS", reducer.As) + } + } + } + } + if options.SortBy != nil { + queryArgs = append(queryArgs, "SORTBY") + sortByOptions := []interface{}{} + for _, sortBy := range options.SortBy { + sortByOptions = append(sortByOptions, sortBy.FieldName) + if sortBy.Asc && sortBy.Desc { + panic("FT.AGGREGATE: ASC and DESC are mutually exclusive") + } + if sortBy.Asc { + sortByOptions = append(sortByOptions, "ASC") + } + if sortBy.Desc { + sortByOptions = append(sortByOptions, "DESC") + } + } + queryArgs = append(queryArgs, len(sortByOptions)) + queryArgs = append(queryArgs, sortByOptions...) + } + if options.SortByMax > 0 { + queryArgs = append(queryArgs, "MAX", options.SortByMax) + } + for _, apply := range options.Apply { + queryArgs = append(queryArgs, "APPLY", apply.Field) + if apply.As != "" { + queryArgs = append(queryArgs, "AS", apply.As) + } + } + if options.LimitOffset > 0 { + queryArgs = append(queryArgs, "LIMIT", options.LimitOffset) + } + if options.Limit > 0 { + queryArgs = append(queryArgs, options.Limit) + } + if options.Filter != "" { + queryArgs = append(queryArgs, "FILTER", options.Filter) + } + if options.WithCursor { + queryArgs = append(queryArgs, "WITHCURSOR") + if options.WithCursorOptions != nil { + if options.WithCursorOptions.Count > 0 { + queryArgs = append(queryArgs, "COUNT", options.WithCursorOptions.Count) + } + if options.WithCursorOptions.MaxIdle > 0 { + queryArgs = append(queryArgs, "MAXIDLE", options.WithCursorOptions.MaxIdle) + } + } + } + if options.Params != nil { + queryArgs = append(queryArgs, "PARAMS", len(options.Params)*2) + for key, value := range options.Params { + queryArgs = append(queryArgs, key, value) + } + } + if options.DialectVersion > 0 { + queryArgs = append(queryArgs, "DIALECT", options.DialectVersion) + } + } + return queryArgs +} + +// 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 { + args := []interface{}{"FT.AGGREGATE", index, query} + if options != nil { + if options.Verbatim { + args = append(args, "VERBATIM") + } + if options.LoadAll && options.Load != nil { + panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") + } + if options.LoadAll { + args = append(args, "LOAD", "*") + } + if options.Load != nil { + args = append(args, "LOAD", len(options.Load)) + for _, load := range options.Load { + args = append(args, load.Field) + if load.As != "" { + args = append(args, "AS", load.As) + } + } + } + if options.Timeout > 0 { + args = append(args, "TIMEOUT", options.Timeout) + } + if options.GroupBy != nil { + for _, groupBy := range options.GroupBy { + args = append(args, "GROUPBY", len(groupBy.Fields)) + args = append(args, groupBy.Fields...) + + for _, reducer := range groupBy.Reduce { + args = append(args, "REDUCE") + args = append(args, reducer.Reducer.String()) + if reducer.Args != nil { + args = append(args, len(reducer.Args)) + args = append(args, reducer.Args...) + } else { + args = append(args, 0) + } + if reducer.As != "" { + args = append(args, "AS", reducer.As) + } + } + } + } + if options.SortBy != nil { + args = append(args, "SORTBY") + sortByOptions := []interface{}{} + for _, sortBy := range options.SortBy { + sortByOptions = append(sortByOptions, sortBy.FieldName) + if sortBy.Asc && sortBy.Desc { + panic("FT.AGGREGATE: ASC and DESC are mutually exclusive") + } + if sortBy.Asc { + sortByOptions = append(sortByOptions, "ASC") + } + if sortBy.Desc { + sortByOptions = append(sortByOptions, "DESC") + } + } + args = append(args, len(sortByOptions)) + args = append(args, sortByOptions...) + } + if options.SortByMax > 0 { + args = append(args, "MAX", options.SortByMax) + } + for _, apply := range options.Apply { + args = append(args, "APPLY", apply.Field) + if apply.As != "" { + args = append(args, "AS", apply.As) + } + } + if options.LimitOffset > 0 { + args = append(args, "LIMIT", options.LimitOffset) + } + if options.Limit > 0 { + args = append(args, options.Limit) + } + if options.Filter != "" { + args = append(args, "FILTER", options.Filter) + } + if options.WithCursor { + args = append(args, "WITHCURSOR") + if options.WithCursorOptions != nil { + if options.WithCursorOptions.Count > 0 { + args = append(args, "COUNT", options.WithCursorOptions.Count) + } + if options.WithCursorOptions.MaxIdle > 0 { + args = append(args, "MAXIDLE", options.WithCursorOptions.MaxIdle) + } + } + } + if options.Params != nil { + args = append(args, "PARAMS", len(options.Params)*2) + for key, value := range options.Params { + args = append(args, key, value) + } + } + if options.DialectVersion > 0 { + args = append(args, "DIALECT", options.DialectVersion) + } + } + + cmd := NewMapStringInterfaceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTAliasAdd - Adds an alias to an index. +// The 'index' parameter specifies the index to which the alias is added, and the 'alias' parameter specifies the alias. +// For more information, please refer to the Redis documentation: [FT.ALIASADD](https://redis.io/commands/ft.aliasadd/) +func (c cmdable) FTAliasAdd(ctx context.Context, index string, alias string) *StatusCmd { + args := []interface{}{"FT.ALIASADD", alias, index} + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTAliasDel - Removes an alias from an index. +// The 'alias' parameter specifies the alias to be removed. +// For more information, please refer to the Redis documentation: [FT.ALIASDEL](https://redis.io/commands/ft.aliasdel/) +func (c cmdable) FTAliasDel(ctx context.Context, alias string) *StatusCmd { + cmd := NewStatusCmd(ctx, "FT.ALIASDEL", alias) + _ = c(ctx, cmd) + return cmd +} + +// FTAliasUpdate - Updates an alias to an index. +// The 'index' parameter specifies the index to which the alias is updated, and the 'alias' parameter specifies the alias. +// If the alias already exists for a different index, it updates the alias to point to the specified index instead. +// For more information, please refer to the Redis documentation: [FT.ALIASUPDATE](https://redis.io/commands/ft.aliasupdate/) +func (c cmdable) FTAliasUpdate(ctx context.Context, index string, alias string) *StatusCmd { + cmd := NewStatusCmd(ctx, "FT.ALIASUPDATE", alias, index) + _ = c(ctx, cmd) + return cmd +} + +// FTAlter - Alters the definition of an existing index. +// The 'index' parameter specifies the index to alter, and the 'skipInitalScan' parameter specifies whether to skip the initial scan. +// The 'definition' parameter specifies the new definition for the index. +// For more information, please refer to the Redis documentation: [FT.ALTER](https://redis.io/commands/ft.alter/) +func (c cmdable) FTAlter(ctx context.Context, index string, skipInitalScan bool, definition []interface{}) *StatusCmd { + args := []interface{}{"FT.ALTER", index} + if skipInitalScan { + args = append(args, "SKIPINITIALSCAN") + } + args = append(args, "SCHEMA", "ADD") + args = append(args, definition...) + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTConfigGet - Retrieves the value of a RediSearch configuration parameter. +// 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) + _ = c(ctx, cmd) + return cmd +} + +// FTConfigSet - Sets the value of a RediSearch configuration parameter. +// The 'option' parameter specifies the configuration parameter to set, and the 'value' parameter specifies the new value. +// For more information, please refer to the Redis documentation: [FT.CONFIG SET](https://redis.io/commands/ft.config-set/) +func (c cmdable) FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd { + cmd := NewStatusCmd(ctx, "FT.CONFIG", "SET", option, value) + _ = c(ctx, cmd) + return cmd +} + +// FTCreate - Creates a new index with the given options and schema. +// The 'index' parameter specifies the name of the index to create. +// The 'options' parameter specifies various options for the index, such as: +// whether to index hashes or JSONs, prefixes, filters, default language, score, score field, payload field, etc. +// The 'schema' parameter specifies the schema for the index, which includes the field name, field type, etc. +// For more information, please refer to the Redis documentation: [FT.CREATE](https://redis.io/commands/ft.create/) +func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOptions, schema ...*SearchSchema) *StatusCmd { + args := []interface{}{"FT.CREATE", index} + if options != nil { + if options.OnHash && !options.OnJSON { + args = append(args, "ON", "HASH") + } + if options.OnJSON && !options.OnHash { + args = append(args, "ON", "JSON") + } + if options.OnHash && options.OnJSON { + panic("FT.CREATE: ON HASH and ON JSON are mutually exclusive") + } + if options.Prefix != nil { + args = append(args, "PREFIX", len(options.Prefix)) + args = append(args, options.Prefix...) + } + if options.Filter != "" { + args = append(args, "FILTER", options.Filter) + } + if options.DefaultLanguage != "" { + args = append(args, "LANGUAGE", options.DefaultLanguage) + } + if options.LanguageField != "" { + args = append(args, "LANGUAGE_FIELD", options.LanguageField) + } + if options.Score > 0 { + args = append(args, "SCORE", options.Score) + } + if options.ScoreField != "" { + args = append(args, "SCORE_FIELD", options.ScoreField) + } + if options.PayloadField != "" { + args = append(args, "PAYLOAD_FIELD", options.PayloadField) + } + if options.MaxTextFields > 0 { + args = append(args, "MAXTEXTFIELDS", options.MaxTextFields) + } + if options.NoOffsets { + args = append(args, "NOOFFSETS") + } + if options.Temporary > 0 { + args = append(args, "TEMPORARY", options.Temporary) + } + if options.NoHL { + args = append(args, "NOHL") + } + if options.NoFields { + args = append(args, "NOFIELDS") + } + if options.NoFreqs { + args = append(args, "NOFREQS") + } + if options.StopWords != nil { + args = append(args, "STOPWORDS", len(options.StopWords)) + args = append(args, options.StopWords...) + } + if options.SkipInitalScan { + args = append(args, "SKIPINITIALSCAN") + } + } + if schema == nil { + panic("FT.CREATE: SCHEMA is required") + } + args = append(args, "SCHEMA") + for _, schema := range schema { + if schema.FieldName == "" || schema.FieldType == "" { + panic("FT.CREATE: SCHEMA FieldName and FieldType are required") + } + args = append(args, schema.FieldName) + if schema.As != "" { + args = append(args, "AS", schema.As) + } + args = append(args, schema.FieldType) + if schema.VectorArgs != nil { + if strings.ToUpper(schema.FieldType) != "VECTOR" { + panic("FT.CREATE: SCHEMA FieldType VECTOR is required for VectorArgs") + } + if schema.VectorArgs.FlatOptions != nil && schema.VectorArgs.HNSWOptions != nil { + panic("FT.CREATE: SCHEMA VectorArgs FlatOptions and HNSWOptions are mutually exclusive") + } + if schema.VectorArgs.FlatOptions != nil { + args = append(args, "FLAT") + if schema.VectorArgs.FlatOptions.Type == "" || schema.VectorArgs.FlatOptions.Dim == 0 || schema.VectorArgs.FlatOptions.DistanceMetric == "" { + panic("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR FLAT") + } + flatArgs := []interface{}{ + "TYPE", schema.VectorArgs.FlatOptions.Type, + "DIM", schema.VectorArgs.FlatOptions.Dim, + "DISTANCE_METRIC", schema.VectorArgs.FlatOptions.DistanceMetric, + } + if schema.VectorArgs.FlatOptions.InitialCapacity > 0 { + flatArgs = append(flatArgs, "INITIAL_CAP", schema.VectorArgs.FlatOptions.InitialCapacity) + } + if schema.VectorArgs.FlatOptions.BlockSize > 0 { + flatArgs = append(flatArgs, "BLOCK_SIZE", schema.VectorArgs.FlatOptions.BlockSize) + } + args = append(args, len(flatArgs)) + args = append(args, flatArgs...) + } + if schema.VectorArgs.HNSWOptions != nil { + args = append(args, "HNSW") + if schema.VectorArgs.HNSWOptions.Type == "" || schema.VectorArgs.HNSWOptions.Dim == 0 || schema.VectorArgs.HNSWOptions.DistanceMetric == "" { + panic("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR HNSW") + } + hnswArgs := []interface{}{ + "TYPE", schema.VectorArgs.HNSWOptions.Type, + "DIM", schema.VectorArgs.HNSWOptions.Dim, + "DISTANCE_METRIC", schema.VectorArgs.HNSWOptions.DistanceMetric, + } + if schema.VectorArgs.HNSWOptions.InitialCapacity > 0 { + hnswArgs = append(hnswArgs, "INITIAL_CAP", schema.VectorArgs.HNSWOptions.InitialCapacity) + } + if schema.VectorArgs.HNSWOptions.MaxEdgesPerNode > 0 { + hnswArgs = append(hnswArgs, "M", schema.VectorArgs.HNSWOptions.MaxEdgesPerNode) + } + if schema.VectorArgs.HNSWOptions.MaxAllowedEdgesPerNode > 0 { + hnswArgs = append(hnswArgs, "EF_CONSTRUCTION", schema.VectorArgs.HNSWOptions.MaxAllowedEdgesPerNode) + } + if schema.VectorArgs.HNSWOptions.EFRunTime > 0 { + hnswArgs = append(hnswArgs, "EF_RUNTIME", schema.VectorArgs.HNSWOptions.EFRunTime) + } + if schema.VectorArgs.HNSWOptions.Epsilon > 0 { + hnswArgs = append(hnswArgs, "EPSILON", schema.VectorArgs.HNSWOptions.Epsilon) + } + args = append(args, len(hnswArgs)) + args = append(args, hnswArgs...) + } + } + if schema.GeoShapeFieldType != "" { + if strings.ToUpper(schema.FieldType) != "GEOSHAPE" { + panic("FT.CREATE: SCHEMA FieldType GEOSHAPE is required for GeoShapeFieldType") + } + args = append(args, schema.GeoShapeFieldType) + } + if schema.NoStem { + args = append(args, "NOSTEM") + } + if schema.Sortable { + args = append(args, "SORTABLE") + } + if schema.UNF { + args = append(args, "UNF") + } + if schema.NoIndex { + args = append(args, "NOINDEX") + } + if schema.PhoneticMatcher != "" { + args = append(args, "PHONETIC", schema.PhoneticMatcher) + } + if schema.Weight > 0 { + args = append(args, "WEIGHT", schema.Weight) + } + if schema.Seperator != "" { + args = append(args, "SEPERATOR", schema.Seperator) + } + if schema.CaseSensitive { + args = append(args, "CASESENSITIVE") + } + if schema.WithSuffixtrie { + args = append(args, "WITHSUFFIXTRIE") + } + } + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTCursorDel - Deletes a cursor from an existing index. +// The 'index' parameter specifies the index from which to delete the cursor, and the 'cursorId' parameter specifies the ID of the cursor to delete. +// For more information, please refer to the Redis documentation: [FT.CURSOR DEL](https://redis.io/commands/ft.cursor-del/) +func (c cmdable) FTCursorDel(ctx context.Context, index string, cursorId int) *StatusCmd { + cmd := NewStatusCmd(ctx, "FT.CURSOR", "DEL", index, cursorId) + _ = c(ctx, cmd) + return cmd +} + +// FTCursorRead - Reads the next results from an existing cursor. +// 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. +// For more information, please refer to the Redis documentation: [FT.CURSOR READ](https://redis.io/commands/ft.cursor-read/) +func (c cmdable) FTCursorRead(ctx context.Context, index string, cursorId int, count int) *MapStringInterfaceCmd { + args := []interface{}{"FT.CURSOR", "READ", index, cursorId} + if count > 0 { + args = append(args, "COUNT", count) + } + cmd := NewMapStringInterfaceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTDictAdd - Adds terms to a dictionary. +// 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 { + args := []interface{}{"FT.DICTADD", dict} + args = append(args, term...) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTDictDel - Deletes terms from a dictionary. +// 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 { + args := []interface{}{"FT.DICTDEL", dict} + args = append(args, term...) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTDictDump - Returns all terms in the specified dictionary. +// The 'dict' parameter specifies the dictionary from which to return the terms. +// For more information, please refer to the Redis documentation: [FT.DICTDUMP](https://redis.io/commands/ft.dictdump/) +func (c cmdable) FTDictDump(ctx context.Context, dict string) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "FT.DICTDUMP", dict) + _ = c(ctx, cmd) + return cmd +} + +// FTDropIndex - Deletes an index. +// The 'index' parameter specifies the index to delete. +// For more information, please refer to the Redis documentation: [FT.DROPINDEX](https://redis.io/commands/ft.dropindex/) +func (c cmdable) FTDropIndex(ctx context.Context, index string) *StatusCmd { + args := []interface{}{"FT.DROPINDEX", index} + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTDropIndexWithArgs - Deletes an index with options. +// The 'index' parameter specifies the index to delete, and the 'options' parameter specifies the DeleteDocs option for docs deletion. +// For more information, please refer to the Redis documentation: [FT.DROPINDEX](https://redis.io/commands/ft.dropindex/) +func (c cmdable) FTDropIndexWithArgs(ctx context.Context, index string, options *FTDropIndexOptions) *StatusCmd { + args := []interface{}{"FT.DROPINDEX", index} + if options != nil { + if options.DeleteDocs { + args = append(args, "DD") + } + } + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTExplain - Returns the execution plan for a complex query. +// The 'index' parameter specifies the index to query, and the 'query' parameter specifies the query string. +// For more information, please refer to the Redis documentation: [FT.EXPLAIN](https://redis.io/commands/ft.explain/) +func (c cmdable) FTExplain(ctx context.Context, index string, query string) *StringCmd { + cmd := NewStringCmd(ctx, "FT.EXPLAIN", index, query) + _ = c(ctx, cmd) + return cmd +} + +// FTExplainWithArgs - Returns the execution plan for a complex query with options. +// 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. +// For more information, please refer to the Redis documentation: [FT.EXPLAIN](https://redis.io/commands/ft.explain/) +func (c cmdable) FTExplainWithArgs(ctx context.Context, index string, query string, options *FTExplainOptions) *StringCmd { + args := []interface{}{"FT.EXPLAIN", index, query} + if options.Dialect != "" { + args = append(args, "DIALECT", options.Dialect) + } + cmd := NewStringCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// 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 +} + +// 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 query. +// For more information, please refer to the Redis documentation: [FT.PROFILE SEARCH](https://redis.io/commands/ft.profile/) +func (c cmdable) FTProfileSearch(ctx context.Context, index string, limited bool, query SearchQuery) *MapStringInterfaceCmd { + args := []interface{}{"FT.PROFILE", index, "SEARCH"} + if limited { + args = append(args, "LIMITED") + } + args = append(args, "QUERY") + args = append(args, query...) + + cmd := NewMapStringInterfaceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTProfileAggregate - Executes an aggregate 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 aggregate query. +// For more information, please refer to the Redis documentation: [FT.PROFILE AGGREGATE](https://redis.io/commands/ft.aggregate/) +func (c cmdable) FTProfileAggregate(ctx context.Context, index string, limited bool, query AggregateQuery) *MapStringInterfaceCmd { + args := []interface{}{"FT.PROFILE", index, "AGGREGATE"} + if limited { + args = append(args, "LIMITED") + } + args = append(args, "QUERY") + args = append(args, query...) + + cmd := NewMapStringInterfaceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTSpellCheck - Checks a query string for spelling errors. +// For more details about spellcheck query please follow: +// 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) + _ = c(ctx, cmd) + return cmd +} + +// FTSpellCheckWithArgs - Checks a query string for spelling errors with additional options. +// For more details about spellcheck query please follow: +// 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 { + 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.Dialect > 0 { + args = append(args, "DIALECT", options.Dialect) + } + } + cmd := NewMapStringInterfaceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// 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 { + args := []interface{}{"FT.SEARCH", index, query} + cmd := NewCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +type SearchQuery []interface{} + +func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { + queryArgs := []interface{}{query} + if options != nil { + if options.NoContent { + queryArgs = append(queryArgs, "NOCONTENT") + } + if options.Verbatim { + queryArgs = append(queryArgs, "VERBATIM") + } + if options.NoStopWrods { + queryArgs = append(queryArgs, "NOSTOPWORDS") + } + if options.WithScores { + queryArgs = append(queryArgs, "WITHSCORES") + } + if options.WithPayloads { + queryArgs = append(queryArgs, "WITHPAYLOADS") + } + if options.WithSortKeys { + queryArgs = append(queryArgs, "WITHSORTKEYS") + } + if options.Filters != nil { + for _, filter := range options.Filters { + queryArgs = append(queryArgs, "FILTER", filter.FieldName, filter.Min, filter.Max) + } + } + if options.GeoFilter != nil { + for _, geoFilter := range options.GeoFilter { + queryArgs = append(queryArgs, "GEOFILTER", geoFilter.FieldName, geoFilter.Longitude, geoFilter.Latitude, geoFilter.Radius, geoFilter.Unit) + } + } + if options.InKeys != nil { + queryArgs = append(queryArgs, "INKEYS", len(options.InKeys)) + queryArgs = append(queryArgs, options.InKeys...) + } + if options.InFields != nil { + queryArgs = append(queryArgs, "INFIELDS", len(options.InFields)) + queryArgs = append(queryArgs, options.InFields...) + } + if options.Return != nil { + queryArgs = append(queryArgs, "RETURN") + queryArgsReturn := []interface{}{} + for _, ret := range options.Return { + queryArgsReturn = append(queryArgsReturn, ret.FieldName) + if ret.As != "" { + queryArgsReturn = append(queryArgsReturn, "AS", ret.As) + } + } + queryArgs = append(queryArgs, len(queryArgsReturn)) + queryArgs = append(queryArgs, queryArgsReturn...) + } + if options.Slop > 0 { + queryArgs = append(queryArgs, "SLOP", options.Slop) + } + if options.Timeout > 0 { + queryArgs = append(queryArgs, "TIMEOUT", options.Timeout) + } + if options.InOrder { + queryArgs = append(queryArgs, "INORDER") + } + if options.Language != "" { + queryArgs = append(queryArgs, "LANGUAGE", options.Language) + } + if options.Expander != "" { + queryArgs = append(queryArgs, "EXPANDER", options.Expander) + } + if options.Scorer != "" { + queryArgs = append(queryArgs, "SCORER", options.Scorer) + } + if options.ExplainScore { + queryArgs = append(queryArgs, "EXPLAINSCORE") + } + if options.Payload != "" { + queryArgs = append(queryArgs, "PAYLOAD", options.Payload) + } + if options.SortBy != nil { + queryArgs = append(queryArgs, "SORTBY") + for _, sortBy := range options.SortBy { + queryArgs = append(queryArgs, sortBy.FieldName) + if sortBy.Asc && sortBy.Desc { + panic("FT.SEARCH: ASC and DESC are mutually exclusive") + } + if sortBy.Asc { + queryArgs = append(queryArgs, "ASC") + } + if sortBy.Desc { + queryArgs = append(queryArgs, "DESC") + } + } + if options.SortByWithCount { + queryArgs = append(queryArgs, "WITHCOUT") + } + } + if options.LimitOffset >= 0 && options.Limit > 0 { + queryArgs = append(queryArgs, "LIMIT", options.LimitOffset, options.Limit) + } + if options.Params != nil { + queryArgs = append(queryArgs, "PARAMS", len(options.Params)*2) + for key, value := range options.Params { + queryArgs = append(queryArgs, key, value) + } + } + if options.DialectVersion > 0 { + queryArgs = append(queryArgs, "DIALECT", options.DialectVersion) + } + } + return queryArgs +} + +// FTSearchWithArgs - Executes a search query on an index with additional options. +// The 'index' parameter specifies the index to search, the 'query' parameter specifies the search query, +// and the 'options' parameter specifies additional options for the search. +// For more information, please refer to the Redis documentation: [FT.SEARCH](https://redis.io/commands/ft.search/) +func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *Cmd { + args := []interface{}{"FT.SEARCH", index, query} + if options != nil { + if options.NoContent { + args = append(args, "NOCONTENT") + } + if options.Verbatim { + args = append(args, "VERBATIM") + } + if options.NoStopWrods { + args = append(args, "NOSTOPWORDS") + } + if options.WithScores { + args = append(args, "WITHSCORES") + } + if options.WithPayloads { + args = append(args, "WITHPAYLOADS") + } + if options.WithSortKeys { + args = append(args, "WITHSORTKEYS") + } + if options.Filters != nil { + for _, filter := range options.Filters { + args = append(args, "FILTER", filter.FieldName, filter.Min, filter.Max) + } + } + if options.GeoFilter != nil { + for _, geoFilter := range options.GeoFilter { + args = append(args, "GEOFILTER", geoFilter.FieldName, geoFilter.Longitude, geoFilter.Latitude, geoFilter.Radius, geoFilter.Unit) + } + } + if options.InKeys != nil { + args = append(args, "INKEYS", len(options.InKeys)) + args = append(args, options.InKeys...) + } + if options.InFields != nil { + args = append(args, "INFIELDS", len(options.InFields)) + args = append(args, options.InFields...) + } + if options.Return != nil { + args = append(args, "RETURN") + argsReturn := []interface{}{} + for _, ret := range options.Return { + argsReturn = append(argsReturn, ret.FieldName) + if ret.As != "" { + argsReturn = append(argsReturn, "AS", ret.As) + } + } + args = append(args, len(argsReturn)) + args = append(args, argsReturn...) + } + if options.Slop > 0 { + args = append(args, "SLOP", options.Slop) + } + if options.Timeout > 0 { + args = append(args, "TIMEOUT", options.Timeout) + } + if options.InOrder { + args = append(args, "INORDER") + } + if options.Language != "" { + args = append(args, "LANGUAGE", options.Language) + } + if options.Expander != "" { + args = append(args, "EXPANDER", options.Expander) + } + if options.Scorer != "" { + args = append(args, "SCORER", options.Scorer) + } + if options.ExplainScore { + args = append(args, "EXPLAINSCORE") + } + if options.Payload != "" { + args = append(args, "PAYLOAD", options.Payload) + } + if options.SortBy != nil { + args = append(args, "SORTBY") + for _, sortBy := range options.SortBy { + args = append(args, sortBy.FieldName) + if sortBy.Asc && sortBy.Desc { + panic("FT.SEARCH: ASC and DESC are mutually exclusive") + } + if sortBy.Asc { + args = append(args, "ASC") + } + if sortBy.Desc { + args = append(args, "DESC") + } + } + if options.SortByWithCount { + args = append(args, "WITHCOUT") + } + } + if options.LimitOffset >= 0 && options.Limit > 0 { + args = append(args, "LIMIT", options.LimitOffset, options.Limit) + } + if options.Params != nil { + args = append(args, "PARAMS", len(options.Params)*2) + for key, value := range options.Params { + args = append(args, key, value) + } + } + if options.DialectVersion > 0 { + args = append(args, "DIALECT", options.DialectVersion) + } + } + cmd := NewCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTSynDump - Dumps the synonyms data structure. +// 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) + _ = c(ctx, cmd) + return cmd +} + +// FTSynUpdate - Creates or updates a synonym group with additional terms. +// The 'index' parameter specifies the index to update, the 'synGroupId' parameter specifies the synonym group id, and the 'terms' parameter specifies the additional terms. +// For more information, please refer to the Redis documentation: [FT.SYNUPDATE](https://redis.io/commands/ft.synupdate/) +func (c cmdable) FTSynUpdate(ctx context.Context, index string, synGroupId interface{}, terms []interface{}) *StatusCmd { + args := []interface{}{"FT.SYNUPDATE", index, synGroupId} + args = append(args, terms...) + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTSynUpdateWithArgs - Creates or updates a synonym group with additional terms and options. +// 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. +// For more information, please refer to the Redis documentation: [FT.SYNUPDATE](https://redis.io/commands/ft.synupdate/) +func (c cmdable) FTSynUpdateWithArgs(ctx context.Context, index string, synGroupId interface{}, options *FTSynUpdateOptions, terms []interface{}) *StatusCmd { + args := []interface{}{"FT.SYNUPDATE", index, synGroupId} + if options.SkipInitialScan { + args = append(args, "SKIPINITIALSCAN") + } + args = append(args, terms...) + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTTagVals - Returns all distinct values indexed in a tag field. +// The 'index' parameter specifies the index to check, and the 'field' parameter specifies the tag field to retrieve values from. +// For more information, please refer to the Redis documentation: [FT.TAGVALS](https://redis.io/commands/ft.tagvals/) +func (c cmdable) FTTagVals(ctx context.Context, index string, field string) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "FT.TAGVALS", index, field) + _ = c(ctx, cmd) + return cmd +} diff --git a/search_test.go b/search_test.go new file mode 100644 index 00000000..3d3fc7b7 --- /dev/null +++ b/search_test.go @@ -0,0 +1,1147 @@ +package redis_test + +import ( + "context" + + . "github.com/bsm/ginkgo/v2" + . "github.com/bsm/gomega" + "github.com/redis/go-redis/v9" +) + +var _ = Describe("RediSearch commands", Label("search"), func() { + ctx := context.TODO() + var client *redis.Client + + BeforeEach(func() { + client = redis.NewClient(&redis.Options{Addr: ":6379"}) + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + Expect(client.Close()).NotTo(HaveOccurred()) + }) + + It("should FTCreate and FTSearch WithScores ", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "txt", FieldType: "TEXT"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + 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()) + 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")) + }) + + 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.SearchSchema{FieldName: "txt", FieldType: "TEXT"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + client.HSet(ctx, "doc1", "txt", "foo baz") + client.HSet(ctx, "doc2", "txt", "hello world") + res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + searchResult1 := res1.(map[interface{}]interface{}) + Expect(searchResult1["total_results"]).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))) + + }) + + It("should FTCreate and FTSearch filters ", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "txt", FieldType: "TEXT"}, &redis.SearchSchema{FieldName: "num", FieldType: "NUMERIC"}, &redis.SearchSchema{FieldName: "loc", FieldType: "GEO"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + client.HSet(ctx, "doc1", "txt", "foo bar", "num", 3.141, "loc", "-0.441,51.458") + 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")) + 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")) + // 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")) + 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(docs).To(ContainElement("doc1")) + Expect(docs).To(ContainElement("doc2")) + + }) + + It("should FTCreate and FTSearch sortby ", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "num", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "txt", FieldType: "TEXT"}, &redis.SearchSchema{FieldName: "num", FieldType: "NUMERIC", Sortable: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + client.HSet(ctx, "doc1", "txt", "foo bar", "num", 1) + client.HSet(ctx, "doc2", "txt", "foo baz", "num", 2) + client.HSet(ctx, "doc3", "txt", "foo qux", "num", 3) + + sortBy1 := redis.FTSearchSortBy{FieldName: "num", Asc: true} + 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")) + + 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")) + + }) + + It("should FTCreate and FTSearch example ", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "title", FieldType: "TEXT", Weight: 5}, &redis.SearchSchema{FieldName: "body", FieldType: "TEXT"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + 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()) + + }) + + It("should FTCreate NoIndex ", Label("search", "ftcreate", "ftsearch"), func() { + text1 := &redis.SearchSchema{FieldName: "field", FieldType: "TEXT"} + text2 := &redis.SearchSchema{FieldName: "text", FieldType: "TEXT", NoIndex: true, Sortable: true} + num := &redis.SearchSchema{FieldName: "numeric", FieldType: "NUMERIC", NoIndex: true, Sortable: true} + geo := &redis.SearchSchema{FieldName: "geo", FieldType: "GEO", NoIndex: true, Sortable: true} + tag := &redis.SearchSchema{FieldName: "tag", FieldType: "TAG", NoIndex: true, Sortable: true} + val, err := client.FTCreate(ctx, "idx", &redis.FTCreateOptions{}, text1, text2, num, geo, tag).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + client.HSet(ctx, "doc1", "field", "aaa", "text", "1", "numeric", 1, "geo", "1,1", "tag", "1") + 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))) + res2, err := client.FTSearch(ctx, "idx", "@field:aa*").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.(map[interface{}]interface{})["total_results"]).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")) + 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")) + 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")) + 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")) + 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")) + + }) + + It("should FTExplain ", Label("search", "ftexplain"), func() { + text1 := &redis.SearchSchema{FieldName: "f1", FieldType: "TEXT"} + text2 := &redis.SearchSchema{FieldName: "f2", FieldType: "TEXT"} + text3 := &redis.SearchSchema{FieldName: "f3", FieldType: "TEXT"} + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, text1, text2, text3).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + res1, err := client.FTExplain(ctx, "txt", "@f3:f3_val @f2:f2_val @f1:f1_val").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1).ToNot(BeEmpty()) + + }) + + It("should FTAlias ", Label("search", "ftexplain"), func() { + text1 := &redis.SearchSchema{FieldName: "name", FieldType: "TEXT"} + text2 := &redis.SearchSchema{FieldName: "name", FieldType: "TEXT"} + val1, err := client.FTCreate(ctx, "testAlias", &redis.FTCreateOptions{Prefix: []interface{}{"index1:"}}, text1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val1).To(BeEquivalentTo("OK")) + val2, err := client.FTCreate(ctx, "testAlias2", &redis.FTCreateOptions{Prefix: []interface{}{"index2:"}}, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val2).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "index1:lonestar", "name", "lonestar") + client.HSet(ctx, "index2:yogurt", "name", "yogurt") + + 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")) + + aliasAddRes, err := client.FTAliasAdd(ctx, "testAlias", "mj23").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(aliasAddRes).To(BeEquivalentTo("OK")) + + 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")) + + aliasUpdateRes, err := client.FTAliasUpdate(ctx, "testAlias2", "kb24").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(aliasUpdateRes).To(BeEquivalentTo("OK")) + + 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")) + + aliasDelRes, err := client.FTAliasDel(ctx, "mj23").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(aliasDelRes).To(BeEquivalentTo("OK")) + + }) + + It("should FTCreate and FTSearch textfield, sortable and nostem ", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "txt", FieldType: "TEXT", Sortable: true, NoStem: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + resInfo, err := client.FTInfo(ctx, "idx1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resInfo["attributes"].([]interface{})[0].(map[interface{}]interface{})["flags"]).To(ContainElements("SORTABLE", "NOSTEM")) + + }) + + It("should FTAlter ", Label("search", "ftcreate", "ftsearch", "ftalter"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "txt", FieldType: "TEXT"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + resAlter, err := client.FTAlter(ctx, "idx1", false, []interface{}{"body", "TEXT"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resAlter).To(BeEquivalentTo("OK")) + + 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))) + + }) + + It("should FTSpellCheck", Label("search", "ftcreate", "ftsearch", "ftspellcheck"), func() { + text1 := &redis.SearchSchema{FieldName: "f1", FieldType: "TEXT"} + text2 := &redis.SearchSchema{FieldName: "f2", FieldType: "TEXT"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "doc1", "f1", "some valid content", "f2", "this is sample text") + client.HSet(ctx, "doc2", "f1", "very important", "f2", "lorem ipsum") + + resSpellCheck, err := client.FTSpellCheck(ctx, "idx1", "impornant").Result() + Expect(err).NotTo(HaveOccurred()) + res := resSpellCheck["results"].(map[interface{}]interface{})["impornant"].([]interface{})[0].(map[interface{}]interface{}) + Expect("important").To(BeKeyOf(res)) + + 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)) + + // 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{}{}})) + + 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{}))) + + // test spellcheck include + dict := []interface{}{"lore", "lorem", "lorm"} + resDictAdd, err := client.FTDictAdd(ctx, "dict", dict).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDictAdd).To(BeEquivalentTo(3)) + terms := redis.SpellCheckTerms{Include: true, 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{}) + 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)) + + terms2 := redis.SpellCheckTerms{Exclude: true, Dictionary: "dict"} + resSpellCheck6, err := client.FTSpellCheckWithArgs(ctx, "idx1", "lorm", &redis.FTSpellCheckOptions{Terms: terms2}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSpellCheck6["results"]).To(BeEmpty()) + }) + + It("should FTDict opreations ", Label("search", "ftdictdump", "ftdictdel", "ftdictadd"), func() { + text1 := &redis.SearchSchema{FieldName: "f1", FieldType: "TEXT"} + text2 := &redis.SearchSchema{FieldName: "f2", FieldType: "TEXT"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + dict := []interface{}{"item1", "item2", "item3"} + resDictAdd, err := client.FTDictAdd(ctx, "custom_dict", dict).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDictAdd).To(BeEquivalentTo(3)) + + resDictDel, err := client.FTDictDel(ctx, "custom_dict", []interface{}{"item2"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDictDel).To(BeEquivalentTo(1)) + + resDictDump, err := client.FTDictDump(ctx, "custom_dict").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDictDump).To(BeEquivalentTo([]string{"item1", "item3"})) + + resDictDel2, err := client.FTDictDel(ctx, "custom_dict", []interface{}{"item1", "item3"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDictDel2).To(BeEquivalentTo(2)) + }) + + It("should FTSearch phonetic matcher ", Label("search", "ftsearch"), func() { + text1 := &redis.SearchSchema{FieldName: "name", FieldType: "TEXT"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "doc1", "name", "Jon") + client.HSet(ctx, "doc2", "name", "John") + + 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")) + + client.FlushDB(ctx) + text2 := &redis.SearchSchema{FieldName: "name", FieldType: "TEXT", PhoneticMatcher: "dm:en"} + val2, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val2).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "doc1", "name", "Jon") + client.HSet(ctx, "doc2", "name", "John") + + 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(names).To(ContainElement("Jon")) + Expect(names).To(ContainElement("John")) + }) + + It("should FTSearch WithScores", Label("search", "ftsearch"), func() { + text1 := &redis.SearchSchema{FieldName: "description", FieldType: "TEXT"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "doc1", "description", "The quick brown fox jumps over the lazy dog") + client.HSet(ctx, "doc2", "description", "Quick alice was beginning to get very tired of sitting by her quick sister on the bank, and of having nothing to do.") + + 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)) + + 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)) + + 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.1111111111111111)) + + 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.17699114465425977)) + + 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)) + + 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)) + + 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)) + }) + + It("should FTConfigSet and FTConfigGet ", Label("search", "ftconfigget", "ftconfigset"), func() { + val, err := client.FTConfigSet(ctx, "TIMEOUT", "100").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + res, err := client.FTConfigGet(ctx, "*").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res["TIMEOUT"]).To(BeEquivalentTo("100")) + + res, err = client.FTConfigGet(ctx, "TIMEOUT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(map[string]interface{}{"TIMEOUT": "100"})) + + }) + + It("should FTAggregate GroupBy ", Label("search", "ftaggregate"), func() { + text1 := &redis.SearchSchema{FieldName: "title", FieldType: "TEXT"} + text2 := &redis.SearchSchema{FieldName: "body", FieldType: "TEXT"} + text3 := &redis.SearchSchema{FieldName: "parent", FieldType: "TEXT"} + num := &redis.SearchSchema{FieldName: "random_num", FieldType: "NUMERIC"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2, text3, num).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "search", "title", "RediSearch", + "body", "Redisearch impements a search engine on top of redis", + "parent", "redis", + "random_num", 10) + client.HSet(ctx, "ai", "title", "RedisAI", + "body", "RedisAI executes Deep Learning/Machine Learning models and managing their data.", + "parent", "redis", + "random_num", 3) + client.HSet(ctx, "json", "title", "RedisJson", + "body", "RedisJSON implements ECMA-404 The JSON Data Interchange Standard as a native data type.", + "parent", "redis", + "random_num", 8) + + reducer := redis.FTAggregateReducer{Reducer: redis.SearchCount} + 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")) + + 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")) + + 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")) + + 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")) + + 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")) + + 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")) + + 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")) + + 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")) + + 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")) + + 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")) + + 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"})) + + }) + + It("should FTAggregate sort and limit ", Label("search", "ftaggregate"), func() { + text1 := &redis.SearchSchema{FieldName: "t1", FieldType: "TEXT"} + text2 := &redis.SearchSchema{FieldName: "t2", FieldType: "TEXT"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "doc1", "t1", "a", "t2", "b") + client.HSet(ctx, "doc2", "t1", "b", "t2", "a") + + 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"})) + + 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"})) + + 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)) + + 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"})) + }) + + It("should FTAggregate load ", Label("search", "ftaggregate"), func() { + text1 := &redis.SearchSchema{FieldName: "t1", FieldType: "TEXT"} + text2 := &redis.SearchSchema{FieldName: "t2", FieldType: "TEXT"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "doc1", "t1", "hello", "t2", "world") + + 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"})) + + 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"})) + + 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"})) + }) + + It("should FTAggregate apply", Label("search", "ftaggregate"), func() { + text1 := &redis.SearchSchema{FieldName: "PrimaryKey", FieldType: "TEXT", Sortable: true} + num1 := &redis.SearchSchema{FieldName: "CreatedDateTimeUTC", FieldType: "NUMERIC", Sortable: true} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "doc1", "PrimaryKey", "9::362330", "CreatedDateTimeUTC", "637387878524969984") + client.HSet(ctx, "doc2", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "637387875859270016") + + options := &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}} + res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).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")) + + }) + + It("should FTAggregate filter", Label("search", "ftaggregate"), func() { + text1 := &redis.SearchSchema{FieldName: "name", FieldType: "TEXT", Sortable: true} + num1 := &redis.SearchSchema{FieldName: "age", FieldType: "NUMERIC", Sortable: true} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "doc1", "name", "bar", "age", "25") + client.HSet(ctx, "doc2", "name", "foo", "age", "19") + + for _, dlc := range []int{1, 2} { + 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"})) + + 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"})) + } + + }) + + It("should FTSearch SkipInitalScan", Label("search", "ftsearch"), func() { + client.HSet(ctx, "doc1", "foo", "bar") + + text1 := &redis.SearchSchema{FieldName: "foo", FieldType: "TEXT"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{SkipInitalScan: true}, text1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + res, err := client.FTSearch(ctx, "idx1", "@foo:bar").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.(map[interface{}]interface{})["total_results"]).To(BeEquivalentTo(int64(0))) + }) + + It("should FTCreate json", Label("search", "ftcreate"), func() { + + text1 := &redis.SearchSchema{FieldName: "$.name", FieldType: "TEXT"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true, Prefix: []interface{}{"king:"}}, text1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.JSONSet(ctx, "king:1", "$", `{"name": "henry"}`) + client.JSONSet(ctx, "king:2", "$", `{"name": "james"}`) + + 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"}`)) + }) + + It("should FTCreate json fields as names", Label("search", "ftcreate"), func() { + + text1 := &redis.SearchSchema{FieldName: "$.name", FieldType: "TEXT", As: "name"} + num1 := &redis.SearchSchema{FieldName: "$.age", FieldType: "NUMERIC", As: "just_a_number"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true}, text1, num1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.JSONSet(ctx, "doc:1", "$", `{"name": "Jon", "age": 25}`) + + 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")) + }) + + It("should FTCreate CaseSensitive", Label("search", "ftcreate"), func() { + + tag1 := &redis.SearchSchema{FieldName: "t", FieldType: "TAG", CaseSensitive: false} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, tag1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "1", "t", "HELLO") + client.HSet(ctx, "2", "t", "hello") + + 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")) + + res, err = client.FTDropIndex(ctx, "idx1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + tag2 := &redis.SearchSchema{FieldName: "t", FieldType: "TAG", CaseSensitive: true} + val, err = client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, tag2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + 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")) + + }) + + It("should FTSearch ReturnFields", Label("search", "ftsearch"), func() { + resJson, err := client.JSONSet(ctx, "doc:1", "$", `{"t": "riceratops","t2": "telmatosaurus", "n": 9072, "flt": 97.2}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resJson).To(BeEquivalentTo("OK")) + + text1 := &redis.SearchSchema{FieldName: "$.t", FieldType: "TEXT"} + num1 := &redis.SearchSchema{FieldName: "$.flt", FieldType: "NUMERIC"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true}, text1, num1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + 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")) + + 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")) + }) + + It("should FTSynUpdate", Label("search", "ftsynupdate"), func() { + + text1 := &redis.SearchSchema{FieldName: "title", FieldType: "TEXT"} + text2 := &redis.SearchSchema{FieldName: "body", FieldType: "TEXT"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + resSynUpdate, err := client.FTSynUpdateWithArgs(ctx, "idx1", "id1", &redis.FTSynUpdateOptions{SkipInitialScan: true}, []interface{}{"boy", "child", "offspring"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSynUpdate).To(BeEquivalentTo("OK")) + client.HSet(ctx, "doc1", "title", "he is a baby", "body", "this is a test") + + resSynUpdate, err = client.FTSynUpdateWithArgs(ctx, "idx1", "id1", &redis.FTSynUpdateOptions{SkipInitialScan: true}, []interface{}{"baby"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSynUpdate).To(BeEquivalentTo("OK")) + client.HSet(ctx, "doc2", "title", "he is another baby", "body", "another test") + + 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")) + }) + + It("should FTSynDump", Label("search", "ftsyndump"), func() { + + text1 := &redis.SearchSchema{FieldName: "title", FieldType: "TEXT"} + text2 := &redis.SearchSchema{FieldName: "body", FieldType: "TEXT"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + resSynUpdate, err := client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"boy", "child", "offspring"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSynUpdate).To(BeEquivalentTo("OK")) + + resSynUpdate, err = client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"baby", "child"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSynUpdate).To(BeEquivalentTo("OK")) + + resSynUpdate, err = client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"tree", "wood"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSynUpdate).To(BeEquivalentTo("OK")) + + resSynDump, err := client.FTSynDump(ctx, "idx1").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"}})) + }) + + It("should FTCreate json with alias", Label("search", "ftcreate"), func() { + + text1 := &redis.SearchSchema{FieldName: "$.name", FieldType: "TEXT", As: "name"} + num1 := &redis.SearchSchema{FieldName: "$.num", FieldType: "NUMERIC", As: "num"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true, Prefix: []interface{}{"king:"}}, text1, num1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.JSONSet(ctx, "king:1", "$", `{"name": "henry", "num": 42}`) + client.JSONSet(ctx, "king:2", "$", `{"name": "james", "num": 3.14}`) + + 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}`)) + + 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}`)) + }) + + It("should FTCreate json with multipath", Label("search", "ftcreate"), func() { + + tag1 := &redis.SearchSchema{FieldName: "$..name", FieldType: "TAG", As: "name"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true, Prefix: []interface{}{"king:"}}, tag1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.JSONSet(ctx, "king:1", "$", `{"name": "henry", "country": {"name": "england"}}`) + + 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"}}`)) + }) + + It("should FTCreate json with jsonpath", Label("search", "ftcreate"), func() { + + text1 := &redis.SearchSchema{FieldName: `$["prod:name"]`, FieldType: "TEXT", As: "name"} + text2 := &redis.SearchSchema{FieldName: `$.prod:name`, FieldType: "TEXT", As: "name_unsupported"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.JSONSet(ctx, "doc:1", "$", `{"prod:name": "RediSearch"}`) + + 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"}`)) + + 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)) + + 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 FTProfileSearch and FTProfileAggregate", Label("search", "ftprofile"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "t", FieldType: "TEXT"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "1", "t", "hello") + client.HSet(ctx, "2", "t", "world") + + // FTProfileSearch + query := redis.FTSearchQuery("hello|world", &redis.FTSearchOptions{NoContent: true}) + res1, err := client.FTProfileSearch(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")) + + // FTProfileAggregate + aggQuery := redis.FTAggregateQuery("*", &redis.FTAggregateOptions{ + Load: []redis.FTAggregateLoad{{Field: "t"}}, + Apply: []redis.FTAggregateApply{{Field: "startswith(@t, 'hel')", As: "prefix"}}}) + res2, err := client.FTProfileAggregate(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 FTProfileSearch Limited", Label("search", "ftprofile"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "t", FieldType: "TEXT"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "1", "t", "hello") + client.HSet(ctx, "2", "t", "hell") + client.HSet(ctx, "3", "t", "help") + client.HSet(ctx, "4", "t", "helowa") + + // FTProfileSearch + query := redis.FTSearchQuery("%hell% hel*", &redis.FTSearchOptions{}) + res1, err := client.FTProfileSearch(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 FTProfileSearch query params", Label("search", "ftprofile"), func() { + hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"} + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.SearchSchema{FieldName: "v", FieldType: "VECTOR", VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "a", "v", "aaaaaaaa") + client.HSet(ctx, "b", "v", "aaaabaaa") + client.HSet(ctx, "c", "v", "aaaaabaa") + + // FTProfileSearch + 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.FTProfileSearch(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("VECTOR")) + 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() { + hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"} + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.SearchSchema{FieldName: "v", FieldType: "VECTOR", VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "a", "v", "aaaaaaaa") + client.HSet(ctx, "b", "v", "aaaabaaa") + client.HSet(ctx, "c", "v", "aaaaabaa") + + 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"}, + } + 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")) + }) + + It("should FTCreate and FTSearch text params", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "name", FieldType: "TEXT"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "doc1", "name", "Alice") + client.HSet(ctx, "doc2", "name", "Bob") + client.HSet(ctx, "doc3", "name", "Carol") + + 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")) + + }) + + It("should FTCreate and FTSearch numeric params", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "numval", FieldType: "NUMERIC"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "doc1", "numval", 101) + client.HSet(ctx, "doc2", "numval", 102) + client.HSet(ctx, "doc3", "numval", 103) + + 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")) + + }) + + It("should FTCreate and FTSearch geo params", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "g", FieldType: "GEO"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "doc1", "g", "29.69465, 34.95126") + client.HSet(ctx, "doc2", "g", "29.69350, 34.94737") + client.HSet(ctx, "doc3", "g", "29.68746, 34.94882") + + 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")) + + }) + + It("should FTConfigSet and FTConfigGet dialect", Label("search", "ftconfigget", "ftconfigset"), func() { + defDialect, err := client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "1"})) + + res, err := client.FTConfigSet(ctx, "DEFAULT_DIALECT", "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "2"})) + + }) + + It("should FTCreate WithSuffixtrie", Label("search", "ftcreate", "ftinfo"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "txt", FieldType: "TEXT"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + res, err := client.FTInfo(ctx, "idx1").Result() + Expect(err).NotTo(HaveOccurred()) + resAttr0 := res["attributes"].([]interface{})[0].(map[interface{}]interface{}) + Expect(resAttr0["flags"]).To(BeEmpty()) + + resDrop, err := client.FTDropIndex(ctx, "idx1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDrop).To(BeEquivalentTo("OK")) + + // create withsuffixtrie index - text field + val, err = client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "txt", FieldType: "TEXT", WithSuffixtrie: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + 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")) + + resDrop, err = client.FTDropIndex(ctx, "idx1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDrop).To(BeEquivalentTo("OK")) + + // create withsuffixtrie index - tag field + val, err = client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "t", FieldType: "TAG", WithSuffixtrie: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + 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")) + }) + + It("should FTCreate GeoShape", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.SearchSchema{FieldName: "geom", FieldType: "GEOSHAPE", GeoShapeFieldType: "FLAT"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "small", "geom", "POLYGON((1 1, 1 100, 100 100, 100 1, 1 1))") + client.HSet(ctx, "large", "geom", "POLYGON((1 1, 1 200, 200 200, 200 1, 1 1))") + + res1, err := client.FTSearchWithArgs(ctx, "idx1", "@geom:[WITHIN $poly]", + &redis.FTSearchOptions{ + DialectVersion: 3, + 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")) + + res2, err := client.FTSearchWithArgs(ctx, "idx1", "@geom:[CONTAINS $poly]", + &redis.FTSearchOptions{ + DialectVersion: 3, + 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))) + }) +})