This functionality is only applicable when building a QPL query manually, if you want custom operators for the parser, you will need to generate a new grammar in which case, please contact the development team

PyQPL allows to include custom operators in your query and even add translation for these. There are 2 ways to make use of a custom operator, besides the translation process, one quick and simple and another one defining the operator with a class.

For both methods we need to make use of manual operator function (op)


qpl.op(type_='MY_OPERATOR', operands='one', fields=['title'], boost=1.0)


Implementing a Custom Operator


Simple Custom Operator

This method only consist on using the manual operator function (op), and adding the custom operator as the type, in this example GEO

Implementation

qpl = QPL(options=options)

qpl_query = qpl.and_(operands=[
    qpl.or_(operands=[
        qpl.phrase('San Jose'),
        qpl.phrase('Costa Rica'),
        qpl.not_(qpl.phrase('California'))
    ]),
    qpl.op(type_='geo', operands=['9°54\'52.5"N', '84°05\'15.5"W'], fields='geo_point', boost=6)
])

print(qpl_query)


The resulting qpl query will give this

and(or(phrase(San Jose),phrase(Costa Rica),not(phrase(California))),geo_point:geo(9°54'52.5"N,84°05'15.5"W)^6)

From the important part is to know the operands(parameters) provided to the the function, since they could be anything we need to be extra careful when doing the translation

Custom Class Operator

This method is a more formal way to define the custom operator since it relays on a class to process, manipulate and optimize the query

Class Definition

The custom operator inherits from Operator Class, there for you can choose what to overwrite in order to fit your needs, in this case, we are validating that the operands are string, getting the cardinal coordinates and translating them into decimal coordinates

The operands are always expected to be Operators or string, so we need to transform the decimals into strings at the end

import re

from pyqpl.qpl import QPLOptions, QPL, Operator
from pyqpl.qpl.operator import OperatorOrStr



class GeoOP(Operator):

    @staticmethod
    def dms_to_decimal(dms_coordinate):
        # Use regular expressions to extract degrees, minutes, seconds, and cardinal direction
        pattern = r"(\d+)°(\d+)'([\d.]+)\"([NSWE])"
        match = re.match(pattern, dms_coordinate)

        if match:
            degrees = int(match.group(1))
            minutes = int(match.group(2))
            seconds = float(match.group(3))
            cardinal = match.group(4)

            # Convert degrees, minutes, and seconds to decimal degrees
            decimal = degrees + minutes / 60 + seconds / 3600

            # Apply the cardinal direction to determine the sign of the decimal coordinate
            if cardinal in ['S', 'W']:
                decimal = -decimal

            return decimal
        else:
            raise ValueError("Invalid coordinate format")

    def add_operand(self, operand: OperatorOrStr):

        if isinstance(operand, str):
            op = self.dms_to_decimal(operand)
        else:
            raise ValueError('Invalid operand type, str expected')

        super().add_operand(str(op))

    def _optimize(self):
        super()._optimize()


Implementation

Then when initializing the QPLOptions, we add the parameter custom_operators, this parameter accepts a dictionary, indicating the type of the operator and the class which will handle the type. 

The rest of the example is the same but check the result

options = QPLOptions(fields='content', custom_operators={
    'geo': GeoOP
})

qpl = QPL(options=options)

qpl_query = qpl.and_(operands=[
    qpl.or_(operands=[
        qpl.phrase('San Jose'),
        qpl.phrase('Costa Rica'),
        qpl.not_(qpl.phrase('California'))
    ]),
    qpl.op(type_='geo', operands=['9°54\'52.5"N', '84°05\'15.5"W'], fields='geo_poit', boost=6)
])

print(qpl_query)


The result is the same qpl as with the simple method, but this time the coordinates where translated into decimal coordinates

and(or(phrase(San Jose),phrase(Costa Rica),not(phrase(California))),geo_poit:geo(9.914583333333333,-84.08763888888889)^6)

Translating a Custom Operator

Translating a custom operator is the same as overwriting the transformation of a default operator, we will use the same steps as in Query Translation, but jus adjust the example to fit the ones used in this page


import json

from pyqpl.translators import ElasticsearchTranslator
from pyqpl.qpl import OperatorType, Operator

def geo_point_query(operand: Operator):
    field = operand.fields_or_defaults[0]

    boost = float(field.boost)

    if operand.boost != 1:
        boost = operand.boost

    return {

        field.name: {
            "type": "Point",
            "coordinates": [float(operand.operands[0]), float(operand.operands[1])],
            "boots": boost
        }
    }


translator = ElasticsearchTranslator(custom_or_overwrites={
    'geo': geo_point_query
})

engine_query = translator.to_engine_query(qpl_query)

print(json.dumps(engine_query))
{
  "bool": {
    "must": [
      {
        "bool": {
          "should": [
            {
              "multi_match": {
                "type": "phrase",
                "query": "San Jose",
                "fields": [
                  "content"
                ]
              }
            },
            {
              "multi_match": {
                "type": "phrase",
                "query": "Costa Rica",
                "fields": [
                  "content"
                ]
              }
            },
            {
              "bool": {
                "must_not": [
                  {
                    "multi_match": {
                      "type": "phrase",
                      "query": "California",
                      "fields": [
                        "content"
                      ]
                    }
                  }
                ]
              }
            }
          ],
          "minimum_should_match": 1
        }
      },
      {
        "geo_poit": {
          "type": "Point",
          "coordinates": [
            9.914583333333333,
            -84.08763888888889
          ],
          "boots": 6.0
        }
      }
    ]
  }
}
  • No labels