Files
go-redis/search_test.go

4926 lines
203 KiB
Go

package redis_test
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"math"
"strings"
"time"
. "github.com/bsm/ginkgo/v2"
. "github.com/bsm/gomega"
"github.com/redis/go-redis/v9"
"github.com/redis/go-redis/v9/helper"
)
// WaitForIndexing polls FT.INFO until the index has finished both its
// initial indexing pass and any background cleaning. Cleaning can briefly
// report 1 right after index creation (e.g. for HNSW vector indexes) while
// background setup is in progress, so callers that immediately assert on
// FT.INFO fields would otherwise be flaky.
func WaitForIndexing(c *redis.Client, index string) {
deadline := time.Now().Add(30 * time.Second)
for {
res, err := c.FTInfo(context.Background(), index).Result()
Expect(err).NotTo(HaveOccurred())
if res.Indexing == 0 && res.Cleaning == 0 {
return
}
if time.Now().After(deadline) {
Fail(fmt.Sprintf("WaitForIndexing(%q) timed out: indexing=%d cleaning=%d", index, res.Indexing, res.Cleaning))
}
time.Sleep(100 * time.Millisecond)
}
}
func encodeFloat32Vector(vec []float32) []byte {
buf := new(bytes.Buffer)
for _, v := range vec {
binary.Write(buf, binary.LittleEndian, v)
}
return buf.Bytes()
}
func encodeFloat16Vector(vec []float32) []byte {
buf := new(bytes.Buffer)
for _, v := range vec {
// Convert float32 to float16 (16-bit representation)
// This is a simplified conversion - in practice you'd use a proper float16 library
f16 := uint16(v * 1000) // Simple scaling for test purposes
binary.Write(buf, binary.LittleEndian, f16)
}
return buf.Bytes()
}
var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
ctx := context.TODO()
var client redis.UniversalClient
var rawClient *redis.Client
var closeSubject func() error
BeforeEach(func() {
rawClient = reNewClient(2, false)
client, closeSubject = newUniversalSubject(rawClient)
Expect(rawClient.FlushDB(ctx).Err()).NotTo(HaveOccurred())
})
AfterEach(func() {
Expect(closeSubject()).NotTo(HaveOccurred())
})
It("should FTCreate and FTSearch WithScores", Label("search", "ftcreate", "ftsearch"), func() {
val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "txt")
client.HSet(ctx, "doc1", "txt", "foo baz")
client.HSet(ctx, "doc2", "txt", "foo bar")
res, err := client.FTSearchWithArgs(ctx, "txt", "foo ~bar", &redis.FTSearchOptions{WithScores: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(int64(2)))
for _, doc := range res.Docs {
Expect(*doc.Score).To(BeNumerically(">", 0))
Expect(doc.ID).To(Or(Equal("doc1"), Equal("doc2")))
}
})
It("should FTCreate and FTSearch stopwords", Label("search", "ftcreate", "ftsearch"), func() {
val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{StopWords: []interface{}{"foo", "bar", "baz"}}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "txt")
client.HSet(ctx, "doc1", "txt", "foo baz")
client.HSet(ctx, "doc2", "txt", "hello world")
res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(BeEquivalentTo(int64(0)))
res2, err := client.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res2.Total).To(BeEquivalentTo(int64(1)))
})
It("should FTCreate and FTSearch filters", Label("search", "ftcreate", "ftsearch"), func() {
val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}, &redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric}, &redis.FieldSchema{FieldName: "loc", FieldType: redis.SearchFieldTypeGeo}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "txt")
client.HSet(ctx, "doc1", "txt", "foo bar", "num", 3.141, "loc", "-0.441,51.458")
client.HSet(ctx, "doc2", "txt", "foo baz", "num", 2, "loc", "-0.1,51.2")
res1, err := client.FTSearchWithArgs(ctx, "txt", "foo", &redis.FTSearchOptions{Filters: []redis.FTSearchFilter{{FieldName: "num", Min: 0, Max: 2}}, NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(BeEquivalentTo(int64(1)))
Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc2"))
res2, err := client.FTSearchWithArgs(ctx, "txt", "foo", &redis.FTSearchOptions{Filters: []redis.FTSearchFilter{{FieldName: "num", Min: 0, Max: "+inf"}}, NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res2.Total).To(BeEquivalentTo(int64(2)))
Expect(res2.Docs[0].ID).To(BeEquivalentTo("doc1"))
// Test Geo filter
geoFilter1 := redis.FTSearchGeoFilter{FieldName: "loc", Longitude: -0.44, Latitude: 51.45, Radius: 10, Unit: "km"}
geoFilter2 := redis.FTSearchGeoFilter{FieldName: "loc", Longitude: -0.44, Latitude: 51.45, Radius: 100, Unit: "km"}
res3, err := client.FTSearchWithArgs(ctx, "txt", "foo", &redis.FTSearchOptions{GeoFilter: []redis.FTSearchGeoFilter{geoFilter1}, NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res3.Total).To(BeEquivalentTo(int64(1)))
Expect(res3.Docs[0].ID).To(BeEquivalentTo("doc1"))
res4, err := client.FTSearchWithArgs(ctx, "txt", "foo", &redis.FTSearchOptions{GeoFilter: []redis.FTSearchGeoFilter{geoFilter2}, NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res4.Total).To(BeEquivalentTo(int64(2)))
docs := []interface{}{res4.Docs[0].ID, res4.Docs[1].ID}
Expect(docs).To(ContainElement("doc1"))
Expect(docs).To(ContainElement("doc2"))
})
It("should FTCreate and FTSearch sortby", Label("search", "ftcreate", "ftsearch"), func() {
val, err := client.FTCreate(ctx, "num", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}, &redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "num")
client.HSet(ctx, "doc1", "txt", "foo bar", "num", 1)
client.HSet(ctx, "doc2", "txt", "foo baz", "num", 2)
client.HSet(ctx, "doc3", "txt", "foo qux", "num", 3)
sortBy1 := redis.FTSearchSortBy{FieldName: "num", Asc: true}
sortBy2 := redis.FTSearchSortBy{FieldName: "num", Desc: true}
res1, err := client.FTSearchWithArgs(ctx, "num", "foo", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy1}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(BeEquivalentTo(int64(3)))
Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc1"))
Expect(res1.Docs[1].ID).To(BeEquivalentTo("doc2"))
Expect(res1.Docs[2].ID).To(BeEquivalentTo("doc3"))
res2, err := client.FTSearchWithArgs(ctx, "num", "foo", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res2.Total).To(BeEquivalentTo(int64(3)))
Expect(res2.Docs[2].ID).To(BeEquivalentTo("doc1"))
Expect(res2.Docs[1].ID).To(BeEquivalentTo("doc2"))
Expect(res2.Docs[0].ID).To(BeEquivalentTo("doc3"))
res3, err := client.FTSearchWithArgs(ctx, "num", "foo", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}, SortByWithCount: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res3.Total).To(BeEquivalentTo(int64(3)))
res4, err := client.FTSearchWithArgs(ctx, "num", "notpresentf00", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}, SortByWithCount: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res4.Total).To(BeEquivalentTo(int64(0)))
})
It("should FTCreate and FTSearch example", Label("search", "ftcreate", "ftsearch"), func() {
val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Weight: 5}, &redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "txt")
client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch implements a search engine on top of redis")
res1, err := client.FTSearchWithArgs(ctx, "txt", "search engine", &redis.FTSearchOptions{NoContent: true, Verbatim: true, LimitOffset: 0, Limit: 5}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(BeEquivalentTo(int64(1)))
})
It("should FTCreate NoIndex", Label("search", "ftcreate", "ftsearch"), func() {
text1 := &redis.FieldSchema{FieldName: "field", FieldType: redis.SearchFieldTypeText}
text2 := &redis.FieldSchema{FieldName: "text", FieldType: redis.SearchFieldTypeText, NoIndex: true, Sortable: true}
num := &redis.FieldSchema{FieldName: "numeric", FieldType: redis.SearchFieldTypeNumeric, NoIndex: true, Sortable: true}
geo := &redis.FieldSchema{FieldName: "geo", FieldType: redis.SearchFieldTypeGeo, NoIndex: true, Sortable: true}
tag := &redis.FieldSchema{FieldName: "tag", FieldType: redis.SearchFieldTypeTag, NoIndex: true, Sortable: true}
val, err := client.FTCreate(ctx, "idx", &redis.FTCreateOptions{}, text1, text2, num, geo, tag).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx")
client.HSet(ctx, "doc1", "field", "aaa", "text", "1", "numeric", 1, "geo", "1,1", "tag", "1")
client.HSet(ctx, "doc2", "field", "aab", "text", "2", "numeric", 2, "geo", "2,2", "tag", "2")
res1, err := client.FTSearch(ctx, "idx", "@text:aa*").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(BeEquivalentTo(int64(0)))
res2, err := client.FTSearch(ctx, "idx", "@field:aa*").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res2.Total).To(BeEquivalentTo(int64(2)))
res3, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "text", Desc: true}}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res3.Total).To(BeEquivalentTo(int64(2)))
Expect(res3.Docs[0].ID).To(BeEquivalentTo("doc2"))
res4, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "text", Asc: true}}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res4.Total).To(BeEquivalentTo(int64(2)))
Expect(res4.Docs[0].ID).To(BeEquivalentTo("doc1"))
res5, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "numeric", Asc: true}}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res5.Docs[0].ID).To(BeEquivalentTo("doc1"))
res6, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "geo", Asc: true}}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res6.Docs[0].ID).To(BeEquivalentTo("doc1"))
res7, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "tag", Asc: true}}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res7.Docs[0].ID).To(BeEquivalentTo("doc1"))
})
It("should FTExplain", Label("search", "ftexplain"), func() {
text1 := &redis.FieldSchema{FieldName: "f1", FieldType: redis.SearchFieldTypeText}
text2 := &redis.FieldSchema{FieldName: "f2", FieldType: redis.SearchFieldTypeText}
text3 := &redis.FieldSchema{FieldName: "f3", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, text1, text2, text3).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "txt")
res1, err := client.FTExplain(ctx, "txt", "@f3:f3_val @f2:f2_val @f1:f1_val").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1).ToNot(BeEmpty())
})
It("should FTAlias", Label("search", "ftexplain"), func() {
text1 := &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText}
text2 := &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText}
val1, err := client.FTCreate(ctx, "testAlias", &redis.FTCreateOptions{Prefix: []interface{}{"index1:"}}, text1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val1).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "testAlias")
val2, err := client.FTCreate(ctx, "testAlias2", &redis.FTCreateOptions{Prefix: []interface{}{"index2:"}}, text2).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val2).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "testAlias2")
client.HSet(ctx, "index1:lonestar", "name", "lonestar")
client.HSet(ctx, "index2:yogurt", "name", "yogurt")
res1, err := client.FTSearch(ctx, "testAlias", "*").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Docs[0].ID).To(BeEquivalentTo("index1:lonestar"))
aliasAddRes, err := client.FTAliasAdd(ctx, "testAlias", "mj23").Result()
Expect(err).NotTo(HaveOccurred())
Expect(aliasAddRes).To(BeEquivalentTo("OK"))
res1, err = client.FTSearch(ctx, "mj23", "*").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Docs[0].ID).To(BeEquivalentTo("index1:lonestar"))
aliasUpdateRes, err := client.FTAliasUpdate(ctx, "testAlias2", "kb24").Result()
Expect(err).NotTo(HaveOccurred())
Expect(aliasUpdateRes).To(BeEquivalentTo("OK"))
res3, err := client.FTSearch(ctx, "kb24", "*").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res3.Docs[0].ID).To(BeEquivalentTo("index2:yogurt"))
aliasDelRes, err := client.FTAliasDel(ctx, "mj23").Result()
Expect(err).NotTo(HaveOccurred())
Expect(aliasDelRes).To(BeEquivalentTo("OK"))
})
It("should FTCreate and FTSearch textfield, sortable and nostem ", Label("search", "ftcreate", "ftsearch"), func() {
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText, Sortable: true, NoStem: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
resInfo, err := client.FTInfo(ctx, "idx1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resInfo.Attributes[0].Sortable).To(BeTrue())
Expect(resInfo.Attributes[0].NoStem).To(BeTrue())
})
It("should FTAlter", Label("search", "ftcreate", "ftsearch", "ftalter"), func() {
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
resAlter, err := client.FTAlter(ctx, "idx1", false, []interface{}{"body", redis.SearchFieldTypeText.String()}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resAlter).To(BeEquivalentTo("OK"))
client.HSet(ctx, "doc1", "title", "MyTitle", "body", "Some content only in the body")
res1, err := client.FTSearch(ctx, "idx1", "only in the body").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(BeEquivalentTo(int64(1)))
_, err = client.FTSearch(ctx, "idx_not_exist", "only in the body").Result()
Expect(err).To(HaveOccurred())
})
It("should FTSpellCheck", Label("search", "ftcreate", "ftsearch", "ftspellcheck"), func() {
text1 := &redis.FieldSchema{FieldName: "f1", FieldType: redis.SearchFieldTypeText}
text2 := &redis.FieldSchema{FieldName: "f2", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "doc1", "f1", "some valid content", "f2", "this is sample text")
client.HSet(ctx, "doc2", "f1", "very important", "f2", "lorem ipsum")
resSpellCheck, err := client.FTSpellCheck(ctx, "idx1", "impornant").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resSpellCheck[0].Suggestions[0].Suggestion).To(BeEquivalentTo("important"))
resSpellCheck2, err := client.FTSpellCheck(ctx, "idx1", "contnt").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resSpellCheck2[0].Suggestions[0].Suggestion).To(BeEquivalentTo("content"))
// test spellcheck with Levenshtein distance
resSpellCheck3, err := client.FTSpellCheck(ctx, "idx1", "vlis").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resSpellCheck3[0].Term).To(BeEquivalentTo("vlis"))
resSpellCheck4, err := client.FTSpellCheckWithArgs(ctx, "idx1", "vlis", &redis.FTSpellCheckOptions{Distance: 2}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resSpellCheck4[0].Suggestions[0].Suggestion).To(BeEquivalentTo("valid"))
// test spellcheck include
resDictAdd, err := client.FTDictAdd(ctx, "dict", "lore", "lorem", "lorm").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resDictAdd).To(BeEquivalentTo(3))
terms := &redis.FTSpellCheckTerms{Inclusion: "INCLUDE", Dictionary: "dict"}
resSpellCheck5, err := client.FTSpellCheckWithArgs(ctx, "idx1", "lorm", &redis.FTSpellCheckOptions{Terms: terms}).Result()
Expect(err).NotTo(HaveOccurred())
lorm := resSpellCheck5[0].Suggestions
Expect(len(lorm)).To(BeEquivalentTo(3))
Expect(lorm[0].Score).To(BeEquivalentTo(0.5))
Expect(lorm[1].Score).To(BeEquivalentTo(0))
Expect(lorm[2].Score).To(BeEquivalentTo(0))
terms2 := &redis.FTSpellCheckTerms{Inclusion: "EXCLUDE", Dictionary: "dict"}
resSpellCheck6, err := client.FTSpellCheckWithArgs(ctx, "idx1", "lorm", &redis.FTSpellCheckOptions{Terms: terms2}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resSpellCheck6).To(BeEmpty())
})
It("should FTDict opreations", Label("search", "ftdictdump", "ftdictdel", "ftdictadd"), func() {
text1 := &redis.FieldSchema{FieldName: "f1", FieldType: redis.SearchFieldTypeText}
text2 := &redis.FieldSchema{FieldName: "f2", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
resDictAdd, err := client.FTDictAdd(ctx, "custom_dict", "item1", "item2", "item3").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resDictAdd).To(BeEquivalentTo(3))
resDictDel, err := client.FTDictDel(ctx, "custom_dict", "item2").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resDictDel).To(BeEquivalentTo(1))
resDictDump, err := client.FTDictDump(ctx, "custom_dict").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resDictDump).To(BeEquivalentTo([]string{"item1", "item3"}))
resDictDel2, err := client.FTDictDel(ctx, "custom_dict", "item1", "item3").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resDictDel2).To(BeEquivalentTo(2))
})
It("should FTSearch phonetic matcher", Label("search", "ftsearch"), func() {
text1 := &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "doc1", "name", "Jon")
client.HSet(ctx, "doc2", "name", "John")
res1, err := client.FTSearch(ctx, "idx1", "Jon").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(BeEquivalentTo(int64(1)))
Expect(res1.Docs[0].Fields["name"]).To(BeEquivalentTo("Jon"))
rawClient.FlushDB(ctx)
text2 := &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText, PhoneticMatcher: "dm:en"}
val2, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text2).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val2).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "doc1", "name", "Jon")
client.HSet(ctx, "doc2", "name", "John")
res2, err := client.FTSearch(ctx, "idx1", "Jon").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res2.Total).To(BeEquivalentTo(int64(2)))
names := []interface{}{res2.Docs[0].Fields["name"], res2.Docs[1].Fields["name"]}
Expect(names).To(ContainElement("Jon"))
Expect(names).To(ContainElement("John"))
})
// up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25
// this test expect redis major version >= 8
It("should FTSearch WithScores", Label("search", "ftsearch"), func() {
SkipBeforeRedisVersion("7.9", "default scorer is not BM25STD")
text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "doc1", "description", "The quick brown fox jumps over the lazy dog")
client.HSet(ctx, "doc2", "description", "Quick alice was beginning to get very tired of sitting by her quick sister on the bank, and of having nothing to do.")
res, err := client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(*res.Docs[0].Score).To(BeNumerically("<=", 0.236))
res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "TFIDF"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1)))
res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "TFIDF.DOCNORM"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(*res.Docs[0].Score).To(BeEquivalentTo(0.14285714285714285))
res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "BM25"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(*res.Docs[0].Score).To(BeNumerically("<=", 0.22471909420069797))
res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "DISMAX"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(2)))
res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "DOCSCORE"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1)))
res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "HAMMING"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(0)))
})
// up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25
// this test expect redis version < 8.0
It("should FTSearch WithScores", Label("search", "ftsearch"), func() {
SkipAfterRedisVersion("7.9", "default scorer is not TFIDF")
text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "doc1", "description", "The quick brown fox jumps over the lazy dog")
client.HSet(ctx, "doc2", "description", "Quick alice was beginning to get very tired of sitting by her quick sister on the bank, and of having nothing to do.")
res, err := client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1)))
res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "TFIDF"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1)))
res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "TFIDF.DOCNORM"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(*res.Docs[0].Score).To(BeEquivalentTo(0.14285714285714285))
res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "BM25"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(*res.Docs[0].Score).To(BeNumerically("<=", 0.22471909420069797))
res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "DISMAX"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(2)))
res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "DOCSCORE"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1)))
res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "HAMMING"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(0)))
})
It("should FTConfigSet and FTConfigGet ", Label("search", "ftconfigget", "ftconfigset", "NonRedisEnterprise"), func() {
val, err := client.FTConfigSet(ctx, "MINPREFIX", "1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
res, err := client.FTConfigGet(ctx, "*").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res["MINPREFIX"]).To(BeEquivalentTo("1"))
res, err = client.FTConfigGet(ctx, "MINPREFIX").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(BeEquivalentTo(map[string]interface{}{"MINPREFIX": "1"}))
})
It("should FTAggregate GroupBy ", Label("search", "ftaggregate"), func() {
text1 := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}
text2 := &redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}
text3 := &redis.FieldSchema{FieldName: "parent", FieldType: redis.SearchFieldTypeText}
num := &redis.FieldSchema{FieldName: "random_num", FieldType: redis.SearchFieldTypeNumeric}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2, text3, num).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "search", "title", "RediSearch",
"body", "Redisearch implements a search engine on top of redis",
"parent", "redis",
"random_num", 10)
client.HSet(ctx, "ai", "title", "RedisAI",
"body", "RedisAI executes Deep Learning/Machine Learning models and managing their data.",
"parent", "redis",
"random_num", 3)
client.HSet(ctx, "json", "title", "RedisJson",
"body", "RedisJSON implements ECMA-404 The JSON Data Interchange Standard as a native data type.",
"parent", "redis",
"random_num", 8)
reducer := redis.FTAggregateReducer{Reducer: redis.SearchCount}
options := &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}}
res, err := client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis"))
Expect(res.Rows[0].Fields["__generated_aliascount"]).To(BeEquivalentTo("3"))
reducer = redis.FTAggregateReducer{Reducer: redis.SearchCountDistinct, Args: []interface{}{"@title"}}
options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis"))
Expect(res.Rows[0].Fields["__generated_aliascount_distincttitle"]).To(BeEquivalentTo("3"))
reducer = redis.FTAggregateReducer{Reducer: redis.SearchSum, Args: []interface{}{"@random_num"}}
options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis"))
Expect(res.Rows[0].Fields["__generated_aliassumrandom_num"]).To(BeEquivalentTo("21"))
reducer = redis.FTAggregateReducer{Reducer: redis.SearchMin, Args: []interface{}{"@random_num"}}
options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis"))
Expect(res.Rows[0].Fields["__generated_aliasminrandom_num"]).To(BeEquivalentTo("3"))
reducer = redis.FTAggregateReducer{Reducer: redis.SearchMax, Args: []interface{}{"@random_num"}}
options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis"))
Expect(res.Rows[0].Fields["__generated_aliasmaxrandom_num"]).To(BeEquivalentTo("10"))
reducer = redis.FTAggregateReducer{Reducer: redis.SearchAvg, Args: []interface{}{"@random_num"}}
options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis"))
Expect(res.Rows[0].Fields["__generated_aliasavgrandom_num"]).To(BeEquivalentTo("7"))
reducer = redis.FTAggregateReducer{Reducer: redis.SearchStdDev, Args: []interface{}{"@random_num"}}
options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis"))
Expect(res.Rows[0].Fields["__generated_aliasstddevrandom_num"]).To(BeEquivalentTo("3.60555127546"))
reducer = redis.FTAggregateReducer{Reducer: redis.SearchQuantile, Args: []interface{}{"@random_num", 0.5}}
options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis"))
Expect(res.Rows[0].Fields["__generated_aliasquantilerandom_num,0.5"]).To(BeEquivalentTo("8"))
reducer = redis.FTAggregateReducer{Reducer: redis.SearchToList, Args: []interface{}{"@title"}}
options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis"))
Expect(res.Rows[0].Fields["__generated_aliastolisttitle"]).To(ContainElements("RediSearch", "RedisAI", "RedisJson"))
reducer = redis.FTAggregateReducer{Reducer: redis.SearchFirstValue, Args: []interface{}{"@title"}, As: "first"}
options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis"))
Expect(res.Rows[0].Fields["first"]).To(Or(BeEquivalentTo("RediSearch"), BeEquivalentTo("RedisAI"), BeEquivalentTo("RedisJson")))
reducer = redis.FTAggregateReducer{Reducer: redis.SearchRandomSample, Args: []interface{}{"@title", 2}, As: "random"}
options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis"))
Expect(res.Rows[0].Fields["random"]).To(Or(
ContainElement("RediSearch"),
ContainElement("RedisAI"),
ContainElement("RedisJson"),
))
})
It("should FTAggregate sort and limit", Label("search", "ftaggregate"), func() {
text1 := &redis.FieldSchema{FieldName: "t1", FieldType: redis.SearchFieldTypeText}
text2 := &redis.FieldSchema{FieldName: "t2", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "doc1", "t1", "a", "t2", "b")
client.HSet(ctx, "doc2", "t1", "b", "t2", "a")
options := &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t2", Asc: true}, {FieldName: "@t1", Desc: true}}}
res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("b"))
Expect(res.Rows[1].Fields["t1"]).To(BeEquivalentTo("a"))
Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("a"))
Expect(res.Rows[1].Fields["t2"]).To(BeEquivalentTo("b"))
options = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t1"}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("a"))
Expect(res.Rows[1].Fields["t1"]).To(BeEquivalentTo("b"))
options = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t1"}}, SortByMax: 1}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("a"))
options = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t1"}}, Limit: 1, LimitOffset: 1}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("b"))
options = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t1"}}, Limit: 1, LimitOffset: 0}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("a"))
})
It("should FTAggregate load ", Label("search", "ftaggregate"), func() {
text1 := &redis.FieldSchema{FieldName: "t1", FieldType: redis.SearchFieldTypeText}
text2 := &redis.FieldSchema{FieldName: "t2", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "doc1", "t1", "hello", "t2", "world")
options := &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t1"}}}
res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello"))
options = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t2"}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world"))
options = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t2", As: "t2alias"}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["t2alias"]).To(BeEquivalentTo("world"))
options = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t1"}, {Field: "t2", As: "t2alias"}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello"))
Expect(res.Rows[0].Fields["t2alias"]).To(BeEquivalentTo("world"))
options = &redis.FTAggregateOptions{LoadAll: true}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello"))
Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world"))
_, err = client.FTAggregateWithArgs(ctx, "idx_not_exist", "*", &redis.FTAggregateOptions{}).Result()
Expect(err).To(HaveOccurred())
})
It("should FTAggregate with scorer and addscores", Label("search", "ftaggregate", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion("7.4", "no addscores support")
title := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: false}
description := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, Sortable: false}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{"product:"}}, title, description).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "product:1", "title", "New Gaming Laptop", "description", "this is not a desktop")
client.HSet(ctx, "product:2", "title", "Super Old Not Gaming Laptop", "description", "this laptop is not a new laptop but it is a laptop")
client.HSet(ctx, "product:3", "title", "Office PC", "description", "office desktop pc")
options := &redis.FTAggregateOptions{
AddScores: true,
Scorer: "BM25",
SortBy: []redis.FTAggregateSortBy{{
FieldName: "@__score",
Desc: true,
}},
}
res, err := client.FTAggregateWithArgs(ctx, "idx1", "laptop", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).ToNot(BeNil())
Expect(len(res.Rows)).To(BeEquivalentTo(2))
score1, err := helper.ParseFloat(fmt.Sprintf("%s", res.Rows[0].Fields["__score"]))
Expect(err).NotTo(HaveOccurred())
score2, err := helper.ParseFloat(fmt.Sprintf("%s", res.Rows[1].Fields["__score"]))
Expect(err).NotTo(HaveOccurred())
Expect(score1).To(BeNumerically(">", score2))
optionsDM := &redis.FTAggregateOptions{
AddScores: true,
Scorer: "DISMAX",
SortBy: []redis.FTAggregateSortBy{{
FieldName: "@__score",
Desc: true,
}},
}
resDM, err := client.FTAggregateWithArgs(ctx, "idx1", "laptop", optionsDM).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resDM).ToNot(BeNil())
Expect(len(resDM.Rows)).To(BeEquivalentTo(2))
score1DM, err := helper.ParseFloat(fmt.Sprintf("%s", resDM.Rows[0].Fields["__score"]))
Expect(err).NotTo(HaveOccurred())
score2DM, err := helper.ParseFloat(fmt.Sprintf("%s", resDM.Rows[1].Fields["__score"]))
Expect(err).NotTo(HaveOccurred())
Expect(score1DM).To(BeNumerically(">", score2DM))
Expect(score1DM).To(BeEquivalentTo(float64(4)))
Expect(score2DM).To(BeEquivalentTo(float64(1)))
Expect(score1).NotTo(BeEquivalentTo(score1DM))
Expect(score2).NotTo(BeEquivalentTo(score2DM))
})
It("should FTAggregate apply and groupby", Label("search", "ftaggregate"), func() {
text1 := &redis.FieldSchema{FieldName: "PrimaryKey", FieldType: redis.SearchFieldTypeText, Sortable: true}
num1 := &redis.FieldSchema{FieldName: "CreatedDateTimeUTC", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
// 6 feb
client.HSet(ctx, "doc1", "PrimaryKey", "9::362330", "CreatedDateTimeUTC", "1738823999")
// 12 feb
client.HSet(ctx, "doc2", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "1739342399")
client.HSet(ctx, "doc3", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "1739353199")
reducer := redis.FTAggregateReducer{Reducer: redis.SearchCount, As: "perDay"}
options := &redis.FTAggregateOptions{
Apply: []redis.FTAggregateApply{{Field: "floor(@CreatedDateTimeUTC /(60*60*24))", As: "TimestampAsDay"}},
GroupBy: []redis.FTAggregateGroupBy{{
Fields: []interface{}{"@TimestampAsDay"},
Reduce: []redis.FTAggregateReducer{reducer},
}},
SortBy: []redis.FTAggregateSortBy{{
FieldName: "@perDay",
Desc: true,
}},
}
res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).ToNot(BeNil())
Expect(len(res.Rows)).To(BeEquivalentTo(2))
Expect(res.Rows[0].Fields["perDay"]).To(BeEquivalentTo("2"))
Expect(res.Rows[1].Fields["perDay"]).To(BeEquivalentTo("1"))
})
It("should FTAggregate apply", Label("search", "ftaggregate"), func() {
text1 := &redis.FieldSchema{FieldName: "PrimaryKey", FieldType: redis.SearchFieldTypeText, Sortable: true}
num1 := &redis.FieldSchema{FieldName: "CreatedDateTimeUTC", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "doc1", "PrimaryKey", "9::362330", "CreatedDateTimeUTC", "637387878524969984")
client.HSet(ctx, "doc2", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "637387875859270016")
options := &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}}
res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["CreatedDateTimeUTC"]).To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
Expect(res.Rows[1].Fields["CreatedDateTimeUTC"]).To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
})
It("should FTAggregate filter", Label("search", "ftaggregate"), func() {
text1 := &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText, Sortable: true}
num1 := &redis.FieldSchema{FieldName: "age", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "doc1", "name", "bar", "age", "25")
client.HSet(ctx, "doc2", "name", "foo", "age", "19")
for _, dlc := range []int{1, 2} {
options := &redis.FTAggregateOptions{Filter: "@name=='foo' && @age < 20", DialectVersion: dlc}
res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(Or(BeEquivalentTo(2), BeEquivalentTo(1)))
Expect(res.Rows[0].Fields["name"]).To(BeEquivalentTo("foo"))
options = &redis.FTAggregateOptions{Filter: "@age > 15", DialectVersion: dlc, SortBy: []redis.FTAggregateSortBy{{FieldName: "@age"}}}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(2))
Expect(res.Rows[0].Fields["age"]).To(BeEquivalentTo("19"))
Expect(res.Rows[1].Fields["age"]).To(BeEquivalentTo("25"))
}
})
It("should return only the base query when options is nil", Label("search", "ftaggregate"), func() {
args, err := redis.FTAggregateQuery("testQuery", nil)
Expect(err).NotTo(HaveOccurred())
Expect(args).To(Equal(redis.AggregateQuery{"testQuery"}))
})
It("should include VERBATIM and SCORER when options are set", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
Verbatim: true,
Scorer: "BM25",
}
args, err := redis.FTAggregateQuery("testQuery", options)
Expect(err).NotTo(HaveOccurred())
Expect(args[0]).To(Equal("testQuery"))
Expect(args).To(ContainElement("VERBATIM"))
Expect(args).To(ContainElement("SCORER"))
Expect(args).To(ContainElement("BM25"))
})
It("should include ADDSCORES when AddScores is true", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
AddScores: true,
}
args, err := redis.FTAggregateQuery("q", options)
Expect(err).NotTo(HaveOccurred())
Expect(args).To(ContainElement("ADDSCORES"))
})
It("should include LOADALL when LoadAll is true", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
LoadAll: true,
}
args, err := redis.FTAggregateQuery("q", options)
Expect(err).NotTo(HaveOccurred())
Expect(args).To(ContainElement("LOAD"))
Expect(args).To(ContainElement("*"))
})
It("should include LOAD when Load is provided", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
Load: []redis.FTAggregateLoad{
{Field: "field1", As: "alias1"},
{Field: "field2"},
},
}
args, err := redis.FTAggregateQuery("q", options)
Expect(err).NotTo(HaveOccurred())
// Verify LOAD options related arguments
Expect(args).To(ContainElement("LOAD"))
// Check that field names and aliases are present
Expect(args).To(ContainElement("field1"))
Expect(args).To(ContainElement("alias1"))
Expect(args).To(ContainElement("field2"))
})
It("should include TIMEOUT when Timeout > 0", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
Timeout: 500,
}
args, err := redis.FTAggregateQuery("q", options)
Expect(err).NotTo(HaveOccurred())
Expect(args).To(ContainElement("TIMEOUT"))
found := false
for i, a := range args {
if fmt.Sprintf("%s", a) == "TIMEOUT" {
Expect(fmt.Sprintf("%d", args[i+1])).To(Equal("500"))
found = true
break
}
}
Expect(found).To(BeTrue())
})
It("should emit Steps in order with SORTBY MAX and GROUPBY", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
Scorer: "BM25STD.TANH",
AddScores: true,
Steps: []redis.FTAggregateStep{
{SortBy: &redis.FTAggregateSortByStep{
Fields: []redis.FTAggregateSortBy{{FieldName: "@__score", Desc: true}},
Max: 50000,
}},
{GroupBy: &redis.FTAggregateGroupBy{
Fields: []interface{}{"@parent_id", "@profile_id"},
Reduce: []redis.FTAggregateReducer{
{Reducer: redis.SearchSum, Args: []interface{}{"the_score"}, As: "the_score"},
},
}},
{SortBy: &redis.FTAggregateSortByStep{
Fields: []redis.FTAggregateSortBy{{FieldName: "@the_score", Desc: true}},
}},
},
LimitOffset: 0,
Limit: 400,
}
args, err := redis.FTAggregateQuery("q", options)
Expect(err).NotTo(HaveOccurred())
expected := redis.AggregateQuery{
"q",
"SCORER", "BM25STD.TANH",
"ADDSCORES",
"SORTBY", 2, "@__score", "DESC", "MAX", 50000,
"GROUPBY", 2, "@parent_id", "@profile_id",
"REDUCE", "SUM", 1, "the_score", "AS", "the_score",
"SORTBY", 2, "@the_score", "DESC",
"LIMIT", 0, 400,
"DIALECT", 2,
}
Expect(args).To(Equal(expected))
})
It("should emit Steps with LOAD and APPLY interleaved", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
Steps: []redis.FTAggregateStep{
{Load: &redis.FTAggregateLoad{Field: "@price", As: "p"}},
{Load: &redis.FTAggregateLoad{Field: "@quantity"}},
{Apply: &redis.FTAggregateApply{Field: "@p * @quantity", As: "total"}},
{SortBy: &redis.FTAggregateSortByStep{
Fields: []redis.FTAggregateSortBy{{FieldName: "@total", Desc: true}},
Max: 10000,
}},
},
}
args, err := redis.FTAggregateQuery("*", options)
Expect(err).NotTo(HaveOccurred())
expected := redis.AggregateQuery{
"*",
"LOAD", 3, "@price", "AS", "p",
"LOAD", 1, "@quantity",
"APPLY", "@p * @quantity", "AS", "total",
"SORTBY", 2, "@total", "DESC", "MAX", 10000,
"DIALECT", 2,
}
Expect(args).To(Equal(expected))
})
It("should error when LoadAll is combined with a LOAD step", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
LoadAll: true,
Steps: []redis.FTAggregateStep{
{Load: &redis.FTAggregateLoad{Field: "@price"}},
{Apply: &redis.FTAggregateApply{Field: "@price * 2", As: "double"}},
},
}
_, err := redis.FTAggregateQuery("*", options)
Expect(err).To(MatchError(ContainSubstring("LOADALL and LOAD are mutually exclusive")))
})
It("should allow LoadAll together with Steps that don't use LOAD", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
LoadAll: true,
Steps: []redis.FTAggregateStep{
{Apply: &redis.FTAggregateApply{Field: "@price * 2", As: "double"}},
},
}
args, err := redis.FTAggregateQuery("*", options)
Expect(err).NotTo(HaveOccurred())
expected := redis.AggregateQuery{
"*",
"LOAD", "*",
"APPLY", "@price * 2", "AS", "double",
"DIALECT", 2,
}
Expect(args).To(Equal(expected))
})
It("should error when SORTBY step has both ASC and DESC", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
Steps: []redis.FTAggregateStep{
{SortBy: &redis.FTAggregateSortByStep{
Fields: []redis.FTAggregateSortBy{{FieldName: "@x", Asc: true, Desc: true}},
}},
},
}
_, err := redis.FTAggregateQuery("q", options)
Expect(err).To(HaveOccurred())
})
It("should error when a step sets more than one field", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
Steps: []redis.FTAggregateStep{
{
Load: &redis.FTAggregateLoad{Field: "@x"},
Apply: &redis.FTAggregateApply{Field: "@x * 2", As: "y"},
},
},
}
_, err := redis.FTAggregateQuery("q", options)
Expect(err).To(MatchError(ContainSubstring("each step must set exactly one")))
})
It("should error when a step sets no field", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
Steps: []redis.FTAggregateStep{{}},
}
_, err := redis.FTAggregateQuery("q", options)
Expect(err).To(MatchError(ContainSubstring("each step must set exactly one")))
})
It("should error when Steps is combined with deprecated fields", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
Load: []redis.FTAggregateLoad{{Field: "@ignored_load"}},
Steps: []redis.FTAggregateStep{
{Apply: &redis.FTAggregateApply{Field: "@used", As: "u"}},
},
}
_, err := redis.FTAggregateQuery("q", options)
Expect(err).To(MatchError(ContainSubstring("Steps cannot be combined with the deprecated")))
})
It("should error when Steps is combined with deprecated SortByMax", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
SortByMax: 999,
Steps: []redis.FTAggregateStep{
{Apply: &redis.FTAggregateApply{Field: "@used", As: "u"}},
},
}
_, err := redis.FTAggregateQuery("q", options)
Expect(err).To(MatchError(ContainSubstring("Steps cannot be combined with the deprecated")))
})
It("should emit FILTER and LIMIT steps in pipeline order", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
Steps: []redis.FTAggregateStep{
{Load: &redis.FTAggregateLoad{Field: "@price"}},
{Filter: &redis.FTAggregateFilter{Expression: "@price > 100"}},
{Filter: &redis.FTAggregateFilter{Expression: "@price < 1000"}},
{GroupBy: &redis.FTAggregateGroupBy{
Fields: []interface{}{"@category"},
Reduce: []redis.FTAggregateReducer{
{Reducer: redis.SearchSum, Args: []interface{}{"@price"}, As: "total"},
},
}},
{Filter: &redis.FTAggregateFilter{Expression: "@total > 1000"}},
{SortBy: &redis.FTAggregateSortByStep{
Fields: []redis.FTAggregateSortBy{{FieldName: "@total", Desc: true}},
Max: 50,
}},
{Limit: &redis.FTAggregateLimit{Offset: 0, Num: 10}},
},
}
args, err := redis.FTAggregateQuery("*", options)
Expect(err).NotTo(HaveOccurred())
expected := redis.AggregateQuery{
"*",
"LOAD", 1, "@price",
"FILTER", "@price > 100",
"FILTER", "@price < 1000",
"GROUPBY", 1, "@category",
"REDUCE", "SUM", 1, "@price", "AS", "total",
"FILTER", "@total > 1000",
"SORTBY", 2, "@total", "DESC", "MAX", 50,
"LIMIT", 0, 10,
"DIALECT", 2,
}
Expect(args).To(Equal(expected))
})
// Wire order only: the server merges SORTBY and LIMIT steps that are not
// separated by a GROUPBY into one stage.
It("should emit every LIMIT step in the given order", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
Steps: []redis.FTAggregateStep{
{Limit: &redis.FTAggregateLimit{Offset: 0, Num: 100}},
{Apply: &redis.FTAggregateApply{Field: "@price * 2", As: "double"}},
{Limit: &redis.FTAggregateLimit{Offset: 5, Num: 10}},
},
}
args, err := redis.FTAggregateQuery("*", options)
Expect(err).NotTo(HaveOccurred())
expected := redis.AggregateQuery{
"*",
"LIMIT", 0, 100,
"APPLY", "@price * 2", "AS", "double",
"LIMIT", 5, 10,
"DIALECT", 2,
}
Expect(args).To(Equal(expected))
})
It("should keep the legacy LIMIT then FILTER tail order", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
LimitOffset: 0,
Limit: 10,
Filter: "@price > 100",
}
args, err := redis.FTAggregateQuery("*", options)
Expect(err).NotTo(HaveOccurred())
expected := redis.AggregateQuery{
"*",
"LIMIT", 0, 10,
"FILTER", "@price > 100",
"DIALECT", 2,
}
Expect(args).To(Equal(expected))
})
It("should emit Steps before the legacy LIMIT and FILTER", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
Steps: []redis.FTAggregateStep{
{Filter: &redis.FTAggregateFilter{Expression: "@a > 1"}},
{Limit: &redis.FTAggregateLimit{Offset: 0, Num: 100}},
},
LimitOffset: 0,
Limit: 10,
Filter: "@b > 2",
}
args, err := redis.FTAggregateQuery("*", options)
Expect(err).NotTo(HaveOccurred())
expected := redis.AggregateQuery{
"*",
"FILTER", "@a > 1",
"LIMIT", 0, 100,
"LIMIT", 0, 10,
"FILTER", "@b > 2",
"DIALECT", 2,
}
Expect(args).To(Equal(expected))
cmd := client.FTAggregateWithArgs(ctx, "idx_missing", "*", options)
Expect(cmd.Args()[3:]).To(Equal([]interface{}(expected[1:])))
})
It("should leave the one-SORTBY-per-GROUPBY rule to the server", Label("search", "ftaggregate"), func() {
sortBy := func(field string) redis.FTAggregateStep {
return redis.FTAggregateStep{SortBy: &redis.FTAggregateSortByStep{
Fields: []redis.FTAggregateSortBy{{FieldName: field, Asc: true}},
}}
}
options := &redis.FTAggregateOptions{
Steps: []redis.FTAggregateStep{
sortBy("@a"),
{Filter: &redis.FTAggregateFilter{Expression: "@a > 1"}},
sortBy("@b"),
},
}
args, err := redis.FTAggregateQuery("*", options)
Expect(err).NotTo(HaveOccurred())
expected := redis.AggregateQuery{
"*",
"SORTBY", 2, "@a", "ASC",
"FILTER", "@a > 1",
"SORTBY", 2, "@b", "ASC",
"DIALECT", 2,
}
Expect(args).To(Equal(expected))
})
It("should error when a step sets both Filter and Limit", Label("search", "ftaggregate"), func() {
options := &redis.FTAggregateOptions{
Steps: []redis.FTAggregateStep{
{
Filter: &redis.FTAggregateFilter{Expression: "@x > 1"},
Limit: &redis.FTAggregateLimit{Offset: 0, Num: 10},
},
},
}
_, err := redis.FTAggregateQuery("q", options)
Expect(err).To(MatchError(ContainSubstring("each step must set exactly one")))
})
It("should FTAggregate with FILTER and LIMIT steps", Label("search", "ftaggregate"), func() {
category := &redis.FieldSchema{FieldName: "category", FieldType: redis.SearchFieldTypeTag}
price := &redis.FieldSchema{FieldName: "price", FieldType: redis.SearchFieldTypeNumeric}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, category, price).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "p1", "category", "a", "price", 10)
client.HSet(ctx, "p2", "category", "a", "price", 200)
client.HSet(ctx, "p3", "category", "b", "price", 300)
client.HSet(ctx, "p4", "category", "b", "price", 400)
client.HSet(ctx, "p5", "category", "c", "price", 50)
// Filter rows before grouping and groups after grouping. Without the
// first FILTER, the only group with cnt == 1 would be "c".
options := &redis.FTAggregateOptions{
Steps: []redis.FTAggregateStep{
{Load: &redis.FTAggregateLoad{Field: "@category"}},
{Load: &redis.FTAggregateLoad{Field: "@price"}},
{Filter: &redis.FTAggregateFilter{Expression: "@price > 100"}},
{GroupBy: &redis.FTAggregateGroupBy{
Fields: []interface{}{"@category"},
Reduce: []redis.FTAggregateReducer{
{Reducer: redis.SearchSum, Args: []interface{}{"@price"}, As: "total"},
{Reducer: redis.SearchCount, As: "cnt"},
},
}},
{Filter: &redis.FTAggregateFilter{Expression: "@cnt == 1"}},
{Limit: &redis.FTAggregateLimit{Offset: 0, Num: 10}},
},
}
res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows).To(HaveLen(1))
Expect(res.Rows[0].Fields["category"]).To(BeEquivalentTo("a"))
Expect(res.Rows[0].Fields["total"]).To(BeEquivalentTo("200"))
// FILTER before SORTBY and LIMIT pages over the filtered rows.
sortByPrice := redis.FTAggregateStep{SortBy: &redis.FTAggregateSortByStep{
Fields: []redis.FTAggregateSortBy{{FieldName: "@price", Asc: true}},
}}
options = &redis.FTAggregateOptions{
Steps: []redis.FTAggregateStep{
{Load: &redis.FTAggregateLoad{Field: "@price"}},
{Filter: &redis.FTAggregateFilter{Expression: "@price > 100"}},
sortByPrice,
{Limit: &redis.FTAggregateLimit{Offset: 0, Num: 2}},
},
}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows).To(HaveLen(2))
Expect(res.Rows[0].Fields["price"]).To(BeEquivalentTo("200"))
Expect(res.Rows[1].Fields["price"]).To(BeEquivalentTo("300"))
// The legacy Filter field runs after LIMIT: the two cheapest rows
// are taken first and none of them passes the filter.
options = &redis.FTAggregateOptions{
Steps: []redis.FTAggregateStep{
{Load: &redis.FTAggregateLoad{Field: "@price"}},
sortByPrice,
},
LimitOffset: 0,
Limit: 2,
Filter: "@price > 100",
}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows).To(BeEmpty())
})
It("should FTSearch SkipInitialScan", Label("search", "ftsearch"), func() {
client.HSet(ctx, "doc1", "foo", "bar")
text1 := &redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{SkipInitialScan: true}, text1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
res, err := client.FTSearch(ctx, "idx1", "@foo:bar").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(int64(0)))
})
It("should FTCreate json", Label("search", "ftcreate"), func() {
text1 := &redis.FieldSchema{FieldName: "$.name", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true, Prefix: []interface{}{"king:"}}, text1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.JSONSet(ctx, "king:1", "$", `{"name": "henry"}`)
client.JSONSet(ctx, "king:2", "$", `{"name": "james"}`)
res, err := client.FTSearch(ctx, "idx1", "henry").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(1))
Expect(res.Docs[0].ID).To(BeEquivalentTo("king:1"))
Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"name":"henry"}`))
})
It("should FTCreate json fields as names", Label("search", "ftcreate"), func() {
text1 := &redis.FieldSchema{FieldName: "$.name", FieldType: redis.SearchFieldTypeText, As: "name"}
num1 := &redis.FieldSchema{FieldName: "$.age", FieldType: redis.SearchFieldTypeNumeric, As: "just_a_number"}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true}, text1, num1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.JSONSet(ctx, "doc:1", "$", `{"name": "Jon", "age": 25}`)
res, err := client.FTSearchWithArgs(ctx, "idx1", "Jon", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "name"}, {FieldName: "just_a_number"}}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(1))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1"))
Expect(res.Docs[0].Fields["name"]).To(BeEquivalentTo("Jon"))
Expect(res.Docs[0].Fields["just_a_number"]).To(BeEquivalentTo("25"))
})
It("should FTCreate CaseSensitive", Label("search", "ftcreate"), func() {
tag1 := &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeTag, CaseSensitive: false}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, tag1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "1", "t", "HELLO")
client.HSet(ctx, "2", "t", "hello")
res, err := client.FTSearch(ctx, "idx1", "@t:{HELLO}").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(2))
Expect(res.Docs[0].ID).To(BeEquivalentTo("1"))
Expect(res.Docs[1].ID).To(BeEquivalentTo("2"))
resDrop, err := client.FTDropIndex(ctx, "idx1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resDrop).To(BeEquivalentTo("OK"))
tag2 := &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeTag, CaseSensitive: true}
val, err = client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, tag2).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
res, err = client.FTSearch(ctx, "idx1", "@t:{HELLO}").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(1))
Expect(res.Docs[0].ID).To(BeEquivalentTo("1"))
})
It("should FTSearch ReturnFields", Label("search", "ftsearch"), func() {
resJson, err := client.JSONSet(ctx, "doc:1", "$", `{"t": "riceratops","t2": "telmatosaurus", "n": 9072, "flt": 97.2}`).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resJson).To(BeEquivalentTo("OK"))
text1 := &redis.FieldSchema{FieldName: "$.t", FieldType: redis.SearchFieldTypeText}
num1 := &redis.FieldSchema{FieldName: "$.flt", FieldType: redis.SearchFieldTypeNumeric}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true}, text1, num1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
res, err := client.FTSearchWithArgs(ctx, "idx1", "*", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "$.t", As: "txt"}}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(1))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1"))
Expect(res.Docs[0].Fields["txt"]).To(BeEquivalentTo("riceratops"))
res, err = client.FTSearchWithArgs(ctx, "idx1", "*", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "$.t2", As: "txt"}}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(1))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1"))
Expect(res.Docs[0].Fields["txt"]).To(BeEquivalentTo("telmatosaurus"))
})
It("should FTSynUpdate", Label("search", "ftsynupdate"), func() {
text1 := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}
text2 := &redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true}, text1, text2).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
resSynUpdate, err := client.FTSynUpdateWithArgs(ctx, "idx1", "id1", &redis.FTSynUpdateOptions{SkipInitialScan: true}, []interface{}{"boy", "child", "offspring"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resSynUpdate).To(BeEquivalentTo("OK"))
client.HSet(ctx, "doc1", "title", "he is a baby", "body", "this is a test")
resSynUpdate, err = client.FTSynUpdateWithArgs(ctx, "idx1", "id1", &redis.FTSynUpdateOptions{SkipInitialScan: true}, []interface{}{"baby"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resSynUpdate).To(BeEquivalentTo("OK"))
client.HSet(ctx, "doc2", "title", "he is another baby", "body", "another test")
res, err := client.FTSearchWithArgs(ctx, "idx1", "child", &redis.FTSearchOptions{Expander: "SYNONYM"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc2"))
Expect(res.Docs[0].Fields["title"]).To(BeEquivalentTo("he is another baby"))
Expect(res.Docs[0].Fields["body"]).To(BeEquivalentTo("another test"))
})
It("should FTSynDump", Label("search", "ftsyndump"), func() {
text1 := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}
text2 := &redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true}, text1, text2).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
resSynUpdate, err := client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"boy", "child", "offspring"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resSynUpdate).To(BeEquivalentTo("OK"))
resSynUpdate, err = client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"baby", "child"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resSynUpdate).To(BeEquivalentTo("OK"))
resSynUpdate, err = client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"tree", "wood"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resSynUpdate).To(BeEquivalentTo("OK"))
resSynDump, err := client.FTSynDump(ctx, "idx1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resSynDump[0].Term).To(BeEquivalentTo("baby"))
Expect(resSynDump[0].Synonyms).To(BeEquivalentTo([]string{"id1"}))
Expect(resSynDump[1].Term).To(BeEquivalentTo("wood"))
Expect(resSynDump[1].Synonyms).To(BeEquivalentTo([]string{"id1"}))
Expect(resSynDump[2].Term).To(BeEquivalentTo("boy"))
Expect(resSynDump[2].Synonyms).To(BeEquivalentTo([]string{"id1"}))
Expect(resSynDump[3].Term).To(BeEquivalentTo("tree"))
Expect(resSynDump[3].Synonyms).To(BeEquivalentTo([]string{"id1"}))
Expect(resSynDump[4].Term).To(BeEquivalentTo("child"))
Expect(resSynDump[4].Synonyms).To(Or(BeEquivalentTo([]string{"id1"}), BeEquivalentTo([]string{"id1", "id1"})))
Expect(resSynDump[5].Term).To(BeEquivalentTo("offspring"))
Expect(resSynDump[5].Synonyms).To(BeEquivalentTo([]string{"id1"}))
})
It("should FTCreate json with alias", Label("search", "ftcreate"), func() {
text1 := &redis.FieldSchema{FieldName: "$.name", FieldType: redis.SearchFieldTypeText, As: "name"}
num1 := &redis.FieldSchema{FieldName: "$.num", FieldType: redis.SearchFieldTypeNumeric, As: "num"}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true, Prefix: []interface{}{"king:"}}, text1, num1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.JSONSet(ctx, "king:1", "$", `{"name": "henry", "num": 42}`)
client.JSONSet(ctx, "king:2", "$", `{"name": "james", "num": 3.14}`)
res, err := client.FTSearch(ctx, "idx1", "@name:henry").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(1))
Expect(res.Docs[0].ID).To(BeEquivalentTo("king:1"))
Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"name":"henry","num":42}`))
res, err = client.FTSearch(ctx, "idx1", "@num:[0 10]").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(1))
Expect(res.Docs[0].ID).To(BeEquivalentTo("king:2"))
Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"name":"james","num":3.14}`))
})
It("should FTCreate json with multipath", Label("search", "ftcreate"), func() {
tag1 := &redis.FieldSchema{FieldName: "$..name", FieldType: redis.SearchFieldTypeTag, As: "name"}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true, Prefix: []interface{}{"king:"}}, tag1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.JSONSet(ctx, "king:1", "$", `{"name": "henry", "country": {"name": "england"}}`)
res, err := client.FTSearch(ctx, "idx1", "@name:{england}").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(1))
Expect(res.Docs[0].ID).To(BeEquivalentTo("king:1"))
Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"name":"henry","country":{"name":"england"}}`))
})
It("should FTCreate json with jsonpath", Label("search", "ftcreate"), func() {
text1 := &redis.FieldSchema{FieldName: `$["prod:name"]`, FieldType: redis.SearchFieldTypeText, As: "name"}
text2 := &redis.FieldSchema{FieldName: `$.prod:name`, FieldType: redis.SearchFieldTypeText, As: "name_unsupported"}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true}, text1, text2).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.JSONSet(ctx, "doc:1", "$", `{"prod:name": "RediSearch"}`)
res, err := client.FTSearch(ctx, "idx1", "@name:RediSearch").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(1))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1"))
Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"prod:name":"RediSearch"}`))
res, err = client.FTSearch(ctx, "idx1", "@name_unsupported:RediSearch").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(1))
res, err = client.FTSearchWithArgs(ctx, "idx1", "@name:RediSearch", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "name"}}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(1))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1"))
Expect(res.Docs[0].Fields["name"]).To(BeEquivalentTo("RediSearch"))
})
It("should FTCreate VECTOR", Label("search", "ftcreate"), func() {
hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "a", "v", "aaaaaaaa")
client.HSet(ctx, "b", "v", "aaaabaaa")
client.HSet(ctx, "c", "v", "aaaaabaa")
searchOptions := &redis.FTSearchOptions{
Return: []redis.FTSearchReturn{{FieldName: "__v_score"}},
SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}},
DialectVersion: 2,
Params: map[string]interface{}{"vec": "aaaaaaaa"},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("a"))
Expect(res.Docs[0].Fields["__v_score"]).To(BeEquivalentTo("0"))
})
It("should FTCreate VECTOR with dialect 1 ", Label("search", "ftcreate"), func() {
hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "a", "v", "aaaaaaaa")
client.HSet(ctx, "b", "v", "aaaabaaa")
client.HSet(ctx, "c", "v", "aaaaabaa")
searchOptions := &redis.FTSearchOptions{
Return: []redis.FTSearchReturn{{FieldName: "v"}},
SortBy: []redis.FTSearchSortBy{{FieldName: "v", Asc: true}},
Limit: 10,
DialectVersion: 1,
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("a"))
Expect(res.Docs[0].Fields["v"]).To(BeEquivalentTo("aaaaaaaa"))
})
It("should FTCreate VECTOR with default dialect", Label("search", "ftcreate"), func() {
hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "a", "v", "aaaaaaaa")
client.HSet(ctx, "b", "v", "aaaabaaa")
client.HSet(ctx, "c", "v", "aaaaabaa")
searchOptions := &redis.FTSearchOptions{
Return: []redis.FTSearchReturn{{FieldName: "__v_score"}},
SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}},
Params: map[string]interface{}{"vec": "aaaaaaaa"},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("a"))
Expect(res.Docs[0].Fields["__v_score"]).To(BeEquivalentTo("0"))
})
It("should FTCreate and FTSearch text params", Label("search", "ftcreate", "ftsearch"), func() {
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "doc1", "name", "Alice")
client.HSet(ctx, "doc2", "name", "Bob")
client.HSet(ctx, "doc3", "name", "Carol")
res1, err := client.FTSearchWithArgs(ctx, "idx1", "@name:($name1 | $name2 )", &redis.FTSearchOptions{Params: map[string]interface{}{"name1": "Alice", "name2": "Bob"}, DialectVersion: 2}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(BeEquivalentTo(int64(2)))
Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc1"))
Expect(res1.Docs[1].ID).To(BeEquivalentTo("doc2"))
})
It("should FTCreate and FTSearch numeric params", Label("search", "ftcreate", "ftsearch"), func() {
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "numval", FieldType: redis.SearchFieldTypeNumeric}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "doc1", "numval", 101)
client.HSet(ctx, "doc2", "numval", 102)
client.HSet(ctx, "doc3", "numval", 103)
res1, err := client.FTSearchWithArgs(ctx, "idx1", "@numval:[$min $max]", &redis.FTSearchOptions{Params: map[string]interface{}{"min": 101, "max": 102}, DialectVersion: 2}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(BeEquivalentTo(int64(2)))
Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc1"))
Expect(res1.Docs[1].ID).To(BeEquivalentTo("doc2"))
})
It("should FTCreate and FTSearch geo params", Label("search", "ftcreate", "ftsearch"), func() {
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "g", FieldType: redis.SearchFieldTypeGeo}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "doc1", "g", "29.69465, 34.95126")
client.HSet(ctx, "doc2", "g", "29.69350, 34.94737")
client.HSet(ctx, "doc3", "g", "29.68746, 34.94882")
res1, err := client.FTSearchWithArgs(ctx, "idx1", "@g:[$lon $lat $radius $units]", &redis.FTSearchOptions{Params: map[string]interface{}{"lat": "34.95126", "lon": "29.69465", "radius": 1000, "units": "km"}, DialectVersion: 2}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(BeEquivalentTo(int64(3)))
Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc1"))
Expect(res1.Docs[1].ID).To(BeEquivalentTo("doc2"))
Expect(res1.Docs[2].ID).To(BeEquivalentTo("doc3"))
})
It("should FTConfigGet return multiple fields", Label("search", "NonRedisEnterprise"), func() {
res, err := client.FTConfigSet(ctx, "DEFAULT_DIALECT", "1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(BeEquivalentTo("OK"))
defDialect, err := client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result()
Expect(err).NotTo(HaveOccurred())
Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "1"}))
res, err = client.FTConfigSet(ctx, "DEFAULT_DIALECT", "2").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(BeEquivalentTo("OK"))
defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result()
Expect(err).NotTo(HaveOccurred())
Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "2"}))
})
It("should FTConfigSet and FTConfigGet dialect", Label("search", "ftconfigget", "ftconfigset", "NonRedisEnterprise"), func() {
res, err := client.FTConfigSet(ctx, "DEFAULT_DIALECT", "1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(BeEquivalentTo("OK"))
defDialect, err := client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result()
Expect(err).NotTo(HaveOccurred())
Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "1"}))
res, err = client.FTConfigSet(ctx, "DEFAULT_DIALECT", "2").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(BeEquivalentTo("OK"))
defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result()
Expect(err).NotTo(HaveOccurred())
Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "2"}))
})
It("should FTCreate WithSuffixtrie", Label("search", "ftcreate", "ftinfo"), func() {
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
res, err := client.FTInfo(ctx, "idx1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Attributes[0].Attribute).To(BeEquivalentTo("txt"))
resDrop, err := client.FTDropIndex(ctx, "idx1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resDrop).To(BeEquivalentTo("OK"))
// create withsuffixtrie index - text field
val, err = client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText, WithSuffixtrie: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
res, err = client.FTInfo(ctx, "idx1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Attributes[0].WithSuffixtrie).To(BeTrue())
resDrop, err = client.FTDropIndex(ctx, "idx1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resDrop).To(BeEquivalentTo("OK"))
// create withsuffixtrie index - tag field
val, err = client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeTag, WithSuffixtrie: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
res, err = client.FTInfo(ctx, "idx1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Attributes[0].WithSuffixtrie).To(BeTrue())
})
It("should test dialect 4", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion("7.4", "doesn't work with older redis stack images")
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{
Prefix: []interface{}{"resource:"},
}, &redis.FieldSchema{
FieldName: "uuid",
FieldType: redis.SearchFieldTypeTag,
}, &redis.FieldSchema{
FieldName: "tags",
FieldType: redis.SearchFieldTypeTag,
}, &redis.FieldSchema{
FieldName: "description",
FieldType: redis.SearchFieldTypeText,
}, &redis.FieldSchema{
FieldName: "rating",
FieldType: redis.SearchFieldTypeNumeric,
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
client.HSet(ctx, "resource:1", map[string]interface{}{
"uuid": "123e4567-e89b-12d3-a456-426614174000",
"tags": "finance|crypto|$btc|blockchain",
"description": "Analysis of blockchain technologies & Bitcoin's potential.",
"rating": 5,
})
client.HSet(ctx, "resource:2", map[string]interface{}{
"uuid": "987e6543-e21c-12d3-a456-426614174999",
"tags": "health|well-being|fitness|new-year's-resolutions",
"description": "Health trends for the new year, including fitness regimes.",
"rating": 4,
})
res, err := client.FTSearchWithArgs(ctx, "idx1", "@uuid:{$uuid}",
&redis.FTSearchOptions{
DialectVersion: 2,
Params: map[string]interface{}{"uuid": "123e4567-e89b-12d3-a456-426614174000"},
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(int64(1)))
Expect(res.Docs[0].ID).To(BeEquivalentTo("resource:1"))
res, err = client.FTSearchWithArgs(ctx, "idx1", "@uuid:{$uuid}",
&redis.FTSearchOptions{
DialectVersion: 4,
Params: map[string]interface{}{"uuid": "123e4567-e89b-12d3-a456-426614174000"},
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(int64(1)))
Expect(res.Docs[0].ID).To(BeEquivalentTo("resource:1"))
client.HSet(ctx, "test:1", map[string]interface{}{
"uuid": "3d3586fe-0416-4572-8ce",
"email": "adriano@acme.com.ie",
"num": 5,
})
// Create the index
ftCreateOptions := &redis.FTCreateOptions{
Prefix: []interface{}{"test:"},
}
schema := []*redis.FieldSchema{
{
FieldName: "uuid",
FieldType: redis.SearchFieldTypeTag,
},
{
FieldName: "email",
FieldType: redis.SearchFieldTypeTag,
},
{
FieldName: "num",
FieldType: redis.SearchFieldTypeNumeric,
},
}
val, err = client.FTCreate(ctx, "idx_hash", ftCreateOptions, schema...).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(Equal("OK"))
WaitForIndexing(rawClient, "idx_hash")
ftSearchOptions := &redis.FTSearchOptions{
DialectVersion: 4,
Params: map[string]interface{}{
"uuid": "3d3586fe-0416-4572-8ce",
"email": "adriano@acme.com.ie",
},
}
res, err = client.FTSearchWithArgs(ctx, "idx_hash", "@uuid:{$uuid}", ftSearchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("test:1"))
Expect(res.Docs[0].Fields["uuid"]).To(BeEquivalentTo("3d3586fe-0416-4572-8ce"))
res, err = client.FTSearchWithArgs(ctx, "idx_hash", "@email:{$email}", ftSearchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("test:1"))
Expect(res.Docs[0].Fields["email"]).To(BeEquivalentTo("adriano@acme.com.ie"))
ftSearchOptions.Params = map[string]interface{}{"num": 5}
res, err = client.FTSearchWithArgs(ctx, "idx_hash", "@num:[5]", ftSearchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("test:1"))
Expect(res.Docs[0].Fields["num"]).To(BeEquivalentTo("5"))
})
It("should FTCreate GeoShape", Label("search", "ftcreate", "ftsearch"), func() {
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "geom", FieldType: redis.SearchFieldTypeGeoShape, GeoShapeFieldType: "FLAT"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "small", "geom", "POLYGON((1 1, 1 100, 100 100, 100 1, 1 1))")
client.HSet(ctx, "large", "geom", "POLYGON((1 1, 1 200, 200 200, 200 1, 1 1))")
res1, err := client.FTSearchWithArgs(ctx, "idx1", "@geom:[WITHIN $poly]",
&redis.FTSearchOptions{
DialectVersion: 3,
Params: map[string]interface{}{"poly": "POLYGON((0 0, 0 150, 150 150, 150 0, 0 0))"},
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(BeEquivalentTo(int64(1)))
Expect(res1.Docs[0].ID).To(BeEquivalentTo("small"))
res2, err := client.FTSearchWithArgs(ctx, "idx1", "@geom:[CONTAINS $poly]",
&redis.FTSearchOptions{
DialectVersion: 3,
Params: map[string]interface{}{"poly": "POLYGON((2 2, 2 50, 50 50, 50 2, 2 2))"},
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res2.Total).To(BeEquivalentTo(int64(2)))
})
It("should create search index with FLOAT16 and BFLOAT16 vectors", Label("search", "ftcreate", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion("7.4", "doesn't work with older redis stack images")
val, err := client.FTCreate(ctx, "index", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "float16", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: &redis.FTFlatOptions{Type: "FLOAT16", Dim: 768, DistanceMetric: "COSINE"}}},
&redis.FieldSchema{FieldName: "bfloat16", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: &redis.FTFlatOptions{Type: "BFLOAT16", Dim: 768, DistanceMetric: "COSINE"}}},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "index")
})
It("should test geoshapes query intersects and disjoint", Label("NonRedisEnterprise"), func() {
SkipBeforeRedisVersion("7.4", "doesn't work with older redis stack images")
_, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{
FieldName: "g",
FieldType: redis.SearchFieldTypeGeoShape,
GeoShapeFieldType: "FLAT",
}).Result()
Expect(err).NotTo(HaveOccurred())
client.HSet(ctx, "doc_point1", "g", "POINT (10 10)")
client.HSet(ctx, "doc_point2", "g", "POINT (50 50)")
client.HSet(ctx, "doc_polygon1", "g", "POLYGON ((20 20, 25 35, 35 25, 20 20))")
client.HSet(ctx, "doc_polygon2", "g", "POLYGON ((60 60, 65 75, 70 70, 65 55, 60 60))")
intersection, err := client.FTSearchWithArgs(ctx, "idx1", "@g:[intersects $shape]",
&redis.FTSearchOptions{
DialectVersion: 3,
Params: map[string]interface{}{"shape": "POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))"},
}).Result()
Expect(err).NotTo(HaveOccurred())
_assert_geosearch_result(&intersection, []string{"doc_point2", "doc_polygon1"})
disjunction, err := client.FTSearchWithArgs(ctx, "idx1", "@g:[disjoint $shape]",
&redis.FTSearchOptions{
DialectVersion: 3,
Params: map[string]interface{}{"shape": "POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))"},
}).Result()
Expect(err).NotTo(HaveOccurred())
_assert_geosearch_result(&disjunction, []string{"doc_point1", "doc_polygon2"})
})
It("should test geoshapes query contains and within", func() {
_, err := client.FTCreate(ctx, "idx2", &redis.FTCreateOptions{}, &redis.FieldSchema{
FieldName: "g",
FieldType: redis.SearchFieldTypeGeoShape,
GeoShapeFieldType: "FLAT",
}).Result()
Expect(err).NotTo(HaveOccurred())
client.HSet(ctx, "doc_point1", "g", "POINT (10 10)")
client.HSet(ctx, "doc_point2", "g", "POINT (50 50)")
client.HSet(ctx, "doc_polygon1", "g", "POLYGON ((20 20, 25 35, 35 25, 20 20))")
client.HSet(ctx, "doc_polygon2", "g", "POLYGON ((60 60, 65 75, 70 70, 65 55, 60 60))")
containsA, err := client.FTSearchWithArgs(ctx, "idx2", "@g:[contains $shape]",
&redis.FTSearchOptions{
DialectVersion: 3,
Params: map[string]interface{}{"shape": "POINT(25 25)"},
}).Result()
Expect(err).NotTo(HaveOccurred())
_assert_geosearch_result(&containsA, []string{"doc_polygon1"})
containsB, err := client.FTSearchWithArgs(ctx, "idx2", "@g:[contains $shape]",
&redis.FTSearchOptions{
DialectVersion: 3,
Params: map[string]interface{}{"shape": "POLYGON((24 24, 24 26, 25 25, 24 24))"},
}).Result()
Expect(err).NotTo(HaveOccurred())
_assert_geosearch_result(&containsB, []string{"doc_polygon1"})
within, err := client.FTSearchWithArgs(ctx, "idx2", "@g:[within $shape]",
&redis.FTSearchOptions{
DialectVersion: 3,
Params: map[string]interface{}{"shape": "POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))"},
}).Result()
Expect(err).NotTo(HaveOccurred())
_assert_geosearch_result(&within, []string{"doc_point2", "doc_polygon1"})
})
It("should search missing fields", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion("7.4", "doesn't work with older redis stack images")
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{Prefix: []interface{}{"property:"}},
&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: true},
&redis.FieldSchema{FieldName: "features", FieldType: redis.SearchFieldTypeTag, IndexMissing: true},
&redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, IndexMissing: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "property:1", map[string]interface{}{
"title": "Luxury Villa in Malibu",
"features": "pool,sea view,modern",
"description": "A stunning modern villa overlooking the Pacific Ocean.",
})
client.HSet(ctx, "property:2", map[string]interface{}{
"title": "Downtown Flat",
"description": "Modern flat in central Paris with easy access to metro.",
})
client.HSet(ctx, "property:3", map[string]interface{}{
"title": "Beachfront Bungalow",
"features": "beachfront,sun deck",
})
res, err := client.FTSearchWithArgs(ctx, "idx1", "ismissing(@features)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("property:2"))
res, err = client.FTSearchWithArgs(ctx, "idx1", "-ismissing(@features)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1"))
Expect(res.Docs[1].ID).To(BeEquivalentTo("property:3"))
res, err = client.FTSearchWithArgs(ctx, "idx1", "ismissing(@description)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("property:3"))
res, err = client.FTSearchWithArgs(ctx, "idx1", "-ismissing(@description)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1"))
Expect(res.Docs[1].ID).To(BeEquivalentTo("property:2"))
})
It("should search empty fields", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion("7.4", "doesn't work with older redis stack images")
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{Prefix: []interface{}{"property:"}},
&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: true},
&redis.FieldSchema{FieldName: "features", FieldType: redis.SearchFieldTypeTag, IndexEmpty: true},
&redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, IndexEmpty: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "property:1", map[string]interface{}{
"title": "Luxury Villa in Malibu",
"features": "pool,sea view,modern",
"description": "A stunning modern villa overlooking the Pacific Ocean.",
})
client.HSet(ctx, "property:2", map[string]interface{}{
"title": "Downtown Flat",
"features": "",
"description": "Modern flat in central Paris with easy access to metro.",
})
client.HSet(ctx, "property:3", map[string]interface{}{
"title": "Beachfront Bungalow",
"features": "beachfront,sun deck",
"description": "",
})
res, err := client.FTSearchWithArgs(ctx, "idx1", "@features:{\"\"}", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("property:2"))
res, err = client.FTSearchWithArgs(ctx, "idx1", "-@features:{\"\"}", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1"))
Expect(res.Docs[1].ID).To(BeEquivalentTo("property:3"))
res, err = client.FTSearchWithArgs(ctx, "idx1", "@description:''", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("property:3"))
res, err = client.FTSearchWithArgs(ctx, "idx1", "-@description:''", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1"))
Expect(res.Docs[1].ID).To(BeEquivalentTo("property:2"))
})
It("should FTCreate VECTOR with int8 and uint8 types", Label("search", "ftcreate"), func() {
SkipBeforeRedisVersion("7.9", "doesn't work with older redis")
// Define INT8 vector field
hnswOptionsInt8 := &redis.FTHNSWOptions{
Type: "INT8",
Dim: 2,
DistanceMetric: "L2",
}
// Define UINT8 vector field
hnswOptionsUint8 := &redis.FTHNSWOptions{
Type: "UINT8",
Dim: 2,
DistanceMetric: "L2",
}
// Create index with INT8 and UINT8 vector fields
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "int8_vector", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptionsInt8}},
&redis.FieldSchema{FieldName: "uint8_vector", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptionsUint8}},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
// Insert vectors in int8 and uint8 format
client.HSet(ctx, "doc1", "int8_vector", "\x01\x02", "uint8_vector", "\x01\x02")
client.HSet(ctx, "doc2", "int8_vector", "\x03\x04", "uint8_vector", "\x03\x04")
// Perform KNN search on INT8 vector
searchOptionsInt8 := &redis.FTSearchOptions{
Return: []redis.FTSearchReturn{{FieldName: "int8_vector"}},
SortBy: []redis.FTSearchSortBy{{FieldName: "int8_vector", Asc: true}},
DialectVersion: 2,
Params: map[string]interface{}{"vec": "\x01\x02"},
}
resInt8, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 1 @int8_vector $vec]", searchOptionsInt8).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resInt8.Docs[0].ID).To(BeEquivalentTo("doc1"))
// Perform KNN search on UINT8 vector
searchOptionsUint8 := &redis.FTSearchOptions{
Return: []redis.FTSearchReturn{{FieldName: "uint8_vector"}},
SortBy: []redis.FTSearchSortBy{{FieldName: "uint8_vector", Asc: true}},
DialectVersion: 2,
Params: map[string]interface{}{"vec": "\x01\x02"},
}
resUint8, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 1 @uint8_vector $vec]", searchOptionsUint8).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resUint8.Docs[0].ID).To(BeEquivalentTo("doc1"))
})
It("should return special float scores in FT.SEARCH vecsim", Label("search", "ftsearch", "vecsim"), func() {
SkipBeforeRedisVersion("7.4", "doesn't work with older redis stack images")
vecField := &redis.FTFlatOptions{
Type: "FLOAT32",
Dim: 2,
DistanceMetric: "IP",
}
_, err := client.FTCreate(ctx, "idx_vec",
&redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{"doc:"}},
&redis.FieldSchema{FieldName: "vector", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: vecField}}).Result()
Expect(err).NotTo(HaveOccurred())
WaitForIndexing(rawClient, "idx_vec")
bigPos := []float32{1e38, 1e38}
bigNeg := []float32{-1e38, -1e38}
nanVec := []float32{float32(math.NaN()), 0}
negNanVec := []float32{float32(math.Copysign(math.NaN(), -1)), 0}
client.HSet(ctx, "doc:1", "vector", encodeFloat32Vector(bigPos))
client.HSet(ctx, "doc:2", "vector", encodeFloat32Vector(bigNeg))
client.HSet(ctx, "doc:3", "vector", encodeFloat32Vector(nanVec))
client.HSet(ctx, "doc:4", "vector", encodeFloat32Vector(negNanVec))
searchOptions := &redis.FTSearchOptions{WithScores: true, Params: map[string]interface{}{"vec": encodeFloat32Vector(bigPos)}}
res, err := client.FTSearchWithArgs(ctx, "idx_vec", "*=>[KNN 4 @vector $vec]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(4))
var scores []float64
for _, row := range res.Docs {
raw := fmt.Sprintf("%v", row.Fields["__vector_score"])
f, err := helper.ParseFloat(raw)
Expect(err).NotTo(HaveOccurred())
scores = append(scores, f)
}
Expect(scores).To(ContainElement(BeNumerically("==", math.Inf(1))))
Expect(scores).To(ContainElement(BeNumerically("==", math.Inf(-1))))
// For NaN values, use a custom check since NaN != NaN in floating point math
nanCount := 0
for _, score := range scores {
if math.IsNaN(score) {
nanCount++
}
}
Expect(nanCount).To(Equal(2))
})
It("should FTCreate VECTOR with VAMANA algorithm - basic", Label("search", "ftcreate"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 2,
DistanceMetric: "L2",
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "a", "v", "aaaaaaaa")
client.HSet(ctx, "b", "v", "aaaabaaa")
client.HSet(ctx, "c", "v", "aaaaabaa")
searchOptions := &redis.FTSearchOptions{
Return: []redis.FTSearchReturn{{FieldName: "__v_score"}},
SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}},
DialectVersion: 2,
Params: map[string]interface{}{"vec": "aaaaaaaa"},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Docs[0].ID).To(BeEquivalentTo("a"))
Expect(res.Docs[0].Fields["__v_score"]).To(BeEquivalentTo("0"))
})
It("should FTCreate VECTOR with VAMANA algorithm - with compression", Label("search", "ftcreate"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT16",
Dim: 256,
DistanceMetric: "COSINE",
Compression: "LVQ8",
TrainingThreshold: 10240,
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
})
It("should FTCreate VECTOR with HNSW compression and read it back from FTInfo", Label("search", "ftcreate", "ftinfo"), func() {
SkipBeforeRedisVersion("8.12", "HNSW COMPRESSION requires Redis 8.12+")
hnswOptions := &redis.FTHNSWOptions{
Type: "FLOAT32",
Dim: 4,
DistanceMetric: "L2",
Compression: "SQ8",
TrainingThreshold: 1024,
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
resInfo, err := client.FTInfo(ctx, "idx1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resInfo.Attributes).To(HaveLen(1))
Expect(resInfo.Attributes[0].Algorithm).To(BeEquivalentTo("HNSW"))
Expect(resInfo.Attributes[0].Compression).To(BeEquivalentTo("SQ8"))
Expect(resInfo.Attributes[0].TrainingThreshold).To(BeEquivalentTo(1024))
})
It("should FTCreate VECTOR with VAMANA algorithm - advanced parameters", Label("search", "ftcreate"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 512,
DistanceMetric: "IP",
Compression: "LVQ8",
ConstructionWindowSize: 300,
GraphMaxDegree: 128,
SearchWindowSize: 20,
Epsilon: 0.02,
TrainingThreshold: 20480,
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
})
It("should fail FTCreate VECTOR with VAMANA - missing required parameters", Label("search", "ftcreate"), func() {
// Test missing Type
cmd := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{
Dim: 2,
DistanceMetric: "L2",
}}})
Expect(cmd.Err()).To(HaveOccurred())
Expect(cmd.Err().Error()).To(ContainSubstring("Type, Dim and DistanceMetric are required for VECTOR VAMANA"))
// Test missing Dim
cmd = client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{
Type: "FLOAT32",
DistanceMetric: "L2",
}}})
Expect(cmd.Err()).To(HaveOccurred())
Expect(cmd.Err().Error()).To(ContainSubstring("Type, Dim and DistanceMetric are required for VECTOR VAMANA"))
// Test missing DistanceMetric
cmd = client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 2,
}}})
Expect(cmd.Err()).To(HaveOccurred())
Expect(cmd.Err().Error()).To(ContainSubstring("Type, Dim and DistanceMetric are required for VECTOR VAMANA"))
})
It("should fail FTCreate VECTOR with multiple vector options", Label("search", "ftcreate"), func() {
// Test VAMANA + HNSW
cmd := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{
VamanaOptions: &redis.FTVamanaOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"},
HNSWOptions: &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"},
}})
Expect(cmd.Err()).To(HaveOccurred())
Expect(cmd.Err().Error()).To(ContainSubstring("VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions"))
// Test VAMANA + FLAT
cmd = client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{
VamanaOptions: &redis.FTVamanaOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"},
FlatOptions: &redis.FTFlatOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"},
}})
Expect(cmd.Err()).To(HaveOccurred())
Expect(cmd.Err().Error()).To(ContainSubstring("VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions"))
})
It("should test VAMANA L2 distance metric", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 3,
DistanceMetric: "L2",
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
// L2 distance test vectors
vectors := [][]float32{
{1.0, 0.0, 0.0},
{2.0, 0.0, 0.0},
{0.0, 1.0, 0.0},
{5.0, 0.0, 0.0},
}
for i, vec := range vectors {
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
Return: []redis.FTSearchReturn{{FieldName: "score"}},
SortBy: []redis.FTSearchSortBy{{FieldName: "score", Asc: true}},
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 3 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(3))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0"))
})
It("should test VAMANA COSINE distance metric", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 3,
DistanceMetric: "COSINE",
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
vectors := [][]float32{
{1.0, 0.0, 0.0},
{0.707, 0.707, 0.0},
{0.0, 1.0, 0.0},
{-1.0, 0.0, 0.0},
}
for i, vec := range vectors {
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
Return: []redis.FTSearchReturn{{FieldName: "score"}},
SortBy: []redis.FTSearchSortBy{{FieldName: "score", Asc: true}},
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 3 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(3))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0"))
})
It("should test VAMANA IP distance metric", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 3,
DistanceMetric: "IP",
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
vectors := [][]float32{
{1.0, 2.0, 3.0},
{2.0, 1.0, 1.0},
{3.0, 3.0, 3.0},
{0.1, 0.1, 0.1},
}
for i, vec := range vectors {
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
Return: []redis.FTSearchReturn{{FieldName: "score"}},
SortBy: []redis.FTSearchSortBy{{FieldName: "score", Asc: true}},
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 3 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(3))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc2"))
})
It("should test VAMANA basic functionality", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 4,
DistanceMetric: "L2",
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
vectors := [][]float32{
{1.0, 2.0, 3.0, 4.0},
{2.0, 3.0, 4.0, 5.0},
{3.0, 4.0, 5.0, 6.0},
{10.0, 11.0, 12.0, 13.0},
}
for i, vec := range vectors {
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
Return: []redis.FTSearchReturn{{FieldName: "__v_score"}},
SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}},
DialectVersion: 2,
Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 3 @v $vec]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(3))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) // Should be closest to itself
Expect(res.Docs[0].Fields["__v_score"]).To(BeEquivalentTo("0"))
})
It("should test VAMANA FLOAT16 type", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT16",
Dim: 4,
DistanceMetric: "L2",
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
vectors := [][]float32{
{1.5, 2.5, 3.5, 4.5},
{2.5, 3.5, 4.5, 5.5},
{3.5, 4.5, 5.5, 6.5},
}
for i, vec := range vectors {
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat16Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat16Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(2))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0"))
})
It("should test VAMANA FLOAT32 type", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 4,
DistanceMetric: "L2",
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
vectors := [][]float32{
{1.0, 2.0, 3.0, 4.0},
{2.0, 3.0, 4.0, 5.0},
{3.0, 4.0, 5.0, 6.0},
}
for i, vec := range vectors {
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(2))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0"))
})
It("should test VAMANA with default dialect", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 2,
DistanceMetric: "L2",
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "a", "v", "aaaaaaaa")
client.HSet(ctx, "b", "v", "aaaabaaa")
client.HSet(ctx, "c", "v", "aaaaabaa")
searchOptions := &redis.FTSearchOptions{
Return: []redis.FTSearchReturn{{FieldName: "__v_score"}},
SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}},
Params: map[string]interface{}{"vec": "aaaaaaaa"},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(2))
})
It("should test VAMANA with LVQ8 compression", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 8,
DistanceMetric: "L2",
Compression: "LVQ8",
TrainingThreshold: 1024,
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
vectors := make([][]float32, 20)
for i := 0; i < 20; i++ {
vec := make([]float32, 8)
for j := 0; j < 8; j++ {
vec[j] = float32(i + j)
}
vectors[i] = vec
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(5))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0"))
})
It("should test VAMANA compression with both vector types", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
// Test FLOAT16 with LVQ8
vamanaOptions16 := &redis.FTVamanaOptions{
Type: "FLOAT16",
Dim: 8,
DistanceMetric: "L2",
Compression: "LVQ8",
TrainingThreshold: 1024,
}
val, err := client.FTCreate(ctx, "idx16",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v16", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions16}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx16")
// Test FLOAT32 with LVQ8
vamanaOptions32 := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 8,
DistanceMetric: "L2",
Compression: "LVQ8",
TrainingThreshold: 1024,
}
val, err = client.FTCreate(ctx, "idx32",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v32", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions32}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx32")
// Add data to both indices
for i := 0; i < 15; i++ {
vec := make([]float32, 8)
for j := 0; j < 8; j++ {
vec[j] = float32(i + j)
}
client.HSet(ctx, fmt.Sprintf("doc16_%d", i), "v16", encodeFloat16Vector(vec))
client.HSet(ctx, fmt.Sprintf("doc32_%d", i), "v32", encodeFloat32Vector(vec))
}
queryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
// Test FLOAT16 index
searchOptions16 := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat16Vector(queryVec)},
}
res16, err := client.FTSearchWithArgs(ctx, "idx16", "*=>[KNN 3 @v16 $vec as score]", searchOptions16).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res16.Total).To(BeEquivalentTo(3))
// Test FLOAT32 index
searchOptions32 := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(queryVec)},
}
res32, err := client.FTSearchWithArgs(ctx, "idx32", "*=>[KNN 3 @v32 $vec as score]", searchOptions32).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res32.Total).To(BeEquivalentTo(3))
})
It("should test VAMANA construction window size", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 6,
DistanceMetric: "L2",
ConstructionWindowSize: 300,
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
vectors := make([][]float32, 20)
for i := 0; i < 20; i++ {
vec := make([]float32, 6)
for j := 0; j < 6; j++ {
vec[j] = float32(i + j)
}
vectors[i] = vec
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(5))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0"))
})
It("should test VAMANA graph max degree", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 6,
DistanceMetric: "COSINE",
GraphMaxDegree: 64,
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
vectors := make([][]float32, 25)
for i := 0; i < 25; i++ {
vec := make([]float32, 6)
for j := 0; j < 6; j++ {
vec[j] = float32(i + j)
}
vectors[i] = vec
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 6 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(6))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0"))
})
It("should test VAMANA search window size", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 6,
DistanceMetric: "L2",
SearchWindowSize: 20,
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
vectors := make([][]float32, 30)
for i := 0; i < 30; i++ {
vec := make([]float32, 6)
for j := 0; j < 6; j++ {
vec[j] = float32(i + j)
}
vectors[i] = vec
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 8 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(8))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0"))
})
It("should test VAMANA all advanced parameters", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 8,
DistanceMetric: "L2",
Compression: "LVQ8",
ConstructionWindowSize: 200,
GraphMaxDegree: 32,
SearchWindowSize: 15,
Epsilon: 0.01,
TrainingThreshold: 1024,
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
vectors := make([][]float32, 15)
for i := 0; i < 15; i++ {
vec := make([]float32, 8)
for j := 0; j < 8; j++ {
vec[j] = float32(i + j)
}
vectors[i] = vec
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(5))
Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0"))
})
It("should fail when using a non-zero offset with a zero limit", Label("search", "ftsearch"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "testIdx", &redis.FTCreateOptions{}, &redis.FieldSchema{
FieldName: "txt",
FieldType: redis.SearchFieldTypeText,
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "testIdx")
client.HSet(ctx, "doc1", "txt", "hello world")
// Attempt to search with a non-zero offset and zero limit.
_, err = client.FTSearchWithArgs(ctx, "testIdx", "hello", &redis.FTSearchOptions{
LimitOffset: 5,
Limit: 0,
}).Result()
Expect(err).To(HaveOccurred())
})
It("should evaluate exponentiation precedence in APPLY expressions correctly", Label("search", "ftaggregate"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "txns", &redis.FTCreateOptions{}, &redis.FieldSchema{
FieldName: "dummy",
FieldType: redis.SearchFieldTypeText,
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "txns")
client.HSet(ctx, "doc1", "dummy", "dummy")
correctOptions := &redis.FTAggregateOptions{
Apply: []redis.FTAggregateApply{
{Field: "(2*3^2)", As: "Value"},
},
Limit: 1,
LimitOffset: 0,
}
correctRes, err := client.FTAggregateWithArgs(ctx, "txns", "*", correctOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(correctRes.Rows[0].Fields["Value"]).To(BeEquivalentTo("18"))
})
It("should return a syntax error when empty strings are used for numeric parameters", Label("search", "ftsearch"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "idx", &redis.FTCreateOptions{}, &redis.FieldSchema{
FieldName: "n",
FieldType: redis.SearchFieldTypeNumeric,
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx")
client.HSet(ctx, "doc1", "n", 0)
_, err = client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{
Filters: []redis.FTSearchFilter{{
FieldName: "n",
Min: "",
Max: "",
}},
DialectVersion: 2,
}).Result()
Expect(err).To(HaveOccurred())
})
It("should return NaN as default for AVG reducer when no numeric values are present", Label("search", "ftaggregate"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "aggTestAvg", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText},
&redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "aggTestAvg")
client.HSet(ctx, "doc1", "grp", "g1")
reducers := []redis.FTAggregateReducer{
{Reducer: redis.SearchAvg, Args: []interface{}{"@n"}, As: "avg"},
}
groupBy := []redis.FTAggregateGroupBy{
{Fields: []interface{}{"@grp"}, Reduce: reducers},
}
options := &redis.FTAggregateOptions{GroupBy: groupBy}
res, err := client.FTAggregateWithArgs(ctx, "aggTestAvg", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows).ToNot(BeEmpty())
Expect(res.Rows[0].Fields["avg"]).To(SatisfyAny(Equal("nan"), Equal("NaN")))
})
It("should return 1 as default for COUNT reducer when no numeric values are present", Label("search", "ftaggregate"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "aggTestCount", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText},
&redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "aggTestCount")
client.HSet(ctx, "doc1", "grp", "g1")
reducers := []redis.FTAggregateReducer{
{Reducer: redis.SearchCount, As: "cnt"},
}
groupBy := []redis.FTAggregateGroupBy{
{Fields: []interface{}{"@grp"}, Reduce: reducers},
}
options := &redis.FTAggregateOptions{GroupBy: groupBy}
res, err := client.FTAggregateWithArgs(ctx, "aggTestCount", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows).ToNot(BeEmpty())
Expect(res.Rows[0].Fields["cnt"]).To(BeEquivalentTo("1"))
})
It("should return NaN as default for SUM reducer when no numeric values are present", Label("search", "ftaggregate"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "aggTestSum", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText},
&redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "aggTestSum")
client.HSet(ctx, "doc1", "grp", "g1")
reducers := []redis.FTAggregateReducer{
{Reducer: redis.SearchSum, Args: []interface{}{"@n"}, As: "sum"},
}
groupBy := []redis.FTAggregateGroupBy{
{Fields: []interface{}{"@grp"}, Reduce: reducers},
}
options := &redis.FTAggregateOptions{GroupBy: groupBy}
res, err := client.FTAggregateWithArgs(ctx, "aggTestSum", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows).ToNot(BeEmpty())
Expect(res.Rows[0].Fields["sum"]).To(SatisfyAny(Equal("nan"), Equal("NaN")))
})
It("should return the full requested number of results by re-running the query when some results expire", Label("search", "ftsearch"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "aggExpired", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "order", FieldType: redis.SearchFieldTypeNumeric, Sortable: true},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "aggExpired")
for i := 1; i <= 15; i++ {
key := fmt.Sprintf("doc%d", i)
_, err := client.HSet(ctx, key, "order", i).Result()
Expect(err).NotTo(HaveOccurred())
}
_, err = client.Del(ctx, "doc3", "doc7").Result()
Expect(err).NotTo(HaveOccurred())
options := &redis.FTSearchOptions{
SortBy: []redis.FTSearchSortBy{{FieldName: "order", Asc: true}},
LimitOffset: 0,
Limit: 10,
}
res, err := client.FTSearchWithArgs(ctx, "aggExpired", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(len(res.Docs)).To(BeEquivalentTo(10))
for _, doc := range res.Docs {
Expect(doc.ID).ToNot(Or(Equal("doc3"), Equal("doc7")))
}
})
// NonRedisEnterprise: relies on the "search-timeout" module CONFIG parameter,
// which Redis Enterprise does not support; without it the heavy FT.AGGREGATE
// query never times out and the spec hangs until the suite timeout.
It("should stop processing and return an error when a timeout occurs", Label("search", "ftaggregate", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
// The 1 ms timeout is only enforced deterministically when the query
// runs on the main thread. With search-workers > 0 the aggregate is
// offloaded to the worker pool, where the timeout check can be skipped
// and the query completes successfully. Pin workers to 0 for this spec
// and restore the previous values afterwards.
prevConfig, err := rawClient.ConfigGet(ctx, "search-*").Result()
Expect(err).NotTo(HaveOccurred())
for _, param := range []string{"search-workers", "search-on-timeout"} {
Expect(prevConfig).To(HaveKey(param), "server does not expose %s", param)
}
defer func() {
for _, param := range []string{"search-workers", "search-on-timeout"} {
Expect(rawClient.ConfigSet(ctx, param, prevConfig[param]).Err()).NotTo(HaveOccurred())
}
}()
Expect(rawClient.ConfigSet(ctx, "search-workers", "0").Err()).NotTo(HaveOccurred())
val, err := client.FTCreate(ctx, "aggTimeoutHeavy", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric, Sortable: true},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "aggTimeoutHeavy")
const totalDocs = 100000
for i := 0; i < totalDocs; i++ {
key := fmt.Sprintf("doc%d", i)
_, err := client.HSet(ctx, key, "n", i).Result()
Expect(err).NotTo(HaveOccurred())
}
// default behaviour was changed in 8.0.1, set to fail to validate the timeout was triggered
Expect(rawClient.ConfigSet(ctx, "search-on-timeout", "fail").Err()).NotTo(HaveOccurred())
options := &redis.FTAggregateOptions{
SortBy: []redis.FTAggregateSortBy{{FieldName: "@n", Desc: true}},
LimitOffset: 0,
Limit: 100000,
Timeout: 1, // 1 ms timeout, expected to trigger a timeout error.
}
_, err = client.FTAggregateWithArgs(ctx, "aggTimeoutHeavy", "*", options).Result()
Expect(err).To(HaveOccurred())
Expect(strings.ToLower(err.Error())).To(ContainSubstring("timeout"))
})
It("should return 0 as default for COUNT_DISTINCT reducer when no values are present", Label("search", "ftaggregate"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "aggTestCountDistinct", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText},
&redis.FieldSchema{FieldName: "x", FieldType: redis.SearchFieldTypeText},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "aggTestCountDistinct")
client.HSet(ctx, "doc1", "grp", "g1")
reducers := []redis.FTAggregateReducer{
{Reducer: redis.SearchCountDistinct, Args: []interface{}{"@x"}, As: "distinct_count"},
}
groupBy := []redis.FTAggregateGroupBy{
{Fields: []interface{}{"@grp"}, Reduce: reducers},
}
options := &redis.FTAggregateOptions{GroupBy: groupBy}
res, err := client.FTAggregateWithArgs(ctx, "aggTestCountDistinct", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows).ToNot(BeEmpty())
Expect(res.Rows[0].Fields["distinct_count"]).To(BeEquivalentTo("0"))
})
It("should return 0 as default for COUNT_DISTINCTISH reducer when no values are present", Label("search", "ftaggregate"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "aggTestCountDistinctIsh", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText},
&redis.FieldSchema{FieldName: "y", FieldType: redis.SearchFieldTypeText},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "aggTestCountDistinctIsh")
_, err = client.HSet(ctx, "doc1", "grp", "g1").Result()
Expect(err).NotTo(HaveOccurred())
reducers := []redis.FTAggregateReducer{
{Reducer: redis.SearchCountDistinctish, Args: []interface{}{"@y"}, As: "distinctish_count"},
}
groupBy := []redis.FTAggregateGroupBy{
{Fields: []interface{}{"@grp"}, Reduce: reducers},
}
options := &redis.FTAggregateOptions{GroupBy: groupBy}
res, err := client.FTAggregateWithArgs(ctx, "aggTestCountDistinctIsh", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows).ToNot(BeEmpty())
Expect(res.Rows[0].Fields["distinctish_count"]).To(BeEquivalentTo("0"))
})
It("should use BM25 as the default scorer", Label("search", "ftsearch"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "scoringTest", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "scoringTest")
_, err = client.HSet(ctx, "doc1", "description", "red apple").Result()
Expect(err).NotTo(HaveOccurred())
_, err = client.HSet(ctx, "doc2", "description", "green apple").Result()
Expect(err).NotTo(HaveOccurred())
resDefault, err := client.FTSearchWithArgs(ctx, "scoringTest", "apple", &redis.FTSearchOptions{WithScores: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resDefault.Total).To(BeNumerically(">", 0))
resBM25, err := client.FTSearchWithArgs(ctx, "scoringTest", "apple", &redis.FTSearchOptions{WithScores: true, Scorer: "BM25"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resBM25.Total).To(BeNumerically(">", 0))
Expect(resDefault.Total).To(BeEquivalentTo(resBM25.Total))
Expect(resDefault.Docs[0].ID).To(BeElementOf("doc1", "doc2"))
Expect(resDefault.Docs[1].ID).To(BeElementOf("doc1", "doc2"))
})
It("should return 0 as default for STDDEV reducer when no numeric values are present", Label("search", "ftaggregate"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "aggTestStddev", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText},
&redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "aggTestStddev")
_, err = client.HSet(ctx, "doc1", "grp", "g1").Result()
Expect(err).NotTo(HaveOccurred())
reducers := []redis.FTAggregateReducer{
{Reducer: redis.SearchStdDev, Args: []interface{}{"@n"}, As: "stddev"},
}
groupBy := []redis.FTAggregateGroupBy{
{Fields: []interface{}{"@grp"}, Reduce: reducers},
}
options := &redis.FTAggregateOptions{GroupBy: groupBy}
res, err := client.FTAggregateWithArgs(ctx, "aggTestStddev", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows).ToNot(BeEmpty())
Expect(res.Rows[0].Fields["stddev"]).To(BeEquivalentTo("0"))
})
It("should return NaN as default for QUANTILE reducer when no numeric values are present", Label("search", "ftaggregate"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "aggTestQuantile", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText},
&redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "aggTestQuantile")
_, err = client.HSet(ctx, "doc1", "grp", "g1").Result()
Expect(err).NotTo(HaveOccurred())
reducers := []redis.FTAggregateReducer{
{Reducer: redis.SearchQuantile, Args: []interface{}{"@n", 0.5}, As: "quantile"},
}
groupBy := []redis.FTAggregateGroupBy{
{Fields: []interface{}{"@grp"}, Reduce: reducers},
}
options := &redis.FTAggregateOptions{GroupBy: groupBy}
res, err := client.FTAggregateWithArgs(ctx, "aggTestQuantile", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows).ToNot(BeEmpty())
Expect(res.Rows[0].Fields["quantile"]).To(SatisfyAny(Equal("nan"), Equal("NaN")))
})
It("should return nil as default for FIRST_VALUE reducer when no values are present", Label("search", "ftaggregate"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "aggTestFirstValue", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText},
&redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "aggTestFirstValue")
_, err = client.HSet(ctx, "doc1", "grp", "g1").Result()
Expect(err).NotTo(HaveOccurred())
reducers := []redis.FTAggregateReducer{
{Reducer: redis.SearchFirstValue, Args: []interface{}{"@t"}, As: "first_val"},
}
groupBy := []redis.FTAggregateGroupBy{
{Fields: []interface{}{"@grp"}, Reduce: reducers},
}
options := &redis.FTAggregateOptions{GroupBy: groupBy}
res, err := client.FTAggregateWithArgs(ctx, "aggTestFirstValue", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows).ToNot(BeEmpty())
Expect(res.Rows[0].Fields["first_val"]).To(BeNil())
})
It("should fail to add an alias that is an existing index name", Label("search", "ftalias"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
val, err = client.FTCreate(ctx, "idx2", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx2")
_, err = client.FTAliasAdd(ctx, "idx2", "idx1").Result()
Expect(err).To(HaveOccurred())
Expect(strings.ToLower(err.Error())).To(ContainSubstring("alias"))
})
It("should test ft.search with CountOnly param", Label("search", "ftsearch"), func() {
val, err := client.FTCreate(ctx, "txtIndex", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "txtIndex")
_, err = client.HSet(ctx, "doc1", "txt", "hello world").Result()
Expect(err).NotTo(HaveOccurred())
_, err = client.HSet(ctx, "doc2", "txt", "hello go").Result()
Expect(err).NotTo(HaveOccurred())
_, err = client.HSet(ctx, "doc3", "txt", "hello redis").Result()
Expect(err).NotTo(HaveOccurred())
optsCountOnly := &redis.FTSearchOptions{
CountOnly: true,
LimitOffset: 0,
Limit: 2, // even though we limit to 2, with count-only no docs are returned
DialectVersion: 2,
}
resCountOnly, err := client.FTSearchWithArgs(ctx, "txtIndex", "hello", optsCountOnly).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resCountOnly.Total).To(BeEquivalentTo(3))
Expect(len(resCountOnly.Docs)).To(BeEquivalentTo(0))
optsLimit := &redis.FTSearchOptions{
CountOnly: false,
LimitOffset: 0,
Limit: 2, // we expect to get 2 documents even though total count is 3
DialectVersion: 2,
}
resLimit, err := client.FTSearchWithArgs(ctx, "txtIndex", "hello", optsLimit).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resLimit.Total).To(BeEquivalentTo(3))
Expect(len(resLimit.Docs)).To(BeEquivalentTo(2))
})
It("should reject deprecated configuration keys", Label("search", "ftconfig"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
// List of deprecated configuration keys.
deprecatedKeys := []string{
"_FREE_RESOURCE_ON_THREAD",
"_NUMERIC_COMPRESS",
"_NUMERIC_RANGES_PARENTS",
"_PRINT_PROFILE_CLOCK",
"_PRIORITIZE_INTERSECT_UNION_CHILDREN",
"BG_INDEX_SLEEP_GAP",
"CONN_PER_SHARD",
"CURSOR_MAX_IDLE",
"CURSOR_REPLY_THRESHOLD",
"DEFAULT_DIALECT",
"EXTLOAD",
"FORK_GC_CLEAN_THRESHOLD",
"FORK_GC_RETRY_INTERVAL",
"FORK_GC_RUN_INTERVAL",
"FORKGC_SLEEP_BEFORE_EXIT",
"FRISOINI",
"GC_POLICY",
"GCSCANSIZE",
"INDEX_CURSOR_LIMIT",
"MAXAGGREGATERESULTS",
"MAXDOCTABLESIZE",
"MAXPREFIXEXPANSIONS",
"MAXSEARCHRESULTS",
"MIN_OPERATION_WORKERS",
"MIN_PHONETIC_TERM_LEN",
"MINPREFIX",
"MINSTEMLEN",
"NO_MEM_POOLS",
"NOGC",
"ON_TIMEOUT",
"MULTI_TEXT_SLOP",
"PARTIAL_INDEXED_DOCS",
"RAW_DOCID_ENCODING",
"SEARCH_THREADS",
"TIERED_HNSW_BUFFER_LIMIT",
"TIMEOUT",
"TOPOLOGY_VALIDATION_TIMEOUT",
"UNION_ITERATOR_HEAP",
"VSS_MAX_RESIZE",
"WORKERS",
"WORKERS_PRIORITY_BIAS_THRESHOLD",
"MT_MODE",
"WORKER_THREADS",
}
for _, key := range deprecatedKeys {
_, err := client.FTConfigSet(ctx, key, "test_value").Result()
Expect(err).To(HaveOccurred())
}
val, err := rawClient.ConfigGet(ctx, "*").Result()
Expect(err).NotTo(HaveOccurred())
// Since FT.CONFIG is deprecated since redis 8, use CONFIG instead with new search parameters.
keys := make([]string, 0, len(val))
for key := range val {
keys = append(keys, key)
}
Expect(keys).To(ContainElement(ContainSubstring("search")))
})
It("should return INF for MIN reducer and -INF for MAX reducer when no numeric values are present", Label("search", "ftaggregate"), func() {
SkipBeforeRedisVersion("7.9", "requires Redis 8.x")
val, err := client.FTCreate(ctx, "aggTestMinMax", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText},
&redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "aggTestMinMax")
_, err = client.HSet(ctx, "doc1", "grp", "g1").Result()
Expect(err).NotTo(HaveOccurred())
reducers := []redis.FTAggregateReducer{
{Reducer: redis.SearchMin, Args: []interface{}{"@n"}, As: "minValue"},
{Reducer: redis.SearchMax, Args: []interface{}{"@n"}, As: "maxValue"},
}
groupBy := []redis.FTAggregateGroupBy{
{Fields: []interface{}{"@grp"}, Reduce: reducers},
}
options := &redis.FTAggregateOptions{GroupBy: groupBy}
res, err := client.FTAggregateWithArgs(ctx, "aggTestMinMax", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows).ToNot(BeEmpty())
Expect(res.Rows[0].Fields["minValue"]).To(BeEquivalentTo("inf"))
Expect(res.Rows[0].Fields["maxValue"]).To(BeEquivalentTo("-inf"))
})
It("should test VAMANA with LVQ4 compression", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 8,
DistanceMetric: "L2",
Compression: "LVQ4",
TrainingThreshold: 1024,
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
vectors := make([][]float32, 20)
for i := 0; i < 20; i++ {
vec := make([]float32, 8)
for j := 0; j < 8; j++ {
vec[j] = float32(i + j)
}
vectors[i] = vec
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(5))
// Don't check specific document ID as vector search is probabilistic
Expect(res.Docs).To(HaveLen(5))
})
It("should test VAMANA with LeanVec4x8 compression and reduce parameter", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 8,
DistanceMetric: "L2",
Compression: "LeanVec4x8",
TrainingThreshold: 1024,
ReduceDim: 4, // Reduce dimension to 4 (half of original 8)
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
vectors := make([][]float32, 20)
for i := 0; i < 20; i++ {
vec := make([]float32, 8)
for j := 0; j < 8; j++ {
vec[j] = float32(i + j)
}
vectors[i] = vec
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(5))
// Don't check specific document ID as vector search is probabilistic
Expect(res.Docs).To(HaveLen(5))
})
It("should test VAMANA compression algorithms with FLOAT16 type", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
compressionAlgorithms := []string{"LVQ4", "LVQ4x4", "LVQ4x8", "LeanVec4x8", "LeanVec8x8"}
for _, compression := range compressionAlgorithms {
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT16",
Dim: 8,
DistanceMetric: "L2",
Compression: compression,
TrainingThreshold: 1024,
}
// Add reduce parameter for LeanVec compressions
if strings.HasPrefix(compression, "LeanVec") {
vamanaOptions.ReduceDim = 4
}
indexName := fmt.Sprintf("idx_%s", compression)
val, err := client.FTCreate(ctx, indexName,
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, indexName)
for i := 0; i < 15; i++ {
vec := make([]float32, 8)
for j := 0; j < 8; j++ {
vec[j] = float32(i + j)
}
client.HSet(ctx, fmt.Sprintf("doc_%s_%d", compression, i), "v", encodeFloat16Vector(vec))
}
queryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat16Vector(queryVec)},
}
res, err := client.FTSearchWithArgs(ctx, indexName, "*=>[KNN 3 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(3))
}
})
It("should test VAMANA compression algorithms with FLOAT32 type", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
compressionAlgorithms := []string{"LVQ4", "LVQ4x4", "LVQ4x8", "LeanVec4x8", "LeanVec8x8"}
for _, compression := range compressionAlgorithms {
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 8,
DistanceMetric: "L2",
Compression: compression,
TrainingThreshold: 1024,
}
// Add reduce parameter for LeanVec compressions
if strings.HasPrefix(compression, "LeanVec") {
vamanaOptions.ReduceDim = 4
}
indexName := fmt.Sprintf("idx_%s", compression)
val, err := client.FTCreate(ctx, indexName,
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, indexName)
for i := 0; i < 15; i++ {
vec := make([]float32, 8)
for j := 0; j < 8; j++ {
vec[j] = float32(i + j)
}
client.HSet(ctx, fmt.Sprintf("doc_%s_%d", compression, i), "v", encodeFloat32Vector(vec))
}
queryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(queryVec)},
}
res, err := client.FTSearchWithArgs(ctx, indexName, "*=>[KNN 3 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(3))
}
})
It("should test VAMANA compression with different distance metrics", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
compressionAlgorithms := []string{"LVQ4", "LVQ4x4", "LVQ4x8", "LeanVec4x8", "LeanVec8x8"}
distanceMetrics := []string{"L2", "COSINE", "IP"}
for _, compression := range compressionAlgorithms {
for _, metric := range distanceMetrics {
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 8,
DistanceMetric: metric,
Compression: compression,
TrainingThreshold: 1024,
}
// Add reduce parameter for LeanVec compressions
if strings.HasPrefix(compression, "LeanVec") {
vamanaOptions.ReduceDim = 4
}
indexName := fmt.Sprintf("idx_%s_%s", compression, metric)
val, err := client.FTCreate(ctx, indexName,
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, indexName)
for i := 0; i < 10; i++ {
vec := make([]float32, 8)
for j := 0; j < 8; j++ {
vec[j] = float32(i + j)
}
client.HSet(ctx, fmt.Sprintf("doc_%s_%s_%d", compression, metric, i), "v", encodeFloat32Vector(vec))
}
queryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(queryVec)},
}
res, err := client.FTSearchWithArgs(ctx, indexName, "*=>[KNN 3 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(3))
}
}
})
It("should test VAMANA compression with all advanced parameters", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
compressionAlgorithms := []string{"LVQ4", "LVQ4x4", "LVQ4x8", "LeanVec4x8", "LeanVec8x8"}
for _, compression := range compressionAlgorithms {
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 8,
DistanceMetric: "L2",
Compression: compression,
ConstructionWindowSize: 200,
GraphMaxDegree: 32,
SearchWindowSize: 15,
Epsilon: 0.01,
TrainingThreshold: 1024,
}
// Add reduce parameter for LeanVec compressions
if strings.HasPrefix(compression, "LeanVec") {
vamanaOptions.ReduceDim = 4
}
indexName := fmt.Sprintf("idx_%s_advanced", compression)
val, err := client.FTCreate(ctx, indexName,
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, indexName)
for i := 0; i < 15; i++ {
vec := make([]float32, 8)
for j := 0; j < 8; j++ {
vec[j] = float32(i + j)
}
client.HSet(ctx, fmt.Sprintf("doc_%s_advanced_%d", compression, i), "v", encodeFloat32Vector(vec))
}
queryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(queryVec)},
}
res, err := client.FTSearchWithArgs(ctx, indexName, "*=>[KNN 5 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(5))
}
})
It("should fail when using reduce parameter with non-LeanVec compression", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 8,
DistanceMetric: "L2",
Compression: "LVQ8",
TrainingThreshold: 1024,
ReduceDim: 4, // This should fail for LVQ8
}
_, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).To(HaveOccurred())
})
It("should test VAMANA with LVQ4 compression in RESP3", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 8,
DistanceMetric: "L2",
Compression: "LVQ4",
TrainingThreshold: 1024,
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
vectors := make([][]float32, 20)
for i := 0; i < 20; i++ {
vec := make([]float32, 8)
for j := 0; j < 8; j++ {
vec[j] = float32(i + j)
}
vectors[i] = vec
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(5))
// Don't check specific document ID as vector search is probabilistic
Expect(res.Docs).To(HaveLen(5))
})
It("should test VAMANA with LeanVec4x8 compression and reduce parameter in RESP3", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT32",
Dim: 8,
DistanceMetric: "L2",
Compression: "LeanVec4x8",
TrainingThreshold: 1024,
ReduceDim: 4, // Reduce dimension to 4 (half of original 8)
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
vectors := make([][]float32, 20)
for i := 0; i < 20; i++ {
vec := make([]float32, 8)
for j := 0; j < 8; j++ {
vec[j] = float32(i + j)
}
vectors[i] = vec
client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec))
}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])},
}
res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(5))
// Don't check specific document ID as vector search is probabilistic
Expect(res.Docs).To(HaveLen(5))
})
It("should test VAMANA compression algorithms with FLOAT16 type in RESP3", Label("search", "ftcreate", "vamana"), func() {
SkipBeforeRedisVersion("8.2", "VAMANA requires Redis 8.2+")
compressionAlgorithms := []string{"LVQ4", "LVQ4x4", "LVQ4x8", "LeanVec4x8", "LeanVec8x8"}
for _, compression := range compressionAlgorithms {
vamanaOptions := &redis.FTVamanaOptions{
Type: "FLOAT16",
Dim: 8,
DistanceMetric: "L2",
Compression: compression,
TrainingThreshold: 1024,
}
// Add reduce parameter for LeanVec compressions
if strings.HasPrefix(compression, "LeanVec") {
vamanaOptions.ReduceDim = 4
}
indexName := fmt.Sprintf("idx_resp3_%s", compression)
val, err := client.FTCreate(ctx, indexName,
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, indexName)
// Add test data
for i := 0; i < 15; i++ {
vec := make([]float32, 8)
for j := 0; j < 8; j++ {
vec[j] = float32(i + j)
}
client.HSet(ctx, fmt.Sprintf("doc_resp3_%s_%d", compression, i), "v", encodeFloat16Vector(vec))
}
queryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
searchOptions := &redis.FTSearchOptions{
DialectVersion: 2,
NoContent: true,
Params: map[string]interface{}{"vec": encodeFloat16Vector(queryVec)},
}
res, err := client.FTSearchWithArgs(ctx, indexName, "*=>[KNN 3 @v $vec as score]", searchOptions).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Total).To(BeEquivalentTo(3))
}
})
It("should parse FTInfo response with vector fields", Label("search", "ftinfo", "NonRedisEnterprise"), func() {
// Scope to a prefix so that hashes left over from previous specs in
// this Describe (e.g. doc_resp3_*) cannot be matched by this index
// and skew NumTerms / NumRecords.
val, err := client.FTCreate(ctx, "idx_vector",
&redis.FTCreateOptions{Prefix: []interface{}{"idx_vector:"}},
&redis.FieldSchema{
FieldName: "prompt",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "response",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "exact_digest",
FieldType: redis.SearchFieldTypeTag,
Separator: ",",
},
&redis.FieldSchema{
FieldName: "prompt_vector",
FieldType: redis.SearchFieldTypeVector,
VectorArgs: &redis.FTVectorArgs{HNSWOptions: &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 1536, DistanceMetric: "COSINE", MaxEdgesPerNode: 16, MaxAllowedEdgesPerNode: 64}},
},
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx_vector")
// Get FT.INFO
resInfo, err := client.FTInfo(ctx, "idx_vector").Result()
Expect(err).NotTo(HaveOccurred())
// Validate index definition
Expect(resInfo.IndexDefinition.KeyType).To(Equal("HASH"))
Expect(resInfo.IndexDefinition.DefaultScore).To(Equal(float64(1)))
// Validate attributes
Expect(len(resInfo.Attributes)).To(Equal(4))
// Check prompt field (TEXT)
promptAttr := resInfo.Attributes[0]
Expect(promptAttr.Identifier).To(Equal("prompt"))
Expect(promptAttr.Attribute).To(Equal("prompt"))
Expect(promptAttr.Type).To(Equal("TEXT"))
// Check response field (TEXT)
responseAttr := resInfo.Attributes[1]
Expect(responseAttr.Identifier).To(Equal("response"))
Expect(responseAttr.Attribute).To(Equal("response"))
Expect(responseAttr.Type).To(Equal("TEXT"))
// Check exact_digest field (TAG)
tagAttr := resInfo.Attributes[2]
Expect(tagAttr.Identifier).To(Equal("exact_digest"))
Expect(tagAttr.Attribute).To(Equal("exact_digest"))
Expect(tagAttr.Type).To(Equal("TAG"))
// Check prompt_vector field (VECTOR)
vectorAttr := resInfo.Attributes[3]
Expect(vectorAttr.Identifier).To(Equal("prompt_vector"))
Expect(vectorAttr.Attribute).To(Equal("prompt_vector"))
Expect(vectorAttr.Type).To(Equal("VECTOR"))
// Validate numeric fields
Expect(resInfo.NumDocs).To(BeEquivalentTo(0))
Expect(resInfo.MaxDocID).To(BeEquivalentTo(0))
Expect(resInfo.NumTerms).To(BeEquivalentTo(0))
Expect(resInfo.NumRecords).To(BeEquivalentTo(0))
Expect(resInfo.Indexing).To(BeEquivalentTo(0))
Expect(resInfo.PercentIndexed).To(BeEquivalentTo(1))
Expect(resInfo.HashIndexingFailures).To(BeEquivalentTo(0))
Expect(resInfo.Cleaning).To(BeEquivalentTo(0))
// Validate memory stats
Expect(resInfo.InvertedSzMB).To(BeEquivalentTo(0))
Expect(resInfo.VectorIndexSzMB).To(BeEquivalentTo(0))
Expect(resInfo.TotalInvertedIndexBlocks).To(BeEquivalentTo(0))
Expect(resInfo.OffsetVectorsSzMB).To(BeEquivalentTo(0))
Expect(resInfo.DocTableSizeMB).To(BeNumerically(">=", 0))
Expect(resInfo.SortableValuesSizeMB).To(BeEquivalentTo(0))
Expect(resInfo.TagOverheadSzMB).To(BeEquivalentTo(0))
Expect(resInfo.TextOverheadSzMB).To(BeEquivalentTo(0))
Expect(resInfo.TotalIndexMemorySzMB).To(BeNumerically(">=", 0))
Expect(resInfo.GeoshapesSzMB).To(BeEquivalentTo(0))
// Validate average stats (should be "nan" for empty index)
Expect(resInfo.RecordsPerDocAvg).To(Equal("nan"))
Expect(resInfo.BytesPerRecordAvg).To(Equal("nan"))
Expect(resInfo.OffsetsPerTermAvg).To(Equal("nan"))
Expect(resInfo.OffsetBitsPerRecordAvg).To(Equal("nan"))
// Validate cursor stats
Expect(resInfo.CursorStats.GlobalIdle).To(BeEquivalentTo(0))
Expect(resInfo.CursorStats.GlobalTotal).To(BeEquivalentTo(0))
Expect(resInfo.CursorStats.IndexCapacity).To(BeNumerically(">=", 0))
Expect(resInfo.CursorStats.IndexTotal).To(BeEquivalentTo(0))
// Validate dialect stats
Expect(resInfo.DialectStats).To(HaveKey("dialect_1"))
Expect(resInfo.DialectStats).To(HaveKey("dialect_2"))
Expect(resInfo.DialectStats).To(HaveKey("dialect_3"))
Expect(resInfo.DialectStats).To(HaveKey("dialect_4"))
// Validate GC stats
Expect(resInfo.GCStats.BytesCollected).To(BeEquivalentTo(0))
Expect(resInfo.GCStats.TotalMsRun).To(BeEquivalentTo(0))
Expect(resInfo.GCStats.TotalCycles).To(BeEquivalentTo(0))
Expect(resInfo.GCStats.AverageCycleTimeMs).To(Equal("nan"))
Expect(resInfo.GCStats.LastRunTimeMs).To(BeEquivalentTo(0))
Expect(resInfo.GCStats.GCNumericTreesMissed).To(BeEquivalentTo(0))
Expect(resInfo.GCStats.GCBlocksDenied).To(BeEquivalentTo(0))
// Validate Index Errors
Expect(resInfo.IndexErrors.IndexingFailures).To(BeEquivalentTo(0))
Expect(resInfo.IndexErrors.LastIndexingError).To(Equal("N/A"))
Expect(resInfo.IndexErrors.LastIndexingErrorKey).To(Equal("N/A"))
// Validate field statistics
Expect(len(resInfo.FieldStatistics)).To(Equal(4))
for _, fieldStat := range resInfo.FieldStatistics {
Expect(fieldStat.Identifier).To(BeElementOf("prompt", "response", "exact_digest", "prompt_vector"))
Expect(fieldStat.Attribute).To(BeElementOf("prompt", "response", "exact_digest", "prompt_vector"))
Expect(fieldStat.IndexErrors.IndexingFailures).To(BeEquivalentTo(0))
Expect(fieldStat.IndexErrors.LastIndexingError).To(Equal("N/A"))
Expect(fieldStat.IndexErrors.LastIndexingErrorKey).To(Equal("N/A"))
}
})
})
// Hybrid Search Tests
var _ = Describe("FT.HYBRID Commands", func() {
ctx := context.TODO()
var client redis.UniversalClient
var rawClient *redis.Client
var closeSubject func() error
BeforeEach(func() {
rawClient = reNewClient(2, false)
client, closeSubject = newUniversalSubject(rawClient)
// Create index with text, numeric, tag fields and vector fields
err := client.FTCreate(ctx, "hybrid_idx", &redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText},
&redis.FieldSchema{FieldName: "price", FieldType: redis.SearchFieldTypeNumeric},
&redis.FieldSchema{FieldName: "color", FieldType: redis.SearchFieldTypeTag},
&redis.FieldSchema{FieldName: "item_type", FieldType: redis.SearchFieldTypeTag},
&redis.FieldSchema{FieldName: "size", FieldType: redis.SearchFieldTypeNumeric},
&redis.FieldSchema{
FieldName: "embedding",
FieldType: redis.SearchFieldTypeVector,
VectorArgs: &redis.FTVectorArgs{
FlatOptions: &redis.FTFlatOptions{
Type: "FLOAT32",
Dim: 4,
DistanceMetric: "L2",
},
},
},
&redis.FieldSchema{
FieldName: "embedding_hnsw",
FieldType: redis.SearchFieldTypeVector,
VectorArgs: &redis.FTVectorArgs{
HNSWOptions: &redis.FTHNSWOptions{
Type: "FLOAT32",
Dim: 4,
DistanceMetric: "L2",
},
},
}).Err()
Expect(err).NotTo(HaveOccurred())
WaitForIndexing(rawClient, "hybrid_idx")
// Add test data
items := []struct {
key string
description string
price int
color string
itemType string
size int
embedding []float32
}{
{"item:0", "red shoes", 15, "red", "shoes", 10, []float32{1.0, 2.0, 7.0, 8.0}},
{"item:1", "green shoes with red laces", 16, "green", "shoes", 11, []float32{1.0, 4.0, 7.0, 8.0}},
{"item:2", "red dress", 17, "red", "dress", 12, []float32{1.0, 2.0, 6.0, 5.0}},
{"item:3", "orange dress", 18, "orange", "dress", 10, []float32{2.0, 3.0, 6.0, 5.0}},
{"item:4", "black shoes", 19, "black", "shoes", 11, []float32{5.0, 6.0, 7.0, 8.0}},
}
for _, item := range items {
client.HSet(ctx, item.key, map[string]interface{}{
"description": item.description,
"price": item.price,
"color": item.color,
"item_type": item.itemType,
"size": item.size,
"embedding": encodeFloat32Vector(item.embedding),
"embedding_hnsw": encodeFloat32Vector(item.embedding),
})
}
})
AfterEach(func() {
err := client.FTDropIndex(ctx, "hybrid_idx").Err()
Expect(err).NotTo(HaveOccurred())
Expect(closeSubject()).NotTo(HaveOccurred())
})
It("should perform basic hybrid search", Label("search", "fthybrid"), func() {
SkipBeforeRedisVersion("8.4", "no support")
// Basic hybrid search combining text and vector search
searchQuery := "@color:{red}"
vectorData := encodeFloat32Vector([]float32{-100, -200, -200, -300})
cmd := client.FTHybrid(ctx, "hybrid_idx", searchQuery, "embedding", &redis.VectorFP32{Val: vectorData})
res, err := cmd.Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
Expect(len(res.Results)).To(BeNumerically(">", 0))
// Check that results contain expected fields
for _, result := range res.Results {
Expect(result).To(HaveKey("__score"))
Expect(result).To(HaveKey("__key"))
}
})
It("should perform hybrid search with scorer", Label("search", "fthybrid", "scorer"), func() {
SkipBeforeRedisVersion("8.4", "no support")
// Test with TFIDF scorer
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{
Query: "@color:{red}",
Scorer: "TFIDF",
},
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 2, 3})},
},
},
Load: []string{"@description", "@color", "@price", "@size", "@__score"},
LimitOffset: 0,
Limit: 3,
}
cmd := client.FTHybridWithArgs(ctx, "hybrid_idx", options)
res, err := cmd.Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
Expect(len(res.Results)).To(BeNumerically("<=", 3))
// Verify that we got results with the fields we asked for
for _, result := range res.Results {
// Since we're using TFIDF scorer, the search results should be scored accordingly
Expect(result).To(HaveKey("__score"))
}
})
It("should perform hybrid search with vector filter", Label("search", "fthybrid", "filter"), func() {
SkipBeforeRedisVersion("8.4", "no support")
// This query won't have results from search, so we can validate vector filter
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{none}"}, // This won't match anything
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 2, 3})},
Filter: "@price:[15 16] @size:[10 11]",
},
},
Load: []string{"@description", "@color", "@price", "@size", "@__score"},
}
cmd := client.FTHybridWithArgs(ctx, "hybrid_idx", options)
res, err := cmd.Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
// Verify that all results match the filter criteria
for _, result := range res.Results {
if price, exists := result["price"]; exists {
priceStr := fmt.Sprintf("%v", price)
priceFloat, err := helper.ParseFloat(priceStr)
Expect(err).NotTo(HaveOccurred())
Expect(priceFloat).To(BeNumerically(">=", 15))
Expect(priceFloat).To(BeNumerically("<=", 16))
}
if size, exists := result["size"]; exists {
sizeStr := fmt.Sprintf("%v", size)
sizeFloat, err := helper.ParseFloat(sizeStr)
Expect(err).NotTo(HaveOccurred())
Expect(sizeFloat).To(BeNumerically(">=", 10))
Expect(sizeFloat).To(BeNumerically("<=", 11))
}
}
})
It("should perform hybrid search with KNN method", Label("search", "fthybrid", "knn"), func() {
SkipBeforeRedisVersion("8.4", "no support")
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{none}"}, // This won't match anything
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 2, 3})},
Method: "KNN",
MethodParams: []interface{}{"K", 3}, // K=3 as key-value pair
},
},
}
cmd := client.FTHybridWithArgs(ctx, "hybrid_idx", options)
res, err := cmd.Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(Equal(3)) // Should return exactly K=3 results
Expect(len(res.Results)).To(Equal(3))
})
It("should perform hybrid search with RANGE method", Label("search", "fthybrid", "range"), func() {
SkipBeforeRedisVersion("8.4", "no support")
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{none}"}, // This won't match anything
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},
Method: "RANGE",
MethodParams: []interface{}{"RADIUS", 2}, // RADIUS=2 as key-value pair
},
},
LimitOffset: 0,
Limit: 3,
}
cmd := client.FTHybridWithArgs(ctx, "hybrid_idx", options)
res, err := cmd.Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
Expect(len(res.Results)).To(BeNumerically("<=", 3))
})
It("should perform hybrid search with LINEAR combine method", Label("search", "fthybrid", "combine"), func() {
SkipBeforeRedisVersion("8.4", "no support")
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{red}"},
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},
},
},
Combine: &redis.FTHybridCombineOptions{
Method: redis.FTHybridCombineLinear,
Alpha: 0.5,
Beta: 0.5,
},
LimitOffset: 0,
Limit: 3,
}
cmd := client.FTHybridWithArgs(ctx, "hybrid_idx", options)
res, err := cmd.Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
Expect(len(res.Results)).To(BeNumerically("<=", 3))
})
It("should perform hybrid search with RRF combine method", Label("search", "fthybrid", "rrf"), func() {
SkipBeforeRedisVersion("8.4", "no support")
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{red}"},
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},
},
},
Combine: &redis.FTHybridCombineOptions{
Method: redis.FTHybridCombineRRF,
Window: 3,
Constant: 0.5,
},
LimitOffset: 0,
Limit: 3,
}
res, err := client.FTHybridWithArgs(ctx, "hybrid_idx", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
Expect(len(res.Results)).To(BeNumerically("<=", 3))
})
It("should perform hybrid search with LOAD and APPLY", Label("search", "fthybrid", "load", "apply"), func() {
SkipBeforeRedisVersion("8.4", "no support")
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{red}"},
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},
},
},
Load: []string{"@description", "@color", "@price", "@size", "@__score"},
Apply: []redis.FTHybridApply{
{
Expression: "@price - (@price * 0.1)",
AsField: "price_discount",
},
{
Expression: "@price_discount * 0.2",
AsField: "tax_discount",
},
},
LimitOffset: 0,
Limit: 3,
}
cmd := client.FTHybridWithArgs(ctx, "hybrid_idx", options)
res, err := cmd.Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
Expect(len(res.Results)).To(BeNumerically("<=", 3))
// Verify that applied fields exist
for _, result := range res.Results {
Expect(result).To(HaveKey("price_discount"))
Expect(result).To(HaveKey("tax_discount"))
}
})
It("should perform hybrid search with LIMIT", Label("search", "fthybrid", "limit"), func() {
SkipBeforeRedisVersion("8.4", "no support")
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{red}"},
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},
},
},
LimitOffset: 0,
Limit: 2,
}
cmd := client.FTHybridWithArgs(ctx, "hybrid_idx", options)
res, err := cmd.Result()
Expect(err).NotTo(HaveOccurred())
Expect(len(res.Results)).To(BeNumerically("<=", 2))
})
It("should perform hybrid search with SORTBY", Label("search", "fthybrid", "sortby"), func() {
SkipBeforeRedisVersion("8.4", "no support")
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{red}"},
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},
},
},
Load: []string{"@color", "@price"},
Apply: []redis.FTHybridApply{
{
Expression: "@price - (@price * 0.1)",
AsField: "price_discount",
},
},
SortBy: []redis.FTSearchSortBy{
{FieldName: "@price_discount", Desc: true},
{FieldName: "@color", Asc: true},
},
LimitOffset: 0,
Limit: 5,
}
cmd := client.FTHybridWithArgs(ctx, "hybrid_idx", options)
res, err := cmd.Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
Expect(len(res.Results)).To(BeNumerically("<=", 5))
// Check that results are sorted - first result should have higher price_discount
if len(res.Results) > 1 {
firstPriceStr := fmt.Sprintf("%v", res.Results[0]["price_discount"])
secondPriceStr := fmt.Sprintf("%v", res.Results[1]["price_discount"])
firstPrice, err1 := helper.ParseFloat(firstPriceStr)
secondPrice, err2 := helper.ParseFloat(secondPriceStr)
if err1 == nil && err2 == nil && firstPrice != secondPrice {
Expect(firstPrice).To(BeNumerically(">=", secondPrice))
}
}
})
// Redis 8.6+ tests using PARAMS-based vector approach
It("should perform basic hybrid search (8.6+ PARAMS)", Label("search", "fthybrid", "params"), func() {
SkipBeforeRedisVersion("8.6", "PARAMS-based vector support requires Redis 8.6+")
// Basic hybrid search combining text and vector search
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{red}"},
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{-100, -200, -200, -300})},
VectorParamName: "vec",
},
},
}
res, err := client.FTHybridWithArgs(ctx, "hybrid_idx", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
Expect(len(res.Results)).To(BeNumerically(">", 0))
// Check that results contain expected fields
for _, result := range res.Results {
Expect(result).To(HaveKey("__score"))
Expect(result).To(HaveKey("__key"))
}
})
It("should perform hybrid search with scorer (8.6+ PARAMS)", Label("search", "fthybrid", "scorer", "params"), func() {
SkipBeforeRedisVersion("8.6", "PARAMS-based vector support requires Redis 8.6+")
// Test with TFIDF scorer
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{
Query: "@color:{red}",
Scorer: "TFIDF",
},
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 2, 3})},
VectorParamName: "vec",
},
},
Load: []string{"@description", "@color", "@price", "@size", "@__score"},
LimitOffset: 0,
Limit: 3,
}
res, err := client.FTHybridWithArgs(ctx, "hybrid_idx", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
Expect(len(res.Results)).To(BeNumerically("<=", 3))
// Verify that we got results with the fields we asked for
for _, result := range res.Results {
// Since we're using TFIDF scorer, the search results should be scored accordingly
Expect(result).To(HaveKey("__score"))
}
})
It("should perform hybrid search with vector filter (8.6+ PARAMS)", Label("search", "fthybrid", "filter", "params"), func() {
SkipBeforeRedisVersion("8.6", "PARAMS-based vector support requires Redis 8.6+")
// This query won't have results from search, so we can validate vector filter
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{none}"}, // This won't match anything
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 2, 3})},
VectorParamName: "vec",
Filter: "@price:[15 16] @size:[10 11]",
},
},
Load: []string{"@description", "@color", "@price", "@size", "@__score"},
}
res, err := client.FTHybridWithArgs(ctx, "hybrid_idx", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
// Verify that all results match the filter criteria
for _, result := range res.Results {
if price, exists := result["price"]; exists {
priceStr := fmt.Sprintf("%v", price)
priceFloat, err := helper.ParseFloat(priceStr)
Expect(err).NotTo(HaveOccurred())
Expect(priceFloat).To(BeNumerically(">=", 15))
Expect(priceFloat).To(BeNumerically("<=", 16))
}
if size, exists := result["size"]; exists {
sizeStr := fmt.Sprintf("%v", size)
sizeFloat, err := helper.ParseFloat(sizeStr)
Expect(err).NotTo(HaveOccurred())
Expect(sizeFloat).To(BeNumerically(">=", 10))
Expect(sizeFloat).To(BeNumerically("<=", 11))
}
}
})
It("should perform hybrid search with KNN method (8.6+ PARAMS)", Label("search", "fthybrid", "knn", "params"), func() {
SkipBeforeRedisVersion("8.6", "PARAMS-based vector support requires Redis 8.6+")
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{none}"}, // This won't match anything
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 2, 3})},
VectorParamName: "vec",
Method: "KNN",
MethodParams: []interface{}{"K", 3}, // K=3 as key-value pair
},
},
}
res, err := client.FTHybridWithArgs(ctx, "hybrid_idx", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(Equal(3)) // Should return exactly K=3 results
Expect(len(res.Results)).To(Equal(3))
})
It("should perform hybrid search with RANGE method (8.6+ PARAMS)", Label("search", "fthybrid", "range", "params"), func() {
SkipBeforeRedisVersion("8.6", "PARAMS-based vector support requires Redis 8.6+")
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{none}"}, // This won't match anything
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},
VectorParamName: "vec",
Method: "RANGE",
MethodParams: []interface{}{"RADIUS", 2}, // RADIUS=2 as key-value pair
},
},
LimitOffset: 0,
Limit: 3,
}
res, err := client.FTHybridWithArgs(ctx, "hybrid_idx", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
Expect(len(res.Results)).To(BeNumerically("<=", 3))
})
It("should perform hybrid search with LINEAR combine method (8.6+ PARAMS)", Label("search", "fthybrid", "combine", "params"), func() {
SkipBeforeRedisVersion("8.6", "PARAMS-based vector support requires Redis 8.6+")
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{red}"},
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},
VectorParamName: "vec",
},
},
Combine: &redis.FTHybridCombineOptions{
Method: redis.FTHybridCombineLinear,
Alpha: 0.5,
Beta: 0.5,
},
LimitOffset: 0,
Limit: 3,
}
res, err := client.FTHybridWithArgs(ctx, "hybrid_idx", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
Expect(len(res.Results)).To(BeNumerically("<=", 3))
})
It("should perform hybrid search with RRF combine method (8.6+ PARAMS)", Label("search", "fthybrid", "rrf", "params"), func() {
SkipBeforeRedisVersion("8.6", "PARAMS-based vector support requires Redis 8.6+")
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{red}"},
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},
VectorParamName: "vec",
},
},
Combine: &redis.FTHybridCombineOptions{
Method: redis.FTHybridCombineRRF,
Window: 3,
Constant: 0.5,
},
LimitOffset: 0,
Limit: 3,
}
res, err := client.FTHybridWithArgs(ctx, "hybrid_idx", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
Expect(len(res.Results)).To(BeNumerically("<=", 3))
})
It("should perform hybrid search with LOAD and APPLY (8.6+ PARAMS)", Label("search", "fthybrid", "load", "apply", "params"), func() {
SkipBeforeRedisVersion("8.6", "PARAMS-based vector support requires Redis 8.6+")
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{red}"},
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},
VectorParamName: "vec",
},
},
Load: []string{"@description", "@color", "@price", "@size", "@__score"},
Apply: []redis.FTHybridApply{
{
Expression: "@price - (@price * 0.1)",
AsField: "price_discount",
},
{
Expression: "@price_discount * 0.2",
AsField: "tax_discount",
},
},
LimitOffset: 0,
Limit: 3,
}
res, err := client.FTHybridWithArgs(ctx, "hybrid_idx", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
Expect(len(res.Results)).To(BeNumerically("<=", 3))
// Verify that applied fields exist
for _, result := range res.Results {
Expect(result).To(HaveKey("price_discount"))
Expect(result).To(HaveKey("tax_discount"))
}
})
It("should perform hybrid search with LIMIT (8.6+ PARAMS)", Label("search", "fthybrid", "limit", "params"), func() {
SkipBeforeRedisVersion("8.6", "PARAMS-based vector support requires Redis 8.6+")
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{red}"},
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},
VectorParamName: "vec",
},
},
LimitOffset: 0,
Limit: 2,
}
res, err := client.FTHybridWithArgs(ctx, "hybrid_idx", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(len(res.Results)).To(BeNumerically("<=", 2))
})
It("should perform hybrid search with SORTBY (8.6+ PARAMS)", Label("search", "fthybrid", "sortby", "params"), func() {
SkipBeforeRedisVersion("8.6", "PARAMS-based vector support requires Redis 8.6+")
options := &redis.FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []redis.FTHybridSearchExpression{
{Query: "@color:{red}"},
},
VectorExpressions: []redis.FTHybridVectorExpression{
{
VectorField: "embedding",
VectorData: &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},
VectorParamName: "vec",
},
},
Load: []string{"@color", "@price"},
Apply: []redis.FTHybridApply{
{
Expression: "@price - (@price * 0.1)",
AsField: "price_discount",
},
},
SortBy: []redis.FTSearchSortBy{
{FieldName: "@price_discount", Desc: true},
{FieldName: "@color", Asc: true},
},
LimitOffset: 0,
Limit: 5,
}
res, err := client.FTHybridWithArgs(ctx, "hybrid_idx", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.TotalResults).To(BeNumerically(">", 0))
Expect(len(res.Results)).To(BeNumerically("<=", 5))
// Check that results are sorted - first result should have higher price_discount
if len(res.Results) > 1 {
firstPriceStr := fmt.Sprintf("%v", res.Results[0]["price_discount"])
secondPriceStr := fmt.Sprintf("%v", res.Results[1]["price_discount"])
firstPrice, err1 := helper.ParseFloat(firstPriceStr)
secondPrice, err2 := helper.ParseFloat(secondPriceStr)
if err1 == nil && err2 == nil && firstPrice != secondPrice {
Expect(firstPrice).To(BeNumerically(">=", secondPrice))
}
}
})
})
func _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []string) {
ids := make([]string, len(result.Docs))
for i, doc := range result.Docs {
ids[i] = doc.ID
}
Expect(ids).To(ConsistOf(expectedDocIDs))
Expect(result.Total).To(BeEquivalentTo(len(expectedDocIDs)))
}
var _ = Describe("RediSearch FT.Config with Resp2 and Resp3", Label("search", "NonRedisEnterprise"), func() {
var clientResp2 *redis.Client
var clientResp3 *redis.Client
BeforeEach(func() {
clientResp2 = reNewClient(2, false)
clientResp3 = reNewClient(3, true)
Expect(clientResp3.FlushDB(ctx).Err()).NotTo(HaveOccurred())
})
AfterEach(func() {
Expect(clientResp2.Close()).NotTo(HaveOccurred())
Expect(clientResp3.Close()).NotTo(HaveOccurred())
})
It("should FTConfigSet and FTConfigGet with resp2 and resp3", Label("search", "ftconfigget", "ftconfigset", "NonRedisEnterprise"), func() {
val, err := clientResp3.FTConfigSet(ctx, "MINPREFIX", "1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
res2, err := clientResp2.FTConfigGet(ctx, "MINPREFIX").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res2).To(BeEquivalentTo(map[string]interface{}{"MINPREFIX": "1"}))
res3, err := clientResp3.FTConfigGet(ctx, "MINPREFIX").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res3).To(BeEquivalentTo(map[string]interface{}{"MINPREFIX": "1"}))
})
It("should FTConfigGet all resp2 and resp3", Label("search", "NonRedisEnterprise"), func() {
res2, err := clientResp2.FTConfigGet(ctx, "*").Result()
Expect(err).NotTo(HaveOccurred())
res3, err := clientResp3.FTConfigGet(ctx, "*").Result()
Expect(err).NotTo(HaveOccurred())
Expect(len(res3)).To(BeEquivalentTo(len(res2)))
Expect(res2["DEFAULT_DIALECT"]).To(BeEquivalentTo(res2["DEFAULT_DIALECT"]))
})
})
var _ = Describe("RediSearch commands Resp 3", Label("search"), func() {
ctx := context.TODO()
var client redis.UniversalClient
var rawClient *redis.Client
var closeSubject func() error
var client2 *redis.Client
BeforeEach(func() {
rawClient = reNewClient(3, true)
client, closeSubject = newUniversalSubject(rawClient)
client2 = reNewClient(3, false)
Expect(rawClient.FlushDB(ctx).Err()).NotTo(HaveOccurred())
})
AfterEach(func() {
Expect(closeSubject()).NotTo(HaveOccurred())
// client2 is spec-local like the subject; leaking it across specs
// accumulates pools and goroutines over the suite.
Expect(client2.Close()).NotTo(HaveOccurred())
})
It("should handle FTAggregate with RESP3", Label("search", "ftcreate", "ftaggregate"), func() {
text1 := &redis.FieldSchema{FieldName: "PrimaryKey", FieldType: redis.SearchFieldTypeText, Sortable: true}
num1 := &redis.FieldSchema{FieldName: "CreatedDateTimeUTC", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "doc1", "PrimaryKey", "9::362330", "CreatedDateTimeUTC", "637387878524969984")
client.HSet(ctx, "doc2", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "637387875859270016")
WaitForIndexing(rawClient, "idx1")
options := &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}}
// Test with UnstableResp3 true - both RawResult and Result should work
cmd := client.FTAggregateWithArgs(ctx, "idx1", "*", options)
res, err := cmd.RawResult()
Expect(err).NotTo(HaveOccurred())
results := res.(map[interface{}]interface{})["results"].([]interface{})
Expect(results[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["CreatedDateTimeUTC"]).
To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
Expect(results[1].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["CreatedDateTimeUTC"]).
To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
// Test Result() also works with UnstableResp3 true
result, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(result.Total).To(BeEquivalentTo(2))
Expect(result.Rows).To(HaveLen(2))
Expect(result.Rows[0].Fields["CreatedDateTimeUTC"]).To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
Expect(result.Rows[1].Fields["CreatedDateTimeUTC"]).To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
// Test with UnstableResp3 false - should also work with stable RESP3 parsing
options = &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}}
result2, err := client2.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(result2.Total).To(BeEquivalentTo(2))
Expect(result2.Rows).To(HaveLen(2))
Expect(result2.Rows[0].Fields["CreatedDateTimeUTC"]).To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
Expect(result2.Rows[1].Fields["CreatedDateTimeUTC"]).To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
})
It("should handle FTInfo with RESP3", Label("search", "ftcreate", "ftinfo"), func() {
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText, Sortable: true, NoStem: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
// Test with UnstableResp3 true - both RawResult and Result should work
resInfo, err := client.FTInfo(ctx, "idx1").RawResult()
Expect(err).NotTo(HaveOccurred())
attributes := resInfo.(map[interface{}]interface{})["attributes"].([]interface{})
flags := attributes[0].(map[interface{}]interface{})["flags"].([]interface{})
Expect(flags).To(ConsistOf("SORTABLE", "NOSTEM"))
valInfo := client.FTInfo(ctx, "idx1").RawVal()
attributes = valInfo.(map[interface{}]interface{})["attributes"].([]interface{})
flags = attributes[0].(map[interface{}]interface{})["flags"].([]interface{})
Expect(flags).To(ConsistOf("SORTABLE", "NOSTEM"))
// Test Result() also works with UnstableResp3 true
result, err := client.FTInfo(ctx, "idx1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(result.IndexName).To(BeEquivalentTo("idx1"))
Expect(result.Attributes).To(HaveLen(1))
Expect(result.Attributes[0].Attribute).To(BeEquivalentTo("txt"))
// Test with UnstableResp3 false - should also work with stable RESP3 parsing
result2, err := client2.FTInfo(ctx, "idx1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(result2.IndexName).To(BeEquivalentTo("idx1"))
Expect(result2.Attributes).To(HaveLen(1))
Expect(result2.Attributes[0].Attribute).To(BeEquivalentTo("txt"))
})
It("should handle FTSpellCheck with RESP3", Label("search", "ftcreate", "ftspellcheck"), func() {
text1 := &redis.FieldSchema{FieldName: "f1", FieldType: redis.SearchFieldTypeText}
text2 := &redis.FieldSchema{FieldName: "f2", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
client.HSet(ctx, "doc1", "f1", "some valid content", "f2", "this is sample text")
client.HSet(ctx, "doc2", "f1", "very important", "f2", "lorem ipsum")
// Test with UnstableResp3 true - both RawResult and Result should work
resSpellCheck, err := client.FTSpellCheck(ctx, "idx1", "impornant").RawResult()
valSpellCheck := client.FTSpellCheck(ctx, "idx1", "impornant").RawVal()
Expect(err).NotTo(HaveOccurred())
Expect(valSpellCheck).To(BeEquivalentTo(resSpellCheck))
results := resSpellCheck.(map[interface{}]interface{})["results"].(map[interface{}]interface{})
Expect(results["impornant"].([]interface{})[0].(map[interface{}]interface{})["important"]).To(BeEquivalentTo(0.5))
// Test Result() also works with UnstableResp3 true
result, err := client.FTSpellCheck(ctx, "idx1", "impornant").Result()
Expect(err).NotTo(HaveOccurred())
Expect(result).To(HaveLen(1))
Expect(result[0].Term).To(BeEquivalentTo("impornant"))
Expect(result[0].Suggestions).To(HaveLen(1))
Expect(result[0].Suggestions[0].Suggestion).To(BeEquivalentTo("important"))
Expect(result[0].Suggestions[0].Score).To(BeEquivalentTo(0.5))
// Test with UnstableResp3 false - should also work with stable RESP3 parsing
result2, err := client2.FTSpellCheck(ctx, "idx1", "impornant").Result()
Expect(err).NotTo(HaveOccurred())
Expect(result2).To(HaveLen(1))
Expect(result2[0].Term).To(BeEquivalentTo("impornant"))
Expect(result2[0].Suggestions).To(HaveLen(1))
Expect(result2[0].Suggestions[0].Suggestion).To(BeEquivalentTo("important"))
Expect(result2[0].Suggestions[0].Score).To(BeEquivalentTo(0.5))
})
It("should handle FTSearch with RESP3", Label("search", "ftcreate", "ftsearch"), func() {
val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{StopWords: []interface{}{"foo", "bar", "baz"}}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "txt")
client.HSet(ctx, "doc1", "txt", "foo baz")
client.HSet(ctx, "doc2", "txt", "hello world")
// Test with UnstableResp3 true - both RawResult and Result should work
res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{NoContent: true}).RawResult()
val1 := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{NoContent: true}).RawVal()
Expect(err).NotTo(HaveOccurred())
Expect(val1).To(BeEquivalentTo(res1))
totalResults := res1.(map[interface{}]interface{})["total_results"]
Expect(totalResults).To(BeEquivalentTo(int64(0)))
res2, err := client.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).RawResult()
Expect(err).NotTo(HaveOccurred())
totalResults2 := res2.(map[interface{}]interface{})["total_results"]
Expect(totalResults2).To(BeEquivalentTo(int64(1)))
// Test Result() also works with UnstableResp3 true
result, err := client.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(result.Total).To(BeEquivalentTo(1))
Expect(result.Docs).To(HaveLen(1))
Expect(result.Docs[0].ID).To(BeEquivalentTo("doc2"))
// Test with UnstableResp3 false - should also work with stable RESP3 parsing
result2, err := client2.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(result2.Total).To(BeEquivalentTo(1))
Expect(result2.Docs).To(HaveLen(1))
Expect(result2.Docs[0].ID).To(BeEquivalentTo("doc2"))
// Test with content
result3, err := client2.FTSearchWithArgs(ctx, "txt", "hello world", &redis.FTSearchOptions{}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(result3.Total).To(BeEquivalentTo(1))
Expect(result3.Docs).To(HaveLen(1))
Expect(result3.Docs[0].ID).To(BeEquivalentTo("doc2"))
Expect(result3.Docs[0].Fields["txt"]).To(BeEquivalentTo("hello world"))
})
It("should handle FTSynDump with RESP3", Label("search", "ftsyndump"), func() {
text1 := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}
text2 := &redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true}, text1, text2).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
resSynUpdate, err := client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"boy", "child", "offspring"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resSynUpdate).To(BeEquivalentTo("OK"))
resSynUpdate, err = client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"baby", "child"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resSynUpdate).To(BeEquivalentTo("OK"))
resSynUpdate, err = client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"tree", "wood"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resSynUpdate).To(BeEquivalentTo("OK"))
// Test with UnstableResp3 true - both RawResult and Result should work
resSynDump, err := client.FTSynDump(ctx, "idx1").RawResult()
valSynDump := client.FTSynDump(ctx, "idx1").RawVal()
Expect(err).NotTo(HaveOccurred())
Expect(valSynDump).To(BeEquivalentTo(resSynDump))
Expect(resSynDump.(map[interface{}]interface{})["baby"]).To(BeEquivalentTo([]interface{}{"id1"}))
// Test Result() also works with UnstableResp3 true
result, err := client.FTSynDump(ctx, "idx1").Result()
Expect(err).NotTo(HaveOccurred())
// Find the "baby" term in results
var foundBaby bool
for _, r := range result {
if r.Term == "baby" {
foundBaby = true
Expect(r.Synonyms).To(ContainElement("id1"))
break
}
}
Expect(foundBaby).To(BeTrue())
// Test with UnstableResp3 false - should also work with stable RESP3 parsing
result2, err := client2.FTSynDump(ctx, "idx1").Result()
Expect(err).NotTo(HaveOccurred())
// Find the "baby" term in results
foundBaby = false
for _, r := range result2 {
if r.Term == "baby" {
foundBaby = true
Expect(r.Synonyms).To(ContainElement("id1"))
break
}
}
Expect(foundBaby).To(BeTrue())
})
It("should test not affected Resp 3 Search method - FTExplain", Label("search", "ftexplain"), func() {
text1 := &redis.FieldSchema{FieldName: "f1", FieldType: redis.SearchFieldTypeText}
text2 := &redis.FieldSchema{FieldName: "f2", FieldType: redis.SearchFieldTypeText}
text3 := &redis.FieldSchema{FieldName: "f3", FieldType: redis.SearchFieldTypeText}
val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, text1, text2, text3).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "txt")
res1, err := client.FTExplain(ctx, "txt", "@f3:f3_val @f2:f2_val @f1:f1_val").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1).ToNot(BeEmpty())
// Test with UnstableResp3 false
Expect(func() {
res2, err := client2.FTExplain(ctx, "txt", "@f3:f3_val @f2:f2_val @f1:f1_val").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res2).ToNot(BeEmpty())
}).ShouldNot(Panic())
})
It("should read HNSW compression back from FTInfo with RESP3", Label("search", "ftcreate", "ftinfo"), func() {
SkipBeforeRedisVersion("8.12", "HNSW COMPRESSION requires Redis 8.12+")
hnswOptions := &redis.FTHNSWOptions{
Type: "FLOAT32",
Dim: 4,
DistanceMetric: "L2",
Compression: "SQ8",
TrainingThreshold: 1024,
}
val, err := client.FTCreate(ctx, "idx1",
&redis.FTCreateOptions{},
&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(rawClient, "idx1")
// RESP3 attributes arrive as maps and go through parseFTAttributeFromMap.
for _, c := range []redis.Cmdable{client, client2} {
resInfo, err := c.FTInfo(ctx, "idx1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resInfo.Attributes).To(HaveLen(1))
Expect(resInfo.Attributes[0].Algorithm).To(BeEquivalentTo("HNSW"))
Expect(resInfo.Attributes[0].Compression).To(BeEquivalentTo("SQ8"))
Expect(resInfo.Attributes[0].TrainingThreshold).To(BeEquivalentTo(1024))
}
})
})