We are building our own query language similar to Mysql using antlr4. Except we only use where clause, in other words user does not enter select/from statements.
I was able to create grammar for it and generate lexers/parsers/listeners in golang.
Below our grammar file 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
;
...
Query example: color="red" and price=20000 or model="hyundai" and (seats=4 or year=2001)
ElasticSearch supports sql queries with plugin here: https://github.com/elastic/elasticsearch/tree/master/x-pack/plugin/sql.
Having hard time to understand java code.
Since we have Logical Operators I am quite not sure how to get parse tree and convert it to ES query. Can somebody help/suggest ideas?
Update 1: Added more examples with corresponding ES query
Query Example 1: color="red" AND price=2000
ES query 1:
{
"query": {
"bool": {
"must": [
{
"terms": {
"color": [
"red"
]
}
},
{
"terms": {
"price": [
2000
]
}
}
]
}
},
"size": 100
}
Query Example 2: color="red" AND price=2000 AND (model="hyundai" OR model="bmw")
ES query 2:
{
"query": {
"bool": {
"must": [
{
"bool": {
"must": {
"terms": {
"color": ["red"]
}
}
}
},
{
"bool": {
"must": {
"terms": {
"price": [2000]
}
}
}
},
{
"bool": {
"should": [
{
"term": {
"model": "hyundai"
}
},
{
"term": {
"region": "bmw"
}
}
]
}
}
]
}
},
"size": 100
}
Query Example 3: color="red" OR color="blue"
ES query 3:
{
"query": {
"bool": {
"should": [
{
"bool": {
"must": {
"terms": {
"color": ["red"]
}
}
}
},
{
"bool": {
"must": {
"terms": {
"color": ["blue"]
}
}
}
}
]
}
},
"size": 100
}
Working demo url: https://github.com/omurbekjk/convert-dsl-to-es-query-with-antlr, estimated time spent: ~3 weeks
After investigating antlr4 and several examples I found simple solution with listener and stack. Similar to how expressions are calculated using stack.
We need to overwrite to default base listener with ours to get triggers for each enter/exit grammar rules. Important rules are:
Comparison expression (price=200, price>190)
Logical operators (OR, AND)
Brackets (in order to correctly build es query we need to write correct grammar file remembering operator precedence, that's why brackets are in the first place in the grammar file)
Below my custom listener code written in 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)
}
And main file:
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"
}
}
}
}
]
}
}
*/
Have you thought about converting your sql-like statements to query string queries?
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"
}
}
}
'
If your use-cases stay simple like color="red" and price=20000 or model="hyundai" and (seats=4 or year=2001), I'd go with the above. The syntax is quite powerful but the queries are guaranteed to run more slowly than the native, spelled-out DSL queries since the ES parser will need to convert them to the DSL for you.
There is a software called Dremio https://www.dremio.com/
It can translate SQL query to elastic search query
https://www.dremio.com/tutorials/unlocking-sql-on-elasticsearch/
Related
Customer appointments with top level locationId sample data set:
[
{
"locationId": 9999,
"customerAppointments": [
{
"customerId": "1",
"appointments": [
{
"appointmentId": "cbbce566-da59-42c2-8845-53976ba63d56",
"locationName": "Sullivan St"
},
{
"appointmentId": "5f09e2af-ddae-47aa-9f7c-fd1001a9c5e6",
"locationName": "Oak St"
}
]
},
{
"customerId": "2",
"appointments": [
{
"appointmentId": "964a3c1c-ccec-4082-99e2-65795352ba79",
"locationName": "Kellet St"
}
]
},
{
"customerId": "3",
"appointments": []
}
]
},
{
...
},
{
...
}
]
I need to pull out appointment by locationId and customerId and only get the appointment for that customerId e.g
Sample response:
[
{
"appointmentId": "964a3c1c-ccec-4082-99e2-65795352ba79",
"locationName": "Kellet St"
}
]
Tried below query, but it just returns all records for all customers ids (which is kind of expected):
db.getCollection("appointments").find(
{
"locationId" : NumberInt(9999),
"customerAppointments" : {
"$elemMatch" : {
"customerId" : "2"
}
}
}
);
But how can I get just the appointment record for a specific customerId?
When asking this question I was unaware of the older version of MongoDB driver (< v5) so we cannot use the $getField operator.
However, this query seems to work well:
db.getCollection("appointments").aggregate([
{
$match: {
"locationId": NumberInt(9999)
}
},
{
$unwind: "$customerAppointments"
},
{
$match: {
"customerAppointments.customerId": "2"
}
},
{
$project: {
appointments: "$customerAppointments.appointments"
}
}
]);
Yields:
{
"_id" : ObjectId("63eebe95c7a0da54804c1db2"),
"appointments" : [
{
"appointmentId" : "964a3c1c-ccec-4082-99e2-65795352ba79",
"locationName" : "Kellet St"
}
]
}
I wrote this query in SQL and now I needed it in the elastic search.
How can I do that?
select * from listings where condition1 = true or (condition2 = 1 and condition3 = false)
Here you go:
POST listings/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"condition1": {
"value": "true"
}
}
},
{
"bool": {
"must": [
{
"term": {
"condition2": {
"value": "1"
}
}
},
{
"term": {
"condition3": {
"value": "false"
}
}
}
]
}
}
]
}
}
}
You need to use should clause for or and must clause for and.
You need to use term or match query based on your requirement.
I was working on a query that I found. It's a little bit more complex than I thought. This is part of each data document which is distinguished by an event name.
"eventTime" : "2021-07-11T08:29:00-0800",
"userId" : "P9QuPERPURPC3swJpyBb4",
"eventName" : "mko", // mko and mkp are two possible values
"eventData" : {}
The target is: userIds who have eventName('mko') AND does not have eventName('mkp')
I could not precisely understand what is the best way to handle 'not exist' in Elasticsearch queries. I'd appreciate any help.
I think the below queries might help you.
Get a user with the name mko:
{
"query": {
"bool": {
"must": [
{
"term": {
"eventName": "mko"
}
}
]
}
}
}
Get a user with a name other than mko:
{
"query": {
"bool": {
"must_not": [
{
"term": {
"eventName": "mko"
}
}
]
}
}
}
Get a user with the name mkp:
{
"query": {
"bool": {
"must": [
{
"term": {
"eventName": "mkp"
}
}
]
}
}
}
Get a user with a name other than mkp:
{
"query": {
"bool": {
"must_not": [
{
"term": {
"eventName": "mkp"
}
}
]
}
}
}
To get the selected fields only you can use the _source field in query: https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html.
And to check whether the field eventName is exists in a document. You can use the exists query within the must queries: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html
'''select count(*) as count from activity where project_id in (61,129) and (entry_device_id in (1068,1069) or exit_device_id in (1068,1069) );'''
I tried with should in elastic query and match but not getting the desired results.
Got some idea from elasticsearch bool query combine must with OR
And tried but not getting the correct results.
Need help in this
Depending on your index's mapping, a combination of terms queries should get you started:
GET your_activity_index/_count
{
"query": {
"bool": {
"must": [
{
"terms": {
"project_id": [ 61, 129 ]
}
},
{
"bool": {
"should": [
{
"terms": {
"entry_device_id": [ 1068, 1069 ]
}
},
{
"terms": {
"exit_device_id": [ 1068, 1069 ]
}
}
]
}
}
]
}
}
}
I want to regex search an integer value in MongoDB. Is this possible?
I'm building a CRUD type interface that allows * for wildcards on the various fields. I'm trying to keep the UI consistent for a few fields that are integers.
Consider:
> db.seDemo.insert({ "example" : 1234 });
> db.seDemo.find({ "example" : 1234 });
{ "_id" : ObjectId("4bfc2bfea2004adae015220a"), "example" : 1234 }
> db.seDemo.find({ "example" : /^123.*/ });
>
As you can see, I insert an object and I'm able to find it by the value. If I try a simple regex, I can't actually find the object.
Thanks!
If you are wanting to do a pattern match on numbers, the way to do it in mongo is use the $where expression and pass in a pattern match.
> db.test.find({ $where: "/^123.*/.test(this.example)" })
{ "_id" : ObjectId("4bfc3187fec861325f34b132"), "example" : 1234 }
I am not a big fan of using the $where query operator because of the way it evaluates the query expression, it doesn't use indexes and the security risk if the query uses user input data.
Starting from MongoDB 4.2 you can use the $regexMatch|$regexFind|$regexFindAll available in MongoDB 4.1.9+ and the $expr to do this.
let regex = /123/;
$regexMatch and $regexFind
db.col.find({
"$expr": {
"$regexMatch": {
"input": {"$toString": "$name"},
"regex": /123/
}
}
})
$regexFinAll
db.col.find({
"$expr": {
"$gt": [
{
"$size": {
"$regexFindAll": {
"input": {"$toString": "$name"},
"regex": "123"
}
}
},
0
]
}
})
From MongoDB 4.0 you can use the $toString operator which is a wrapper around the $convert operator to stringify integers.
db.seDemo.aggregate([
{ "$redact": {
"$cond": [
{ "$gt": [
{ "$indexOfCP": [
{ "$toString": "$example" },
"123"
] },
-1
] },
"$$KEEP",
"$$PRUNE"
]
}}
])
If what you want is retrieve all the document which contain a particular substring, starting from release 3.4, you can use the $redact operator which allows a $conditional logic processing.$indexOfCP.
db.seDemo.aggregate([
{ "$redact": {
"$cond": [
{ "$gt": [
{ "$indexOfCP": [
{ "$toLower": "$example" },
"123"
] },
-1
] },
"$$KEEP",
"$$PRUNE"
]
}}
])
which produces:
{
"_id" : ObjectId("579c668c1c52188b56a235b7"),
"example" : 1234
}
{
"_id" : ObjectId("579c66971c52188b56a235b9"),
"example" : 12334
}
Prior to MongoDB 3.4, you need to $project your document and add another computed field which is the string value of your number.
The $toLower and his sibling $toUpper operators respectively convert a string to lowercase and uppercase but they have a little unknown feature which is that they can be used to convert an integer to string.
The $match operator returns all those documents that match your pattern using the $regex operator.
db.seDemo.aggregate(
[
{ "$project": {
"stringifyExample": { "$toLower": "$example" },
"example": 1
}},
{ "$match": { "stringifyExample": /^123.*/ } }
]
)
which yields:
{
"_id" : ObjectId("579c668c1c52188b56a235b7"),
"example" : 1234,
"stringifyExample" : "1234"
}
{
"_id" : ObjectId("579c66971c52188b56a235b9"),
"example" : 12334,
"stringifyExample" : "12334"
}
Now, if what you want is retrieve all the document which contain a particular substring, the easier and better way to do this is in the upcoming release of MongoDB (as of this writing) using the $redact operator which allows a $conditional logic processing.$indexOfCP.
db.seDemo.aggregate([
{ "$redact": {
"$cond": [
{ "$gt": [
{ "$indexOfCP": [
{ "$toLower": "$example" },
"123"
] },
-1
] },
"$$KEEP",
"$$PRUNE"
]
}}
])