Skip to content

Commit

Permalink
ElasticSearch support (Beta) (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
steve-r-west authored Mar 22, 2024
1 parent c44b717 commit 7b44503
Show file tree
Hide file tree
Showing 4 changed files with 997 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ root = true
[{go.mod,go.sum,*.go,.gitmodules}]
indent_style = tab
indent_size = 4

[*.json]
indent_style = space
indent_size = 2
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,42 @@ func (l *LowerCaseEmail) VisitEq(first, second string) (*bson.D, error) {

You can of course use the `FieldTypes` and `CustomQueryBuilder` together.

#### Elastic Search (Open Search)

The following examples shows how to generate a Elastic Search Query with this library.

```go
package example
import epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3"
import epsearchast_v3_els "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3/els"

func Example(ast *epsearchast_v3.AstNode, tenantBoundaryId string) (string, error) {
// Not Shown: Validation

// Create query builder
var qb epsearchast_v3.SemanticReducer[epsearchast_v3_els.JsonObject] = epsearchast_v3_els.LowerCaseEmail{}

// Create Query Object
queryObj, err := epsearchast_v3.SemanticReduceAst(ast, qb)

if err != nil {
return nil, err
}

...
}

type LowerCaseEmail struct {
epsearchast_v3_els.DefaultElsQueryBuilder
}

func (l *LowerCaseEmail) VisitEq(first, second string) (*bson.D, error) {
....
}

```


### FAQ

#### Design
Expand Down
192 changes: 192 additions & 0 deletions external/epsearchast/v3/els/els_query_builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package epsearchast_v3_els

import (
epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3"
"strings"
)

type DefaultElsQueryBuilder struct {
OpTypeToFieldNames map[string]*OperatorTypeToMultiFieldName
}

type JsonObject map[string]interface{}

// Elastic Search can encode data in multiple formats using multi fields
// https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html

type OperatorTypeToMultiFieldName struct {
// The field name to use for equality operators (eq, in)
Equality string

// The field name to use for relational operators (lt, gt, le, ge)
Relational string

// The field name to use for text fields (nothing yet)
Text string

// The field name for use with array fields (not yet)
Array string

// The field name for wild card fields
Wildcard string
}

var _ epsearchast_v3.SemanticReducer[JsonObject] = (*DefaultElsQueryBuilder)(nil)

func (d DefaultElsQueryBuilder) PostVisitAnd(rs []*JsonObject) (*JsonObject, error) {
return (*JsonObject)(&map[string]interface{}{
"bool": map[string]interface{}{
"must": rs,
},
}), nil
}

func (d DefaultElsQueryBuilder) VisitIn(args ...string) (*JsonObject, error) {
return (*JsonObject)(&map[string]interface{}{
"terms": map[string]interface{}{
d.getFieldMapping(args[0]).Equality: args[1:],
},
}), nil
}

func (d DefaultElsQueryBuilder) VisitEq(first, second string) (*JsonObject, error) {
return (*JsonObject)(&map[string]interface{}{
"term": map[string]interface{}{
d.getFieldMapping(first).Equality: second,
},
}), nil
}

func (d DefaultElsQueryBuilder) VisitText(first, second string) (*JsonObject, error) {
return (*JsonObject)(&map[string]interface{}{
"match_phrase": map[string]interface{}{
d.getFieldMapping(first).Text: second,
},
}), nil
}

// Useful doc: https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-range-query.html

func (d DefaultElsQueryBuilder) VisitLe(first, second string) (*JsonObject, error) {
return (*JsonObject)(&map[string]interface{}{
"range": map[string]interface{}{
d.getFieldMapping(first).Relational: map[string]interface{}{
"lte": second,
},
},
}), nil
}

func (d DefaultElsQueryBuilder) VisitLt(first, second string) (*JsonObject, error) {
return (*JsonObject)(&map[string]interface{}{
"range": map[string]interface{}{
d.getFieldMapping(first).Relational: map[string]interface{}{
"lt": second,
},
},
}), nil
}

func (d DefaultElsQueryBuilder) VisitGe(first, second string) (*JsonObject, error) {
return (*JsonObject)(&map[string]interface{}{
"range": map[string]interface{}{
d.getFieldMapping(first).Relational: map[string]interface{}{
"gte": second,
},
},
}), nil
}

func (d DefaultElsQueryBuilder) VisitGt(first, second string) (*JsonObject, error) {
return (*JsonObject)(&map[string]interface{}{
"range": map[string]interface{}{
d.getFieldMapping(first).Relational: map[string]interface{}{
"gt": second,
},
},
}), nil
}

func (d DefaultElsQueryBuilder) VisitLike(first, second string) (*JsonObject, error) {
return (*JsonObject)(&map[string]interface{}{
"wildcard": map[string]interface{}{
d.getFieldMapping(first).Wildcard: d.EscapeWildcardString(second),
},
}), nil
}

func (d DefaultElsQueryBuilder) VisitIsNull(first string) (*JsonObject, error) {
return (*JsonObject)(&map[string]interface{}{
"bool": map[string]interface{}{
"must_not": map[string]interface{}{
"exists": map[string]interface{}{
"field": d.getFieldMapping(first).Equality,
},
},
},
}), nil
}

// getFieldMapping returns the field name to use for a given operator type, the struct is always guaranteed to return f, if nothing was set.
func (d DefaultElsQueryBuilder) getFieldMapping(f string) *OperatorTypeToMultiFieldName {
var o *OperatorTypeToMultiFieldName

if d.OpTypeToFieldNames[f] == nil {
o = &OperatorTypeToMultiFieldName{
Equality: f,
Relational: f,
Text: f,
Array: f,
Wildcard: f,
}
}

if v, ok := d.OpTypeToFieldNames[f]; ok {
o = &OperatorTypeToMultiFieldName{
Equality: v.Equality,
Relational: v.Relational,
Text: v.Text,
Array: v.Array,
Wildcard: v.Wildcard,
}

if o.Equality == "" {
o.Equality = f
}

if o.Relational == "" {
o.Relational = f
}

if o.Text == "" {
o.Text = f
}

if o.Array == "" {
o.Array = f
}

if o.Wildcard == "" {
o.Wildcard = f
}
}

return o
}

func (d DefaultElsQueryBuilder) EscapeWildcardString(s string) string {
str := strings.ReplaceAll(s, "?", `\?`)
str = strings.ReplaceAll(str, "*", `\*`)

if strings.HasPrefix(str, `\*`) {
str = str[1:]
}

if strings.HasSuffix(str, `\*`) {
str = str[:len(str)-2] + "*"
}

return str
}

// Generate an implementation of SemanticReducer[JsonObject] for the Elasticsearch query builder.
Loading

0 comments on commit 7b44503

Please sign in to comment.