Converter sql como consultas dsl personalizadas em ElasticSearch?

8

Estamos construindo nossa própria linguagem de consulta semelhante ao Mysql usando antlr4. Exceto que usamos apenas where clause, em outras palavras, o usuário não insere select/frominstruções.

Eu era capaz de criar gramática e gerar lexers / parsers / listeners em golang.

Abaixo do nosso arquivo gramatical EsDslQuery.g4:

grammar EsDslQuery;

options {
language = Go;
}

query
   : leftBracket = '(' query rightBracket = ')'                             #bracketExp
   | leftQuery=query op=OR rightQuery=query                                 #orLogicalExp
   | leftQuery=query op=AND rightQuery=query                                #andLogicalExp
   | propertyName=attrPath op=COMPARISON_OPERATOR propertyValue=attrValue   #compareExp
   ;

attrPath
   : ATTRNAME ('.' attrPath)?
   ;

fragment ATTR_NAME_CHAR
   : '-' | '_' | ':' | DIGIT | ALPHA
   ;

fragment DIGIT
   : ('0'..'9')
   ;

fragment ALPHA
   : ( 'A'..'Z' | 'a'..'z' )
   ;

attrValue
   : BOOLEAN           #boolean
   | NULL              #null
   | STRING            #string
   | DOUBLE            #double
   | '-'? INT EXP?     #long
   ;

...

Exemplo de consulta: color="red" and price=20000 or model="hyundai" and (seats=4 or year=2001)

O ElasticSearch suporta consultas sql com plug-in aqui: https://github.com/elastic/elasticsearch/tree/master/x-pack/plugin/sql .

Tendo dificuldade para entender o código java.

Como temos operadores lógicos, não tenho certeza de como obter a árvore de análise e convertê-la em consulta ES. Alguém pode ajudar / sugerir idéias?

Atualização 1: Adicionado mais exemplos com a consulta ES correspondente

Exemplo de consulta 1: color="red" AND price=2000

Consulta ES 1:

{
    "query": {
      "bool": {
        "must": [
          {
            "terms": {
              "color": [
                "red"
              ]
            }
          },
          {
            "terms": {
              "price": [
                2000
              ]
            }
          }
        ]
      }
    },
    "size": 100
  }

Exemplo de consulta 2: color="red" AND price=2000 AND (model="hyundai" OR model="bmw")

Consulta ES 2:

{
  "query": {
    "bool": {
      "must": [
        {
          "bool": {
            "must": {
              "terms": {
                "color": ["red"]
              }
            }
          }
        },
        {
          "bool": {
            "must": {
              "terms": {
                "price": [2000]
              }
            }
          }
        },
        {
          "bool": {
            "should": [
              {
                "term": {
                  "model": "hyundai"
                }
              },
              {
                "term": {
                  "region": "bmw"
                }
              }
            ]
          }
        }
      ]
    }
  },
  "size": 100
}

Exemplo de consulta 3: color="red" OR color="blue"

Consulta ES 3:

{
    "query": {
      "bool": {
        "should": [
          {
            "bool": {
              "must": {
                "terms": {
                  "color": ["red"]
                }
              }
            }
          },
          {
            "bool": {
              "must": {
                "terms": {
                    "color": ["blue"]
                }
              }
            }
          }
        ]
      }
    },
    "size": 100
  }
Omurbek Kadyrbekov
fonte
Considere adicionar algum exemplo de saída. Como é a color="red" and price=20000 or model="hyundai" and (seats=4 or year=2001sintaxe do ES? Você quer a sintaxe JSON, a sintaxe curta da cadeia de caracteres de consulta ou algo completamente diferente? Também ajuda se você adicionar mais de 1 exemplo. Além disso, você já tentou alguma coisa?
Bart Kiers
Eu adicionei mais exemplos. Sim, quero criar a sintaxe json a partir da árvore de análise. Estou em andamento com Golang, ainda não o terminei
Omurbek Kadyrbekov

Respostas:

6

URL da demonstração de trabalho: https://github.com/omurbekjk/convert-dsl-to-es-query-with-antlr , tempo estimado gasto: ~ 3 semanas

Depois de investigar o antlr4 e vários exemplos, encontrei uma solução simples com ouvinte e pilha. Semelhante a como as expressões são calculadas usando a pilha.

Precisamos substituir o ouvinte base padrão pelo nosso para obter gatilhos para cada regra de gramática de entrada / saída. Regras importantes são:

  1. Expressão de comparação (preço = 200, preço> 190)
  2. Operadores lógicos (OR, AND)
  3. Parênteses (para criar uma consulta correta, precisamos escrever um arquivo gramatical correto, lembrando a precedência do operador, é por isso que os parênteses estão em primeiro lugar no arquivo gramatical)

Abaixo do meu código de ouvinte personalizado, escrito em golang:

package parser

import (
    "github.com/olivere/elastic"
    "strings"
)

type MyDslQueryListener struct {
    *BaseDslQueryListener
    Stack []*elastic.BoolQuery
}

func (ql *MyDslQueryListener) ExitCompareExp(c *CompareExpContext) {
    boolQuery := elastic.NewBoolQuery()

    attrName := c.GetPropertyName().GetText()
    attrValue := strings.Trim(c.GetPropertyValue().GetText(), `\"`)
    // Based on operator type we build different queries, default is terms query(=)
    termsQuery := elastic.NewTermQuery(attrName, attrValue)
    boolQuery.Must(termsQuery)
    ql.Stack = append(ql.Stack, boolQuery)
}

func (ql *MyDslQueryListener) ExitAndLogicalExp(c *AndLogicalExpContext) {
    size := len(ql.Stack)
    right := ql.Stack[size-1]
    left := ql.Stack[size-2]
    ql.Stack = ql.Stack[:size-2] // Pop last two elements
    boolQuery := elastic.NewBoolQuery()
    boolQuery.Must(right)
    boolQuery.Must(left)
    ql.Stack = append(ql.Stack, boolQuery)
}

func (ql *MyDslQueryListener) ExitOrLogicalExp(c *OrLogicalExpContext) {
    size := len(ql.Stack)
    right := ql.Stack[size-1]
    left := ql.Stack[size-2]
    ql.Stack = ql.Stack[:size-2] // Pop last two elements
    boolQuery := elastic.NewBoolQuery()
    boolQuery.Should(right)
    boolQuery.Should(left)
    ql.Stack = append(ql.Stack, boolQuery)
}

E arquivo principal:

package main

import (
    "encoding/json"
    "fmt"
    "github.com/antlr/antlr4/runtime/Go/antlr"
    "github.com/omurbekjk/convert-dsl-to-es-query-with-antlr/parser"
)

func main() {
    fmt.Println("Starting here")
    query := "price=2000 OR model=\"hyundai\" AND (color=\"red\" OR color=\"blue\")"
    stream := antlr.NewInputStream(query)
    lexer := parser.NewDslQueryLexer(stream)
    tokenStream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel)
    dslParser := parser.NewDslQueryParser(tokenStream)
    tree := dslParser.Start()

    listener := &parser.MyDslQueryListener{}
    antlr.ParseTreeWalkerDefault.Walk(listener, tree)

    esQuery := listener.Stack[0]

    src, err := esQuery.Source()
    if err != nil {
        panic(err)
    }
    data, err := json.MarshalIndent(src, "", "  ")
    if err != nil {
        panic(err)
    }

    stringEsQuery := string(data)
    fmt.Println(stringEsQuery)
}

/**     Generated es query
{
  "bool": {
    "should": [
      {
        "bool": {
          "must": [
            {
              "bool": {
                "should": [
                  {
                    "bool": {
                      "must": {
                        "term": {
                          "color": "blue"
                        }
                      }
                    }
                  },
                  {
                    "bool": {
                      "must": {
                        "term": {
                          "color": "red"
                        }
                      }
                    }
                  }
                ]
              }
            },
            {
              "bool": {
                "must": {
                  "term": {
                    "model": "hyundai"
                  }
                }
              }
            }
          ]
        }
      },
      {
        "bool": {
          "must": {
            "term": {
              "price": "2000"
            }
          }
        }
      }
    ]
  }
}

*/
Omurbek Kadyrbekov
fonte
2

Você já pensou em converter suas instruções do tipo sql em consultas de string ?

curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
    "query": {
        "query_string" : {
            "query" : "(new york city) OR (big apple)",
            "default_field" : "content"
        }
    }
}
'

Se seus casos de uso continuarem simples color="red" and price=20000 or model="hyundai" and (seats=4 or year=2001), eu usaria o que foi dito acima. A sintaxe é bastante poderosa, mas é garantido que as consultas são executadas mais lentamente do que as consultas DSL nativas e detalhadas, pois o analisador ES precisará convertê-las em DSL para você.

jzzfs
fonte
A coisa aqui é que vou precisar de validação de propriedades passadas. Diga se o usuário digitou incorretamente "pricee" ou, em vez de passar o número, passa um valor inválido. (ex: "preço = adfasdf")
Omurbek Kadyrbekov
Bem, isso é outra história. Você pode querer obter seu mapeamento primeiro ( GET index_name/_mapping), identificar quais campos você deseja expor aos usuários para pesquisar (para poder criar seu validador ou uma funcionalidade "você quis dizer"). Se você deseja aplicar os tipos de dados do valor do campo, também pode extrair essas informações do mapeamento ...
jzzfs 27/02