mirror of
https://github.com/redis/go-redis.git
synced 2026-10-28 08:24:54 +08:00
* feat(search): add FILTER and LIMIT aggregate steps FTAggregateStep now supports Filter and Limit, so FILTER and LIMIT can be placed anywhere in the FT.AGGREGATE pipeline, e.g. FILTER before and after GROUPBY. AggregateBuilder.Filter now adds a FILTER at its call position instead of setting the tail Filter field, and AggregateBuilder.Limit is added. The tail Filter and LimitOffset/Limit fields are deprecated. Their wire output is unchanged: they are still sent after Steps, LIMIT then FILTER. * docs(search): document one SORTBY per GROUPBY segment The server allows only one SORTBY between two GROUPBY steps and returns "Multiple SORTBY steps are not allowed" for a second one. The builder SortBy doc said a SortBy after another step starts a new SORTBY step, without saying that the server rejects it. State the rule on SortBy, FTAggregateSortByStep and FTAggregateStep. * docs(search): leave the one-SORTBY rule to the server go-redis does not check that only one SORTBY appears between two GROUPBY steps. Say so in the docs, and add a test that a second SORTBY is sent as built, so the client keeps working if the server relaxes the rule.
724 lines
26 KiB
Go
724 lines
26 KiB
Go
package redis_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
. "github.com/bsm/ginkgo/v2"
|
|
. "github.com/bsm/gomega"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
var _ = Describe("RediSearch Builders", Label("search", "builders"), func() {
|
|
ctx := context.Background()
|
|
var client *redis.Client
|
|
|
|
BeforeEach(func() {
|
|
client = reNewClient(2, false)
|
|
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
|
|
})
|
|
|
|
AfterEach(func() {
|
|
expectCloseErr := client.Close()
|
|
Expect(expectCloseErr).NotTo(HaveOccurred())
|
|
})
|
|
|
|
It("should create index and search with scores using builders", Label("search", "ftcreate", "ftsearch"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx1").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
|
|
WaitForIndexing(client, "idx1")
|
|
|
|
client.HSet(ctx, "doc1", "foo", "hello world")
|
|
client.HSet(ctx, "doc2", "foo", "hello redis")
|
|
|
|
res, err := client.NewSearchBuilder(ctx, "idx1", "hello").WithScores().Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res.Total).To(Equal(2))
|
|
for _, doc := range res.Docs {
|
|
Expect(*doc.Score).To(BeNumerically(">", 0))
|
|
}
|
|
})
|
|
|
|
It("should aggregate using builders", Label("search", "ftaggregate"), func() {
|
|
_, err := client.NewCreateIndexBuilder(ctx, "idx2").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
WaitForIndexing(client, "idx2")
|
|
|
|
client.HSet(ctx, "d1", "n", 1)
|
|
client.HSet(ctx, "d2", "n", 2)
|
|
|
|
agg, err := client.NewAggregateBuilder(ctx, "idx2", "*").
|
|
GroupBy("@n").
|
|
ReduceAs(redis.SearchCount, "count").
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(len(agg.Rows)).To(Equal(2))
|
|
})
|
|
|
|
It("should aggregate with positional Filter and Limit using builders", Label("search", "ftaggregate"), func() {
|
|
_, err := client.NewCreateIndexBuilder(ctx, "idx_steps").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "category", FieldType: redis.SearchFieldTypeTag}).
|
|
Schema(&redis.FieldSchema{FieldName: "price", FieldType: redis.SearchFieldTypeNumeric}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
WaitForIndexing(client, "idx_steps")
|
|
|
|
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)
|
|
|
|
agg, err := client.NewAggregateBuilder(ctx, "idx_steps", "*").
|
|
Load("@category").
|
|
Load("@price").
|
|
Filter("@price > 100").
|
|
GroupBy("@category").
|
|
ReduceAs(redis.SearchSum, "total", "@price").
|
|
ReduceAs(redis.SearchCount, "cnt").
|
|
Filter("@cnt == 1").
|
|
Limit(0, 10).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(agg.Rows).To(HaveLen(1))
|
|
Expect(agg.Rows[0].Fields["category"]).To(BeEquivalentTo("a"))
|
|
Expect(agg.Rows[0].Fields["total"]).To(BeEquivalentTo("200"))
|
|
|
|
agg, err = client.NewAggregateBuilder(ctx, "idx_steps", "*").
|
|
Load("@price").
|
|
Filter("@price > 100").
|
|
SortBy("@price", true).
|
|
Limit(1, 2).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(agg.Rows).To(HaveLen(2))
|
|
Expect(agg.Rows[0].Fields["price"]).To(BeEquivalentTo("300"))
|
|
Expect(agg.Rows[1].Fields["price"]).To(BeEquivalentTo("400"))
|
|
})
|
|
|
|
It("should drop index using builder", Label("search", "ftdropindex"), func() {
|
|
Expect(client.NewCreateIndexBuilder(ctx, "idx3").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "x", FieldType: redis.SearchFieldTypeText}).
|
|
Run()).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx3")
|
|
|
|
dropVal, err := client.NewDropIndexBuilder(ctx, "idx3").Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(dropVal).To(Equal("OK"))
|
|
})
|
|
|
|
It("should manage aliases using builder", Label("search", "ftalias"), func() {
|
|
Expect(client.NewCreateIndexBuilder(ctx, "idx4").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}).
|
|
Run()).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx4")
|
|
|
|
addVal, err := client.NewAliasBuilder(ctx, "alias1").Add("idx4").Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(addVal).To(Equal("OK"))
|
|
|
|
_, err = client.NewSearchBuilder(ctx, "alias1", "*").Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
delVal, err := client.NewAliasBuilder(ctx, "alias1").Del().Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(delVal).To(Equal("OK"))
|
|
})
|
|
|
|
It("should explain query using ExplainBuilder", Label("search", "builders", "ftexplain"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_explain").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_explain")
|
|
|
|
expl, err := client.NewExplainBuilder(ctx, "idx_explain", "foo").Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(expl).To(ContainSubstring("UNION"))
|
|
})
|
|
|
|
It("should retrieve info using SearchInfo builder", Label("search", "builders", "ftinfo"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_info").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_info")
|
|
|
|
i, err := client.NewSearchInfoBuilder(ctx, "idx_info").Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(i.IndexName).To(Equal("idx_info"))
|
|
})
|
|
|
|
It("should spellcheck using builder", Label("search", "builders", "ftspellcheck"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_spell").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_spell")
|
|
|
|
client.HSet(ctx, "doc1", "foo", "bar")
|
|
|
|
_, err = client.NewSpellCheckBuilder(ctx, "idx_spell", "ba").Distance(1).Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
})
|
|
|
|
It("should manage dictionary using DictBuilder", Label("search", "ftdict"), func() {
|
|
addCount, err := client.NewDictBuilder(ctx, "dict1").Add("a", "b").Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(addCount).To(Equal(int64(2)))
|
|
|
|
dump, err := client.NewDictBuilder(ctx, "dict1").Dump().Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(dump).To(ContainElements("a", "b"))
|
|
|
|
delCount, err := client.NewDictBuilder(ctx, "dict1").Del("a").Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(delCount).To(Equal(int64(1)))
|
|
})
|
|
|
|
It("should tag values using TagValsBuilder", Label("search", "builders", "fttagvals"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_tag").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "tags", FieldType: redis.SearchFieldTypeTag}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_tag")
|
|
|
|
client.HSet(ctx, "doc1", "tags", "red,blue")
|
|
client.HSet(ctx, "doc2", "tags", "green,blue")
|
|
|
|
vals, err := client.NewTagValsBuilder(ctx, "idx_tag", "tags").Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(vals).To(BeAssignableToTypeOf([]string{}))
|
|
})
|
|
|
|
It("should cursor read and delete using CursorBuilder", Label("search", "builders", "ftcursor"), func() {
|
|
Expect(client.NewCreateIndexBuilder(ctx, "idx5").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "f", FieldType: redis.SearchFieldTypeText}).
|
|
Run()).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx5")
|
|
client.HSet(ctx, "doc1", "f", "hello")
|
|
client.HSet(ctx, "doc2", "f", "world")
|
|
|
|
cursorBuilder := client.NewCursorBuilder(ctx, "idx5", 1)
|
|
Expect(cursorBuilder).NotTo(BeNil())
|
|
|
|
cursorBuilder = cursorBuilder.Count(10)
|
|
Expect(cursorBuilder).NotTo(BeNil())
|
|
|
|
delBuilder := client.NewCursorBuilder(ctx, "idx5", 1)
|
|
Expect(delBuilder).NotTo(BeNil())
|
|
})
|
|
|
|
It("should update synonyms using SynUpdateBuilder", Label("search", "builders", "ftsynupdate"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_syn").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_syn")
|
|
|
|
syn, err := client.NewSynUpdateBuilder(ctx, "idx_syn", "grp1").Terms("a", "b").Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(syn).To(Equal("OK"))
|
|
})
|
|
|
|
It("should test SearchBuilder with NoContent and Verbatim", Label("search", "ftsearch", "builders"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_nocontent").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Weight: 5}).
|
|
Schema(&redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_nocontent")
|
|
|
|
client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch implements a search engine on top of redis")
|
|
|
|
res, err := client.NewSearchBuilder(ctx, "idx_nocontent", "search engine").
|
|
NoContent().
|
|
Verbatim().
|
|
Limit(0, 5).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res.Total).To(Equal(1))
|
|
Expect(res.Docs[0].ID).To(Equal("doc1"))
|
|
// NoContent means no fields should be returned
|
|
Expect(res.Docs[0].Fields).To(BeEmpty())
|
|
})
|
|
|
|
It("should test SearchBuilder with NoStopWords", Label("search", "ftsearch", "builders"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_nostop").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_nostop")
|
|
|
|
client.HSet(ctx, "doc1", "txt", "hello world")
|
|
client.HSet(ctx, "doc2", "txt", "test document")
|
|
|
|
// Test that NoStopWords method can be called and search works
|
|
res, err := client.NewSearchBuilder(ctx, "idx_nostop", "hello").NoContent().NoStopWords().Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res.Total).To(Equal(1))
|
|
})
|
|
|
|
It("should test SearchBuilder with filters", Label("search", "ftsearch", "builders"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_filters").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).
|
|
Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric}).
|
|
Schema(&redis.FieldSchema{FieldName: "loc", FieldType: redis.SearchFieldTypeGeo}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_filters")
|
|
|
|
client.HSet(ctx, "doc1", "txt", "foo bar", "num", 3.141, "loc", "-0.441,51.458")
|
|
client.HSet(ctx, "doc2", "txt", "foo baz", "num", 2, "loc", "-0.1,51.2")
|
|
|
|
// Test numeric filter
|
|
res1, err := client.NewSearchBuilder(ctx, "idx_filters", "foo").
|
|
Filter("num", 2, 4).
|
|
NoContent().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res1.Total).To(Equal(2))
|
|
|
|
// Test geo filter
|
|
res2, err := client.NewSearchBuilder(ctx, "idx_filters", "foo").
|
|
GeoFilter("loc", -0.44, 51.45, 10, "km").
|
|
NoContent().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res2.Total).To(Equal(1))
|
|
})
|
|
|
|
It("should test SearchBuilder with sorting", Label("search", "ftsearch", "builders"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_sort").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).
|
|
Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_sort")
|
|
|
|
client.HSet(ctx, "doc1", "txt", "foo bar", "num", 1)
|
|
client.HSet(ctx, "doc2", "txt", "foo baz", "num", 2)
|
|
client.HSet(ctx, "doc3", "txt", "foo qux", "num", 3)
|
|
|
|
// Test ascending sort
|
|
res1, err := client.NewSearchBuilder(ctx, "idx_sort", "foo").
|
|
SortBy("num", true).
|
|
NoContent().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res1.Total).To(Equal(3))
|
|
Expect(res1.Docs[0].ID).To(Equal("doc1"))
|
|
Expect(res1.Docs[1].ID).To(Equal("doc2"))
|
|
Expect(res1.Docs[2].ID).To(Equal("doc3"))
|
|
|
|
// Test descending sort
|
|
res2, err := client.NewSearchBuilder(ctx, "idx_sort", "foo").
|
|
SortBy("num", false).
|
|
NoContent().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res2.Total).To(Equal(3))
|
|
Expect(res2.Docs[0].ID).To(Equal("doc3"))
|
|
Expect(res2.Docs[1].ID).To(Equal("doc2"))
|
|
Expect(res2.Docs[2].ID).To(Equal("doc1"))
|
|
})
|
|
|
|
It("should test SearchBuilder with InKeys and InFields", Label("search", "ftsearch", "builders"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_in").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}).
|
|
Schema(&redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_in")
|
|
|
|
client.HSet(ctx, "doc1", "title", "hello world", "body", "lorem ipsum")
|
|
client.HSet(ctx, "doc2", "title", "foo bar", "body", "hello world")
|
|
client.HSet(ctx, "doc3", "title", "baz qux", "body", "dolor sit")
|
|
|
|
// Test InKeys
|
|
res1, err := client.NewSearchBuilder(ctx, "idx_in", "hello").
|
|
InKeys("doc1", "doc2").
|
|
NoContent().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res1.Total).To(Equal(2))
|
|
|
|
// Test InFields
|
|
res2, err := client.NewSearchBuilder(ctx, "idx_in", "hello").
|
|
InFields("title").
|
|
NoContent().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res2.Total).To(Equal(1))
|
|
Expect(res2.Docs[0].ID).To(Equal("doc1"))
|
|
})
|
|
|
|
It("should test SearchBuilder with Return fields", Label("search", "ftsearch", "builders"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_return").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}).
|
|
Schema(&redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}).
|
|
Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_return")
|
|
|
|
client.HSet(ctx, "doc1", "title", "hello", "body", "world", "num", 42)
|
|
|
|
// Test ReturnFields
|
|
res1, err := client.NewSearchBuilder(ctx, "idx_return", "hello").
|
|
ReturnFields("title", "num").
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res1.Total).To(Equal(1))
|
|
Expect(res1.Docs[0].Fields).To(HaveKey("title"))
|
|
Expect(res1.Docs[0].Fields).To(HaveKey("num"))
|
|
Expect(res1.Docs[0].Fields).NotTo(HaveKey("body"))
|
|
|
|
// Test ReturnAs
|
|
res2, err := client.NewSearchBuilder(ctx, "idx_return", "hello").
|
|
ReturnAs("title", "doc_title").
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res2.Total).To(Equal(1))
|
|
Expect(res2.Docs[0].Fields).To(HaveKey("doc_title"))
|
|
Expect(res2.Docs[0].Fields).NotTo(HaveKey("title"))
|
|
})
|
|
|
|
It("should test SearchBuilder with advanced options", Label("search", "ftsearch", "builders"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_advanced").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_advanced")
|
|
|
|
client.HSet(ctx, "doc1", "description", "The quick brown fox jumps over the lazy dog")
|
|
client.HSet(ctx, "doc2", "description", "Quick alice was beginning to get very tired of sitting by her quick sister on the bank")
|
|
|
|
// Test with scores and different scorers
|
|
res1, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick").
|
|
WithScores().
|
|
Scorer("TFIDF").
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res1.Total).To(Equal(2))
|
|
for _, doc := range res1.Docs {
|
|
Expect(*doc.Score).To(BeNumerically(">", 0))
|
|
}
|
|
|
|
res2, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick").
|
|
WithScores().
|
|
Payload("test_payload").
|
|
NoContent().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res2.Total).To(Equal(2))
|
|
|
|
// Test with Slop and InOrder
|
|
res3, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick brown").
|
|
Slop(1).
|
|
InOrder().
|
|
NoContent().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res3.Total).To(Equal(1))
|
|
|
|
// Test with Language and Expander
|
|
res4, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick").
|
|
Language("english").
|
|
Expander("SYNONYM").
|
|
NoContent().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res4.Total).To(BeNumerically(">=", 0))
|
|
|
|
// Test with Timeout
|
|
res5, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick").
|
|
Timeout(1000).
|
|
NoContent().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res5.Total).To(Equal(2))
|
|
})
|
|
|
|
It("should test SearchBuilder with Params and Dialect", Label("search", "ftsearch", "builders"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_params").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_params")
|
|
|
|
client.HSet(ctx, "doc1", "name", "Alice")
|
|
client.HSet(ctx, "doc2", "name", "Bob")
|
|
client.HSet(ctx, "doc3", "name", "Carol")
|
|
|
|
// Test with single param
|
|
res1, err := client.NewSearchBuilder(ctx, "idx_params", "@name:$name").
|
|
Param("name", "Alice").
|
|
NoContent().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res1.Total).To(Equal(1))
|
|
Expect(res1.Docs[0].ID).To(Equal("doc1"))
|
|
|
|
// Test with multiple params using ParamsMap
|
|
params := map[string]interface{}{
|
|
"name1": "Bob",
|
|
"name2": "Carol",
|
|
}
|
|
res2, err := client.NewSearchBuilder(ctx, "idx_params", "@name:($name1|$name2)").
|
|
ParamsMap(params).
|
|
Dialect(2).
|
|
NoContent().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res2.Total).To(Equal(2))
|
|
})
|
|
|
|
It("should test SearchBuilder with Limit and CountOnly", Label("search", "ftsearch", "builders"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_limit").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_limit")
|
|
|
|
for i := 1; i <= 10; i++ {
|
|
client.HSet(ctx, fmt.Sprintf("doc%d", i), "txt", "test document")
|
|
}
|
|
|
|
// Test with Limit
|
|
res1, err := client.NewSearchBuilder(ctx, "idx_limit", "test").
|
|
Limit(2, 3).
|
|
NoContent().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res1.Total).To(Equal(10))
|
|
Expect(len(res1.Docs)).To(Equal(3))
|
|
|
|
// Test with CountOnly
|
|
res2, err := client.NewSearchBuilder(ctx, "idx_limit", "test").
|
|
CountOnly().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res2.Total).To(Equal(10))
|
|
Expect(len(res2.Docs)).To(Equal(0))
|
|
})
|
|
|
|
It("should test SearchBuilder with WithSortByCount and SortBy", Label("search", "ftsearch", "builders"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_payloads").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).
|
|
Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_payloads")
|
|
|
|
client.HSet(ctx, "doc1", "txt", "hello", "num", 1)
|
|
client.HSet(ctx, "doc2", "txt", "world", "num", 2)
|
|
|
|
// Test WithSortByCount and SortBy
|
|
res, err := client.NewSearchBuilder(ctx, "idx_payloads", "*").
|
|
SortBy("num", true).
|
|
WithSortByCount().
|
|
NoContent().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res.Total).To(Equal(2))
|
|
})
|
|
|
|
It("should test SearchBuilder with JSON", Label("search", "ftsearch", "builders", "json"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_json").
|
|
OnJSON().
|
|
Prefix("king:").
|
|
Schema(&redis.FieldSchema{FieldName: "$.name", FieldType: redis.SearchFieldTypeText}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_json")
|
|
|
|
client.JSONSet(ctx, "king:1", "$", `{"name": "henry"}`)
|
|
client.JSONSet(ctx, "king:2", "$", `{"name": "james"}`)
|
|
|
|
res, err := client.NewSearchBuilder(ctx, "idx_json", "henry").Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res.Total).To(Equal(1))
|
|
Expect(res.Docs[0].ID).To(Equal("king:1"))
|
|
Expect(res.Docs[0].Fields["$"]).To(Equal(`{"name":"henry"}`))
|
|
})
|
|
|
|
It("should test SearchBuilder with vector search", Label("search", "ftsearch", "builders", "vector"), func() {
|
|
hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"}
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_vector").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_vector")
|
|
|
|
client.HSet(ctx, "a", "v", "aaaaaaaa")
|
|
client.HSet(ctx, "b", "v", "aaaabaaa")
|
|
client.HSet(ctx, "c", "v", "aaaaabaa")
|
|
|
|
res, err := client.NewSearchBuilder(ctx, "idx_vector", "*=>[KNN 2 @v $vec]").
|
|
ReturnFields("__v_score").
|
|
SortBy("__v_score", true).
|
|
Dialect(2).
|
|
Param("vec", "aaaaaaaa").
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res.Docs[0].ID).To(Equal("a"))
|
|
Expect(res.Docs[0].Fields["__v_score"]).To(Equal("0"))
|
|
})
|
|
|
|
It("should test SearchBuilder with complex filtering and aggregation", Label("search", "ftsearch", "builders"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_complex").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "category", FieldType: redis.SearchFieldTypeTag}).
|
|
Schema(&redis.FieldSchema{FieldName: "price", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}).
|
|
Schema(&redis.FieldSchema{FieldName: "location", FieldType: redis.SearchFieldTypeGeo}).
|
|
Schema(&redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_complex")
|
|
|
|
client.HSet(ctx, "product1", "category", "electronics", "price", 100, "location", "-0.1,51.5", "description", "smartphone device")
|
|
client.HSet(ctx, "product2", "category", "electronics", "price", 200, "location", "-0.2,51.6", "description", "laptop computer")
|
|
client.HSet(ctx, "product3", "category", "books", "price", 20, "location", "-0.3,51.7", "description", "programming guide")
|
|
|
|
res, err := client.NewSearchBuilder(ctx, "idx_complex", "@category:{electronics} @description:(device|computer)").
|
|
Filter("price", 50, 250).
|
|
GeoFilter("location", -0.15, 51.55, 50, "km").
|
|
SortBy("price", true).
|
|
ReturnFields("category", "price", "description").
|
|
Limit(0, 10).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res.Total).To(BeNumerically(">=", 1))
|
|
|
|
res2, err := client.NewSearchBuilder(ctx, "idx_complex", "@category:{$cat} @price:[$min $max]").
|
|
ParamsMap(map[string]interface{}{
|
|
"cat": "electronics",
|
|
"min": 150,
|
|
"max": 300,
|
|
}).
|
|
Dialect(2).
|
|
WithScores().
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res2.Total).To(Equal(1))
|
|
Expect(res2.Docs[0].ID).To(Equal("product2"))
|
|
})
|
|
|
|
It("should test SearchBuilder error handling and edge cases", Label("search", "ftsearch", "builders", "edge-cases"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_edge").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_edge")
|
|
|
|
client.HSet(ctx, "doc1", "txt", "hello world")
|
|
|
|
// Test empty query
|
|
res1, err := client.NewSearchBuilder(ctx, "idx_edge", "*").NoContent().Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res1.Total).To(Equal(1))
|
|
|
|
// Test query with no results
|
|
res2, err := client.NewSearchBuilder(ctx, "idx_edge", "nonexistent").NoContent().Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res2.Total).To(Equal(0))
|
|
|
|
// Test with multiple chained methods
|
|
res3, err := client.NewSearchBuilder(ctx, "idx_edge", "hello").
|
|
WithScores().
|
|
NoContent().
|
|
Verbatim().
|
|
InOrder().
|
|
Slop(0).
|
|
Timeout(5000).
|
|
Language("english").
|
|
Dialect(2).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res3.Total).To(Equal(1))
|
|
})
|
|
|
|
It("should test SearchBuilder method chaining", Label("search", "ftsearch", "builders", "fluent"), func() {
|
|
createVal, err := client.NewCreateIndexBuilder(ctx, "idx_fluent").
|
|
OnHash().
|
|
Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}).
|
|
Schema(&redis.FieldSchema{FieldName: "tags", FieldType: redis.SearchFieldTypeTag}).
|
|
Schema(&redis.FieldSchema{FieldName: "score", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}).
|
|
Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(createVal).To(Equal("OK"))
|
|
WaitForIndexing(client, "idx_fluent")
|
|
|
|
client.HSet(ctx, "doc1", "title", "Redis Search Tutorial", "tags", "redis,search,tutorial", "score", 95)
|
|
client.HSet(ctx, "doc2", "title", "Advanced Redis", "tags", "redis,advanced", "score", 88)
|
|
|
|
builder := client.NewSearchBuilder(ctx, "idx_fluent", "@title:(redis) @tags:{search}")
|
|
result := builder.
|
|
WithScores().
|
|
Filter("score", 90, 100).
|
|
SortBy("score", false).
|
|
ReturnFields("title", "score").
|
|
Limit(0, 5).
|
|
Dialect(2).
|
|
Timeout(1000).
|
|
Language("english")
|
|
|
|
res, err := result.Run()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(res.Total).To(Equal(1))
|
|
Expect(res.Docs[0].ID).To(Equal("doc1"))
|
|
Expect(res.Docs[0].Fields["title"]).To(Equal("Redis Search Tutorial"))
|
|
Expect(*res.Docs[0].Score).To(BeNumerically(">", 0))
|
|
})
|
|
})
|