From be6dfd55d992375cd1fa029a5961e0d77a92b433 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 16 Jul 2025 23:06:47 +0300 Subject: [PATCH 1/5] Add search module builders and tests (#1) * Add search module builders and tests * Add tests --- search_builders.go | 759 ++++++++++++++++++++++++++++++++++++++++ search_builders_test.go | 680 +++++++++++++++++++++++++++++++++++ 2 files changed, 1439 insertions(+) create mode 100644 search_builders.go create mode 100644 search_builders_test.go diff --git a/search_builders.go b/search_builders.go new file mode 100644 index 000000000..964b26878 --- /dev/null +++ b/search_builders.go @@ -0,0 +1,759 @@ +package redis + +import ( + "context" +) + +// ---------------------- +// Search Module Builders +// ---------------------- + +// SearchBuilder provides a fluent API for FT.SEARCH +// (see original FTSearchOptions for all options). +type SearchBuilder struct { + c *Client + ctx context.Context + index string + query string + options *FTSearchOptions +} + +// Search starts building an FT.SEARCH command. +func (c *Client) Search(ctx context.Context, index, query string) *SearchBuilder { + b := &SearchBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTSearchOptions{LimitOffset: -1}} + return b +} + +// WithScores includes WITHSCORES. +func (b *SearchBuilder) WithScores() *SearchBuilder { + b.options.WithScores = true + return b +} + +// NoContent includes NOCONTENT. +func (b *SearchBuilder) NoContent() *SearchBuilder { b.options.NoContent = true; return b } + +// Verbatim includes VERBATIM. +func (b *SearchBuilder) Verbatim() *SearchBuilder { b.options.Verbatim = true; return b } + +// NoStopWords includes NOSTOPWORDS. +func (b *SearchBuilder) NoStopWords() *SearchBuilder { b.options.NoStopWords = true; return b } + +// WithPayloads includes WITHPAYLOADS. +func (b *SearchBuilder) WithPayloads() *SearchBuilder { + b.options.WithPayloads = true + return b +} + +// WithSortKeys includes WITHSORTKEYS. +func (b *SearchBuilder) WithSortKeys() *SearchBuilder { + b.options.WithSortKeys = true + return b +} + +// Filter adds a FILTER clause: FILTER . +func (b *SearchBuilder) Filter(field string, min, max interface{}) *SearchBuilder { + b.options.Filters = append(b.options.Filters, FTSearchFilter{ + FieldName: field, + Min: min, + Max: max, + }) + return b +} + +// GeoFilter adds a GEOFILTER clause: GEOFILTER . +func (b *SearchBuilder) GeoFilter(field string, lon, lat, radius float64, unit string) *SearchBuilder { + b.options.GeoFilter = append(b.options.GeoFilter, FTSearchGeoFilter{ + FieldName: field, + Longitude: lon, + Latitude: lat, + Radius: radius, + Unit: unit, + }) + return b +} + +// InKeys restricts the search to the given keys. +func (b *SearchBuilder) InKeys(keys ...interface{}) *SearchBuilder { + b.options.InKeys = append(b.options.InKeys, keys...) + return b +} + +// InFields restricts the search to the given fields. +func (b *SearchBuilder) InFields(fields ...interface{}) *SearchBuilder { + b.options.InFields = append(b.options.InFields, fields...) + return b +} + +// ReturnFields adds simple RETURN ... +func (b *SearchBuilder) ReturnFields(fields ...string) *SearchBuilder { + for _, f := range fields { + b.options.Return = append(b.options.Return, FTSearchReturn{FieldName: f}) + } + return b +} + +// ReturnAs adds RETURN AS . +func (b *SearchBuilder) ReturnAs(field, alias string) *SearchBuilder { + b.options.Return = append(b.options.Return, FTSearchReturn{FieldName: field, As: alias}) + return b +} + +// Slop adds SLOP . +func (b *SearchBuilder) Slop(slop int) *SearchBuilder { + b.options.Slop = slop + return b +} + +// Timeout adds TIMEOUT . +func (b *SearchBuilder) Timeout(timeout int) *SearchBuilder { + b.options.Timeout = timeout + return b +} + +// InOrder includes INORDER. +func (b *SearchBuilder) InOrder() *SearchBuilder { + b.options.InOrder = true + return b +} + +// Language sets LANGUAGE . +func (b *SearchBuilder) Language(lang string) *SearchBuilder { + b.options.Language = lang + return b +} + +// Expander sets EXPANDER . +func (b *SearchBuilder) Expander(expander string) *SearchBuilder { + b.options.Expander = expander + return b +} + +// Scorer sets SCORER . +func (b *SearchBuilder) Scorer(scorer string) *SearchBuilder { + b.options.Scorer = scorer + return b +} + +// ExplainScore includes EXPLAINSCORE. +func (b *SearchBuilder) ExplainScore() *SearchBuilder { + b.options.ExplainScore = true + return b +} + +// Payload sets PAYLOAD . +func (b *SearchBuilder) Payload(payload string) *SearchBuilder { + b.options.Payload = payload + return b +} + +// SortBy adds SORTBY ASC|DESC. +func (b *SearchBuilder) SortBy(field string, asc bool) *SearchBuilder { + b.options.SortBy = append(b.options.SortBy, FTSearchSortBy{ + FieldName: field, + Asc: asc, + Desc: !asc, + }) + return b +} + +// WithSortByCount includes WITHCOUNT (when used with SortBy). +func (b *SearchBuilder) WithSortByCount() *SearchBuilder { + b.options.SortByWithCount = true + return b +} + +// Param adds a single PARAMS . +func (b *SearchBuilder) Param(key string, value interface{}) *SearchBuilder { + if b.options.Params == nil { + b.options.Params = make(map[string]interface{}, 1) + } + b.options.Params[key] = value + return b +} + +// ParamsMap adds multiple PARAMS at once. +func (b *SearchBuilder) ParamsMap(p map[string]interface{}) *SearchBuilder { + if b.options.Params == nil { + b.options.Params = make(map[string]interface{}, len(p)) + } + for k, v := range p { + b.options.Params[k] = v + } + return b +} + +// Dialect sets DIALECT . +func (b *SearchBuilder) Dialect(version int) *SearchBuilder { + b.options.DialectVersion = version + return b +} + +// Limit sets OFFSET and COUNT. CountOnly uses LIMIT 0 0. +func (b *SearchBuilder) Limit(offset, count int) *SearchBuilder { + b.options.LimitOffset = offset + b.options.Limit = count + return b +} +func (b *SearchBuilder) CountOnly() *SearchBuilder { b.options.CountOnly = true; return b } + +// Run executes FT.SEARCH and returns a typed result. +func (b *SearchBuilder) Run() (FTSearchResult, error) { + cmd := b.c.FTSearchWithArgs(b.ctx, b.index, b.query, b.options) + return cmd.Result() +} + +// ---------------------- +// AggregateBuilder for FT.AGGREGATE +// ---------------------- + +type AggregateBuilder struct { + c *Client + ctx context.Context + index string + query string + options *FTAggregateOptions +} + +// Aggregate starts building an FT.AGGREGATE command. +func (c *Client) Aggregate(ctx context.Context, index, query string) *AggregateBuilder { + return &AggregateBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTAggregateOptions{LimitOffset: -1}} +} + +// Verbatim includes VERBATIM. +func (b *AggregateBuilder) Verbatim() *AggregateBuilder { b.options.Verbatim = true; return b } + +// AddScores includes ADDSCORES. +func (b *AggregateBuilder) AddScores() *AggregateBuilder { b.options.AddScores = true; return b } + +// Scorer sets SCORER . +func (b *AggregateBuilder) Scorer(s string) *AggregateBuilder { + b.options.Scorer = s + return b +} + +// LoadAll includes LOAD * (mutually exclusive with Load). +func (b *AggregateBuilder) LoadAll() *AggregateBuilder { + b.options.LoadAll = true + return b +} + +// Load adds LOAD [AS alias]... +// You can call it multiple times for multiple fields. +func (b *AggregateBuilder) Load(field string, alias ...string) *AggregateBuilder { + // each Load entry becomes one element in options.Load + l := FTAggregateLoad{Field: field} + if len(alias) > 0 { + l.As = alias[0] + } + b.options.Load = append(b.options.Load, l) + return b +} + +// Timeout sets TIMEOUT . +func (b *AggregateBuilder) Timeout(ms int) *AggregateBuilder { + b.options.Timeout = ms + return b +} + +// Apply adds APPLY [AS alias]. +func (b *AggregateBuilder) Apply(field string, alias ...string) *AggregateBuilder { + a := FTAggregateApply{Field: field} + if len(alias) > 0 { + a.As = alias[0] + } + b.options.Apply = append(b.options.Apply, a) + return b +} + +// GroupBy starts a new GROUPBY clause. +func (b *AggregateBuilder) GroupBy(fields ...interface{}) *AggregateBuilder { + b.options.GroupBy = append(b.options.GroupBy, FTAggregateGroupBy{ + Fields: fields, + }) + return b +} + +// Reduce adds a REDUCE [<#args> ] clause to the *last* GROUPBY. +func (b *AggregateBuilder) Reduce(fn SearchAggregator, args ...interface{}) *AggregateBuilder { + if len(b.options.GroupBy) == 0 { + // no GROUPBY yet — nothing to attach to + return b + } + idx := len(b.options.GroupBy) - 1 + b.options.GroupBy[idx].Reduce = append(b.options.GroupBy[idx].Reduce, FTAggregateReducer{ + Reducer: fn, + Args: args, + }) + return b +} + +// ReduceAs does the same but also sets an alias: REDUCE … AS +func (b *AggregateBuilder) ReduceAs(fn SearchAggregator, alias string, args ...interface{}) *AggregateBuilder { + if len(b.options.GroupBy) == 0 { + return b + } + idx := len(b.options.GroupBy) - 1 + b.options.GroupBy[idx].Reduce = append(b.options.GroupBy[idx].Reduce, FTAggregateReducer{ + Reducer: fn, + Args: args, + As: alias, + }) + return b +} + +// SortBy adds SORTBY ASC|DESC. +func (b *AggregateBuilder) SortBy(field string, asc bool) *AggregateBuilder { + sb := FTAggregateSortBy{FieldName: field, Asc: asc, Desc: !asc} + b.options.SortBy = append(b.options.SortBy, sb) + return b +} + +// SortByMax sets MAX (only if SortBy was called). +func (b *AggregateBuilder) SortByMax(max int) *AggregateBuilder { + b.options.SortByMax = max + return b +} + +// Filter sets FILTER . +func (b *AggregateBuilder) Filter(expr string) *AggregateBuilder { + b.options.Filter = expr + return b +} + +// WithCursor enables WITHCURSOR [COUNT ] [MAXIDLE ]. +func (b *AggregateBuilder) WithCursor(count, maxIdle int) *AggregateBuilder { + b.options.WithCursor = true + if b.options.WithCursorOptions == nil { + b.options.WithCursorOptions = &FTAggregateWithCursor{} + } + b.options.WithCursorOptions.Count = count + b.options.WithCursorOptions.MaxIdle = maxIdle + return b +} + +// Params adds PARAMS pairs. +func (b *AggregateBuilder) Params(p map[string]interface{}) *AggregateBuilder { + if b.options.Params == nil { + b.options.Params = make(map[string]interface{}, len(p)) + } + for k, v := range p { + b.options.Params[k] = v + } + return b +} + +// Dialect sets DIALECT . +func (b *AggregateBuilder) Dialect(version int) *AggregateBuilder { + b.options.DialectVersion = version + return b +} + +// Run executes FT.AGGREGATE and returns a typed result. +func (b *AggregateBuilder) Run() (*FTAggregateResult, error) { + cmd := b.c.FTAggregateWithArgs(b.ctx, b.index, b.query, b.options) + return cmd.Result() +} + +// ---------------------- +// CreateIndexBuilder for FT.CREATE +// ---------------------- + +type CreateIndexBuilder struct { + c *Client + ctx context.Context + index string + options *FTCreateOptions + schema []*FieldSchema +} + +// CreateIndex starts building an FT.CREATE command. +func (c *Client) CreateIndex(ctx context.Context, index string) *CreateIndexBuilder { + return &CreateIndexBuilder{c: c, ctx: ctx, index: index, options: &FTCreateOptions{}} +} + +// OnHash sets ON HASH. +func (b *CreateIndexBuilder) OnHash() *CreateIndexBuilder { b.options.OnHash = true; return b } + +// OnJSON sets ON JSON. +func (b *CreateIndexBuilder) OnJSON() *CreateIndexBuilder { b.options.OnJSON = true; return b } + +// Prefix sets PREFIX. +func (b *CreateIndexBuilder) Prefix(prefixes ...interface{}) *CreateIndexBuilder { + b.options.Prefix = prefixes + return b +} + +// Filter sets FILTER. +func (b *CreateIndexBuilder) Filter(filter string) *CreateIndexBuilder { + b.options.Filter = filter + return b +} + +// DefaultLanguage sets LANGUAGE. +func (b *CreateIndexBuilder) DefaultLanguage(lang string) *CreateIndexBuilder { + b.options.DefaultLanguage = lang + return b +} + +// LanguageField sets LANGUAGE_FIELD. +func (b *CreateIndexBuilder) LanguageField(field string) *CreateIndexBuilder { + b.options.LanguageField = field + return b +} + +// Score sets SCORE. +func (b *CreateIndexBuilder) Score(score float64) *CreateIndexBuilder { + b.options.Score = score + return b +} + +// ScoreField sets SCORE_FIELD. +func (b *CreateIndexBuilder) ScoreField(field string) *CreateIndexBuilder { + b.options.ScoreField = field + return b +} + +// PayloadField sets PAYLOAD_FIELD. +func (b *CreateIndexBuilder) PayloadField(field string) *CreateIndexBuilder { + b.options.PayloadField = field + return b +} + +// NoOffsets includes NOOFFSETS. +func (b *CreateIndexBuilder) NoOffsets() *CreateIndexBuilder { b.options.NoOffsets = true; return b } + +// Temporary sets TEMPORARY seconds. +func (b *CreateIndexBuilder) Temporary(sec int) *CreateIndexBuilder { + b.options.Temporary = sec + return b +} + +// NoHL includes NOHL. +func (b *CreateIndexBuilder) NoHL() *CreateIndexBuilder { b.options.NoHL = true; return b } + +// NoFields includes NOFIELDS. +func (b *CreateIndexBuilder) NoFields() *CreateIndexBuilder { b.options.NoFields = true; return b } + +// NoFreqs includes NOFREQS. +func (b *CreateIndexBuilder) NoFreqs() *CreateIndexBuilder { b.options.NoFreqs = true; return b } + +// StopWords sets STOPWORDS. +func (b *CreateIndexBuilder) StopWords(words ...interface{}) *CreateIndexBuilder { + b.options.StopWords = words + return b +} + +// SkipInitialScan includes SKIPINITIALSCAN. +func (b *CreateIndexBuilder) SkipInitialScan() *CreateIndexBuilder { + b.options.SkipInitialScan = true + return b +} + +// Schema adds a FieldSchema. +func (b *CreateIndexBuilder) Schema(field *FieldSchema) *CreateIndexBuilder { + b.schema = append(b.schema, field) + return b +} + +// Run executes FT.CREATE and returns the status. +func (b *CreateIndexBuilder) Run() (string, error) { + cmd := b.c.FTCreate(b.ctx, b.index, b.options, b.schema...) + return cmd.Result() +} + +// ---------------------- +// DropIndexBuilder for FT.DROPINDEX +// ---------------------- + +type DropIndexBuilder struct { + c *Client + ctx context.Context + index string + options *FTDropIndexOptions +} + +// DropIndex starts FT.DROPINDEX builder. +func (c *Client) DropIndex(ctx context.Context, index string) *DropIndexBuilder { + return &DropIndexBuilder{c: c, ctx: ctx, index: index} +} + +// DeleteRuncs includes DD. +func (b *DropIndexBuilder) DeleteDocs() *DropIndexBuilder { b.options.DeleteDocs = true; return b } + +// Run executes FT.DROPINDEX. +func (b *DropIndexBuilder) Run() (string, error) { + cmd := b.c.FTDropIndexWithArgs(b.ctx, b.index, b.options) + return cmd.Result() +} + +// ---------------------- +// AliasBuilder for FT.ALIAS* commands +// ---------------------- + +type AliasBuilder struct { + c *Client + ctx context.Context + alias string + index string + action string // add|del|update +} + +// AliasAdd starts FT.ALIASADD builder. +func (c *Client) AliasAdd(ctx context.Context, alias, index string) *AliasBuilder { + return &AliasBuilder{c: c, ctx: ctx, alias: alias, index: index, action: "add"} +} + +// AliasDel starts FT.ALIASDEL builder. +func (c *Client) AliasDel(ctx context.Context, alias string) *AliasBuilder { + return &AliasBuilder{c: c, ctx: ctx, alias: alias, action: "del"} +} + +// AliasUpdate starts FT.ALIASUPDATE builder. +func (c *Client) AliasUpdate(ctx context.Context, alias, index string) *AliasBuilder { + return &AliasBuilder{c: c, ctx: ctx, alias: alias, index: index, action: "update"} +} + +// Run executes the configured alias command. +func (b *AliasBuilder) Run() (string, error) { + switch b.action { + case "add": + cmd := b.c.FTAliasAdd(b.ctx, b.index, b.alias) + return cmd.Result() + case "del": + cmd := b.c.FTAliasDel(b.ctx, b.alias) + return cmd.Result() + case "update": + cmd := b.c.FTAliasUpdate(b.ctx, b.index, b.alias) + return cmd.Result() + } + return "", nil +} + +// ---------------------- +// ExplainBuilder for FT.EXPLAIN +// ---------------------- + +type ExplainBuilder struct { + c *Client + ctx context.Context + index string + query string + options *FTExplainOptions +} + +// Explain starts FT.EXPLAIN builder. +func (c *Client) Explain(ctx context.Context, index, query string) *ExplainBuilder { + return &ExplainBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTExplainOptions{}} +} + +// Dialect sets dialect for EXPLAINCLI. +func (b *ExplainBuilder) Dialect(d string) *ExplainBuilder { b.options.Dialect = d; return b } + +// Run executes FT.EXPLAIN and returns the plan. +func (b *ExplainBuilder) Run() (string, error) { + cmd := b.c.FTExplainWithArgs(b.ctx, b.index, b.query, b.options) + return cmd.Result() +} + +// ---------------------- +// InfoBuilder for FT.INFO +// ---------------------- + +type FTInfoBuilder struct { + c *Client + ctx context.Context + index string +} + +// SearchInfo starts building an FT.INFO command for RediSearch. +func (c *Client) SearchInfo(ctx context.Context, index string) *FTInfoBuilder { + return &FTInfoBuilder{c: c, ctx: ctx, index: index} +} + +// Run executes FT.INFO and returns detailed info. +func (b *FTInfoBuilder) Run() (FTInfoResult, error) { + cmd := b.c.FTInfo(b.ctx, b.index) + return cmd.Result() +} + +// ---------------------- +// SpellCheckBuilder for FT.SPELLCHECK +// ---------------------- + +type SpellCheckBuilder struct { + c *Client + ctx context.Context + index string + query string + options *FTSpellCheckOptions +} + +// SpellCheck starts FT.SPELLCHECK builder. +func (c *Client) SpellCheck(ctx context.Context, index, query string) *SpellCheckBuilder { + return &SpellCheckBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTSpellCheckOptions{}} +} + +// Distance sets MAXDISTANCE. +func (b *SpellCheckBuilder) Distance(d int) *SpellCheckBuilder { b.options.Distance = d; return b } + +// Terms sets INCLUDE or EXCLUDE terms. +func (b *SpellCheckBuilder) Terms(include bool, dictionary string, terms ...interface{}) *SpellCheckBuilder { + if b.options.Terms == nil { + b.options.Terms = &FTSpellCheckTerms{} + } + if include { + b.options.Terms.Inclusion = "INCLUDE" + } else { + b.options.Terms.Inclusion = "EXCLUDE" + } + b.options.Terms.Dictionary = dictionary + b.options.Terms.Terms = terms + return b +} + +// Dialect sets dialect version. +func (b *SpellCheckBuilder) Dialect(d int) *SpellCheckBuilder { b.options.Dialect = d; return b } + +// Run executes FT.SPELLCHECK and returns suggestions. +func (b *SpellCheckBuilder) Run() ([]SpellCheckResult, error) { + cmd := b.c.FTSpellCheckWithArgs(b.ctx, b.index, b.query, b.options) + return cmd.Result() +} + +// ---------------------- +// DictBuilder for FT.DICT* commands +// ---------------------- + +type DictBuilder struct { + c *Client + ctx context.Context + dict string + terms []interface{} + action string // add|del|dump +} + +// DictAdd starts FT.DICTADD builder. +func (c *Client) DictAdd(ctx context.Context, dict string, terms ...interface{}) *DictBuilder { + return &DictBuilder{c: c, ctx: ctx, dict: dict, terms: terms, action: "add"} +} + +// DictDel starts FT.DICTDEL builder. +func (c *Client) DictDel(ctx context.Context, dict string, terms ...interface{}) *DictBuilder { + return &DictBuilder{c: c, ctx: ctx, dict: dict, terms: terms, action: "del"} +} + +// DictDump starts FT.DICTDUMP builder. +func (c *Client) DictDump(ctx context.Context, dict string) *DictBuilder { + return &DictBuilder{c: c, ctx: ctx, dict: dict, action: "dump"} +} + +// Run executes the configured dictionary command. +func (b *DictBuilder) Run() (interface{}, error) { + switch b.action { + case "add": + cmd := b.c.FTDictAdd(b.ctx, b.dict, b.terms...) + return cmd.Result() + case "del": + cmd := b.c.FTDictDel(b.ctx, b.dict, b.terms...) + return cmd.Result() + case "dump": + cmd := b.c.FTDictDump(b.ctx, b.dict) + return cmd.Result() + } + return nil, nil +} + +// ---------------------- +// TagValsBuilder for FT.TAGVALS +// ---------------------- + +type TagValsBuilder struct { + c *Client + ctx context.Context + index string + field string +} + +// TagVals starts FT.TAGVALS builder. +func (c *Client) TagVals(ctx context.Context, index, field string) *TagValsBuilder { + return &TagValsBuilder{c: c, ctx: ctx, index: index, field: field} +} + +// Run executes FT.TAGVALS and returns tag values. +func (b *TagValsBuilder) Run() ([]string, error) { + cmd := b.c.FTTagVals(b.ctx, b.index, b.field) + return cmd.Result() +} + +// ---------------------- +// CursorBuilder for FT.CURSOR* +// ---------------------- + +type CursorBuilder struct { + c *Client + ctx context.Context + index string + cursorId int64 + count int + action string // read|del +} + +// CursorRead starts FT.CURSOR READ builder. +func (c *Client) CursorRead(ctx context.Context, index string, cursorId int64) *CursorBuilder { + return &CursorBuilder{c: c, ctx: ctx, index: index, cursorId: cursorId, action: "read"} +} + +// CursorDel starts FT.CURSOR DEL builder. +func (c *Client) CursorDel(ctx context.Context, index string, cursorId int64) *CursorBuilder { + return &CursorBuilder{c: c, ctx: ctx, index: index, cursorId: cursorId, action: "del"} +} + +// Count for READ. +func (b *CursorBuilder) Count(count int) *CursorBuilder { b.count = count; return b } + +// Run executes the cursor command. +func (b *CursorBuilder) Run() (interface{}, error) { + switch b.action { + case "read": + cmd := b.c.FTCursorRead(b.ctx, b.index, int(b.cursorId), b.count) + return cmd.Result() + case "del": + cmd := b.c.FTCursorDel(b.ctx, b.index, int(b.cursorId)) + return cmd.Result() + } + return nil, nil +} + +// ---------------------- +// SynUpdateBuilder for FT.SYNUPDATE +// ---------------------- + +type SynUpdateBuilder struct { + c *Client + ctx context.Context + index string + groupId interface{} + options *FTSynUpdateOptions + terms []interface{} +} + +// SynUpdate starts FT.SYNUPDATE builder. +func (c *Client) SynUpdate(ctx context.Context, index string, groupId interface{}) *SynUpdateBuilder { + return &SynUpdateBuilder{c: c, ctx: ctx, index: index, groupId: groupId, options: &FTSynUpdateOptions{}} +} + +// SkipInitialScan includes SKIPINITIALSCAN. +func (b *SynUpdateBuilder) SkipInitialScan() *SynUpdateBuilder { + b.options.SkipInitialScan = true + return b +} + +// Terms adds synonyms to the group. +func (b *SynUpdateBuilder) Terms(terms ...interface{}) *SynUpdateBuilder { b.terms = terms; return b } + +// Run executes FT.SYNUPDATE. +func (b *SynUpdateBuilder) Run() (string, error) { + cmd := b.c.FTSynUpdateWithArgs(b.ctx, b.index, b.groupId, b.options, b.terms) + return cmd.Result() +} diff --git a/search_builders_test.go b/search_builders_test.go new file mode 100644 index 000000000..0fedf83a9 --- /dev/null +++ b/search_builders_test.go @@ -0,0 +1,680 @@ +package redis_test + +import ( + "context" + "fmt" + + . "github.com/bsm/ginkgo/v2" + . "github.com/bsm/gomega" + "github.com/redis/go-redis/v9" +) + +var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { + ctx := context.Background() + var client *redis.Client + + BeforeEach(func() { + client = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 2}) + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + expectCloseErr := client.Close() + Expect(expectCloseErr).NotTo(HaveOccurred()) + }) + + It("should create index and search with scores using builders", Label("search", "ftcreate", "ftsearch"), func() { + createVal, err := client.CreateIndex(ctx, "idx1"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "foo", "hello world") + client.HSet(ctx, "doc2", "foo", "hello redis") + + res, err := client.Search(ctx, "idx1", "hello").WithScores().Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(Equal(2)) + for _, doc := range res.Docs { + Expect(*doc.Score).To(BeNumerically(">", 0)) + } + }) + + It("should aggregate using builders", Label("search", "ftaggregate"), func() { + _, err := client.CreateIndex(ctx, "idx2"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric}). + Run() + Expect(err).NotTo(HaveOccurred()) + WaitForIndexing(client, "idx2") + + client.HSet(ctx, "d1", "n", 1) + client.HSet(ctx, "d2", "n", 2) + + agg, err := client.Aggregate(ctx, "idx2", "*"). + GroupBy("@n"). + ReduceAs(redis.SearchCount, "count"). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(len(agg.Rows)).To(Equal(2)) + }) + + It("should drop index using builder", Label("search", "ftdropindex"), func() { + Expect(client.CreateIndex(ctx, "idx3"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "x", FieldType: redis.SearchFieldTypeText}). + Run()).To(Equal("OK")) + WaitForIndexing(client, "idx3") + + dropVal, err := client.DropIndex(ctx, "idx3").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(dropVal).To(Equal("OK")) + }) + + It("should manage aliases using builder", Label("search", "ftalias"), func() { + Expect(client.CreateIndex(ctx, "idx4"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}). + Run()).To(Equal("OK")) + WaitForIndexing(client, "idx4") + + addVal, err := client.AliasAdd(ctx, "alias1", "idx4").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(addVal).To(Equal("OK")) + + _, err = client.Search(ctx, "alias1", "*").Run() + Expect(err).NotTo(HaveOccurred()) + + delVal, err := client.AliasDel(ctx, "alias1").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(delVal).To(Equal("OK")) + }) + + It("should explain query using ExplainBuilder", Label("search", "builders", "ftexplain"), func() { + createVal, err := client.CreateIndex(ctx, "idx_explain"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_explain") + + expl, err := client.Explain(ctx, "idx_explain", "foo").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(expl).To(ContainSubstring("UNION")) + }) + + It("should retrieve info using SearchInfo builder", Label("search", "builders", "ftinfo"), func() { + createVal, err := client.CreateIndex(ctx, "idx_info"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_info") + + i, err := client.SearchInfo(ctx, "idx_info").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(i.IndexName).To(Equal("idx_info")) + }) + + It("should spellcheck using builder", Label("search", "builders", "ftspellcheck"), func() { + createVal, err := client.CreateIndex(ctx, "idx_spell"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_spell") + + client.HSet(ctx, "doc1", "foo", "bar") + + _, err = client.SpellCheck(ctx, "idx_spell", "ba").Distance(1).Run() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should manage dictionary using DictBuilder", Label("search", "ftdict"), func() { + addCount, err := client.DictAdd(ctx, "dict1", "a", "b").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(addCount).To(Equal(int64(2))) + + dump, err := client.DictDump(ctx, "dict1").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(dump).To(ContainElements("a", "b")) + + delCount, err := client.DictDel(ctx, "dict1", "a").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(delCount).To(Equal(int64(1))) + }) + + It("should tag values using TagValsBuilder", Label("search", "builders", "fttagvals"), func() { + createVal, err := client.CreateIndex(ctx, "idx_tag"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "tags", FieldType: redis.SearchFieldTypeTag}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_tag") + + client.HSet(ctx, "doc1", "tags", "red,blue") + client.HSet(ctx, "doc2", "tags", "green,blue") + + vals, err := client.TagVals(ctx, "idx_tag", "tags").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(vals).To(BeAssignableToTypeOf([]string{})) + }) + + It("should cursor read and delete using CursorBuilder", Label("search", "builders", "ftcursor"), func() { + Expect(client.CreateIndex(ctx, "idx5"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "f", FieldType: redis.SearchFieldTypeText}). + Run()).To(Equal("OK")) + WaitForIndexing(client, "idx5") + client.HSet(ctx, "doc1", "f", "hello") + client.HSet(ctx, "doc2", "f", "world") + + cursorBuilder := client.CursorRead(ctx, "idx5", 1) + Expect(cursorBuilder).NotTo(BeNil()) + + cursorBuilder = cursorBuilder.Count(10) + Expect(cursorBuilder).NotTo(BeNil()) + + delBuilder := client.CursorDel(ctx, "idx5", 1) + Expect(delBuilder).NotTo(BeNil()) + }) + + It("should update synonyms using SynUpdateBuilder", Label("search", "builders", "ftsynupdate"), func() { + createVal, err := client.CreateIndex(ctx, "idx_syn"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_syn") + + syn, err := client.SynUpdate(ctx, "idx_syn", "grp1").Terms("a", "b").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(syn).To(Equal("OK")) + }) + + It("should test SearchBuilder with NoContent and Verbatim", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.CreateIndex(ctx, "idx_nocontent"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Weight: 5}). + Schema(&redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_nocontent") + + client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch implements a search engine on top of redis") + + res, err := client.Search(ctx, "idx_nocontent", "search engine"). + NoContent(). + Verbatim(). + Limit(0, 5). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(Equal(1)) + Expect(res.Docs[0].ID).To(Equal("doc1")) + // NoContent means no fields should be returned + Expect(res.Docs[0].Fields).To(BeEmpty()) + }) + + It("should test SearchBuilder with NoStopWords", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.CreateIndex(ctx, "idx_nostop"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_nostop") + + client.HSet(ctx, "doc1", "txt", "hello world") + client.HSet(ctx, "doc2", "txt", "test document") + + // Test that NoStopWords method can be called and search works + res, err := client.Search(ctx, "idx_nostop", "hello").NoContent().NoStopWords().Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(Equal(1)) + }) + + It("should test SearchBuilder with filters", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.CreateIndex(ctx, "idx_filters"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). + Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric}). + Schema(&redis.FieldSchema{FieldName: "loc", FieldType: redis.SearchFieldTypeGeo}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_filters") + + 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") + + // Test numeric filter + res1, err := client.Search(ctx, "idx_filters", "foo"). + Filter("num", 2, 4). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(2)) + + // Test geo filter + res2, err := client.Search(ctx, "idx_filters", "foo"). + GeoFilter("loc", -0.44, 51.45, 10, "km"). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(1)) + }) + + It("should test SearchBuilder with sorting", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.CreateIndex(ctx, "idx_sort"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). + Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_sort") + + 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) + + // Test ascending sort + res1, err := client.Search(ctx, "idx_sort", "foo"). + SortBy("num", true). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(3)) + Expect(res1.Docs[0].ID).To(Equal("doc1")) + Expect(res1.Docs[1].ID).To(Equal("doc2")) + Expect(res1.Docs[2].ID).To(Equal("doc3")) + + // Test descending sort + res2, err := client.Search(ctx, "idx_sort", "foo"). + SortBy("num", false). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(3)) + Expect(res2.Docs[0].ID).To(Equal("doc3")) + Expect(res2.Docs[1].ID).To(Equal("doc2")) + Expect(res2.Docs[2].ID).To(Equal("doc1")) + }) + + It("should test SearchBuilder with InKeys and InFields", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.CreateIndex(ctx, "idx_in"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}). + Schema(&redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_in") + + client.HSet(ctx, "doc1", "title", "hello world", "body", "lorem ipsum") + client.HSet(ctx, "doc2", "title", "foo bar", "body", "hello world") + client.HSet(ctx, "doc3", "title", "baz qux", "body", "dolor sit") + + // Test InKeys + res1, err := client.Search(ctx, "idx_in", "hello"). + InKeys("doc1", "doc2"). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(2)) + + // Test InFields + res2, err := client.Search(ctx, "idx_in", "hello"). + InFields("title"). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(1)) + Expect(res2.Docs[0].ID).To(Equal("doc1")) + }) + + It("should test SearchBuilder with Return fields", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.CreateIndex(ctx, "idx_return"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}). + Schema(&redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}). + Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_return") + + client.HSet(ctx, "doc1", "title", "hello", "body", "world", "num", 42) + + // Test ReturnFields + res1, err := client.Search(ctx, "idx_return", "hello"). + ReturnFields("title", "num"). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(1)) + Expect(res1.Docs[0].Fields).To(HaveKey("title")) + Expect(res1.Docs[0].Fields).To(HaveKey("num")) + Expect(res1.Docs[0].Fields).NotTo(HaveKey("body")) + + // Test ReturnAs + res2, err := client.Search(ctx, "idx_return", "hello"). + ReturnAs("title", "doc_title"). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(1)) + Expect(res2.Docs[0].Fields).To(HaveKey("doc_title")) + Expect(res2.Docs[0].Fields).NotTo(HaveKey("title")) + }) + + It("should test SearchBuilder with advanced options", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.CreateIndex(ctx, "idx_advanced"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_advanced") + + 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") + + // Test with scores and different scorers + res1, err := client.Search(ctx, "idx_advanced", "quick"). + WithScores(). + Scorer("TFIDF"). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(2)) + for _, doc := range res1.Docs { + Expect(*doc.Score).To(BeNumerically(">", 0)) + } + + res2, err := client.Search(ctx, "idx_advanced", "quick"). + WithScores(). + Payload("test_payload"). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(2)) + + // Test with Slop and InOrder + res3, err := client.Search(ctx, "idx_advanced", "quick brown"). + Slop(1). + InOrder(). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res3.Total).To(Equal(1)) + + // Test with Language and Expander + res4, err := client.Search(ctx, "idx_advanced", "quick"). + Language("english"). + Expander("SYNONYM"). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res4.Total).To(BeNumerically(">=", 0)) + + // Test with Timeout + res5, err := client.Search(ctx, "idx_advanced", "quick"). + Timeout(1000). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res5.Total).To(Equal(2)) + }) + + It("should test SearchBuilder with Params and Dialect", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.CreateIndex(ctx, "idx_params"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_params") + + client.HSet(ctx, "doc1", "name", "Alice") + client.HSet(ctx, "doc2", "name", "Bob") + client.HSet(ctx, "doc3", "name", "Carol") + + // Test with single param + res1, err := client.Search(ctx, "idx_params", "@name:$name"). + Param("name", "Alice"). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(1)) + Expect(res1.Docs[0].ID).To(Equal("doc1")) + + // Test with multiple params using ParamsMap + params := map[string]interface{}{ + "name1": "Bob", + "name2": "Carol", + } + res2, err := client.Search(ctx, "idx_params", "@name:($name1|$name2)"). + ParamsMap(params). + Dialect(2). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(2)) + }) + + It("should test SearchBuilder with Limit and CountOnly", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.CreateIndex(ctx, "idx_limit"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_limit") + + for i := 1; i <= 10; i++ { + client.HSet(ctx, fmt.Sprintf("doc%d", i), "txt", "test document") + } + + // Test with Limit + res1, err := client.Search(ctx, "idx_limit", "test"). + Limit(2, 3). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(10)) + Expect(len(res1.Docs)).To(Equal(3)) + + // Test with CountOnly + res2, err := client.Search(ctx, "idx_limit", "test"). + CountOnly(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(10)) + Expect(len(res2.Docs)).To(Equal(0)) + }) + + It("should test SearchBuilder with WithSortByCount and SortBy", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.CreateIndex(ctx, "idx_payloads"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). + Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_payloads") + + client.HSet(ctx, "doc1", "txt", "hello", "num", 1) + client.HSet(ctx, "doc2", "txt", "world", "num", 2) + + // Test WithSortByCount and SortBy + res, err := client.Search(ctx, "idx_payloads", "*"). + SortBy("num", true). + WithSortByCount(). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(Equal(2)) + }) + + It("should test SearchBuilder with JSON", Label("search", "ftsearch", "builders", "json"), func() { + createVal, err := client.CreateIndex(ctx, "idx_json"). + OnJSON(). + Prefix("king:"). + Schema(&redis.FieldSchema{FieldName: "$.name", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_json") + + client.JSONSet(ctx, "king:1", "$", `{"name": "henry"}`) + client.JSONSet(ctx, "king:2", "$", `{"name": "james"}`) + + res, err := client.Search(ctx, "idx_json", "henry").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(Equal(1)) + Expect(res.Docs[0].ID).To(Equal("king:1")) + Expect(res.Docs[0].Fields["$"]).To(Equal(`{"name":"henry"}`)) + }) + + It("should test SearchBuilder with vector search", Label("search", "ftsearch", "builders", "vector"), func() { + hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"} + createVal, err := client.CreateIndex(ctx, "idx_vector"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_vector") + + client.HSet(ctx, "a", "v", "aaaaaaaa") + client.HSet(ctx, "b", "v", "aaaabaaa") + client.HSet(ctx, "c", "v", "aaaaabaa") + + res, err := client.Search(ctx, "idx_vector", "*=>[KNN 2 @v $vec]"). + ReturnFields("__v_score"). + SortBy("__v_score", true). + Dialect(2). + Param("vec", "aaaaaaaa"). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(Equal("a")) + Expect(res.Docs[0].Fields["__v_score"]).To(Equal("0")) + }) + + It("should test SearchBuilder with complex filtering and aggregation", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.CreateIndex(ctx, "idx_complex"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "category", FieldType: redis.SearchFieldTypeTag}). + Schema(&redis.FieldSchema{FieldName: "price", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}). + Schema(&redis.FieldSchema{FieldName: "location", FieldType: redis.SearchFieldTypeGeo}). + Schema(&redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_complex") + + client.HSet(ctx, "product1", "category", "electronics", "price", 100, "location", "-0.1,51.5", "description", "smartphone device") + client.HSet(ctx, "product2", "category", "electronics", "price", 200, "location", "-0.2,51.6", "description", "laptop computer") + client.HSet(ctx, "product3", "category", "books", "price", 20, "location", "-0.3,51.7", "description", "programming guide") + + res, err := client.Search(ctx, "idx_complex", "@category:{electronics} @description:(device|computer)"). + Filter("price", 50, 250). + GeoFilter("location", -0.15, 51.55, 50, "km"). + SortBy("price", true). + ReturnFields("category", "price", "description"). + Limit(0, 10). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeNumerically(">=", 1)) + + res2, err := client.Search(ctx, "idx_complex", "@category:{$cat} @price:[$min $max]"). + ParamsMap(map[string]interface{}{ + "cat": "electronics", + "min": 150, + "max": 300, + }). + Dialect(2). + WithScores(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(1)) + Expect(res2.Docs[0].ID).To(Equal("product2")) + }) + + It("should test SearchBuilder error handling and edge cases", Label("search", "ftsearch", "builders", "edge-cases"), func() { + createVal, err := client.CreateIndex(ctx, "idx_edge"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_edge") + + client.HSet(ctx, "doc1", "txt", "hello world") + + // Test empty query + res1, err := client.Search(ctx, "idx_edge", "*").NoContent().Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(1)) + + // Test query with no results + res2, err := client.Search(ctx, "idx_edge", "nonexistent").NoContent().Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(0)) + + // Test with multiple chained methods + res3, err := client.Search(ctx, "idx_edge", "hello"). + WithScores(). + NoContent(). + Verbatim(). + InOrder(). + Slop(0). + Timeout(5000). + Language("english"). + Dialect(2). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res3.Total).To(Equal(1)) + }) + + It("should test SearchBuilder method chaining", Label("search", "ftsearch", "builders", "fluent"), func() { + createVal, err := client.CreateIndex(ctx, "idx_fluent"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}). + Schema(&redis.FieldSchema{FieldName: "tags", FieldType: redis.SearchFieldTypeTag}). + Schema(&redis.FieldSchema{FieldName: "score", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_fluent") + + client.HSet(ctx, "doc1", "title", "Redis Search Tutorial", "tags", "redis,search,tutorial", "score", 95) + client.HSet(ctx, "doc2", "title", "Advanced Redis", "tags", "redis,advanced", "score", 88) + + builder := client.Search(ctx, "idx_fluent", "@title:(redis) @tags:{search}") + result := builder. + WithScores(). + Filter("score", 90, 100). + SortBy("score", false). + ReturnFields("title", "score"). + Limit(0, 5). + Dialect(2). + Timeout(1000). + Language("english") + + res, err := result.Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(Equal(1)) + Expect(res.Docs[0].ID).To(Equal("doc1")) + Expect(res.Docs[0].Fields["title"]).To(Equal("Redis Search Tutorial")) + Expect(*res.Docs[0].Score).To(BeNumerically(">", 0)) + }) +}) From 3dc30512d274062ee11bc928475bd6a52c09f5e1 Mon Sep 17 00:00:00 2001 From: ofekshenawa Date: Sun, 3 Aug 2025 17:55:57 +0300 Subject: [PATCH 2/5] Use builders and Actions in more clean way --- search_builders.go | 129 +++++++++++++++++++++++++------------ search_builders_test.go | 138 ++++++++++++++++++++-------------------- 2 files changed, 156 insertions(+), 111 deletions(-) diff --git a/search_builders.go b/search_builders.go index 964b26878..a35d6e928 100644 --- a/search_builders.go +++ b/search_builders.go @@ -18,8 +18,8 @@ type SearchBuilder struct { options *FTSearchOptions } -// Search starts building an FT.SEARCH command. -func (c *Client) Search(ctx context.Context, index, query string) *SearchBuilder { +// NewSearchBuilder creates a new SearchBuilder for FT.SEARCH commands. +func (c *Client) NewSearchBuilder(ctx context.Context, index, query string) *SearchBuilder { b := &SearchBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTSearchOptions{LimitOffset: -1}} return b } @@ -215,8 +215,8 @@ type AggregateBuilder struct { options *FTAggregateOptions } -// Aggregate starts building an FT.AGGREGATE command. -func (c *Client) Aggregate(ctx context.Context, index, query string) *AggregateBuilder { +// NewAggregateBuilder creates a new AggregateBuilder for FT.AGGREGATE commands. +func (c *Client) NewAggregateBuilder(ctx context.Context, index, query string) *AggregateBuilder { return &AggregateBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTAggregateOptions{LimitOffset: -1}} } @@ -367,8 +367,8 @@ type CreateIndexBuilder struct { schema []*FieldSchema } -// CreateIndex starts building an FT.CREATE command. -func (c *Client) CreateIndex(ctx context.Context, index string) *CreateIndexBuilder { +// NewCreateIndexBuilder creates a new CreateIndexBuilder for FT.CREATE commands. +func (c *Client) NewCreateIndexBuilder(ctx context.Context, index string) *CreateIndexBuilder { return &CreateIndexBuilder{c: c, ctx: ctx, index: index, options: &FTCreateOptions{}} } @@ -473,8 +473,8 @@ type DropIndexBuilder struct { options *FTDropIndexOptions } -// DropIndex starts FT.DROPINDEX builder. -func (c *Client) DropIndex(ctx context.Context, index string) *DropIndexBuilder { +// NewDropIndexBuilder creates a new DropIndexBuilder for FT.DROPINDEX commands. +func (c *Client) NewDropIndexBuilder(ctx context.Context, index string) *DropIndexBuilder { return &DropIndexBuilder{c: c, ctx: ctx, index: index} } @@ -499,19 +499,35 @@ type AliasBuilder struct { action string // add|del|update } -// AliasAdd starts FT.ALIASADD builder. -func (c *Client) AliasAdd(ctx context.Context, alias, index string) *AliasBuilder { - return &AliasBuilder{c: c, ctx: ctx, alias: alias, index: index, action: "add"} +// NewAliasBuilder creates a new AliasBuilder for FT.ALIAS* commands. +func (c *Client) NewAliasBuilder(ctx context.Context, alias string) *AliasBuilder { + return &AliasBuilder{c: c, ctx: ctx, alias: alias} } -// AliasDel starts FT.ALIASDEL builder. -func (c *Client) AliasDel(ctx context.Context, alias string) *AliasBuilder { - return &AliasBuilder{c: c, ctx: ctx, alias: alias, action: "del"} +// Action sets the action for the alias builder. +func (b *AliasBuilder) Action(action string) *AliasBuilder { + b.action = action + return b +} + +// Add sets the action to "add" and requires an index. +func (b *AliasBuilder) Add(index string) *AliasBuilder { + b.action = "add" + b.index = index + return b +} + +// Del sets the action to "del". +func (b *AliasBuilder) Del() *AliasBuilder { + b.action = "del" + return b } -// AliasUpdate starts FT.ALIASUPDATE builder. -func (c *Client) AliasUpdate(ctx context.Context, alias, index string) *AliasBuilder { - return &AliasBuilder{c: c, ctx: ctx, alias: alias, index: index, action: "update"} +// Update sets the action to "update" and requires an index. +func (b *AliasBuilder) Update(index string) *AliasBuilder { + b.action = "update" + b.index = index + return b } // Run executes the configured alias command. @@ -542,8 +558,8 @@ type ExplainBuilder struct { options *FTExplainOptions } -// Explain starts FT.EXPLAIN builder. -func (c *Client) Explain(ctx context.Context, index, query string) *ExplainBuilder { +// NewExplainBuilder creates a new ExplainBuilder for FT.EXPLAIN commands. +func (c *Client) NewExplainBuilder(ctx context.Context, index, query string) *ExplainBuilder { return &ExplainBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTExplainOptions{}} } @@ -566,8 +582,8 @@ type FTInfoBuilder struct { index string } -// SearchInfo starts building an FT.INFO command for RediSearch. -func (c *Client) SearchInfo(ctx context.Context, index string) *FTInfoBuilder { +// NewSearchInfoBuilder creates a new FTInfoBuilder for FT.INFO commands. +func (c *Client) NewSearchInfoBuilder(ctx context.Context, index string) *FTInfoBuilder { return &FTInfoBuilder{c: c, ctx: ctx, index: index} } @@ -589,8 +605,8 @@ type SpellCheckBuilder struct { options *FTSpellCheckOptions } -// SpellCheck starts FT.SPELLCHECK builder. -func (c *Client) SpellCheck(ctx context.Context, index, query string) *SpellCheckBuilder { +// NewSpellCheckBuilder creates a new SpellCheckBuilder for FT.SPELLCHECK commands. +func (c *Client) NewSpellCheckBuilder(ctx context.Context, index, query string) *SpellCheckBuilder { return &SpellCheckBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTSpellCheckOptions{}} } @@ -633,19 +649,35 @@ type DictBuilder struct { action string // add|del|dump } -// DictAdd starts FT.DICTADD builder. -func (c *Client) DictAdd(ctx context.Context, dict string, terms ...interface{}) *DictBuilder { - return &DictBuilder{c: c, ctx: ctx, dict: dict, terms: terms, action: "add"} +// NewDictBuilder creates a new DictBuilder for FT.DICT* commands. +func (c *Client) NewDictBuilder(ctx context.Context, dict string) *DictBuilder { + return &DictBuilder{c: c, ctx: ctx, dict: dict} } -// DictDel starts FT.DICTDEL builder. -func (c *Client) DictDel(ctx context.Context, dict string, terms ...interface{}) *DictBuilder { - return &DictBuilder{c: c, ctx: ctx, dict: dict, terms: terms, action: "del"} +// Action sets the action for the dictionary builder. +func (b *DictBuilder) Action(action string) *DictBuilder { + b.action = action + return b } -// DictDump starts FT.DICTDUMP builder. -func (c *Client) DictDump(ctx context.Context, dict string) *DictBuilder { - return &DictBuilder{c: c, ctx: ctx, dict: dict, action: "dump"} +// Add sets the action to "add" and requires terms. +func (b *DictBuilder) Add(terms ...interface{}) *DictBuilder { + b.action = "add" + b.terms = terms + return b +} + +// Del sets the action to "del" and requires terms. +func (b *DictBuilder) Del(terms ...interface{}) *DictBuilder { + b.action = "del" + b.terms = terms + return b +} + +// Dump sets the action to "dump". +func (b *DictBuilder) Dump() *DictBuilder { + b.action = "dump" + return b } // Run executes the configured dictionary command. @@ -675,8 +707,8 @@ type TagValsBuilder struct { field string } -// TagVals starts FT.TAGVALS builder. -func (c *Client) TagVals(ctx context.Context, index, field string) *TagValsBuilder { +// NewTagValsBuilder creates a new TagValsBuilder for FT.TAGVALS commands. +func (c *Client) NewTagValsBuilder(ctx context.Context, index, field string) *TagValsBuilder { return &TagValsBuilder{c: c, ctx: ctx, index: index, field: field} } @@ -699,14 +731,27 @@ type CursorBuilder struct { action string // read|del } -// CursorRead starts FT.CURSOR READ builder. -func (c *Client) CursorRead(ctx context.Context, index string, cursorId int64) *CursorBuilder { - return &CursorBuilder{c: c, ctx: ctx, index: index, cursorId: cursorId, action: "read"} +// NewCursorBuilder creates a new CursorBuilder for FT.CURSOR* commands. +func (c *Client) NewCursorBuilder(ctx context.Context, index string, cursorId int64) *CursorBuilder { + return &CursorBuilder{c: c, ctx: ctx, index: index, cursorId: cursorId} } -// CursorDel starts FT.CURSOR DEL builder. -func (c *Client) CursorDel(ctx context.Context, index string, cursorId int64) *CursorBuilder { - return &CursorBuilder{c: c, ctx: ctx, index: index, cursorId: cursorId, action: "del"} +// Action sets the action for the cursor builder. +func (b *CursorBuilder) Action(action string) *CursorBuilder { + b.action = action + return b +} + +// Read sets the action to "read". +func (b *CursorBuilder) Read() *CursorBuilder { + b.action = "read" + return b +} + +// Del sets the action to "del". +func (b *CursorBuilder) Del() *CursorBuilder { + b.action = "del" + return b } // Count for READ. @@ -738,8 +783,8 @@ type SynUpdateBuilder struct { terms []interface{} } -// SynUpdate starts FT.SYNUPDATE builder. -func (c *Client) SynUpdate(ctx context.Context, index string, groupId interface{}) *SynUpdateBuilder { +// NewSynUpdateBuilder creates a new SynUpdateBuilder for FT.SYNUPDATE commands. +func (c *Client) NewSynUpdateBuilder(ctx context.Context, index string, groupId interface{}) *SynUpdateBuilder { return &SynUpdateBuilder{c: c, ctx: ctx, index: index, groupId: groupId, options: &FTSynUpdateOptions{}} } diff --git a/search_builders_test.go b/search_builders_test.go index 0fedf83a9..bd8b6ff7c 100644 --- a/search_builders_test.go +++ b/search_builders_test.go @@ -24,7 +24,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should create index and search with scores using builders", Label("search", "ftcreate", "ftsearch"), func() { - createVal, err := client.CreateIndex(ctx, "idx1"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx1"). OnHash(). Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). Run() @@ -36,7 +36,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc1", "foo", "hello world") client.HSet(ctx, "doc2", "foo", "hello redis") - res, err := client.Search(ctx, "idx1", "hello").WithScores().Run() + res, err := client.NewSearchBuilder(ctx, "idx1", "hello").WithScores().Run() Expect(err).NotTo(HaveOccurred()) Expect(res.Total).To(Equal(2)) for _, doc := range res.Docs { @@ -45,7 +45,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should aggregate using builders", Label("search", "ftaggregate"), func() { - _, err := client.CreateIndex(ctx, "idx2"). + _, err := client.NewCreateIndexBuilder(ctx, "idx2"). OnHash(). Schema(&redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric}). Run() @@ -55,7 +55,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "d1", "n", 1) client.HSet(ctx, "d2", "n", 2) - agg, err := client.Aggregate(ctx, "idx2", "*"). + agg, err := client.NewAggregateBuilder(ctx, "idx2", "*"). GroupBy("@n"). ReduceAs(redis.SearchCount, "count"). Run() @@ -64,38 +64,38 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should drop index using builder", Label("search", "ftdropindex"), func() { - Expect(client.CreateIndex(ctx, "idx3"). + Expect(client.NewCreateIndexBuilder(ctx, "idx3"). OnHash(). Schema(&redis.FieldSchema{FieldName: "x", FieldType: redis.SearchFieldTypeText}). Run()).To(Equal("OK")) WaitForIndexing(client, "idx3") - dropVal, err := client.DropIndex(ctx, "idx3").Run() + dropVal, err := client.NewDropIndexBuilder(ctx, "idx3").Run() Expect(err).NotTo(HaveOccurred()) Expect(dropVal).To(Equal("OK")) }) It("should manage aliases using builder", Label("search", "ftalias"), func() { - Expect(client.CreateIndex(ctx, "idx4"). + Expect(client.NewCreateIndexBuilder(ctx, "idx4"). OnHash(). Schema(&redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}). Run()).To(Equal("OK")) WaitForIndexing(client, "idx4") - addVal, err := client.AliasAdd(ctx, "alias1", "idx4").Run() + addVal, err := client.NewAliasBuilder(ctx, "alias1").Add("idx4").Run() Expect(err).NotTo(HaveOccurred()) Expect(addVal).To(Equal("OK")) - _, err = client.Search(ctx, "alias1", "*").Run() + _, err = client.NewSearchBuilder(ctx, "alias1", "*").Run() Expect(err).NotTo(HaveOccurred()) - delVal, err := client.AliasDel(ctx, "alias1").Run() + delVal, err := client.NewAliasBuilder(ctx, "alias1").Del().Run() Expect(err).NotTo(HaveOccurred()) Expect(delVal).To(Equal("OK")) }) It("should explain query using ExplainBuilder", Label("search", "builders", "ftexplain"), func() { - createVal, err := client.CreateIndex(ctx, "idx_explain"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_explain"). OnHash(). Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). Run() @@ -103,13 +103,13 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { Expect(createVal).To(Equal("OK")) WaitForIndexing(client, "idx_explain") - expl, err := client.Explain(ctx, "idx_explain", "foo").Run() + expl, err := client.NewExplainBuilder(ctx, "idx_explain", "foo").Run() Expect(err).NotTo(HaveOccurred()) Expect(expl).To(ContainSubstring("UNION")) }) It("should retrieve info using SearchInfo builder", Label("search", "builders", "ftinfo"), func() { - createVal, err := client.CreateIndex(ctx, "idx_info"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_info"). OnHash(). Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). Run() @@ -117,13 +117,13 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { Expect(createVal).To(Equal("OK")) WaitForIndexing(client, "idx_info") - i, err := client.SearchInfo(ctx, "idx_info").Run() + i, err := client.NewSearchInfoBuilder(ctx, "idx_info").Run() Expect(err).NotTo(HaveOccurred()) Expect(i.IndexName).To(Equal("idx_info")) }) It("should spellcheck using builder", Label("search", "builders", "ftspellcheck"), func() { - createVal, err := client.CreateIndex(ctx, "idx_spell"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_spell"). OnHash(). Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). Run() @@ -133,26 +133,26 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc1", "foo", "bar") - _, err = client.SpellCheck(ctx, "idx_spell", "ba").Distance(1).Run() + _, err = client.NewSpellCheckBuilder(ctx, "idx_spell", "ba").Distance(1).Run() Expect(err).NotTo(HaveOccurred()) }) It("should manage dictionary using DictBuilder", Label("search", "ftdict"), func() { - addCount, err := client.DictAdd(ctx, "dict1", "a", "b").Run() + addCount, err := client.NewDictBuilder(ctx, "dict1").Add("a", "b").Run() Expect(err).NotTo(HaveOccurred()) Expect(addCount).To(Equal(int64(2))) - dump, err := client.DictDump(ctx, "dict1").Run() + dump, err := client.NewDictBuilder(ctx, "dict1").Dump().Run() Expect(err).NotTo(HaveOccurred()) Expect(dump).To(ContainElements("a", "b")) - delCount, err := client.DictDel(ctx, "dict1", "a").Run() + delCount, err := client.NewDictBuilder(ctx, "dict1").Del("a").Run() Expect(err).NotTo(HaveOccurred()) Expect(delCount).To(Equal(int64(1))) }) It("should tag values using TagValsBuilder", Label("search", "builders", "fttagvals"), func() { - createVal, err := client.CreateIndex(ctx, "idx_tag"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_tag"). OnHash(). Schema(&redis.FieldSchema{FieldName: "tags", FieldType: redis.SearchFieldTypeTag}). Run() @@ -163,13 +163,13 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc1", "tags", "red,blue") client.HSet(ctx, "doc2", "tags", "green,blue") - vals, err := client.TagVals(ctx, "idx_tag", "tags").Run() + vals, err := client.NewTagValsBuilder(ctx, "idx_tag", "tags").Run() Expect(err).NotTo(HaveOccurred()) Expect(vals).To(BeAssignableToTypeOf([]string{})) }) It("should cursor read and delete using CursorBuilder", Label("search", "builders", "ftcursor"), func() { - Expect(client.CreateIndex(ctx, "idx5"). + Expect(client.NewCreateIndexBuilder(ctx, "idx5"). OnHash(). Schema(&redis.FieldSchema{FieldName: "f", FieldType: redis.SearchFieldTypeText}). Run()).To(Equal("OK")) @@ -177,18 +177,18 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc1", "f", "hello") client.HSet(ctx, "doc2", "f", "world") - cursorBuilder := client.CursorRead(ctx, "idx5", 1) + cursorBuilder := client.NewCursorBuilder(ctx, "idx5", 1) Expect(cursorBuilder).NotTo(BeNil()) cursorBuilder = cursorBuilder.Count(10) Expect(cursorBuilder).NotTo(BeNil()) - delBuilder := client.CursorDel(ctx, "idx5", 1) + delBuilder := client.NewCursorBuilder(ctx, "idx5", 1) Expect(delBuilder).NotTo(BeNil()) }) It("should update synonyms using SynUpdateBuilder", Label("search", "builders", "ftsynupdate"), func() { - createVal, err := client.CreateIndex(ctx, "idx_syn"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_syn"). OnHash(). Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). Run() @@ -196,13 +196,13 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { Expect(createVal).To(Equal("OK")) WaitForIndexing(client, "idx_syn") - syn, err := client.SynUpdate(ctx, "idx_syn", "grp1").Terms("a", "b").Run() + syn, err := client.NewSynUpdateBuilder(ctx, "idx_syn", "grp1").Terms("a", "b").Run() Expect(err).NotTo(HaveOccurred()) Expect(syn).To(Equal("OK")) }) It("should test SearchBuilder with NoContent and Verbatim", Label("search", "ftsearch", "builders"), func() { - createVal, err := client.CreateIndex(ctx, "idx_nocontent"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_nocontent"). OnHash(). Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Weight: 5}). Schema(&redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}). @@ -213,7 +213,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch implements a search engine on top of redis") - res, err := client.Search(ctx, "idx_nocontent", "search engine"). + res, err := client.NewSearchBuilder(ctx, "idx_nocontent", "search engine"). NoContent(). Verbatim(). Limit(0, 5). @@ -226,7 +226,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should test SearchBuilder with NoStopWords", Label("search", "ftsearch", "builders"), func() { - createVal, err := client.CreateIndex(ctx, "idx_nostop"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_nostop"). OnHash(). Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). Run() @@ -238,13 +238,13 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc2", "txt", "test document") // Test that NoStopWords method can be called and search works - res, err := client.Search(ctx, "idx_nostop", "hello").NoContent().NoStopWords().Run() + res, err := client.NewSearchBuilder(ctx, "idx_nostop", "hello").NoContent().NoStopWords().Run() Expect(err).NotTo(HaveOccurred()) Expect(res.Total).To(Equal(1)) }) It("should test SearchBuilder with filters", Label("search", "ftsearch", "builders"), func() { - createVal, err := client.CreateIndex(ctx, "idx_filters"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_filters"). OnHash(). Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric}). @@ -258,7 +258,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc2", "txt", "foo baz", "num", 2, "loc", "-0.1,51.2") // Test numeric filter - res1, err := client.Search(ctx, "idx_filters", "foo"). + res1, err := client.NewSearchBuilder(ctx, "idx_filters", "foo"). Filter("num", 2, 4). NoContent(). Run() @@ -266,7 +266,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { Expect(res1.Total).To(Equal(2)) // Test geo filter - res2, err := client.Search(ctx, "idx_filters", "foo"). + res2, err := client.NewSearchBuilder(ctx, "idx_filters", "foo"). GeoFilter("loc", -0.44, 51.45, 10, "km"). NoContent(). Run() @@ -275,7 +275,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should test SearchBuilder with sorting", Label("search", "ftsearch", "builders"), func() { - createVal, err := client.CreateIndex(ctx, "idx_sort"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_sort"). OnHash(). Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}). @@ -289,7 +289,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc3", "txt", "foo qux", "num", 3) // Test ascending sort - res1, err := client.Search(ctx, "idx_sort", "foo"). + res1, err := client.NewSearchBuilder(ctx, "idx_sort", "foo"). SortBy("num", true). NoContent(). Run() @@ -300,7 +300,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { Expect(res1.Docs[2].ID).To(Equal("doc3")) // Test descending sort - res2, err := client.Search(ctx, "idx_sort", "foo"). + res2, err := client.NewSearchBuilder(ctx, "idx_sort", "foo"). SortBy("num", false). NoContent(). Run() @@ -312,7 +312,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should test SearchBuilder with InKeys and InFields", Label("search", "ftsearch", "builders"), func() { - createVal, err := client.CreateIndex(ctx, "idx_in"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_in"). OnHash(). Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}). Schema(&redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}). @@ -326,7 +326,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc3", "title", "baz qux", "body", "dolor sit") // Test InKeys - res1, err := client.Search(ctx, "idx_in", "hello"). + res1, err := client.NewSearchBuilder(ctx, "idx_in", "hello"). InKeys("doc1", "doc2"). NoContent(). Run() @@ -334,7 +334,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { Expect(res1.Total).To(Equal(2)) // Test InFields - res2, err := client.Search(ctx, "idx_in", "hello"). + res2, err := client.NewSearchBuilder(ctx, "idx_in", "hello"). InFields("title"). NoContent(). Run() @@ -344,7 +344,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should test SearchBuilder with Return fields", Label("search", "ftsearch", "builders"), func() { - createVal, err := client.CreateIndex(ctx, "idx_return"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_return"). OnHash(). Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}). Schema(&redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}). @@ -357,7 +357,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc1", "title", "hello", "body", "world", "num", 42) // Test ReturnFields - res1, err := client.Search(ctx, "idx_return", "hello"). + res1, err := client.NewSearchBuilder(ctx, "idx_return", "hello"). ReturnFields("title", "num"). Run() Expect(err).NotTo(HaveOccurred()) @@ -367,7 +367,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { Expect(res1.Docs[0].Fields).NotTo(HaveKey("body")) // Test ReturnAs - res2, err := client.Search(ctx, "idx_return", "hello"). + res2, err := client.NewSearchBuilder(ctx, "idx_return", "hello"). ReturnAs("title", "doc_title"). Run() Expect(err).NotTo(HaveOccurred()) @@ -377,7 +377,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should test SearchBuilder with advanced options", Label("search", "ftsearch", "builders"), func() { - createVal, err := client.CreateIndex(ctx, "idx_advanced"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_advanced"). OnHash(). Schema(&redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText}). Run() @@ -389,7 +389,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc2", "description", "Quick alice was beginning to get very tired of sitting by her quick sister on the bank") // Test with scores and different scorers - res1, err := client.Search(ctx, "idx_advanced", "quick"). + res1, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick"). WithScores(). Scorer("TFIDF"). Run() @@ -399,7 +399,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { Expect(*doc.Score).To(BeNumerically(">", 0)) } - res2, err := client.Search(ctx, "idx_advanced", "quick"). + res2, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick"). WithScores(). Payload("test_payload"). NoContent(). @@ -408,7 +408,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { Expect(res2.Total).To(Equal(2)) // Test with Slop and InOrder - res3, err := client.Search(ctx, "idx_advanced", "quick brown"). + res3, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick brown"). Slop(1). InOrder(). NoContent(). @@ -417,7 +417,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { Expect(res3.Total).To(Equal(1)) // Test with Language and Expander - res4, err := client.Search(ctx, "idx_advanced", "quick"). + res4, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick"). Language("english"). Expander("SYNONYM"). NoContent(). @@ -426,7 +426,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { Expect(res4.Total).To(BeNumerically(">=", 0)) // Test with Timeout - res5, err := client.Search(ctx, "idx_advanced", "quick"). + res5, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick"). Timeout(1000). NoContent(). Run() @@ -435,7 +435,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should test SearchBuilder with Params and Dialect", Label("search", "ftsearch", "builders"), func() { - createVal, err := client.CreateIndex(ctx, "idx_params"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_params"). OnHash(). Schema(&redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText}). Run() @@ -448,7 +448,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc3", "name", "Carol") // Test with single param - res1, err := client.Search(ctx, "idx_params", "@name:$name"). + res1, err := client.NewSearchBuilder(ctx, "idx_params", "@name:$name"). Param("name", "Alice"). NoContent(). Run() @@ -461,7 +461,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { "name1": "Bob", "name2": "Carol", } - res2, err := client.Search(ctx, "idx_params", "@name:($name1|$name2)"). + res2, err := client.NewSearchBuilder(ctx, "idx_params", "@name:($name1|$name2)"). ParamsMap(params). Dialect(2). NoContent(). @@ -471,7 +471,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should test SearchBuilder with Limit and CountOnly", Label("search", "ftsearch", "builders"), func() { - createVal, err := client.CreateIndex(ctx, "idx_limit"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_limit"). OnHash(). Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). Run() @@ -484,7 +484,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { } // Test with Limit - res1, err := client.Search(ctx, "idx_limit", "test"). + res1, err := client.NewSearchBuilder(ctx, "idx_limit", "test"). Limit(2, 3). NoContent(). Run() @@ -493,7 +493,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { Expect(len(res1.Docs)).To(Equal(3)) // Test with CountOnly - res2, err := client.Search(ctx, "idx_limit", "test"). + res2, err := client.NewSearchBuilder(ctx, "idx_limit", "test"). CountOnly(). Run() Expect(err).NotTo(HaveOccurred()) @@ -502,7 +502,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should test SearchBuilder with WithSortByCount and SortBy", Label("search", "ftsearch", "builders"), func() { - createVal, err := client.CreateIndex(ctx, "idx_payloads"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_payloads"). OnHash(). Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}). @@ -515,7 +515,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc2", "txt", "world", "num", 2) // Test WithSortByCount and SortBy - res, err := client.Search(ctx, "idx_payloads", "*"). + res, err := client.NewSearchBuilder(ctx, "idx_payloads", "*"). SortBy("num", true). WithSortByCount(). NoContent(). @@ -525,7 +525,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should test SearchBuilder with JSON", Label("search", "ftsearch", "builders", "json"), func() { - createVal, err := client.CreateIndex(ctx, "idx_json"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_json"). OnJSON(). Prefix("king:"). Schema(&redis.FieldSchema{FieldName: "$.name", FieldType: redis.SearchFieldTypeText}). @@ -537,7 +537,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.JSONSet(ctx, "king:1", "$", `{"name": "henry"}`) client.JSONSet(ctx, "king:2", "$", `{"name": "james"}`) - res, err := client.Search(ctx, "idx_json", "henry").Run() + res, err := client.NewSearchBuilder(ctx, "idx_json", "henry").Run() Expect(err).NotTo(HaveOccurred()) Expect(res.Total).To(Equal(1)) Expect(res.Docs[0].ID).To(Equal("king:1")) @@ -546,7 +546,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { It("should test SearchBuilder with vector search", Label("search", "ftsearch", "builders", "vector"), func() { hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"} - createVal, err := client.CreateIndex(ctx, "idx_vector"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_vector"). OnHash(). Schema(&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}). Run() @@ -558,7 +558,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "b", "v", "aaaabaaa") client.HSet(ctx, "c", "v", "aaaaabaa") - res, err := client.Search(ctx, "idx_vector", "*=>[KNN 2 @v $vec]"). + res, err := client.NewSearchBuilder(ctx, "idx_vector", "*=>[KNN 2 @v $vec]"). ReturnFields("__v_score"). SortBy("__v_score", true). Dialect(2). @@ -570,7 +570,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should test SearchBuilder with complex filtering and aggregation", Label("search", "ftsearch", "builders"), func() { - createVal, err := client.CreateIndex(ctx, "idx_complex"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_complex"). OnHash(). Schema(&redis.FieldSchema{FieldName: "category", FieldType: redis.SearchFieldTypeTag}). Schema(&redis.FieldSchema{FieldName: "price", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}). @@ -585,7 +585,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "product2", "category", "electronics", "price", 200, "location", "-0.2,51.6", "description", "laptop computer") client.HSet(ctx, "product3", "category", "books", "price", 20, "location", "-0.3,51.7", "description", "programming guide") - res, err := client.Search(ctx, "idx_complex", "@category:{electronics} @description:(device|computer)"). + res, err := client.NewSearchBuilder(ctx, "idx_complex", "@category:{electronics} @description:(device|computer)"). Filter("price", 50, 250). GeoFilter("location", -0.15, 51.55, 50, "km"). SortBy("price", true). @@ -595,7 +595,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { Expect(err).NotTo(HaveOccurred()) Expect(res.Total).To(BeNumerically(">=", 1)) - res2, err := client.Search(ctx, "idx_complex", "@category:{$cat} @price:[$min $max]"). + res2, err := client.NewSearchBuilder(ctx, "idx_complex", "@category:{$cat} @price:[$min $max]"). ParamsMap(map[string]interface{}{ "cat": "electronics", "min": 150, @@ -610,7 +610,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should test SearchBuilder error handling and edge cases", Label("search", "ftsearch", "builders", "edge-cases"), func() { - createVal, err := client.CreateIndex(ctx, "idx_edge"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_edge"). OnHash(). Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). Run() @@ -621,17 +621,17 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc1", "txt", "hello world") // Test empty query - res1, err := client.Search(ctx, "idx_edge", "*").NoContent().Run() + res1, err := client.NewSearchBuilder(ctx, "idx_edge", "*").NoContent().Run() Expect(err).NotTo(HaveOccurred()) Expect(res1.Total).To(Equal(1)) // Test query with no results - res2, err := client.Search(ctx, "idx_edge", "nonexistent").NoContent().Run() + res2, err := client.NewSearchBuilder(ctx, "idx_edge", "nonexistent").NoContent().Run() Expect(err).NotTo(HaveOccurred()) Expect(res2.Total).To(Equal(0)) // Test with multiple chained methods - res3, err := client.Search(ctx, "idx_edge", "hello"). + res3, err := client.NewSearchBuilder(ctx, "idx_edge", "hello"). WithScores(). NoContent(). Verbatim(). @@ -646,7 +646,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { }) It("should test SearchBuilder method chaining", Label("search", "ftsearch", "builders", "fluent"), func() { - createVal, err := client.CreateIndex(ctx, "idx_fluent"). + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_fluent"). OnHash(). Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}). Schema(&redis.FieldSchema{FieldName: "tags", FieldType: redis.SearchFieldTypeTag}). @@ -659,7 +659,7 @@ var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { client.HSet(ctx, "doc1", "title", "Redis Search Tutorial", "tags", "redis,search,tutorial", "score", 95) client.HSet(ctx, "doc2", "title", "Advanced Redis", "tags", "redis,advanced", "score", 88) - builder := client.Search(ctx, "idx_fluent", "@title:(redis) @tags:{search}") + builder := client.NewSearchBuilder(ctx, "idx_fluent", "@title:(redis) @tags:{search}") result := builder. WithScores(). Filter("score", 90, 100). From e4c4833c4cffb459a980ec0e7fe3dfbbb6378c44 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:51:08 +0300 Subject: [PATCH 3/5] Update search_builders.go Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- search_builders.go | 1 + 1 file changed, 1 insertion(+) diff --git a/search_builders.go b/search_builders.go index a35d6e928..2e86aa7ca 100644 --- a/search_builders.go +++ b/search_builders.go @@ -10,6 +10,7 @@ import ( // SearchBuilder provides a fluent API for FT.SEARCH // (see original FTSearchOptions for all options). +// EXPERIMENTAL: this API is subject to change, use with caution. type SearchBuilder struct { c *Client ctx context.Context From 1928265b9fdd8f6a57c925da6ffa01a9660eb166 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:51:21 +0300 Subject: [PATCH 4/5] Update search_builders.go Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- search_builders.go | 1 + 1 file changed, 1 insertion(+) diff --git a/search_builders.go b/search_builders.go index 2e86aa7ca..5e6e760f7 100644 --- a/search_builders.go +++ b/search_builders.go @@ -20,6 +20,7 @@ type SearchBuilder struct { } // NewSearchBuilder creates a new SearchBuilder for FT.SEARCH commands. +// EXPERIMENTAL: this API is subject to change, use with caution. func (c *Client) NewSearchBuilder(ctx context.Context, index, query string) *SearchBuilder { b := &SearchBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTSearchOptions{LimitOffset: -1}} return b From fcf645ecb6d0f575d74c65af7de19e32ad2a1d59 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:53:16 +0300 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- search_builders.go | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/search_builders.go b/search_builders.go index 5e6e760f7..91f063404 100644 --- a/search_builders.go +++ b/search_builders.go @@ -218,6 +218,7 @@ type AggregateBuilder struct { } // NewAggregateBuilder creates a new AggregateBuilder for FT.AGGREGATE commands. +// EXPERIMENTAL: this API is subject to change, use with caution. func (c *Client) NewAggregateBuilder(ctx context.Context, index, query string) *AggregateBuilder { return &AggregateBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTAggregateOptions{LimitOffset: -1}} } @@ -360,7 +361,8 @@ func (b *AggregateBuilder) Run() (*FTAggregateResult, error) { // ---------------------- // CreateIndexBuilder for FT.CREATE // ---------------------- - +// CreateIndexBuilder is builder for FT.CREATE +// EXPERIMENTAL: this API is subject to change, use with caution. type CreateIndexBuilder struct { c *Client ctx context.Context @@ -370,6 +372,7 @@ type CreateIndexBuilder struct { } // NewCreateIndexBuilder creates a new CreateIndexBuilder for FT.CREATE commands. +// EXPERIMENTAL: this API is subject to change, use with caution. func (c *Client) NewCreateIndexBuilder(ctx context.Context, index string) *CreateIndexBuilder { return &CreateIndexBuilder{c: c, ctx: ctx, index: index, options: &FTCreateOptions{}} } @@ -467,7 +470,8 @@ func (b *CreateIndexBuilder) Run() (string, error) { // ---------------------- // DropIndexBuilder for FT.DROPINDEX // ---------------------- - +// DropIndexBuilder is a builder for FT.DROPINDEX +// EXPERIMENTAL: this API is subject to change, use with caution. type DropIndexBuilder struct { c *Client ctx context.Context @@ -476,6 +480,7 @@ type DropIndexBuilder struct { } // NewDropIndexBuilder creates a new DropIndexBuilder for FT.DROPINDEX commands. +// EXPERIMENTAL: this API is subject to change, use with caution. func (c *Client) NewDropIndexBuilder(ctx context.Context, index string) *DropIndexBuilder { return &DropIndexBuilder{c: c, ctx: ctx, index: index} } @@ -492,7 +497,8 @@ func (b *DropIndexBuilder) Run() (string, error) { // ---------------------- // AliasBuilder for FT.ALIAS* commands // ---------------------- - +// AliasBuilder is builder for FT.ALIAS* commands +// EXPERIMENTAL: this API is subject to change, use with caution. type AliasBuilder struct { c *Client ctx context.Context @@ -502,6 +508,7 @@ type AliasBuilder struct { } // NewAliasBuilder creates a new AliasBuilder for FT.ALIAS* commands. +// EXPERIMENTAL: this API is subject to change, use with caution. func (c *Client) NewAliasBuilder(ctx context.Context, alias string) *AliasBuilder { return &AliasBuilder{c: c, ctx: ctx, alias: alias} } @@ -551,7 +558,8 @@ func (b *AliasBuilder) Run() (string, error) { // ---------------------- // ExplainBuilder for FT.EXPLAIN // ---------------------- - +// ExplainBuilder is builder for FT.EXPLAIN +// EXPERIMENTAL: this API is subject to change, use with caution. type ExplainBuilder struct { c *Client ctx context.Context @@ -561,6 +569,7 @@ type ExplainBuilder struct { } // NewExplainBuilder creates a new ExplainBuilder for FT.EXPLAIN commands. +// EXPERIMENTAL: this API is subject to change, use with caution. func (c *Client) NewExplainBuilder(ctx context.Context, index, query string) *ExplainBuilder { return &ExplainBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTExplainOptions{}} } @@ -598,7 +607,8 @@ func (b *FTInfoBuilder) Run() (FTInfoResult, error) { // ---------------------- // SpellCheckBuilder for FT.SPELLCHECK // ---------------------- - +// SpellCheckBuilder is builder for FT.SPELLCHECK +// EXPERIMENTAL: this API is subject to change, use with caution. type SpellCheckBuilder struct { c *Client ctx context.Context @@ -608,6 +618,7 @@ type SpellCheckBuilder struct { } // NewSpellCheckBuilder creates a new SpellCheckBuilder for FT.SPELLCHECK commands. +// EXPERIMENTAL: this API is subject to change, use with caution. func (c *Client) NewSpellCheckBuilder(ctx context.Context, index, query string) *SpellCheckBuilder { return &SpellCheckBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTSpellCheckOptions{}} } @@ -642,7 +653,8 @@ func (b *SpellCheckBuilder) Run() ([]SpellCheckResult, error) { // ---------------------- // DictBuilder for FT.DICT* commands // ---------------------- - +// DictBuilder is builder for FT.DICT* commands +// EXPERIMENTAL: this API is subject to change, use with caution. type DictBuilder struct { c *Client ctx context.Context @@ -652,6 +664,7 @@ type DictBuilder struct { } // NewDictBuilder creates a new DictBuilder for FT.DICT* commands. +// EXPERIMENTAL: this API is subject to change, use with caution. func (c *Client) NewDictBuilder(ctx context.Context, dict string) *DictBuilder { return &DictBuilder{c: c, ctx: ctx, dict: dict} } @@ -701,7 +714,8 @@ func (b *DictBuilder) Run() (interface{}, error) { // ---------------------- // TagValsBuilder for FT.TAGVALS // ---------------------- - +// TagValsBuilder is builder for FT.TAGVALS +// EXPERIMENTAL: this API is subject to change, use with caution. type TagValsBuilder struct { c *Client ctx context.Context @@ -710,6 +724,7 @@ type TagValsBuilder struct { } // NewTagValsBuilder creates a new TagValsBuilder for FT.TAGVALS commands. +// EXPERIMENTAL: this API is subject to change, use with caution. func (c *Client) NewTagValsBuilder(ctx context.Context, index, field string) *TagValsBuilder { return &TagValsBuilder{c: c, ctx: ctx, index: index, field: field} } @@ -723,7 +738,8 @@ func (b *TagValsBuilder) Run() ([]string, error) { // ---------------------- // CursorBuilder for FT.CURSOR* // ---------------------- - +// CursorBuilder is builder for FT.CURSOR* commands +// EXPERIMENTAL: this API is subject to change, use with caution. type CursorBuilder struct { c *Client ctx context.Context @@ -734,6 +750,7 @@ type CursorBuilder struct { } // NewCursorBuilder creates a new CursorBuilder for FT.CURSOR* commands. +// EXPERIMENTAL: this API is subject to change, use with caution. func (c *Client) NewCursorBuilder(ctx context.Context, index string, cursorId int64) *CursorBuilder { return &CursorBuilder{c: c, ctx: ctx, index: index, cursorId: cursorId} } @@ -775,7 +792,8 @@ func (b *CursorBuilder) Run() (interface{}, error) { // ---------------------- // SynUpdateBuilder for FT.SYNUPDATE // ---------------------- - +// SyncUpdateBuilder is builder for FT.SYNCUPDATE +// EXPERIMENTAL: this API is subject to change, use with caution. type SynUpdateBuilder struct { c *Client ctx context.Context @@ -786,6 +804,7 @@ type SynUpdateBuilder struct { } // NewSynUpdateBuilder creates a new SynUpdateBuilder for FT.SYNUPDATE commands. +// EXPERIMENTAL: this API is subject to change, use with caution. func (c *Client) NewSynUpdateBuilder(ctx context.Context, index string, groupId interface{}) *SynUpdateBuilder { return &SynUpdateBuilder{c: c, ctx: ctx, index: index, groupId: groupId, options: &FTSynUpdateOptions{}} }