From 86426344881e7653553cb1d43753fa768357567b Mon Sep 17 00:00:00 2001 From: ofekshenawa Date: Tue, 14 Nov 2023 23:40:23 +0200 Subject: [PATCH] Add RediSearch commands and tests --- redis_search.go | 765 ++++++++++++++++++++++++++++++++++++++++- redis_search_test.go | 544 ++++++++++++++++++++++++++++- timeseries_commands.go | 21 ++ 3 files changed, 1318 insertions(+), 12 deletions(-) diff --git a/redis_search.go b/redis_search.go index acc554be..93caf048 100644 --- a/redis_search.go +++ b/redis_search.go @@ -8,20 +8,343 @@ 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 } -// TODO - consider remove due to temporary command +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) FTAliasAdd(ctx context.Context, alias string, index string) *StatusCmd { +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) @@ -34,7 +357,7 @@ func (c cmdable) FTAliasDel(ctx context.Context, alias string) *StatusCmd { return cmd } -func (c cmdable) FTAliasUpdate(ctx context.Context, alias string, index string) *StatusCmd { +func (c cmdable) FTAliasUpdate(ctx context.Context, index string, alias string) *StatusCmd { cmd := NewStatusCmd(ctx, "FT.ALIASUPDATE", alias, index) _ = c(ctx, cmd) return cmd @@ -52,18 +375,124 @@ func (c cmdable) FTAlter(ctx context.Context, index string, skipInitalScan bool, 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 } -// func (c cmdable) FTCreate(ctx context.Context, index string, schema string, ) *StatusCmd { -// args := []interface{}{"FT.CREATE", index, "SCHEMA", schema} -// cmd := NewStatusCmd(ctx, args...) -// _ = 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) @@ -71,6 +500,17 @@ func (c cmdable) FTCursorDel(ctx context.Context, index string, cursorId int) *S 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...) @@ -100,7 +540,7 @@ func (c cmdable) FTDropIndex(ctx context.Context, index string) *StatusCmd { return cmd } -func (c cmdable) FTDropIndexWithOptions(ctx context.Context, index string, options *FTDropIndexOptions) *StatusCmd { +func (c cmdable) FTDropIndexWithArgs(ctx context.Context, index string, options *FTDropIndexOptions) *StatusCmd { args := []interface{}{"FT.DROPINDEX", index} if options != nil { if options.DeleteDocs { @@ -111,3 +551,308 @@ func (c cmdable) FTDropIndexWithOptions(ctx context.Context, index string, optio _ = 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 index 84a45036..994bbbb6 100644 --- a/redis_search_test.go +++ b/redis_search_test.go @@ -8,7 +8,7 @@ import ( "github.com/redis/go-redis/v9" ) -var _ = Describe("RediSearch commands", Label("gears"), func() { +var _ = Describe("RediSearch commands", Label("search"), func() { ctx := context.TODO() var client *redis.Client @@ -21,7 +21,547 @@ var _ = Describe("RediSearch commands", Label("gears"), func() { Expect(client.Close()).NotTo(HaveOccurred()) }) - It("should FT_List ", Label("search", "ft_list"), func() { + 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/timeseries_commands.go b/timeseries_commands.go index 61cc3a5b..0d6fdaa9 100644 --- a/timeseries_commands.go +++ b/timeseries_commands.go @@ -82,6 +82,13 @@ const ( Max Range Count + CountDistinct + CountDistinctish + StdDev + Quantile + ToList + FirstValue + RandomSample First Last StdP @@ -107,6 +114,20 @@ func (a Aggregator) String() string { return "RANGE" case Count: return "COUNT" + case CountDistinct: + return "COUNT_DISTINCT" + case CountDistinctish: + return "COUNT_DISTINCTISH" + case StdDev: + return "STDDEV" + case Quantile: + return "QUANTILE" + case ToList: + return "TOLIST" + case FirstValue: + return "FIRST_VALUE" + case RandomSample: + return "RANDOM_SAMPLE" case First: return "FIRST" case Last: