From 36f0bd1a12a5bd3049953639ae0af9b70d64bd2a Mon Sep 17 00:00:00 2001 From: Andrei Cheboksarov <37665782+cheb0@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:10:46 +0400 Subject: [PATCH 1/2] wildcard suffix query dedicated path --- frac/active_token_list.go | 15 +++++++++++ frac/sealed/token/block_loader.go | 43 +++++++++++++++++++++++++++++++ frac/sealed/token/provider.go | 14 ++++++++++ pattern/pattern.go | 16 ++++++++++++ pattern/pattern_test.go | 36 ++++++++++++++++++++++++++ 5 files changed, 124 insertions(+) diff --git a/frac/active_token_list.go b/frac/active_token_list.go index c6867417..a37eb4d2 100644 --- a/frac/active_token_list.go +++ b/frac/active_token_list.go @@ -53,6 +53,21 @@ func (tp *activeTokenProvider) FindContains(needle []byte) ([]uint32, error) { return tids, nil } +// FindSuffix finds tids of tokens which end with a provided suffix. +func (tp *activeTokenProvider) FindSuffix(suffix []byte) ([]uint32, error) { + if len(suffix) == 0 { + return nil, nil + } + var tids []uint32 + for tid := tp.FirstTID(); tid <= tp.LastTID(); tid++ { + token := tp.GetToken(tid) + if len(token) >= len(suffix) && bytes.Equal(token[len(token)-len(suffix):], suffix) { + tids = append(tids, tid) + } + } + return tids, nil +} + // FindToken finds tids of tokens which suffice a provided searcher (predicate). func (tp *activeTokenProvider) FindToken(searcher pattern.Searcher) ([]uint32, error) { firstTID := searcher.FirstTID() diff --git a/frac/sealed/token/block_loader.go b/frac/sealed/token/block_loader.go index aaf52121..e316f3e1 100644 --- a/frac/sealed/token/block_loader.go +++ b/frac/sealed/token/block_loader.go @@ -262,6 +262,49 @@ func (b *Block) containsV5(from, to int, needle []byte) ([]int, error) { return indexes, nil } +func (b *Block) suffix(from, to int, suffix []byte) ([]int, error) { + if b.FracVer >= config.BinaryDataV6 { + return b.suffixV6(from, to, suffix), nil + } + + return b.suffixV5(from, to, suffix), nil +} + +// TODO(cheb0) delete this when Block.FracVer is deleted +func (b *Block) suffixV5(from, to int, suffix []byte) []int { + indexes := make([]int, 0) + suffixLen := len(suffix) + + for i := from; i <= to; i++ { + token := b.GetToken(i) + if len(token) >= suffixLen && bytes.Equal(token[len(token)-suffixLen:], suffix) { + indexes = append(indexes, i) + } + } + + return indexes +} + +func (b *Block) suffixV6(from int, to int, suffix []byte) []int { + indexes := make([]int, 0) + suffixLen := uint32(len(suffix)) + + offsets := b.Offsets[from : to+2] + + for i := 1; i < len(offsets); i++ { + endPos := offsets[i] + tokLen := endPos - offsets[i-1] + if tokLen >= suffixLen { + tokSuffix := b.Payload[endPos-suffixLen : endPos] + if bytes.Equal(tokSuffix, suffix) { + indexes = append(indexes, from+i-1) + } + } + } + + return indexes +} + func (b *Block) find(from, to int, searcher pattern.Searcher) ([]int, error) { indexes := make([]int, 0) for i := from; i <= to; i++ { diff --git a/frac/sealed/token/provider.go b/frac/sealed/token/provider.go index d3428c51..561f5f6c 100644 --- a/frac/sealed/token/provider.go +++ b/frac/sealed/token/provider.go @@ -74,6 +74,20 @@ func (tp *Provider) FindContains(needle []byte) ([]uint32, error) { }) } +func (tp *Provider) FindSuffix(suffix []byte) ([]uint32, error) { + requiredLetters := util.NewLettersBitset(suffix) + + return tp.findInBlocks( + tp.FirstTID(), + tp.LastTID(), + func(e *TableEntry) bool { + return e.Letters.IsNil() || e.Letters.ContainsAll(requiredLetters) + }, + func(b *Block, firstIndex, lastIndex int) ([]int, error) { + return b.suffix(firstIndex, lastIndex, suffix) + }) +} + func (tp *Provider) FindToken(searcher pattern.Searcher) ([]uint32, error) { return tp.findInBlocks( searcher.FirstTID(), diff --git a/pattern/pattern.go b/pattern/pattern.go index 504d3929..ebdf5259 100644 --- a/pattern/pattern.go +++ b/pattern/pattern.go @@ -18,6 +18,7 @@ import ( type tokenProvider interface { GetToken(uint32) []byte FindContains(needle []byte) ([]uint32, error) + FindSuffix(suffix []byte) ([]uint32, error) FindToken(searcher Searcher) ([]uint32, error) FirstTID() uint32 LastTID() uint32 @@ -424,6 +425,18 @@ func isSimpleWildcardContains(token parser.Token) (needle []byte, ok bool) { return []byte(lit.Terms[1].Data), true } +// isSimpleWildcardSuffix checks if this AST token is simple wildcard like '*abc' +func isSimpleWildcardSuffix(token parser.Token) (suffix []byte, ok bool) { + lit, ok := token.(*parser.Literal) + if !ok || len(lit.Terms) != 2 { + return nil, false + } + if !lit.Terms[0].IsWildcard() || lit.Terms[1].Kind != parser.TermText { + return nil, false + } + return []byte(lit.Terms[1].Data), true +} + func Search(ctx context.Context, t parser.Token, tp tokenProvider) ([]uint32, error) { if util.IsCancelled(ctx) { return nil, ctx.Err() @@ -431,6 +444,9 @@ func Search(ctx context.Context, t parser.Token, tp tokenProvider) ([]uint32, er if needle, ok := isSimpleWildcardContains(t); ok { return tp.FindContains(needle) } + if suffix, ok := isSimpleWildcardSuffix(t); ok { + return tp.FindSuffix(suffix) + } s := newSearcher(t, tp) return tp.FindToken(s) } diff --git a/pattern/pattern_test.go b/pattern/pattern_test.go index f89766e9..3cdf9f56 100644 --- a/pattern/pattern_test.go +++ b/pattern/pattern_test.go @@ -113,6 +113,20 @@ func (tp *simpleTokenProvider) FindContains(needle []byte) ([]uint32, error) { return tids, nil } +func (tp *simpleTokenProvider) FindSuffix(suffix []byte) ([]uint32, error) { + if len(suffix) == 0 { + return nil, nil + } + var tids []uint32 + for t := tp.FirstTID(); t <= tp.LastTID(); t++ { + token := tp.GetToken(t) + if len(token) >= len(suffix) && bytes.Equal(token[len(token)-len(suffix):], suffix) { + tids = append(tids, t) + } + } + return tids, nil +} + func (tp *simpleTokenProvider) FindToken(searcher Searcher) ([]uint32, error) { firstTID := searcher.FirstTID() lastTID := searcher.LastTID() @@ -335,6 +349,28 @@ func TestPatternSuffix2(t *testing.T) { testAll(t, tp, tests) } +func TestPatternSuffixOnly(t *testing.T) { + tp := newTestTokenProvider([]string{ + "abc", + "xabc", + "xyabc", + "xyzabc", + "abcx", + "xabcx", + "notabc", + "nothing", + }) + + tests := []testCase{ + {"*abc", []string{"abc", "notabc", "xabc", "xyabc", "xyzabc"}}, + {"*x", []string{"abcx", "xabcx"}}, + {"*ng", []string{"nothing"}}, + {"*g", []string{"nothing"}}, + } + + testAll(t, tp, tests) +} + func TestPatternMiddle(t *testing.T) { tp := newTestTokenProvider([]string{ "a:b:a", From 0d3e5e6942bca52703408c5d6df3d94f4916837d Mon Sep 17 00:00:00 2001 From: Andrei Cheboksarov <37665782+cheb0@users.noreply.github.com> Date: Thu, 13 Aug 2026 08:59:19 +0400 Subject: [PATCH 2/2] linter fixes --- frac/sealed/token/block_loader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frac/sealed/token/block_loader.go b/frac/sealed/token/block_loader.go index e316f3e1..24345f7d 100644 --- a/frac/sealed/token/block_loader.go +++ b/frac/sealed/token/block_loader.go @@ -285,7 +285,7 @@ func (b *Block) suffixV5(from, to int, suffix []byte) []int { return indexes } -func (b *Block) suffixV6(from int, to int, suffix []byte) []int { +func (b *Block) suffixV6(from, to int, suffix []byte) []int { indexes := make([]int, 0) suffixLen := uint32(len(suffix))