Initial QSfera import
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
// Package bleve provides the ability to work with bleve queries.
|
||||
package bleve
|
||||
|
||||
import (
|
||||
bQuery "github.com/blevesearch/bleve/v2/search/query"
|
||||
|
||||
"github.com/qsfera/server/pkg/kql"
|
||||
"github.com/qsfera/server/services/search/pkg/query"
|
||||
)
|
||||
|
||||
// Creator is combines a Builder and a Compiler which is used to Create the query.
|
||||
type Creator[T any] struct {
|
||||
builder query.Builder
|
||||
compiler query.Compiler[T]
|
||||
}
|
||||
|
||||
// Create implements the Creator interface
|
||||
func (c Creator[T]) Create(qs string) (T, error) {
|
||||
var t T
|
||||
builderAst, err := c.builder.Build(qs)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
|
||||
t, err = c.compiler.Compile(builderAst)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// DefaultCreator exposes a kql to bleve query creator.
|
||||
var DefaultCreator = Creator[bQuery.Query]{kql.Builder{}, Compiler{}}
|
||||
@@ -0,0 +1,365 @@
|
||||
package bleve
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
bleveQuery "github.com/blevesearch/bleve/v2/search/query"
|
||||
"github.com/qsfera/server/pkg/ast"
|
||||
"github.com/qsfera/server/pkg/kql"
|
||||
)
|
||||
|
||||
// lowercaseFields lists the bleve fields whose index mapping uses a
|
||||
// lowercasing analyzer. Values bound to these fields are pre-lowercased
|
||||
// so query-side matching stays consistent with the index.
|
||||
// Keep in sync with services/search/pkg/bleve/index.go NewMapping.
|
||||
var lowercaseFields = map[string]struct{}{
|
||||
"Name": {},
|
||||
"Tags": {},
|
||||
"Favorites": {},
|
||||
"Content": {},
|
||||
}
|
||||
|
||||
var _fields = map[string]string{
|
||||
"rootid": "RootID",
|
||||
"path": "Path",
|
||||
"id": "ID",
|
||||
"name": "Name",
|
||||
"size": "Size",
|
||||
"mtime": "Mtime",
|
||||
"mediatype": "MimeType",
|
||||
"type": "Type",
|
||||
"tag": "Tags",
|
||||
"tags": "Tags",
|
||||
"content": "Content",
|
||||
"hidden": "Hidden",
|
||||
"favorite": "Favorites",
|
||||
}
|
||||
|
||||
// The following quoted string enumerates the characters which may be escaped: "+-=&|><!(){}[]^\"~*?:\\/ "
|
||||
// based on bleve docs https://blevesearch.com/docs/Query-String-Query/
|
||||
// Wildcards * and ? are excluded
|
||||
var bleveEscaper = strings.NewReplacer(
|
||||
`+`, `\+`,
|
||||
`-`, `\-`,
|
||||
`=`, `\=`,
|
||||
`&`, `\&`,
|
||||
`|`, `\|`,
|
||||
`>`, `\>`,
|
||||
`<`, `\<`,
|
||||
`!`, `\!`,
|
||||
`(`, `\(`,
|
||||
`)`, `\)`,
|
||||
`{`, `\{`,
|
||||
`}`, `\}`,
|
||||
`{`, `\}`,
|
||||
`[`, `\[`,
|
||||
`]`, `\]`,
|
||||
`^`, `\^`,
|
||||
`"`, `\"`,
|
||||
`~`, `\~`,
|
||||
`:`, `\:`,
|
||||
`\`, `\\`,
|
||||
`/`, `\/`,
|
||||
` `, `\ `,
|
||||
)
|
||||
|
||||
// Compiler represents a KQL query search string to the bleve query formatter.
|
||||
type Compiler struct{}
|
||||
|
||||
// Compile implements the query formatter which converts the KQL query search string to the bleve query.
|
||||
func (c Compiler) Compile(givenAst *ast.Ast) (bleveQuery.Query, error) {
|
||||
q, err := compile(givenAst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func compile(a *ast.Ast) (bleveQuery.Query, error) {
|
||||
q, _, err := walk(0, a.Nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch q.(type) {
|
||||
case *bleveQuery.ConjunctionQuery, *bleveQuery.DisjunctionQuery:
|
||||
return q, nil
|
||||
}
|
||||
return bleve.NewConjunctionQuery(q), nil
|
||||
}
|
||||
|
||||
func walk(offset int, nodes []ast.Node) (bleveQuery.Query, int, error) {
|
||||
var prev, next bleveQuery.Query
|
||||
var operator *ast.OperatorNode
|
||||
var isGroup bool
|
||||
for i := offset; i < len(nodes); i++ {
|
||||
switch n := nodes[i].(type) {
|
||||
case *ast.StringNode:
|
||||
k := getField(n.Key)
|
||||
v := n.Value
|
||||
if k != "ID" && k != "Size" {
|
||||
v = bleveEscaper.Replace(n.Value)
|
||||
}
|
||||
|
||||
if _, ok := lowercaseFields[k]; ok {
|
||||
v = strings.ToLower(v)
|
||||
}
|
||||
|
||||
var q bleveQuery.Query
|
||||
var group bool
|
||||
switch k {
|
||||
case "MimeType":
|
||||
q, group = mimeType(k, v)
|
||||
if prev == nil {
|
||||
isGroup = group
|
||||
}
|
||||
default:
|
||||
q = bleveQuery.NewQueryStringQuery(k + ":" + v)
|
||||
}
|
||||
|
||||
if prev == nil {
|
||||
prev = q
|
||||
} else {
|
||||
next = q
|
||||
}
|
||||
case *ast.DateTimeNode:
|
||||
q := &bleveQuery.DateRangeQuery{
|
||||
Start: bleveQuery.BleveQueryTime{},
|
||||
End: bleveQuery.BleveQueryTime{},
|
||||
InclusiveStart: nil,
|
||||
InclusiveEnd: nil,
|
||||
FieldVal: getField(n.Key),
|
||||
}
|
||||
|
||||
if n.Operator == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch n.Operator.Value {
|
||||
case ">":
|
||||
q.Start.Time = n.Value
|
||||
q.InclusiveStart = &[]bool{false}[0]
|
||||
case ">=":
|
||||
q.Start.Time = n.Value
|
||||
q.InclusiveStart = &[]bool{true}[0]
|
||||
case "<":
|
||||
q.End.Time = n.Value
|
||||
q.InclusiveEnd = &[]bool{false}[0]
|
||||
case "<=":
|
||||
q.End.Time = n.Value
|
||||
q.InclusiveEnd = &[]bool{true}[0]
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if prev == nil {
|
||||
prev = q
|
||||
} else {
|
||||
next = q
|
||||
}
|
||||
case *ast.BooleanNode:
|
||||
q := bleveQuery.NewQueryStringQuery(getField(n.Key) + fmt.Sprintf(":%v", n.Value))
|
||||
if prev == nil {
|
||||
prev = q
|
||||
} else {
|
||||
next = q
|
||||
}
|
||||
case *ast.GroupNode:
|
||||
if n.Key != "" {
|
||||
n = normalizeGroupingProperty(n)
|
||||
}
|
||||
q, _, err := walk(0, n.Nodes)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if prev == nil {
|
||||
prev = q
|
||||
isGroup = true
|
||||
} else {
|
||||
next = q
|
||||
}
|
||||
case *ast.OperatorNode:
|
||||
if n.Value == kql.BoolAND || n.Value == kql.BoolOR {
|
||||
operator = n
|
||||
} else if n.Value == kql.BoolNOT {
|
||||
var err error
|
||||
next, offset, err = nextNode(i+1, nodes)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
q := bleve.NewBooleanQuery()
|
||||
q.AddMustNot(next)
|
||||
if prev == nil {
|
||||
// unary in the beginning
|
||||
prev = q
|
||||
} else {
|
||||
next = q
|
||||
}
|
||||
}
|
||||
}
|
||||
if prev != nil && next != nil && operator != nil {
|
||||
prev = mapBinary(operator, prev, next, isGroup)
|
||||
isGroup = false
|
||||
operator = nil
|
||||
next = nil
|
||||
}
|
||||
if i < offset {
|
||||
i = offset
|
||||
}
|
||||
}
|
||||
if prev == nil {
|
||||
return nil, 0, fmt.Errorf("can not compile the query")
|
||||
}
|
||||
return prev, offset, nil
|
||||
}
|
||||
|
||||
func nextNode(offset int, nodes []ast.Node) (bleveQuery.Query, int, error) {
|
||||
if n, ok := nodes[offset].(*ast.GroupNode); ok {
|
||||
gq, _, err := walk(0, n.Nodes)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return gq, offset + 1, nil
|
||||
}
|
||||
if n, ok := nodes[offset].(*ast.OperatorNode); ok {
|
||||
if n.Value == kql.BoolNOT {
|
||||
return walk(offset, nodes)
|
||||
}
|
||||
}
|
||||
one := nodes[:offset+1]
|
||||
return walk(offset, one)
|
||||
}
|
||||
|
||||
func mapBinary(operator *ast.OperatorNode, ln, rn bleveQuery.Query, leftIsGroup bool) bleveQuery.Query {
|
||||
if operator.Value == kql.BoolOR {
|
||||
right, ok := rn.(*bleveQuery.DisjunctionQuery)
|
||||
switch left := ln.(type) {
|
||||
case *bleveQuery.DisjunctionQuery:
|
||||
if ok {
|
||||
left.AddQuery(right.Disjuncts...)
|
||||
} else {
|
||||
left.AddQuery(rn)
|
||||
}
|
||||
return left
|
||||
case *bleveQuery.ConjunctionQuery:
|
||||
return bleveQuery.NewDisjunctionQuery([]bleveQuery.Query{ln, rn})
|
||||
default:
|
||||
if ok {
|
||||
left := bleveQuery.NewDisjunctionQuery([]bleveQuery.Query{ln})
|
||||
left.AddQuery(right.Disjuncts...)
|
||||
return left
|
||||
}
|
||||
return bleveQuery.NewDisjunctionQuery([]bleveQuery.Query{ln, rn})
|
||||
}
|
||||
}
|
||||
if operator.Value == kql.BoolAND {
|
||||
switch left := ln.(type) {
|
||||
case *bleveQuery.ConjunctionQuery:
|
||||
left.AddQuery(rn)
|
||||
return left
|
||||
case *bleveQuery.DisjunctionQuery:
|
||||
if !leftIsGroup {
|
||||
last := left.Disjuncts[len(left.Disjuncts)-1]
|
||||
rn = bleveQuery.NewConjunctionQuery([]bleveQuery.Query{
|
||||
last,
|
||||
rn,
|
||||
})
|
||||
dj := bleveQuery.NewDisjunctionQuery(left.Disjuncts[:len(left.Disjuncts)-1])
|
||||
dj.AddQuery(rn)
|
||||
return dj
|
||||
}
|
||||
}
|
||||
}
|
||||
return bleveQuery.NewConjunctionQuery([]bleveQuery.Query{
|
||||
ln,
|
||||
rn,
|
||||
})
|
||||
}
|
||||
|
||||
func getField(name string) string {
|
||||
if name == "" {
|
||||
return "Name"
|
||||
}
|
||||
if _, ok := _fields[strings.ToLower(name)]; ok {
|
||||
return _fields[strings.ToLower(name)]
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func normalizeGroupingProperty(group *ast.GroupNode) *ast.GroupNode {
|
||||
for _, n := range group.Nodes {
|
||||
if onode, ok := n.(*ast.StringNode); ok {
|
||||
onode.Key = group.Key
|
||||
}
|
||||
}
|
||||
return group
|
||||
}
|
||||
|
||||
func mimeType(k, v string) (bleveQuery.Query, bool) {
|
||||
switch v {
|
||||
case "file":
|
||||
q := bleve.NewBooleanQuery()
|
||||
q.AddMustNot(bleveQuery.NewQueryStringQuery(k + ":httpd/unix-directory"))
|
||||
return q, false
|
||||
case "folder":
|
||||
return bleveQuery.NewQueryStringQuery(k + ":httpd/unix-directory"), false
|
||||
case "document":
|
||||
return bleveQuery.NewDisjunctionQuery(newQueryStringQueryList(k,
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.form",
|
||||
"application/vnd.oasis.opendocument.text",
|
||||
"text/plain",
|
||||
"text/markdown",
|
||||
"application/rtf",
|
||||
"application/vnd.apple.pages",
|
||||
)), true
|
||||
case "spreadsheet":
|
||||
return bleveQuery.NewDisjunctionQuery(newQueryStringQueryList(k,
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.oasis.opendocument.spreadsheet",
|
||||
"text/csv",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.oasis.opendocument.spreadsheet",
|
||||
"application/vnd.apple.numbers",
|
||||
)), true
|
||||
case "presentation":
|
||||
return bleveQuery.NewDisjunctionQuery(newQueryStringQueryList(k,
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"application/vnd.oasis.opendocument.presentation",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.apple.keynote",
|
||||
)), true
|
||||
case "pdf":
|
||||
return bleveQuery.NewQueryStringQuery(k + ":application/pdf"), false
|
||||
case "image":
|
||||
return bleveQuery.NewQueryStringQuery(k + ":image/*"), false
|
||||
case "video":
|
||||
return bleveQuery.NewQueryStringQuery(k + ":video/*"), false
|
||||
case "audio":
|
||||
return bleveQuery.NewQueryStringQuery(k + ":audio/*"), false
|
||||
case "archive":
|
||||
return bleveQuery.NewDisjunctionQuery(newQueryStringQueryList(k,
|
||||
"application/zip",
|
||||
"application/gzip",
|
||||
"application/x-gzip",
|
||||
"application/x-7z-compressed",
|
||||
"application/x-rar-compressed",
|
||||
"application/x-tar",
|
||||
"application/x-bzip2",
|
||||
"application/x-bzip",
|
||||
"application/x-tgz",
|
||||
)), true
|
||||
default:
|
||||
return bleveQuery.NewQueryStringQuery(k + ":" + v), false
|
||||
}
|
||||
}
|
||||
|
||||
func newQueryStringQueryList(k string, v ...string) []bleveQuery.Query {
|
||||
list := make([]bleveQuery.Query, len(v))
|
||||
for i := 0; i < len(v); i++ {
|
||||
list[i] = bleveQuery.NewQueryStringQuery(k + ":" + v[i])
|
||||
}
|
||||
return list
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
package bleve
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/search/query"
|
||||
"github.com/qsfera/server/pkg/ast"
|
||||
tAssert "github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var timeMustParse = func(t *testing.T, ts string) time.Time {
|
||||
tp, err := time.Parse(time.RFC3339Nano, ts)
|
||||
if err != nil {
|
||||
t.Fatalf("time.Parse(...) error = %v", err)
|
||||
}
|
||||
|
||||
return tp
|
||||
}
|
||||
|
||||
func Test_compile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args *ast.Ast
|
||||
want query.Query
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: `federated`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Value: "federated"},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`Name:federated`),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `"John Smith"`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Value: "John Smith"},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`Name:john\ smith`),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `"John Smith" Jane`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Key: "name", Value: "John Smith"},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Key: "name", Value: "Jane"},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`Name:john\ smith`),
|
||||
query.NewQueryStringQuery(`Name:jane`),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `tag:bestseller tag:book`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Key: "tag", Value: "bestseller"},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Key: "tag", Value: "book"},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`Tags:bestseller`),
|
||||
query.NewQueryStringQuery(`Tags:book`),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `name:"moby di*" OR tag:bestseller AND tag:book`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Key: "name", Value: "moby di*"},
|
||||
&ast.OperatorNode{Value: "OR"},
|
||||
&ast.StringNode{Key: "tag", Value: "bestseller"},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Key: "tag", Value: "book"},
|
||||
},
|
||||
},
|
||||
want: query.NewDisjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`Name:moby\ di*`),
|
||||
query.NewConjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`Tags:bestseller`),
|
||||
query.NewQueryStringQuery(`Tags:book`),
|
||||
}),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `a AND b OR c`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Value: "a"},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Value: "b"},
|
||||
&ast.OperatorNode{Value: "OR"},
|
||||
&ast.StringNode{Value: "c"},
|
||||
},
|
||||
},
|
||||
want: query.NewDisjunctionQuery([]query.Query{
|
||||
query.NewConjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`Name:a`),
|
||||
query.NewQueryStringQuery(`Name:b`),
|
||||
}),
|
||||
query.NewQueryStringQuery(`Name:c`),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `a OR b AND c`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Value: "a"},
|
||||
&ast.OperatorNode{Value: "OR"},
|
||||
&ast.StringNode{Value: "b"},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Value: "c"},
|
||||
},
|
||||
},
|
||||
want: query.NewDisjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`Name:a`),
|
||||
query.NewConjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`Name:b`),
|
||||
query.NewQueryStringQuery(`Name:c`),
|
||||
}),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `(a OR b OR c) AND d`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.GroupNode{Nodes: []ast.Node{
|
||||
&ast.StringNode{Value: "a"},
|
||||
&ast.OperatorNode{Value: "OR"},
|
||||
&ast.StringNode{Value: "b"},
|
||||
&ast.OperatorNode{Value: "OR"},
|
||||
&ast.StringNode{Value: "c"},
|
||||
}},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Value: "d"},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewDisjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`Name:a`),
|
||||
query.NewQueryStringQuery(`Name:b`),
|
||||
query.NewQueryStringQuery(`Name:c`),
|
||||
}),
|
||||
query.NewQueryStringQuery(`Name:d`),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `(name:"moby di*" OR tag:bestseller) AND tag:book`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.GroupNode{Nodes: []ast.Node{
|
||||
&ast.StringNode{Key: "name", Value: "moby di*"},
|
||||
&ast.OperatorNode{Value: "OR"},
|
||||
&ast.StringNode{Key: "tag", Value: "bestseller"},
|
||||
}},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Key: "tag", Value: "book"},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewDisjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`Name:moby\ di*`),
|
||||
query.NewQueryStringQuery(`Tags:bestseller`),
|
||||
}),
|
||||
query.NewQueryStringQuery(`Tags:book`),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `(name:"moby di*" OR tag:bestseller) AND tag:book AND NOT tag:read`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.GroupNode{Nodes: []ast.Node{
|
||||
&ast.StringNode{Key: "name", Value: "moby di*"},
|
||||
&ast.OperatorNode{Value: "OR"},
|
||||
&ast.StringNode{Key: "tag", Value: "bestseller"},
|
||||
}},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Key: "tag", Value: "book"},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.OperatorNode{Value: "NOT"},
|
||||
&ast.StringNode{Key: "tag", Value: "read"},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewDisjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`Name:moby\ di*`),
|
||||
query.NewQueryStringQuery(`Tags:bestseller`),
|
||||
}),
|
||||
query.NewQueryStringQuery(`Tags:book`),
|
||||
query.NewBooleanQuery(nil, nil, []query.Query{query.NewQueryStringQuery(`Tags:read`)}),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `author:("John Smith" Jane)`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.GroupNode{
|
||||
Key: "author",
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Value: "John Smith"},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Value: "Jane"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`author:John\ Smith`),
|
||||
query.NewQueryStringQuery(`author:Jane`),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `author:("John Smith" Jane) AND tag:bestseller`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.GroupNode{
|
||||
Key: "author",
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Value: "John Smith"},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Value: "Jane"},
|
||||
},
|
||||
},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Key: "tag", Value: "bestseller"},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`author:John\ Smith`),
|
||||
query.NewQueryStringQuery(`author:Jane`),
|
||||
query.NewQueryStringQuery(`Tags:bestseller`),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `id:b27d3bf1-b254-459f-92e8-bdba668d6d3f$d0648459-25fb-4ed8-8684-bc62c7dca29c!d0648459-25fb-4ed8-8684-bc62c7dca29c mtime>=2023-09-05T12:40:59.14741+02:00`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{
|
||||
Key: "id",
|
||||
Value: "b27d3bf1-b254-459f-92e8-bdba668d6d3f$d0648459-25fb-4ed8-8684-bc62c7dca29c!d0648459-25fb-4ed8-8684-bc62c7dca29c",
|
||||
},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.DateTimeNode{
|
||||
Key: "Mtime",
|
||||
Operator: &ast.OperatorNode{Value: ">="},
|
||||
Value: timeMustParse(t, "2023-09-05T08:42:11.23554+02:00"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`ID:b27d3bf1-b254-459f-92e8-bdba668d6d3f$d0648459-25fb-4ed8-8684-bc62c7dca29c!d0648459-25fb-4ed8-8684-bc62c7dca29c`),
|
||||
func() query.Query {
|
||||
q := query.NewDateRangeInclusiveQuery(timeMustParse(t, "2023-09-05T08:42:11.23554+02:00"), time.Time{}, &[]bool{true}[0], nil)
|
||||
q.FieldVal = "Mtime"
|
||||
return q
|
||||
}(),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `StringNode value lowercase`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Value: "John Smith"},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Key: "Hidden", Value: "T"},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Key: "hidden", Value: "T"},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`Name:john\ smith`),
|
||||
query.NewQueryStringQuery(`Hidden:T`),
|
||||
query.NewQueryStringQuery(`Hidden:T`),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `NOT tag:physik`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.OperatorNode{Value: "NOT"},
|
||||
&ast.StringNode{Key: "tag", Value: "physik"},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewBooleanQuery(nil, nil, []query.Query{query.NewQueryStringQuery(`Tags:physik`)}),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `ast.DateTimeNode`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.DateTimeNode{
|
||||
Key: "mtime",
|
||||
// "=" is not supported by bleve, ignore
|
||||
Operator: &ast.OperatorNode{Value: "="},
|
||||
Value: timeMustParse(t, "2023-09-05T08:42:11.23554+02:00"),
|
||||
},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.DateTimeNode{
|
||||
Key: "mtime",
|
||||
// ":" is not supported by bleve, ignore
|
||||
Operator: &ast.OperatorNode{Value: ":"},
|
||||
Value: timeMustParse(t, "2023-09-05T08:42:11.23554+02:00"),
|
||||
},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.DateTimeNode{
|
||||
Key: "mtime",
|
||||
// no operator, skip
|
||||
Value: timeMustParse(t, "2023-09-05T08:42:11.23554+02:00"),
|
||||
},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.DateTimeNode{
|
||||
Key: "mtime",
|
||||
Operator: &ast.OperatorNode{Value: ">"},
|
||||
Value: timeMustParse(t, "2023-09-05T08:42:11.23554+02:00"),
|
||||
},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.DateTimeNode{
|
||||
Key: "mtime",
|
||||
Operator: &ast.OperatorNode{Value: ">="},
|
||||
Value: timeMustParse(t, "2023-09-05T08:42:11.23554+02:00"),
|
||||
},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.DateTimeNode{
|
||||
Key: "mtime",
|
||||
Operator: &ast.OperatorNode{Value: "<"},
|
||||
Value: timeMustParse(t, "2023-09-05T08:42:11.23554+02:00"),
|
||||
},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.DateTimeNode{
|
||||
Key: "mtime",
|
||||
Operator: &ast.OperatorNode{Value: "<="},
|
||||
Value: timeMustParse(t, "2023-09-05T08:42:11.23554+02:00"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
func() query.Query {
|
||||
q := query.NewDateRangeInclusiveQuery(timeMustParse(t, "2023-09-05T08:42:11.23554+02:00"), time.Time{}, &[]bool{false}[0], nil)
|
||||
q.FieldVal = "Mtime"
|
||||
return q
|
||||
}(),
|
||||
func() query.Query {
|
||||
q := query.NewDateRangeInclusiveQuery(timeMustParse(t, "2023-09-05T08:42:11.23554+02:00"), time.Time{}, &[]bool{true}[0], nil)
|
||||
q.FieldVal = "Mtime"
|
||||
return q
|
||||
}(),
|
||||
func() query.Query {
|
||||
q := query.NewDateRangeInclusiveQuery(time.Time{}, timeMustParse(t, "2023-09-05T08:42:11.23554+02:00"), nil, &[]bool{false}[0])
|
||||
q.FieldVal = "Mtime"
|
||||
return q
|
||||
}(),
|
||||
func() query.Query {
|
||||
q := query.NewDateRangeInclusiveQuery(time.Time{}, timeMustParse(t, "2023-09-05T08:42:11.23554+02:00"), nil, &[]bool{true}[0])
|
||||
q.FieldVal = "Mtime"
|
||||
return q
|
||||
}(),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `MimeType:document`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Key: "mediatype", Value: "document"},
|
||||
},
|
||||
},
|
||||
want: query.NewDisjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`MimeType:application/msword`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.openxmlformats-officedocument.wordprocessingml.document`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.openxmlformats-officedocument.wordprocessingml.form`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.oasis.opendocument.text`),
|
||||
query.NewQueryStringQuery(`MimeType:text/plain`),
|
||||
query.NewQueryStringQuery(`MimeType:text/markdown`),
|
||||
query.NewQueryStringQuery(`MimeType:application/rtf`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.apple.pages`),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `MimeType:document AND *tdd*`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Key: "mediatype", Value: "document"},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Key: "name", Value: "*tdd*"},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewDisjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`MimeType:application/msword`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.openxmlformats-officedocument.wordprocessingml.document`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.openxmlformats-officedocument.wordprocessingml.form`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.oasis.opendocument.text`),
|
||||
query.NewQueryStringQuery(`MimeType:text/plain`),
|
||||
query.NewQueryStringQuery(`MimeType:text/markdown`),
|
||||
query.NewQueryStringQuery(`MimeType:application/rtf`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.apple.pages`),
|
||||
}),
|
||||
query.NewQueryStringQuery(`Name:*tdd*`),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `MimeType:document OR MimeType:pdf AND *tdd*`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Key: "mediatype", Value: "document"},
|
||||
&ast.OperatorNode{Value: "OR"},
|
||||
&ast.StringNode{Key: "mediatype", Value: "pdf"},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Key: "name", Value: "*tdd*"},
|
||||
},
|
||||
},
|
||||
want: query.NewDisjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`MimeType:application/msword`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.openxmlformats-officedocument.wordprocessingml.document`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.openxmlformats-officedocument.wordprocessingml.form`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.oasis.opendocument.text`),
|
||||
query.NewQueryStringQuery(`MimeType:text/plain`),
|
||||
query.NewQueryStringQuery(`MimeType:text/markdown`),
|
||||
query.NewQueryStringQuery(`MimeType:application/rtf`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.apple.pages`),
|
||||
query.NewConjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`MimeType:application/pdf`),
|
||||
query.NewQueryStringQuery(`Name:*tdd*`),
|
||||
}),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `(MimeType:document OR MimeType:pdf) AND *tdd*`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.GroupNode{Nodes: []ast.Node{
|
||||
&ast.StringNode{Key: "mediatype", Value: "document"},
|
||||
&ast.OperatorNode{Value: "OR"},
|
||||
&ast.StringNode{Key: "mediatype", Value: "pdf"},
|
||||
}},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Key: "name", Value: "*tdd*"},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewDisjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`MimeType:application/msword`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.openxmlformats-officedocument.wordprocessingml.document`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.openxmlformats-officedocument.wordprocessingml.form`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.oasis.opendocument.text`),
|
||||
query.NewQueryStringQuery(`MimeType:text/plain`),
|
||||
query.NewQueryStringQuery(`MimeType:text/markdown`),
|
||||
query.NewQueryStringQuery(`MimeType:application/rtf`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.apple.pages`),
|
||||
query.NewQueryStringQuery(`MimeType:application/pdf`),
|
||||
}),
|
||||
query.NewQueryStringQuery(`Name:*tdd*`),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `(MimeType:pdf OR MimeType:document) AND *tdd*`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.GroupNode{Nodes: []ast.Node{
|
||||
&ast.StringNode{Key: "mediatype", Value: "pdf"},
|
||||
&ast.OperatorNode{Value: "OR"},
|
||||
&ast.StringNode{Key: "mediatype", Value: "document"},
|
||||
}},
|
||||
&ast.OperatorNode{Value: "AND"},
|
||||
&ast.StringNode{Key: "name", Value: "*tdd*"},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewDisjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`MimeType:application/pdf`),
|
||||
query.NewQueryStringQuery(`MimeType:application/msword`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.openxmlformats-officedocument.wordprocessingml.document`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.openxmlformats-officedocument.wordprocessingml.form`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.oasis.opendocument.text`),
|
||||
query.NewQueryStringQuery(`MimeType:text/plain`),
|
||||
query.NewQueryStringQuery(`MimeType:text/markdown`),
|
||||
query.NewQueryStringQuery(`MimeType:application/rtf`),
|
||||
query.NewQueryStringQuery(`MimeType:application/vnd.apple.pages`),
|
||||
}),
|
||||
query.NewQueryStringQuery(`Name:*tdd*`),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: `John Smith`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Value: "John Smith +-=&|><!(){}[]^\"~: "},
|
||||
},
|
||||
},
|
||||
want: query.NewConjunctionQuery([]query.Query{
|
||||
query.NewQueryStringQuery(`Name:john\ smith\ \+\-\=\&\|\>\<\!\(\)\{\}\[\]\^\"\~\:\ `),
|
||||
}),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
assert := tAssert.New(t)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := compile(tt.args)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("compile() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_escape(t *testing.T) {
|
||||
type args struct {
|
||||
str string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "all escaped",
|
||||
args: args{
|
||||
`+-=&|><!(){}[]^"~:\/ `,
|
||||
},
|
||||
want: `\+\-\=\&\|\>\<\!\(\)\{\}\[\]\^\"\~\:\\\/\ `,
|
||||
},
|
||||
{
|
||||
name: "no one escaped",
|
||||
args: args{
|
||||
`@#$%`,
|
||||
},
|
||||
want: `@#$%`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tAssert.Equalf(t, tt.want, bleveEscaper.Replace(tt.args.str), "bleveEscaper(%v)", tt.args.str)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user