Skip to content

Commit

Permalink
Added paging to searchterms DSL
Browse files Browse the repository at this point in the history
  • Loading branch information
warmans committed Aug 1, 2024
1 parent 6e97d34 commit e403588
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 13 deletions.
2 changes: 1 addition & 1 deletion cmd/search/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func NewSearchCommand(logger *slog.Logger) *cobra.Command {
return fmt.Errorf("failed to open index: %w", err)
}
searcher := search.NewBlugeSearch(reader)
res, err := searcher.Search(context.Background(), searchterms.MustParse(args[0]), 1)
res, err := searcher.Search(context.Background(), searchterms.MustParse(args[0]))
if err != nil {
return fmt.Errorf("search failed: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/discord/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func NewBot(
Options: []*discordgo.ApplicationCommandOption{
{
Name: "query",
Description: `Enter a partial quote. Phrase match with "double quotes". Filter with ~publication #s1e01 +10m30s`,
Description: `Phrase match with "quotes". Page with >N e.g. >10. Filter with ~publication #s1e01 +10m30s`,
Type: discordgo.ApplicationCommandOptionString,
Required: true,
Autocomplete: true,
Expand Down Expand Up @@ -406,7 +406,7 @@ func (b *Bot) queryBegin(s *discordgo.Session, i *discordgo.InteractionCreate) {
return
}

res, err := b.searcher.Search(context.Background(), terms, 0)
res, err := b.searcher.Search(context.Background(), terms)
if err != nil {
b.logger.Error("Failed to fetch autocomplete options", slog.String("err", err.Error()))
return
Expand Down
15 changes: 10 additions & 5 deletions pkg/search/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const (
)

type Searcher interface {
Search(ctx context.Context, f []searchterms.Term, page int32) ([]model.DialogDocument, error)
Search(ctx context.Context, f []searchterms.Term) ([]model.DialogDocument, error)
Get(ctx context.Context, id string) (*model.DialogDocument, error)
ListTerms(ctx context.Context, field string) ([]string, error)
}
Expand All @@ -32,7 +32,7 @@ type BlugeSearch struct {
}

func (b *BlugeSearch) Get(ctx context.Context, id string) (*model.DialogDocument, error) {
q, err := bluge_query.NewBlugeQuery([]searchterms.Term{{Field: "_id", Value: searchterms.String(id), Op: searchterms.CompOpEq}})
q, _, err := bluge_query.NewBlugeQuery([]searchterms.Term{{Field: "_id", Value: searchterms.String(id), Op: searchterms.CompOpEq}})
if err != nil {
return nil, fmt.Errorf("filter was invalid: %w", err)
}
Expand All @@ -50,14 +50,19 @@ func (b *BlugeSearch) Get(ctx context.Context, id string) (*model.DialogDocument
return scanDocument(match)
}

func (b *BlugeSearch) Search(ctx context.Context, f []searchterms.Term, page int32) ([]model.DialogDocument, error) {
func (b *BlugeSearch) Search(ctx context.Context, f []searchterms.Term) ([]model.DialogDocument, error) {

query, err := bluge_query.NewBlugeQuery(f)
query, offset, err := bluge_query.NewBlugeQuery(f)
if err != nil {
return nil, err
}

req := bluge.NewTopNSearch(PageSize, query).SetFrom(PageSize * int(page))
setFrom := 0
if offset != nil {
setFrom = int(*offset)
}

req := bluge.NewTopNSearch(PageSize, query).SetFrom(setFrom)

dmi, err := b.index.Search(ctx, req)
if err != nil {
Expand Down
32 changes: 28 additions & 4 deletions pkg/searchterms/bluge_query/bluge.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,43 @@ import (
"github.com/warmans/tvgif/pkg/search/mapping"
"github.com/warmans/tvgif/pkg/search/model"
"github.com/warmans/tvgif/pkg/searchterms"
"github.com/warmans/tvgif/pkg/util"
"math"
"slices"
"strings"
"time"
)

func NewBlugeQuery(terms []searchterms.Term) (bluge.Query, error) {
func extractOffset(terms []searchterms.Term) ([]searchterms.Term, *int64) {
offsetIdx := slices.IndexFunc(terms, func(val searchterms.Term) bool {
return val.Field == "offset"
})
if offsetIdx == -1 {
return terms, nil
}
var offset *int64
if offsetIdx >= 0 {
if offsetVal := terms[offsetIdx].Value.Value().(int64); offsetVal >= 0 {
offset = util.ToPtr(offsetVal)
}
terms = append(terms[:offsetIdx], terms[offsetIdx+1:]...)
}
return terms, offset
}

func NewBlugeQuery(terms []searchterms.Term) (bluge.Query, *int64, error) {

// the paging/offset is included in the filter string but is not a filter so it needs to be
// extracted.
filteredTerms, offset := extractOffset(terms)

q := &BlugeQuery{q: bluge.NewBooleanQuery()}
for _, v := range terms {
for _, v := range filteredTerms {
if err := q.And(v); err != nil {
return nil, err
return nil, nil, err
}
}
return q.q, nil
return q.q, offset, nil
}

type BlugeQuery struct {
Expand Down
105 changes: 105 additions & 0 deletions pkg/searchterms/bluge_query/bluge_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package bluge_query

import (
"github.com/warmans/tvgif/pkg/searchterms"
"github.com/warmans/tvgif/pkg/util"
"reflect"
"testing"
)

func Test_extractOffset(t *testing.T) {
tests := []struct {
name string
terms []searchterms.Term
want []searchterms.Term
want1 *int64
}{
{
name: "empty terms returns empty, nil",
terms: make([]searchterms.Term, 0),
want: make([]searchterms.Term, 0),
want1: nil,
},
{
name: "no offset returns original terms",
terms: []searchterms.Term{
{Field: "actor", Value: searchterms.String("steve"), Op: searchterms.CompOpEq},
{Field: "publication", Value: searchterms.String("xfm"), Op: searchterms.CompOpEq},
{Field: "series", Value: searchterms.Int(1), Op: searchterms.CompOpEq},
},
want: []searchterms.Term{
{Field: "actor", Value: searchterms.String("steve"), Op: searchterms.CompOpEq},
{Field: "publication", Value: searchterms.String("xfm"), Op: searchterms.CompOpEq},
{Field: "series", Value: searchterms.Int(1), Op: searchterms.CompOpEq},
},
want1: nil,
}, {
name: "no offset returns original terms",
terms: []searchterms.Term{
{Field: "actor", Value: searchterms.String("steve"), Op: searchterms.CompOpEq},
{Field: "publication", Value: searchterms.String("xfm"), Op: searchterms.CompOpEq},
{Field: "series", Value: searchterms.Int(1), Op: searchterms.CompOpEq},
},
want: []searchterms.Term{
{Field: "actor", Value: searchterms.String("steve"), Op: searchterms.CompOpEq},
{Field: "publication", Value: searchterms.String("xfm"), Op: searchterms.CompOpEq},
{Field: "series", Value: searchterms.Int(1), Op: searchterms.CompOpEq},
},
want1: nil,
}, {
name: "offset is extracted from last position",
terms: []searchterms.Term{
{Field: "actor", Value: searchterms.String("steve"), Op: searchterms.CompOpEq},
{Field: "publication", Value: searchterms.String("xfm"), Op: searchterms.CompOpEq},
{Field: "offset", Value: searchterms.Int(10), Op: searchterms.CompOpEq},
},
want: []searchterms.Term{
{Field: "actor", Value: searchterms.String("steve"), Op: searchterms.CompOpEq},
{Field: "publication", Value: searchterms.String("xfm"), Op: searchterms.CompOpEq},
},
want1: util.ToPtr(int64(10)),
}, {
name: "offset is extracted from first position",
terms: []searchterms.Term{
{Field: "offset", Value: searchterms.Int(10), Op: searchterms.CompOpEq},
{Field: "actor", Value: searchterms.String("steve"), Op: searchterms.CompOpEq},
{Field: "publication", Value: searchterms.String("xfm"), Op: searchterms.CompOpEq},
},
want: []searchterms.Term{
{Field: "actor", Value: searchterms.String("steve"), Op: searchterms.CompOpEq},
{Field: "publication", Value: searchterms.String("xfm"), Op: searchterms.CompOpEq},
},
want1: util.ToPtr(int64(10)),
}, {
name: "offset is extracted from middle position",
terms: []searchterms.Term{
{Field: "actor", Value: searchterms.String("steve"), Op: searchterms.CompOpEq},
{Field: "offset", Value: searchterms.Int(10), Op: searchterms.CompOpEq},
{Field: "publication", Value: searchterms.String("xfm"), Op: searchterms.CompOpEq},
},
want: []searchterms.Term{
{Field: "actor", Value: searchterms.String("steve"), Op: searchterms.CompOpEq},
{Field: "publication", Value: searchterms.String("xfm"), Op: searchterms.CompOpEq},
},
want1: util.ToPtr(int64(10)),
}, {
name: "offset is only filter",
terms: []searchterms.Term{
{Field: "offset", Value: searchterms.Int(10), Op: searchterms.CompOpEq},
},
want: []searchterms.Term{},
want1: util.ToPtr(int64(10)),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1 := extractOffset(tt.terms)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("extractOffset() got = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got1, tt.want1) {
t.Errorf("extractOffset() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
14 changes: 14 additions & 0 deletions pkg/searchterms/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,20 @@ func (p *parser) parseInner() ([]*Term, error) {
Value: Duration(ts),
Op: CompOpGe,
}}, nil
case tagOffset:
offsetText, err := p.requireNext(tagInt, tagEOF)
if err != nil {
return nil, err
}
intVal, err := strconv.ParseInt(offsetText.lexeme, 10, 64)
if err != nil {
return nil, fmt.Errorf("offset was not a number: %w", err)
}
return []*Term{{
Field: "offset",
Value: Int(intVal),
Op: CompOpEq,
}}, nil
default:
return nil, errors.Errorf("unexpected token '%s'", tok)
}
Expand Down
10 changes: 9 additions & 1 deletion pkg/searchterms/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,24 @@ func TestMustParse(t *testing.T) {
{Field: "start_timestamp", Value: Duration(time.Minute*10 + time.Second*30), Op: CompOpGe},
},
},
{
name: "parse offset",
args: args{s: `>20`},
want: []Term{
{Field: "offset", Value: Int(20), Op: CompOpEq},
},
},
{
name: "parse all",
args: args{s: `@steve ~xfm #s1 +30m "man alive" karl`},
args: args{s: `@steve ~xfm #s1 +30m "man alive" karl >10`},
want: []Term{
{Field: "actor", Value: String("steve"), Op: CompOpEq},
{Field: "publication", Value: String("xfm"), Op: CompOpEq},
{Field: "series", Value: Int(1), Op: CompOpEq},
{Field: "start_timestamp", Value: Duration(time.Minute * 30), Op: CompOpGe},
{Field: "content", Value: String("man alive"), Op: CompOpEq},
{Field: "content", Value: String("karl"), Op: CompOpFuzzyLike},
{Field: "offset", Value: Int(10), Op: CompOpEq},
},
},
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/searchterms/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
tagPublication = "~"
tagId = "#"
tagTimestamp = "+"
tagOffset = ">"

tagQuotedString = "QUOTED_STRING"
tagWord = "WORD"
Expand Down Expand Up @@ -82,6 +83,8 @@ func (s *scanner) next() (token, error) {
return s.emit(tagId), nil
case '+':
return s.emit(tagTimestamp), nil
case '>':
return s.emit(tagOffset), nil
case '"':
return s.scanString()
default:
Expand Down
8 changes: 8 additions & 0 deletions pkg/searchterms/scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ func TestScan(t *testing.T) {
want: []token{{tag: tagTimestamp, lexeme: "+"}, {tag: tagInt, lexeme: "10"}, {tag: tagWord, lexeme: "m"}, {tag: tagEOF}},
wantErr: false,
},
{
name: "scan offset",
args: args{
str: `>10`,
},
want: []token{{tag: tagOffset, lexeme: ">"}, {tag: tagInt, lexeme: "10"}, {tag: tagEOF}},
wantErr: false,
},
{
name: "scan everything",
args: args{
Expand Down

0 comments on commit e403588

Please sign in to comment.