mirror of
https://github.com/redis/go-redis.git
synced 2026-10-28 08:24:54 +08:00
* feat(search): add FT.ALIASLIST support (Redis 8.10)
- FTAliasList(ctx, index) returns the aliases of an index as an
unordered, server-deduplicated collection; an alias-free index
yields an empty result, not an error
- server errors (SEARCH_INDEX_NOT_FOUND, NOPERM) propagate unchanged;
the argument must be an index name, aliases are not resolved
- wire-format unit test plus a RESP2/RESP3 integration test that gates
on the live server supporting the command (the REDIS_VERSION float
cannot tell 8.10 from 8.1)
- runnable example under example/ft-aliaslist
* fix(search): route FT.ALIASLIST as keyless in cluster
The static FT policy table covers aliasadd/aliasupdate/aliasdel but
not aliaslist, so the default cluster resolver returned nil and a cold
command-info cache fell back to treating the index name as a key.
Register it keyless and read-only like the other alias commands.
* test(search): version-gate FT.ALIASLIST specs
Replace the standalone probe test (unknown-command sniffing plus the
REDIS_ALIASLIST_TEST_ADDR env var) with Ginkgo specs in the main suite,
gated on SkipBeforeRedisVersion("8.10"), which compares major.minor as
integers and so distinguishes 8.10 from 8.1. The specs cover RESP2 and
RESP3 and follow the suite conventions (FlushDB per spec, :6379).
* chore(deps): bump grpc to v1.82.1 in otel examples
govulncheck fails every PR on GO-2026-6061 in google.golang.org/grpc
v1.80.0 (indirect via the OTLP exporters); v1.82.1 carries the fix.
* fix(search): route policy-table keyless commands on cold cache
ClusterClient.process computes the slot before routeAndRun consults
the policy table, and with a cold command-info cache
cmdFirstKeyPosWithInfo fell back to treating argument 1 as a key — so
FT.ALIASLIST (and the other keyless FT commands) hashed the index name
into a slot for the first calls of a process. Consult the static
policy table there: entries with default request routing and a keyless
response policy resolve to keyless. Key-carrying (ft.suglen) and
specially-routed (ft.cursor) commands keep the old fallback.
---------
Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com>
103 lines
3.2 KiB
Go
103 lines
3.2 KiB
Go
package redis_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
. "github.com/bsm/ginkgo/v2"
|
|
. "github.com/bsm/gomega"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
// FT.ALIASLIST ships in Redis 8.10 (Query Engine 8.10.0). The specs gate on
|
|
// SkipBeforeRedisVersion, which compares major.minor as integers, and run
|
|
// under both RESP2 and RESP3 against the suite's standard server.
|
|
var _ = Describe("FT.ALIASLIST", Label("search", "ftaliaslist"), func() {
|
|
ctx := context.TODO()
|
|
|
|
for _, protocol := range []int{2, 3} {
|
|
protocol := protocol
|
|
|
|
Describe(fmt.Sprintf("RESP%d", protocol), func() {
|
|
var client *redis.Client
|
|
|
|
index := fmt.Sprintf("aliaslist-test-idx-resp%d", protocol)
|
|
aliases := []string{
|
|
fmt.Sprintf("aliaslist-test-a1-resp%d", protocol),
|
|
fmt.Sprintf("aliaslist-test-a2-resp%d", protocol),
|
|
}
|
|
|
|
createIndex := func() {
|
|
Expect(client.FTCreate(ctx, index, &redis.FTCreateOptions{
|
|
OnHash: true,
|
|
Prefix: []interface{}{fmt.Sprintf("aliaslist-test-doc-resp%d:", protocol)},
|
|
}, &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}).Err()).NotTo(HaveOccurred())
|
|
}
|
|
|
|
BeforeEach(func() {
|
|
SkipBeforeRedisVersion("8.10", "FT.ALIASLIST requires Redis 8.10+")
|
|
client = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: protocol})
|
|
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
|
|
})
|
|
|
|
AfterEach(func() {
|
|
if client == nil {
|
|
return
|
|
}
|
|
// Aliases are global, not per-database; FLUSHDB alone does not
|
|
// remove them unless their index is dropped with them.
|
|
for _, a := range aliases {
|
|
_ = client.FTAliasDel(ctx, a).Err()
|
|
}
|
|
_ = client.FTDropIndex(ctx, index).Err()
|
|
Expect(client.Close()).NotTo(HaveOccurred())
|
|
client = nil
|
|
})
|
|
|
|
It("returns an empty collection for an index with no aliases", func() {
|
|
createIndex()
|
|
|
|
// An existing index with no aliases yields an empty
|
|
// collection, not an error.
|
|
got, err := client.FTAliasList(ctx, index).Result()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(got).To(BeEmpty())
|
|
})
|
|
|
|
It("lists all aliases associated with the index", func() {
|
|
createIndex()
|
|
|
|
for _, a := range aliases {
|
|
Expect(client.FTAliasAdd(ctx, index, a).Err()).NotTo(HaveOccurred())
|
|
}
|
|
|
|
// The reply is an unordered collection; do not assert order.
|
|
got, err := client.FTAliasList(ctx, index).Result()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(got).To(ConsistOf(aliases[0], aliases[1]))
|
|
})
|
|
|
|
It("propagates the server index-not-found error for a missing index", func() {
|
|
err := client.FTAliasList(ctx, fmt.Sprintf("aliaslist-test-missing-resp%d", protocol)).Err()
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(strings.ToLower(err.Error())).To(ContainSubstring("index not found"))
|
|
})
|
|
|
|
It("does not resolve an alias passed in place of the index", func() {
|
|
createIndex()
|
|
|
|
Expect(client.FTAliasAdd(ctx, index, aliases[0]).Err()).NotTo(HaveOccurred())
|
|
|
|
// The argument must be the index name; the server does not
|
|
// resolve aliases here and replies with the same
|
|
// index-not-found error a missing index produces.
|
|
err := client.FTAliasList(ctx, aliases[0]).Err()
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(strings.ToLower(err.Error())).To(ContainSubstring("index not found"))
|
|
})
|
|
})
|
|
}
|
|
})
|