mirror of
https://github.com/hanzoai/tasks.git
synced 2026-11-02 12:22:18 +00:00
## What changed?
Combining query clauses in Elasticsearch.
1. `AND` queries: combined multiple `AND` clause into a single
`BoolQuery`. Example: `a AND b AND c`
- Before:
```json
{
"bool": {
"filter": [
{ ... }, // `a` clause
{
"bool": {
"filter": [
{ ... }, // `b` clause
{ ... } // `c` clause
]
}
}
]
}
}
```
- After:
```json
{
"bool": {
"filter": [
{ ... }, // `a` clause
{ ... }, // `b` clause
{ ... } // `c` clause
]
}
}
```
2. `OR` queries: combined multiple `OR` clause into a single `BoolQuery`
(similar to `AND` above)
3. `NOT (a OR b)`: replace `should` with `must_not`.
- Before:
```json
{
"bool": {
"must_not": {
"bool": {
"should": [
{ ... }, // `a` clause
{ ... } // `b` clause
]
}
}
}
}
```
- After:
```json
{
"bool": {
"must_not": [
{ ... }, // `a` clause
{ ... } // `b` clause
]
}
}
```
4. `a AND !b`: combine `filter` and `must_not` clauses into a single
`BoolQuery`.
- Before:
```json
{
"bool": {
"filter": [
{ ... }, // `a` clause
{
"bool": {
"must_not": [
{ ... } // `b` clause
]
}
}
]
}
}
```
- After:
```json
{
"bool": {
"filter": [
{ ... } // `a` clause
]
"must_not": [
{ ... } // `b` clause
]
}
}
```
## Why?
Reduce nesting depth of bool queries.
## How did you test it?
- [ ] built
- [ ] run locally and tested manually
- [ ] covered by existing tests
- [ ] added new unit test(s)
- [ ] added new functional test(s)
## Potential risks
220 lines
5.9 KiB
Go
220 lines
5.9 KiB
Go
package elasticsearch
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/olivere/elastic/v7"
|
|
"github.com/temporalio/sqlparser"
|
|
"go.temporal.io/server/common/persistence/visibility/store/query"
|
|
)
|
|
|
|
type queryConverter struct{}
|
|
|
|
var _ query.StoreQueryConverter[elastic.Query] = (*queryConverter)(nil)
|
|
|
|
func (c *queryConverter) GetDatetimeFormat() string {
|
|
return time.RFC3339Nano
|
|
}
|
|
|
|
func (c *queryConverter) BuildParenExpr(expr elastic.Query) (elastic.Query, error) {
|
|
if expr == nil {
|
|
return nil, nil
|
|
}
|
|
return expr, nil
|
|
}
|
|
|
|
func (c *queryConverter) BuildNotExpr(expr elastic.Query) (elastic.Query, error) {
|
|
if expr == nil {
|
|
return nil, nil
|
|
}
|
|
if bq, ok := expr.(*boolQuery); ok && len(bq.shouldClauses) > 0 {
|
|
// !(a || b) == !a && !b
|
|
ret := newBoolQuery()
|
|
ret.mustNotClauses = bq.shouldClauses
|
|
return ret, nil
|
|
}
|
|
return newBoolQuery().MustNot(expr), nil
|
|
}
|
|
|
|
func (c *queryConverter) BuildAndExpr(exprs ...elastic.Query) (elastic.Query, error) {
|
|
var reusableBoolQuery *boolQuery
|
|
validExprs := make([]elastic.Query, 0, len(exprs))
|
|
for _, e := range exprs {
|
|
if e == nil {
|
|
continue
|
|
}
|
|
if bq, ok := e.(*boolQuery); !ok || len(bq.filterClauses)+len(bq.mustNotClauses) == 0 {
|
|
validExprs = append(validExprs, e)
|
|
} else if reusableBoolQuery == nil {
|
|
reusableBoolQuery = bq
|
|
} else {
|
|
reusableBoolQuery.Filter(bq.filterClauses...).MustNot(bq.mustNotClauses...)
|
|
}
|
|
}
|
|
if reusableBoolQuery != nil {
|
|
reusableBoolQuery.Filter(validExprs...)
|
|
return reusableBoolQuery, nil
|
|
}
|
|
if len(validExprs) == 0 {
|
|
return nil, nil
|
|
}
|
|
if len(validExprs) == 1 {
|
|
return validExprs[0], nil
|
|
}
|
|
return newBoolQuery().Filter(validExprs...), nil
|
|
}
|
|
|
|
func (c *queryConverter) BuildOrExpr(exprs ...elastic.Query) (elastic.Query, error) {
|
|
var reusableBoolQuery *boolQuery
|
|
validExprs := make([]elastic.Query, 0, len(exprs))
|
|
for _, e := range exprs {
|
|
if e == nil {
|
|
continue
|
|
}
|
|
if bq, ok := e.(*boolQuery); !ok || len(bq.shouldClauses) == 0 {
|
|
validExprs = append(validExprs, e)
|
|
} else if reusableBoolQuery == nil {
|
|
reusableBoolQuery = bq
|
|
} else {
|
|
reusableBoolQuery.Should(bq.shouldClauses...)
|
|
}
|
|
}
|
|
if reusableBoolQuery != nil {
|
|
reusableBoolQuery.Should(validExprs...)
|
|
return reusableBoolQuery, nil
|
|
}
|
|
if len(validExprs) == 0 {
|
|
return nil, nil
|
|
}
|
|
if len(validExprs) == 1 {
|
|
return validExprs[0], nil
|
|
}
|
|
return newBoolQuery().Should(validExprs...).MinimumNumberShouldMatch(1), nil
|
|
}
|
|
|
|
func (c *queryConverter) ConvertComparisonExpr(
|
|
operator string,
|
|
col *query.SAColumn,
|
|
value any,
|
|
) (elastic.Query, error) {
|
|
var res elastic.Query
|
|
negate := false
|
|
colName := col.FieldName
|
|
switch operator {
|
|
case sqlparser.GreaterEqualStr:
|
|
res = elastic.NewRangeQuery(colName).Gte(value)
|
|
case sqlparser.LessEqualStr:
|
|
res = elastic.NewRangeQuery(colName).Lte(value)
|
|
case sqlparser.GreaterThanStr:
|
|
res = elastic.NewRangeQuery(colName).Gt(value)
|
|
case sqlparser.LessThanStr:
|
|
res = elastic.NewRangeQuery(colName).Lt(value)
|
|
case sqlparser.EqualStr, sqlparser.NotEqualStr:
|
|
res = elastic.NewTermQuery(colName, value)
|
|
negate = operator == sqlparser.NotEqualStr
|
|
case sqlparser.InStr, sqlparser.NotInStr:
|
|
res = elastic.NewTermsQuery(colName, value.([]any)...)
|
|
negate = operator == sqlparser.NotInStr
|
|
default:
|
|
return nil, query.NewOperatorNotSupportedError(col.Alias, col.ValueType, operator)
|
|
}
|
|
|
|
if negate {
|
|
res, _ = c.BuildNotExpr(res)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (c *queryConverter) ConvertKeywordComparisonExpr(
|
|
operator string,
|
|
col *query.SAColumn,
|
|
value any,
|
|
) (elastic.Query, error) {
|
|
colName := col.FieldName
|
|
switch operator {
|
|
case sqlparser.StartsWithStr, sqlparser.NotStartsWithStr:
|
|
v, ok := value.(string)
|
|
if !ok {
|
|
return nil, query.NewConverterError(
|
|
"%s: right-hand side of operator '%s' must be a string",
|
|
query.InvalidExpressionErrMessage,
|
|
strings.ToUpper(operator),
|
|
)
|
|
}
|
|
var res elastic.Query = elastic.NewPrefixQuery(colName, v)
|
|
if operator == sqlparser.NotStartsWithStr {
|
|
res, _ = c.BuildNotExpr(res)
|
|
}
|
|
return res, nil
|
|
default:
|
|
return c.ConvertComparisonExpr(operator, col, value)
|
|
}
|
|
}
|
|
|
|
func (c *queryConverter) ConvertKeywordListComparisonExpr(
|
|
operator string,
|
|
col *query.SAColumn,
|
|
value any,
|
|
) (elastic.Query, error) {
|
|
return c.ConvertKeywordComparisonExpr(operator, col, value)
|
|
}
|
|
|
|
func (c *queryConverter) ConvertTextComparisonExpr(
|
|
operator string,
|
|
col *query.SAColumn,
|
|
value any,
|
|
) (elastic.Query, error) {
|
|
colName := col.FieldName
|
|
switch operator {
|
|
case sqlparser.EqualStr:
|
|
return elastic.NewMatchQuery(colName, value), nil
|
|
case sqlparser.NotEqualStr:
|
|
return newBoolQuery().MustNot(elastic.NewMatchQuery(colName, value)), nil
|
|
default:
|
|
return nil, query.NewOperatorNotSupportedError(col.Alias, col.ValueType, operator)
|
|
}
|
|
}
|
|
|
|
func (c *queryConverter) ConvertRangeExpr(
|
|
operator string,
|
|
col *query.SAColumn,
|
|
from, to any,
|
|
) (elastic.Query, error) {
|
|
colName := col.FieldName
|
|
switch operator {
|
|
case sqlparser.BetweenStr:
|
|
return elastic.NewRangeQuery(colName).Gte(from).Lte(to), nil
|
|
case sqlparser.NotBetweenStr:
|
|
return newBoolQuery().MustNot(elastic.NewRangeQuery(colName).Gte(from).Lte(to)), nil
|
|
default:
|
|
// This should be impossible since the query parser only calls this function with one of those
|
|
// operators strings.
|
|
return nil, query.NewConverterError(
|
|
"%s: unexpected operator '%s' for range condition",
|
|
query.MalformedSqlQueryErrMessage,
|
|
strings.ToUpper(operator),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (c *queryConverter) ConvertIsExpr(
|
|
operator string,
|
|
col *query.SAColumn,
|
|
) (elastic.Query, error) {
|
|
colName := col.FieldName
|
|
switch operator {
|
|
case sqlparser.IsNullStr:
|
|
return newBoolQuery().MustNot(elastic.NewExistsQuery(colName)), nil
|
|
case sqlparser.IsNotNullStr:
|
|
return elastic.NewExistsQuery(colName), nil
|
|
default:
|
|
// This should be impossible since the query parser only calls this function with one of those
|
|
// operators strings.
|
|
return nil, query.NewConverterError(
|
|
"%s: 'IS' operator can only be used as 'IS NULL' or 'IS NOT NULL'",
|
|
query.InvalidExpressionErrMessage,
|
|
)
|
|
}
|
|
}
|