工程

Elasticsearch SQL 简介(附练习示例)— 第 1 部分

Elastic Stack 6.3 版是我们近期以来包含功能最丰富的发布版本之一。如果开放 X-Pack 和添加数据上卷汇总功能 (rollup) 尚不足以让您感到兴奋,我们还宣布新增了另外一项试验功能,即 SQL 支持,这定能让您眼前一亮。在 Elasticsearch 相对较短的发展历程中,这是人们呼声最高、请求次数最频繁的功能之一。

在本系列博客文章中,我们将会探索 Elasticsearch SQL 目前支持的某些功能和具备的某些能力。我们还会开诚布公地讨论此版本的某些局限之处,同时也会讲到未来的开发计划。

目标受众

在 Elastic 的发展历程中,出于多种原因,对于向产品中添加 SQL,我们一直都比较犹豫。下边是我们较为担心的常见问题:

  • SQL 支持到底应该包括哪些内容?
  • 我们将支持哪些功能?更常见的情况,这一问题可以表述为“我们是否支持 JOIN 语句?
  • 表达式/函数呢?分组呢?”
  • 我们需要支持 JDBC/ODBC 连接模式吗?
  • 我们真的需要支持 SQL 吗?还是只是由于我们未能向新用户提供足够的材料来让他们熟悉我们自有的领域特定语言 (DSL)?

经过数轮开发过程,我们终于成功筛选出了一些我们认为对用户很实用的必备功能。根据我们与用户的讨论,我们认为 SQL 对下面两类受众的益处最大:

  • Stack 的新用户,他们在入门阶段可能会觉得 Elasticsearch DSL 有点过于繁复,或者他们只是觉得根本没有必要学习完整语法。举个例子,用户可能出于性能和扩展性等原因而需要转换现有的基于 SQL 的应用,然而他们只想使用同等的查询,而不想学习完整语法。我们同时还意识到,学习新“语言”的一种常见策略就是基于已了解的知识寻找同等的内容。
  • 数据消费者,这些人根本没有学习完整 Elasticsearch DSL 的想法或需求。此类人群包括单纯希望提取数据进行外部处理的数据科学家,还包括另外一类人群,即技术背景较为薄弱的 BI 用户,他们对 SQL 比较熟悉而且每天都会使用。

除了上述受众外,另外一点毋庸置疑的是,作为一种声明式编程语言,SQL 对所有用户而言都是一种极具吸引力的范例,这一点在我们的这一博文系列中也得到了很好的体现。SQL 之所以如此流行,毫无疑问是由于其能够表达计算过程的逻辑,也能够表达您希望实现的目标的逻辑,而且还无需定义控制流。除此之外,正如我们所展示的,部分 SQL 查询能够轻松优雅地定义一个问题,而如果使用具有等同效能的 Elasticsearch DSL,则会变得十分繁复冗长。虽然 Elasticsearch DSL 能够轻松描述全文本搜索问题,但是在描述基于结构化分析内容的查询时,SQL 的效果会更好一些。

Elasticsearch SQL 是什么,Elasticsearch SQL 不是什么…

在这一初期发布版本中,Elasticsearch SQL 提供了一个只读接口,此界面符合 ANSI SQL 标准的一个子集,并且允许以表格式数据源来呈现 Elasticsearch。我们在此标准之上还提供了更多运算符,因此相对于 RDBMS 型实施用例而言,其显示出了 Elasticsearch 的独有能力。我们的目标是提供一个轻量型快捷的实施用例,从而将外部依赖因素和移动组件降至最低。然而,无论如何,这一最初发布版本都不能将 Elasticsearch 转变为纯粹的关系型数据库(具有其相关属性),也不能消除数据建模的必要性。尽管 SQL 插件实施了某些数据操作函数和表达式,但是只要结果计数和顺序受到影响,或者请求分组,仍会坚持向下推送原则。这一点将目前 Elasticsearch SQL 插件的数据处理能力限制为仅可进行结果操作(例如针对字段的函数),也将客户端(JDBC 驱动器/CLI 或浏览器)限制为仅具备渲染能力。这一方法充分利用了 Elasticsearch 的可扩展性和性能,让其能够完成较为艰巨的任务。

elasticsearch-sql.png

概念对应:索引和文档 vs. 表格和行

在 Elasticsearch 的早期阶段,我们经常会用索引和类型来类比 RDBMS 数据库和表格,这样做的主要目的是帮助用户理解并应用较为生疏的概念。如 Elasticsearch 6.3 文档中所解释的那样,这一类比实际上并不正确,而且还可能会很危险。虽然我们移除了类型,但我们仍需要在 Elasticsearch 的非模式、基于文档的模型和 SQL 的强类型概念之间建立起正确并且可用的逻辑对等关系。

幸运的是,和 RDBMS 表格一样,Elastic 索引也是实体隔离的,而且使用方式也基本一样(即存储相关数据)。行和文档也可以很自然地进行类比,因为二者都为对字段/列进行分组提供了一种机制,尽管行更为严格(执行力度较强),而文档则较为灵活或松散(然而仍然是具有一定结构的)。Elasticsearch 中的字段代表了一个名称条目,支持多种数据类型,可能会包括多个值(即列表)。除了这些例外的多值字段外,字段的概念可直接对应至 SQL 的列。备注:如果尝试对多值字段进行 SELECT 操作,您将会在搜索时收到错误消息。

其他概念并不能直接对应,SQL 数据库与 Elasticsearch 集群之间的对应性就较低。然而,这些通常并不会影响到 Elasticsearch SQL 的用户。有关这一主题的更多详细信息,请参阅我们的文档 SQL 和 Elasticsearch 之间的概念对应关系

总而言之,在 WHERE 子句中使用索引名称来定位具体的索引/表格。系统便会以行的形式为您返回文档,并将字段映射至列。由于这一基本透明的对应关系,我们在之后会交替使用这些词语。

总而言之,在 WHERE 子句中使用索引名称来定位具体的索引/表格。系统便会以行的形式为您返回文档,并将字段映射至列。由于这一基本透明的对应关系,我们在之后会交替使用这些词语。

实施过程的内部剖析

Elasticsearch SQL 的实施过程包括 4 个执行阶段:

elasticsearch-sql-implementation.png

解析阶段负责将 SQL 查询转变为抽象语法树 (AST)。所有的语法验证都会在此阶段完成,然后分析工具才会验证 AST 并处理表格、列、函数、昵称和名称空间来生成逻辑计划。然后会对此计划进行优化,包括删除任何的冗余表达式,然后才会转化为实体计划(即 Elasticsearch DSL)加以执行。再然后查询执行工具会运行实际查询并将结果输送至客户端,完成任何必需的类型和表格转换(例如聚合树至表格)。

连接方法

无论提供任何 SQL 解决方案,连接性支持都是一个至关重要的考虑因素。虽然部分用户可能会接受纯 REST 接口,但大部分用户都希望能够通过标准接口(通常为 JDBC 和 ODBC)进行连接。虽然我们也计划提供 ODBC 支持并且正在进行开发,但是我们在初次发布中仅会提供 JDBC 供人们下载

十分重要的一点,与这些驱动器的所有通信仍会通过我们的 REST 接口以 HTTP 协议的形式完成。这有数项巨大优势:

  1. 关于授予用户 SQL 访问权限的过程,其和开启并暴露 Elastic 端口然后与安全功能进行本地集成并没有什么两样。所以,我们能够在 Elastic Coud 上的托管式 Elasticsearch Service 中直接支持 SQL,同时现有用户能够直接将其与访问权控制许可进行结合。
  2. 这使得我们能够直接通过 REST 接口利用 SQL,并额外发布 CLI 客户端来提供便利。我们预计 CLI 客户端尤其会受到管理员的欢迎,因为是他们熟悉的 RDBMS 中常见的命令行互动方式。

JDBC 驱动器会利用新创建的 XContent 库,该库负责查询和响应解析(过去,此代码与 Elasticsearch 紧密耦合)。这就避免了驱动器还要依赖全部 Elasticsearch 库的需要,进而确保其能保持轻量型和便携式的特点。我们会在未来的发布版本中继续改善这一解耦过程,以打造更加小巧、更加快捷的驱动器。

几个简单示例

我们接下来看几个结合使用 CLI 和 REST API 的示例。在示例中,我们会使用简单的数据集,这一数据集很快就会在 Kibana 中进行分发。如果等不及发布新版本,您也可以在 demo.elastic.co 中找到这个航班数据集,在那里便可通过 Kibana 控制台执行下列示例。我们提供 demo.elastic.co 的链接,此链接应该能够自动填充本博客文章中的所有相关查询。或者,我们也可提供需在演示 Kibana 控制台中执行的完整查询列表。在某些情况下,如果未对示例查询进行明确排序或限制,结果可能会有差异,这是因为如果未应用相关性或排序顺序,Elasticsearch 会对结果进行自然排序。

提取 Elasticsearch 模式信息:DSL vs. SQL

我们首先需要确定表格/索引的模式,以及可供我们使用的字段。我们将会通过 REST 界面进行这一步骤:

请求

POST _xpack/sql
{
  "query":"DESCRIBE flights"
}

在 demo.elastic.co 上亲自尝试

响应

{
  "columns": [
    {
      "name": "column",
      "type": "keyword"
    },
    {
      "name": "type",
      "type": "keyword"
    }
  ],
  "rows": [
    [ "AvgTicketPrice", "REAL" ],
    [ "Cancelled", "BOOLEAN" ],
    [ "Carrier", "VARCHAR" ],
    [ "Dest", "VARCHAR" ],
    [ "DestAirportID", "VARCHAR" ],
    [ "DestCityName", "VARCHAR" ],
    [ "DestCountry", "VARCHAR" ],
    [ "DestLocation", "OTHER" ],
    [ "DestRegion", "VARCHAR" ],
    [ "DestWeather", "VARCHAR" ],
    [ "DistanceKilometers", "REAL" ],
    [ "DistanceMiles", "REAL" ],
    [ "FlightDelay", "BOOLEAN" ],
    [ "FlightDelayMin", "INTEGER" ],
    [ "FlightDelayType", "VARCHAR" ],
    [ "FlightNum", "VARCHAR" ],
    [ "FlightTimeHour", "VARCHAR" ],
    [ "FlightTimeMin", "REAL" ],
    [ "Origin", "VARCHAR" ],
    [ "OriginAirportID", "VARCHAR" ],
    [ "OriginCityName", "VARCHAR" ],
    [ "OriginCountry", "VARCHAR" ],
    [ "OriginLocation", "OTHER" ],
    [ "OriginRegion", "VARCHAR" ],
    [ "OriginWeather", "VARCHAR" ],
    [ "dayOfWeek", "INTEGER" ],
    [ "timestamp", "TIMESTAMP" ]
  ]
}

上述响应也可通过 URL 参数 ?format=txt 以表格形式来显示。例如:

POST _xpack/sql?format=txt 
{
  "query":"DESCRIBE flights"
}

在 demo.elastic.co 上亲自尝试

column          |     type      
--------------------------+---------------
AvgTicketPrice            |REAL           
Cancelled                 |BOOLEAN        
Carrier                   |VARCHAR        
Carrier.keyword           |VARCHAR        
Dest                      |VARCHAR        
Dest.keyword              |VARCHAR        
DestAirportID             |VARCHAR        
DestAirportID.keyword     |VARCHAR        
DestCityName              |VARCHAR        
DestCityName.keyword      |VARCHAR        
DestCountry               |VARCHAR        
DestCountry.keyword       |VARCHAR        
DestLocation              |STRUCT         
DestLocation.lat          |VARCHAR        
DestLocation.lat.keyword  |VARCHAR        
DestLocation.lon          |VARCHAR        
DestLocation.lon.keyword  |VARCHAR        
DestRegion                |VARCHAR        
DestRegion.keyword        |VARCHAR        
DestWeather               |VARCHAR        
DestWeather.keyword       |VARCHAR        
DistanceKilometers        |REAL           
DistanceMiles             |REAL           
FlightDelay               |BOOLEAN        
FlightDelayMin            |BIGINT         
FlightDelayType           |VARCHAR        
FlightDelayType.keyword   |VARCHAR        
FlightNum                 |VARCHAR        
FlightNum.keyword         |VARCHAR        
FlightTimeHour            |REAL           
FlightTimeMin             |REAL           
Origin                    |VARCHAR        
Origin.keyword            |VARCHAR        
OriginAirportID           |VARCHAR        
OriginAirportID.keyword   |VARCHAR        
OriginCityName            |VARCHAR        
OriginCityName.keyword    |VARCHAR        
OriginCountry             |VARCHAR        
OriginCountry.keyword     |VARCHAR        
OriginLocation            |STRUCT         
OriginLocation.lat        |VARCHAR        
OriginLocation.lat.keyword|VARCHAR        
OriginLocation.lon        |VARCHAR        
OriginLocation.lon.keyword|VARCHAR        
OriginRegion              |VARCHAR        
OriginRegion.keyword      |VARCHAR        
OriginWeather             |VARCHAR        
OriginWeather.keyword     |VARCHAR        
dayOfWeek                 |BIGINT         
timestamp                 |TIMESTAMP

之后如果需要提供来自 REST API 的示例响应,我们都会使用上面所示的表格式响应结构。要想通过控制台实现同样的查询,我们需要使用下列信息登录:

./elasticsearch-sql-cli http://elastic@localhost:9200

回应密码请求之后...

sql> DESCRIBE flights;
column      |     type
------------------+---------------
AvgTicketPrice    |REAL
Cancelled         |BOOLEAN
Carrier           |VARCHAR
Dest              |VARCHAR
DestAirportID     |VARCHAR
DestCityName      |VARCHAR
DestCountry       |VARCHAR
DestLocation      |OTHER
DestRegion        |VARCHAR
DestWeather       |VARCHAR
DistanceKilometers|REAL
DistanceMiles     |REAL
FlightDelay       |BOOLEAN
FlightDelayMin    |INTEGER
FlightDelayType   |VARCHAR
FlightNum         |VARCHAR
FlightTimeHour    |VARCHAR
FlightTimeMin     |REAL
Origin            |VARCHAR
OriginAirportID   |VARCHAR
OriginCityName    |VARCHAR
OriginCountry     |VARCHAR
OriginLocation    |OTHER
OriginRegion      |VARCHAR
OriginWeather     |VARCHAR
dayOfWeek         |INTEGER
timestamp         |TIMESTAMP
sql>

如对 SELECT 子句中显示的字段进行任何查询,也会返回上述模式,这能够为潜在驱动器提供必需的类型信息以便对结果进行格式化或运算。例如,思考一下包含 LIMIT 子句的 SELECT 查询,并确保响应尽可能简短。我们默认会返回 1000 行。

简单的 SELECT

POST _xpack/sql?format=txt
{
  "query":"SELECT FlightNum FROM flights LIMIT 1"
}

在 demo.elastic.co 上亲自尝试(结果可能会不同)

   FlightNum   
---------------
1Y0TZOE

JDBC 驱动器和控制台使用同样的请求/响应,但是用户并不会看到。

sql> SELECT OriginCountry, OriginCityName FROM flights LIMIT 1;
OriginCountry |OriginCityName
---------------+---------------
US      |San Diego

在 demo.elastic.co 上亲自尝试(结果可能会不同)

请注意,如果您请求不存在的字段(区分大小写),表格式强类型存储的语义意味着将会返回错误消息,这与 Elasticsearch 的行为不同,因为在 Elasticsearch 中只是不返回该字段而已。举例说明,更改上述内容,将 “OriginCityName” 更改为 “OrigincityName”,将会出现下面的错误消息来为您提供帮助。

{
  "error": {
    "root_cause": [
      {
        "type": "verification_exception",
        "reason": "Found 1 problem(s)\nline 1:8: Unknown column [OrigincityName], did you mean any of [OriginCityName, DestCityName]?"
      }
    ],
    "type": "verification_exception",
    "reason": "Found 1 problem(s)\nline 1:8: Unknown column [OrigincityName], did you mean any of [OriginCityName, DestCityName]?"
  },
  "status": 400
}

在 demo.elastic.co 上亲自尝试

相似地,如果尝试对不兼容的字段应用函数或表达式,我们也会遇到相应的错误。通常而言,在验证 AST 时,分析工具很快便会遇到故障。为了实现这一点,Elasticsearch 必须了解每个字段的索引映射和能力。出于此原因,如果任何客户端需要访问具有安全功能的 SQL 界面,其都需要获得相应的授权许可

如果我们继续提供每个请求以及相关的响应,我们的博客文章将会变得超长无比!为简单起见,下面挑几个偏复杂的查询,我们还添加了一些您可能会感兴趣的备注。

使用 WHERE 和 ORDER BY 来进行 SELECT

“在飞行时间长于 5 个小时的航班中,找出美国始飞且用时最长的 10 个航班。”

POST _xpack/sql?format=txt
{
  "query":"SELECT OriginCityName, DestCityName FROM flights WHERE FlightTimeHour > 5 AND OriginCountry='US' ORDER BY FlightTimeHour DESC LIMIT 10"
}

在 demo.elastic.co 上亲自尝试

OriginCityName | DestCityName  
---------------+---------------
Atlanta        |Durban         
Louisville     |Melbourne      
Peoria         |Melbourne      
Albuquerque    |Durban         
Birmingham     |Durban         
Bangor         |Brisbane       
Seattle        |Durban         
Huntsville     |Sydney         
Savannah       |Shanghai       
Philadelphia   |Xi'an

对于旨在限制行数量的操作符,其根据 SQL 实施过程的不同而有所不同。对 Elasticsearch SQL 而言。我们在实施 LIMIT 操作符方面与 Postgresql/Mysql 保持一致。

数学题

下面是一些随机的数学题...

sql> SELECT ((1 + 3) * 1.5 / (7 - 6)) * 2 AS random;
    random
---------------
12.0

在 demo.elastic.co 上亲自尝试

这个示例表示服务器端会针对函数完成某些后期处理。这一点在 Elasticsearch DSL 查询中并没有等同项。

函数和表达式

“在 6 月份之后的航班中,查找飞行时间长于 5 个小时的所有航班,并按照从长到短的顺序进行排序。”

POST _xpack/sql?format=txt
{
  "query":"SELECT MONTH_OF_YEAR(timestamp), OriginCityName, DestCityName FROM flights WHERE FlightTimeHour > 5 AND MONTH_OF_YEAR(timestamp) > 6 ORDER BY FlightTimeHour DESC LIMIT 10"
}

在 demo.elastic.co 上亲自尝试

MONTH_OF_YEAR(timestamp [UTC])|OriginCityName |   DestCityName    
------------------------------+---------------+-------------------
7                             |Buenos Aires   |Shanghai           
7                             |Stockholm      |Sydney             
7                             |Chengdu        |Bogota             
7                             |Adelaide       |Cagliari           
7                             |Osaka          |Buenos Aires       
7                             |Buenos Aires   |Chitose / Tomakomai
7                             |Buenos Aires   |Shanghai           
7                             |Adelaide       |Washington         
7                             |Osaka          |Quito              
7                             |Buenos Aires   |Xi'an

要想在 Elasticsearch 中实现同等效果,这些函数通常需要编写 Painless 脚本,而 SQL 的函数式声明则避免了任何脚本编写工作。同时还需要注意在 WHERE 和 SELECT 子句中使用函数的方式。要想在 WHERE 子句使用函数,则仅能使用 Elasticsearch,因为函数会影响结果计数。然而 SELECT 语句中的函数由服务器端插件在演示时进行处理。

请注意可以通过 “SHOW FUNCTIONS” 提取可用函数列表。

在 demo.elastic.co 上亲自尝试

将这一功能与前述的数学计算能力相结合,我们可以着手编写多数用户难以在 DSL 中进行表述的复杂查询。

“在于周一、周二或周三上午 9 点到 11 点之间起飞且距离大于 500 km 的航班中,找到(速度)最快的两个航班的距离和平均速度。将距离和速度四舍五入到最近的整数。如果速度相等,首先显示距离最长的航班。”

在 demo.elastic.co 上亲自尝试

sql> SELECT timestamp, FlightNum, OriginCityName, DestCityName, ROUND(DistanceMiles) AS distance, ROUND(DistanceMiles/FlightTimeHour) AS speed, DAY_OF_WEEK(timestamp) AS day_of_week FROM flights WHERE DAY_OF_WEEK(timestamp) >= 0 AND DAY_OF_WEEK(timestamp) <= 2 AND HOUR_OF_DAY(timestamp) >=9 AND HOUR_OF_DAY(timestamp) <= 10 ORDER BY speed DESC, distance DESC LIMIT 2;
       timestamp        |   FlightNum   |OriginCityName | DestCityName  |   distance    |     speed     |  day_of_week
------------------------+---------------+---------------+---------------+---------------+---------------+---------------
2018-07-03T10:03:11.000Z|REPKGRT        |Melbourne      |Norfolk        |10199          |783            |2
2018-06-05T09:18:29.000Z|J72Y2HS        |Dubai          |Lima           |9219           |783            |2

这是一个十分复杂而且很奇怪的问题,但我们希望您能理解我们要表达的意思。您还要注意我们是如何创建字段昵称并在 ORDER BY 子句中引用这些昵称的。

还有一点需要注意,对于在 WHERE 和 ORDER BY 子句中使用的所有字段,没有必要在 SELECT 子句中一一指明。这一点可能会与您过去在 SQL 实施用例中所使用的方法有所不同。例如,下列脚本就是完全符合要求的:

POST _xpack/sql
{
  "query":"SELECT timestamp, FlightNum FROM flights WHERE AvgTicketPrice > 500 ORDER BY AvgTicketPrice"
}

在 demo.elastic.co 上亲自尝试

将 SQL 查询转化为 DSL

我们都曾遇到过某个难以在 Elasticsearch DSL 中表达的 SQL 查询,或者怀疑其是否为最佳方案。全新 SQL 接口的一个强大功能就是其能帮助 Elasticsearch 的初级用户解决此类问题。只需在 REST 接口上简单地向 “sql” 路径后附加 /translate,即可获取驱动器发放的 Elasticsearch 查询。

我们接下来思考一下之前的几个查询:

POST _xpack/sql/translate
{
  "query":"SELECT OriginCityName, DestCityName FROM flights WHERE FlightTimeHour > 5 AND OriginCountry='US' ORDER BY FlightTimeHour DESC LIMIT 10"
}

在 demo.elastic.co 上亲自尝试

对于任何有经验的 Elasticsearch 用户而言,应该可以很轻松得出等效的 DSL:

{
  "size": 10,
  "query": {
    "bool": {
      "filter": [
        {
          "range": {
            "FlightTimeHour": {
              "from": 5,
              "to": null,
              "include_lower": false,
              "include_upper": false,
              "boost": 1
            }
          }
        },
        {
          "term": {
            "OriginCountry.keyword": {
              "value": "US",
              "boost": 1
            }
          }
        }
      ],
      "adjust_pure_negative": true,
      "boost": 1
    }
  },
  "_source": {
    "includes": [
      "OriginCityName",
      "DestCityName"
    ],
    "excludes": []
  },
  "sort": [
    {
      "FlightTimeHour": {
        "order": "desc"
      }
    }
  ]
}

正如您的预想,WHERE 子句被转换成了 rangeterm 查询。请注意相较于母级 OriginCountry(为类型文本),系统如何将子字段的 OriginCountry.keyword 变体与准确词语进行匹配。用户没有必要了解底层映射行为的不同,因为系统会自动选择正确的字段类型。有趣的是,在有 docvalue_ 字段的情况下(亦即针对准确类型(数值、日期、关键字)启用文档值),界面会尝试使用 docvalue_ 字段(而非 _source)来优化提取性能。我们可以依赖 Elasticsearch SQL 来针对指定查询生成最优 DSL。

现在我们思考一下上面所提到的最复杂的查询:

POST _xpack/sql/translate
{
  "query":"SELECT timestamp, FlightNum, OriginCityName, DestCityName, ROUND(DistanceMiles) AS distance, ROUND(DistanceMiles/FlightTimeHour) AS speed, DAY_OF_WEEK(timestamp) AS day_of_week FROM flights WHERE DAY_OF_WEEK(timestamp) >= 0 AND DAY_OF_WEEK(timestamp) <= 2 AND HOUR_OF_DAY(timestamp) >=9 AND HOUR_OF_DAY(timestamp) <= 10 ORDER BY speed DESC, distance DESC LIMIT 2"
}

在 demo.elastic.co 上亲自尝试

响应是…

{
  "size": 2,
  "query": {
    "bool": {
      "filter": [
        {
          "bool": {
            "filter": [
              {
                "script": {
                  "script": {
                    "source": "(params.v0 <= doc[params.v1].value.getDayOfWeek()) && (doc[params.v2].value.getDayOfWeek() <= params.v3)",
                    "lang": "painless",
                    "params": {
                      "v0": 0,
                      "v1": "timestamp",
                      "v2": "timestamp",
                      "v3": 2
                    }
                  },
                  "boost": 1
                }
              },
              {
                "script": {
                  "script": {
                    "source": "doc[params.v0].value.getHourOfDay() >= params.v1",
                    "lang": "painless",
                    "params": {
                      "v0": "timestamp",
                      "v1": 9
                    }
                  },
                  "boost": 1
                }
              }
            ],
            "adjust_pure_negative": true,
            "boost": 1
          }
        },
        {
          "script": {
            "script": {
              "source": "doc[params.v0].value.getHourOfDay() <= params.v1",
              "lang": "painless",
              "params": {
                "v0": "timestamp",
                "v1": 10
              }
            },
            "boost": 1
          }
        }
      ],
      "adjust_pure_negative": true,
      "boost": 1
    }
  },
  "_source": false,
  "stored_fields": "_none_",
  "docvalue_fields": [
    "timestamp",
    "FlightNum",
    "OriginCityName",
    "DestCityName",
    "DistanceMiles",
    "FlightTimeHour"
  ],
  "sort": [
    {
      "_script": {
        "script": {
          "source": "Math.round((doc[params.v0].value) / (doc[params.v1].value))",
          "lang": "painless",
          "params": {
            "v0": "DistanceMiles",
            "v1": "FlightTimeHour"
          }
        },
        "type": "number",
        "order": "desc"
      }
    },
    {
      "_script": {
        "script": {
          "source": "Math.round(doc[params.v0].value)",
          "lang": "painless",
          "params": {
            "v0": "DistanceMiles"
          }
        },
        "type": "number",
        "order": "desc"
      }
    }
  ]
}

我们的 WHERE 和 ORDER BY 子句已经被转换成了 Painless 脚本,并且已用于由 Elasticsearch 提供的排序脚本查询。系统甚至对这些脚本进行了参数化,以避免再次编译并充分利用脚本的缓存

额外补充一句,虽然上述内容代表 SQL 声明的最佳转译,但是这并不能代表是更广泛问题的最佳解决方案。实际上,我们希望在索引时就对文档的日期(星期几)、时间(小时)以及速度这几个字段进行编码,这样我们只需使用简单的 range 查询就可以了。与使用 Painless 脚本解决这一特定问题相比,此种做法的性能可能会更好一些。因为,这些字段中的某些字段实际上已经在文档中存在了。这是用户应该注意的一个常见问题:尽管我们能够依赖 Elasticsearch SQL 实施过程来实现最佳转化,但由于其仅能使用在查询中指明的字段,因此可能不能为更大范围内的问题提供最佳解决方案。为了实现最佳方法,必须考虑底层平台的优势,而 _translate API 可以作为此过程中的第一步。

下篇文章

Elasticsearch 实用指南第 2 部分中,我们将会继续使用 _translate API 来演示某些更加复杂的 Elasticsearch SQL 功能。我们也会看一下用户在初始发布版本中有可能遇到的一些局限之处。然后,我将会带大家快速了解一下正在开发中的一些功能。