1
0
mirror of https://github.com/redis/go-redis.git synced 2025-08-10 11:03:00 +03:00

feat(search): Add Query Builder for RediSearch commands (#3436)

* Add search module builders and tests (#1)

* Add search module builders and tests

* Add tests

* Use builders and Actions in more clean way

* Update search_builders.go

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>

* Update search_builders.go

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>

---------

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>
This commit is contained in:
ofekshenawa
2025-08-04 14:15:44 +03:00
committed by GitHub
parent 421c8a48b4
commit f93bfa1f36
2 changed files with 1505 additions and 0 deletions

825
search_builders.go Normal file
View File

@@ -0,0 +1,825 @@
package redis
import (
"context"
)
// ----------------------
// Search Module Builders
// ----------------------
// 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
index string
query string
options *FTSearchOptions
}
// 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
}
// 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 <field> <min> <max>.
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 <field> <lon> <lat> <radius> <unit>.
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 <n> <field>...
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 <field> AS <alias>.
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 <n>.
func (b *SearchBuilder) Slop(slop int) *SearchBuilder {
b.options.Slop = slop
return b
}
// Timeout adds TIMEOUT <ms>.
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 <lang>.
func (b *SearchBuilder) Language(lang string) *SearchBuilder {
b.options.Language = lang
return b
}
// Expander sets EXPANDER <expander>.
func (b *SearchBuilder) Expander(expander string) *SearchBuilder {
b.options.Expander = expander
return b
}
// Scorer sets SCORER <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 <payload>.
func (b *SearchBuilder) Payload(payload string) *SearchBuilder {
b.options.Payload = payload
return b
}
// SortBy adds SORTBY <field> 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 <k> <v>.
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 <version>.
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
}
// 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}}
}
// 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 <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 <n> <field> [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 <ms>.
func (b *AggregateBuilder) Timeout(ms int) *AggregateBuilder {
b.options.Timeout = ms
return b
}
// Apply adds APPLY <field> [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 <fields...> clause.
func (b *AggregateBuilder) GroupBy(fields ...interface{}) *AggregateBuilder {
b.options.GroupBy = append(b.options.GroupBy, FTAggregateGroupBy{
Fields: fields,
})
return b
}
// Reduce adds a REDUCE <fn> [<#args> <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 <fn> … AS <alias>
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 <field> 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 <n> (only if SortBy was called).
func (b *AggregateBuilder) SortByMax(max int) *AggregateBuilder {
b.options.SortByMax = max
return b
}
// Filter sets FILTER <expr>.
func (b *AggregateBuilder) Filter(expr string) *AggregateBuilder {
b.options.Filter = expr
return b
}
// WithCursor enables WITHCURSOR [COUNT <n>] [MAXIDLE <ms>].
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 <k v> 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 <version>.
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
// ----------------------
// CreateIndexBuilder is builder for FT.CREATE
// EXPERIMENTAL: this API is subject to change, use with caution.
type CreateIndexBuilder struct {
c *Client
ctx context.Context
index string
options *FTCreateOptions
schema []*FieldSchema
}
// 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{}}
}
// 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
// ----------------------
// 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
index string
options *FTDropIndexOptions
}
// 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}
}
// 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
// ----------------------
// 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
alias string
index string
action string // add|del|update
}
// 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}
}
// 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
}
// 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.
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
// ----------------------
// ExplainBuilder is builder for FT.EXPLAIN
// EXPERIMENTAL: this API is subject to change, use with caution.
type ExplainBuilder struct {
c *Client
ctx context.Context
index string
query string
options *FTExplainOptions
}
// 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{}}
}
// 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
}
// 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}
}
// 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
// ----------------------
// SpellCheckBuilder is builder for FT.SPELLCHECK
// EXPERIMENTAL: this API is subject to change, use with caution.
type SpellCheckBuilder struct {
c *Client
ctx context.Context
index string
query string
options *FTSpellCheckOptions
}
// 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{}}
}
// 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
// ----------------------
// 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
dict string
terms []interface{}
action string // add|del|dump
}
// 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}
}
// Action sets the action for the dictionary builder.
func (b *DictBuilder) Action(action string) *DictBuilder {
b.action = action
return b
}
// 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.
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
// ----------------------
// TagValsBuilder is builder for FT.TAGVALS
// EXPERIMENTAL: this API is subject to change, use with caution.
type TagValsBuilder struct {
c *Client
ctx context.Context
index string
field string
}
// 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}
}
// 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*
// ----------------------
// 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
index string
cursorId int64
count int
action string // read|del
}
// 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}
}
// 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.
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
// ----------------------
// SyncUpdateBuilder is builder for FT.SYNCUPDATE
// EXPERIMENTAL: this API is subject to change, use with caution.
type SynUpdateBuilder struct {
c *Client
ctx context.Context
index string
groupId interface{}
options *FTSynUpdateOptions
terms []interface{}
}
// 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{}}
}
// 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()
}

680
search_builders_test.go Normal file
View File

@@ -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.NewCreateIndexBuilder(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.NewSearchBuilder(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.NewCreateIndexBuilder(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.NewAggregateBuilder(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.NewCreateIndexBuilder(ctx, "idx3").
OnHash().
Schema(&redis.FieldSchema{FieldName: "x", FieldType: redis.SearchFieldTypeText}).
Run()).To(Equal("OK"))
WaitForIndexing(client, "idx3")
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.NewCreateIndexBuilder(ctx, "idx4").
OnHash().
Schema(&redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}).
Run()).To(Equal("OK"))
WaitForIndexing(client, "idx4")
addVal, err := client.NewAliasBuilder(ctx, "alias1").Add("idx4").Run()
Expect(err).NotTo(HaveOccurred())
Expect(addVal).To(Equal("OK"))
_, err = client.NewSearchBuilder(ctx, "alias1", "*").Run()
Expect(err).NotTo(HaveOccurred())
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.NewCreateIndexBuilder(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.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.NewCreateIndexBuilder(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.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.NewCreateIndexBuilder(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.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.NewDictBuilder(ctx, "dict1").Add("a", "b").Run()
Expect(err).NotTo(HaveOccurred())
Expect(addCount).To(Equal(int64(2)))
dump, err := client.NewDictBuilder(ctx, "dict1").Dump().Run()
Expect(err).NotTo(HaveOccurred())
Expect(dump).To(ContainElements("a", "b"))
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.NewCreateIndexBuilder(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.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.NewCreateIndexBuilder(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.NewCursorBuilder(ctx, "idx5", 1)
Expect(cursorBuilder).NotTo(BeNil())
cursorBuilder = cursorBuilder.Count(10)
Expect(cursorBuilder).NotTo(BeNil())
delBuilder := client.NewCursorBuilder(ctx, "idx5", 1)
Expect(delBuilder).NotTo(BeNil())
})
It("should update synonyms using SynUpdateBuilder", Label("search", "builders", "ftsynupdate"), func() {
createVal, err := client.NewCreateIndexBuilder(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.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.NewCreateIndexBuilder(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.NewSearchBuilder(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.NewCreateIndexBuilder(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.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.NewCreateIndexBuilder(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.NewSearchBuilder(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.NewSearchBuilder(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.NewCreateIndexBuilder(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.NewSearchBuilder(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.NewSearchBuilder(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.NewCreateIndexBuilder(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.NewSearchBuilder(ctx, "idx_in", "hello").
InKeys("doc1", "doc2").
NoContent().
Run()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(Equal(2))
// Test InFields
res2, err := client.NewSearchBuilder(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.NewCreateIndexBuilder(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.NewSearchBuilder(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.NewSearchBuilder(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.NewCreateIndexBuilder(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.NewSearchBuilder(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.NewSearchBuilder(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.NewSearchBuilder(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.NewSearchBuilder(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.NewSearchBuilder(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.NewCreateIndexBuilder(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.NewSearchBuilder(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.NewSearchBuilder(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.NewCreateIndexBuilder(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.NewSearchBuilder(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.NewSearchBuilder(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.NewCreateIndexBuilder(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.NewSearchBuilder(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.NewCreateIndexBuilder(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.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"))
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.NewCreateIndexBuilder(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.NewSearchBuilder(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.NewCreateIndexBuilder(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.NewSearchBuilder(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.NewSearchBuilder(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.NewCreateIndexBuilder(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.NewSearchBuilder(ctx, "idx_edge", "*").NoContent().Run()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(Equal(1))
// Test query with no results
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.NewSearchBuilder(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.NewCreateIndexBuilder(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.NewSearchBuilder(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))
})
})