Files
opencloud/services/search/pkg/query/kql/connect.go
T
Florian Schade c0553c7273 [full-ci] enhancement: add more kql spec tests and simplify ast normalization (#7254)
* enhancement: add more kql spec tests and simplify ast normalization

* enhancement: kql parser error if query starts with AND

* enhancement: add kql docs and support for date and time only dateTimeRestriction queries

* enhancement: add the ability to decide how kql nodes get connected

connecting nodes (with edges) seem straight forward when not using group, the default connection for nodes with the same node is always OR. THis only applies for first level nodes, for grouped nodes it is defined differently. The KQL docs are saying, nodes inside a grouped node, with the same key are connected by a AND edge.

* enhancement: explicit error handling for falsy group nodes and queries with leading binary operator

* enhancement: use optimized grammar for kql parser and toolify pigeon

* enhancement: simplify error handling

* fix: kql implicit 'AND' and 'OR' follows the ms html spec instead of the pdf spec
2023-09-11 13:49:53 +02:00

130 lines
3.4 KiB
Go

package kql
import (
"strings"
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
)
// connectNodes connects given nodes
func connectNodes(c Connector, nodes ...ast.Node) []ast.Node {
var connectedNodes []ast.Node
for i := range nodes {
ri := len(nodes) - 1 - i
head := nodes[ri]
if connectionNodes := connectNode(c, head, connectedNodes...); len(connectionNodes) > 0 {
connectedNodes = append(connectionNodes, connectedNodes...)
}
connectedNodes = append([]ast.Node{head}, connectedNodes...)
}
return connectedNodes
}
// connectNode connects a tip node with the rest
func connectNode(c Connector, headNode ast.Node, tailNodes ...ast.Node) []ast.Node {
var nearestNeighborNode ast.Node
var nearestNeighborOperators []*ast.OperatorNode
l:
for _, tailNode := range tailNodes {
switch node := tailNode.(type) {
case *ast.OperatorNode:
nearestNeighborOperators = append(nearestNeighborOperators, node)
default:
nearestNeighborNode = node
break l
}
}
if nearestNeighborNode == nil {
return nil
}
return c.Connect(headNode, nearestNeighborNode, nearestNeighborOperators)
}
// Connector is responsible to decide what node connections are needed
type Connector interface {
Connect(head ast.Node, neighbor ast.Node, connections []*ast.OperatorNode) []ast.Node
}
// DefaultConnector is the default node connector
type DefaultConnector struct {
sameKeyOPValue string
}
// Connect implements the Connector interface and is used to connect the nodes using
// the default logic defined by the kql spec.
func (c DefaultConnector) Connect(head ast.Node, neighbor ast.Node, connections []*ast.OperatorNode) []ast.Node {
switch head.(type) {
case *ast.OperatorNode:
return nil
}
headKey := strings.ToLower(ast.NodeKey(head))
neighborKey := strings.ToLower(ast.NodeKey(neighbor))
connection := &ast.OperatorNode{
Base: &ast.Base{Loc: &ast.Location{Source: &[]string{"implicitly operator"}[0]}},
Value: BoolAND,
}
// if the current node and the neighbor node have the same key
// the connection is of type OR
//
// spec: same
// author:"John Smith" author:"Jane Smith"
// author:"John Smith" OR author:"Jane Smith"
//
// if the nodes have NO key, the edge is a AND connection
//
// spec: same
// cat dog
// cat AND dog
// from the spec:
// To construct complex queries, you can combine multiple
// free-text expressions with KQL query operators.
// If there are multiple free-text expressions without any
// operators in between them, the query behavior is the same
// as using the AND operator.
//
// nodes inside of group node are handled differently,
// if no explicit operator given, it uses AND
//
// spec: same
// author:"John Smith" AND author:"Jane Smith"
// author:("John Smith" "Jane Smith")
if headKey == neighborKey && headKey != "" && neighborKey != "" {
connection.Value = c.sameKeyOPValue
}
// decisions based on nearest neighbor operators
for i, node := range connections {
// consider direct neighbor operator only
if i == 0 {
// no connection is necessary here because an `AND` or `OR` edge is already present
// exit
for _, skipValue := range []string{BoolOR, BoolAND} {
if node.Value == skipValue {
return nil
}
}
// if neighbor node negotiates, an AND edge is needed
//
// spec: same
// cat -dog
// cat AND NOT dog
if node.Value == BoolNOT {
connection.Value = BoolAND
}
}
}
return []ast.Node{connection}
}