¿Convertir SQL como consultas dsl personalizadas a ElasticSearch?

8

Estamos construyendo nuestro propio lenguaje de consulta similar a Mysql usando antlr4. Excepto que solo usamos where clause, en otras palabras, el usuario no ingresa select/fromdeclaraciones.

Pude crear gramática para ello y generar lexers / parsers / listeners en golang.

Debajo de nuestro archivo de gramática 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
   ;

...

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

ElasticSearch admite consultas sql con el complemento aquí: https://github.com/elastic/elasticsearch/tree/master/x-pack/plugin/sql .

Tener dificultades para entender el código Java.

Como tenemos operadores lógicos, no estoy muy seguro de cómo obtener el árbol de análisis y convertirlo a la consulta ES. ¿Alguien puede ayudar / sugerir ideas?

Actualización 1: se agregaron más ejemplos con la consulta ES correspondiente

Consulta Ejemplo 1: color="red" AND price=2000

Consulta ES 1:

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

Consulta Ejemplo 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
}


Consulta Ejemplo 3: color="red" OR color="blue"

ES consulta 3:

{
    "query": {
      "bool": {
        "should": [
          {
            "bool": {
              "must": {
                "terms": {
                  "color": ["red"]
                }
              }
            }
          },
          {
            "bool": {
              "must": {
                "terms": {
                    "color": ["blue"]
                }
              }
            }
          }
        ]
      }
    },
    "size": 100
  }

Omurbek Kadyrbekov
fuente
Considere agregar algunos resultados de ejemplo. ¿Cómo se color="red" and price=20000 or model="hyundai" and (seats=4 or year=2001ve en la sintaxis de ES? ¿Desea la sintaxis JSON, o la sintaxis de cadena de consulta corta, o algo completamente diferente? También ayuda si agrega más de 1 ejemplo. Además, ¿ya has probado algo tú mismo?
Bart Kiers
He agregado más ejemplos. Sí, quiero construir la sintaxis json desde el árbol de análisis. Estoy en progreso de hacerlo con golang, aún no lo he terminado
Omurbek Kadyrbekov

Respuestas:

6

URL de demostración de trabajo: https://github.com/omurbekjk/convert-dsl-to-es-query-with-antlr , tiempo estimado invertido: ~ 3 semanas

Después de investigar antlr4 y varios ejemplos, encontré una solución simple con listener y stack. Similar a cómo se calculan las expresiones usando stack.

Necesitamos sobrescribir al oyente base predeterminado con el nuestro para obtener disparadores para cada regla gramatical de entrada / salida. Las reglas importantes son:

  1. Expresión de comparación (precio = 200, precio> 190)
  2. Operadores lógicos (OR, AND)
  3. Paréntesis (para construir correctamente la consulta es necesario escribir un archivo de gramática correcto recordando la precedencia del operador, es por eso que los paréntesis están en primer lugar en el archivo de gramática)

Debajo de mi código de escucha personalizado escrito en 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)
}

Y archivo 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
fuente
2

¿Has pensado en convertir tus declaraciones tipo sql para consultas de cadena de consulta ?

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"
        }
    }
}
'

Si sus casos de uso siguen siendo simples color="red" and price=20000 or model="hyundai" and (seats=4 or year=2001), iría con lo anterior. La sintaxis es bastante potente, pero se garantiza que las consultas se ejecutarán más lentamente que las consultas DSL nativas y detalladas, ya que el analizador ES necesitará convertirlas a DSL por usted.

jzzfs
fuente
Lo que ocurre aquí es que necesitaré la validación de las propiedades pasadas. Indique si el usuario escribió "precio" incorrectamente o, en lugar de pasar un número, pasa un valor no válido. (ej .: "precio = adfasdf")
Omurbek Kadyrbekov
Bueno, esa es otra historia. Es posible GET index_name/_mappingque desee obtener su mapeo primero ( ), identificar qué campos querrá exponer a los usuarios para que busquen (para que pueda construir su validador o una funcionalidad "did-you-mean"). Si desea aplicar los tipos de datos de valor de campo, también puede extraer esa información de la asignación ...
jzzfs