Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -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)
})
}
}