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 }