실제 예제와 함께 보는 Elasticsearch SQL 소개 - 1부

Elastic Stack의 버전 6.3 릴리스는 최근 들어 가장 풍부한 기능을 탑재한 릴리스 중 하나였습니다. X-Pack의 개방과 추가 롤업에도 그다지 만족스럽지 않으셨다면, 이제 실험적인 기능으로 SQL 지원을 추가했다는 소식을 전해드립니다. 비교적 짧은 Elasticsearch의 역사상 가장 많은 사람들이 원하고 요청해온 기능 중 하나입니다.

이 블로그 시리즈에서는 현재 Elasticsearch SQL이 지원하는 기능과 역량 중 일부를 탐구해 보려고 합니다. 또한 몇 가지 제한사항에 대해서도 공개적으로 다루고 향후 계획도 간단히 소개할 예정입니다.

목표 대상

지금까지 Elastic에서는 많은 이유로 인해 제품에 SQL을 추가하는 것을 주저해 왔습니다. 계속되었던 공통된 질문은 다음과 같습니다.

  • SQL 지원은 무엇으로 구성될 것인가?
  • 우리는 어떤 기능을 지원하게 될 것인가? 좀더 일반적으로 말하자면, 이것은 이런 질문과 이어졌습니다. “우리는 JOIN을 지원하는가?
  • 표현식/함수는? 그룹화는?”
  • 우리가 JDBC/ODBC 연결을 지원할 필요가 있을까?
  • 우리는 SQL을 지원할 필요가 과연 있는가? 아니면 단지 새로운 사용자가 우리 도메인 특정 언어(Domain Specific Language, DSL)에 익숙해지도록 우리가 충분한 자료를 제대로 제공하지 못하고 있는 것이 아닐까?

몇 번의 회의를 거쳐 우리는 사용자에게 유용하리라 생각되는 필수 기능들로 좁힐 수 있었습니다. 사용자들과의 토론을 바탕으로 다음 두 대상 사용자에게 SQL이 가장 큰 혜택을 줄 것으로 예상됩니다.

  • 새 사용자는 스택을 처음 사용하기 때문에 Elasticsearch DSL이 출발점으로는 조금 힘겹다는 생각을 하게 될 수도 있고, 전체 구문을 다 배울 필요가 없을 수도 있습니다. 예를 들어, 사용자는 성능이나 확장성의 이유로 기존 SQL 기반의 애플리케이션을 변환하고 있을 수도 있고, 전체 구문을 배울 필요 없이 동일한 쿼리만 원할 수도 있습니다. 우리는 또한 새로운 “언어"를 배우는데 있어 흔히 사용되는 학습 전략이 이미 알고 있는 것을 바탕으로 그에 상응하는 것을 찾아내는 것임을 잘 알고 있습니다.
  • 데이터 소비자는 Elasticsearch DSL을 전부 배우고 싶은 소망도 필요도 사실상 전혀 없습니다. 이러한 사용자는 단순히 외부 처리를 위해 데이터 추출만을 원하는 데이터 과학자들일 수도 있습니다. 아니면, SQL에 대체로 익숙하고 이것을 일상적으로 매일 사용하는 조금 덜 기술적인 BI 사용자들일 수도 있습니다.

이 블로그 시리즈에서 종종 설명하게 되겠지만 선언형 언어인 SQL은 위의 대상 외에도 모든 사용자에게 극히 매력적인 패러다임이라는 점은 부인할 수 없습니다. SQL이 인기있는 이유가 제어 흐름을 정의할 필요 없이 계산의 논리와 수행하려는 것을 표현하는 이러한 능력을 기반으로 하고 있다는 점에는 의심의 여지가 없습니다. 아울러, 앞으로 보여드리는 것처럼 특정 SQL 쿼리는 Elasticsearch DSL에서 그에 상응하는 것을 사용하면 상당히 장황해질 수 있는 문제를 우아하게 정의합니다. Elasticsearch DSL은 전체 텍스트 검색 문제를 우아하게 묘사하는 반면, 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의 강력하게 유형화된 컨셉 사이에서 정확하고 사용 가능한 논리적 동치가 여전히 필요합니다.

다행히도, Elasticsearch 색인은 RDBMS 표처럼 물리적으로 격리되어 있고 넓은 의미에서 같은 방식으로 사용되어야 합니다(관련 데이터 저장 등). 행과 문서는 또한 필드/열을 그룹화하기 위한 메커니즘을 제공한다는 점에서 자연스러운 비유입니다. 그러나 열이 엄격한 경향(과 좀더 강제성)이 있는 반면 문서는 (구조를 가지면서도) 좀더 유연하거나 느슨한 경향이 있습니다. Elasticsearch의 필드는 이름 항목을 나타내고 다양한 데이터 유형을 지원하며 잠재적으로 여러 값을 포함하고 있습니다(목록 등). 이러한 다중값 필드는 예외로 하고, 이 컨셉은 SQL 열에 직접 매핑합니다. 참고: 다중값 필드에서 SELECT를 시도하면 쿼리 시간에 오류가 발생하게 됩니다.

다른 컨셉은 이처럼 직접 매핑하지 않습니다. SQL 데이터베이스와 Elasticsearch 클러스터는 느슨한 대응성만 가집니다. 그러나 이것이 Elasticsearch SQL의 사용자에게 일반적으로 영향을 미치지는 않습니다. 이 주제에 대한 추가적인 세부사항은 SQL과 Elasticsearch에 걸친 컨셉 매핑 문서에서 찾아볼 수 있습니다.

요약하면, 특정 색인/표를 대상으로 하려면 WHERE 구문에서 색인 이름을 사용하세요. 고유한 문서가 열로 매핑된 필드가 있는 행으로 반환됩니다. 상당히 투명한 이런 매핑 덕분에 앞으로 우리는 이러한 용어들을 동일한 것으로 서로 바꿔 사용할 것입니다.

요약하면, 특정 색인/표를 대상으로 하려면 WHERE 구문에서 색인 이름을 사용하세요. 고유한 문서가 열로 매핑된 필드가 있는 행으로 반환됩니다. 상당히 투명한 이런 매핑 덕분에 앞으로 우리는 이러한 용어들을 동일한 것으로 서로 바꿔 사용할 것입니다.

구현 내부 메커니즘

Elasticsearch SQL 구현은 4개의 실행 단계로 구성되어 있습니다.

elasticsearch-sql-implementation.png

구문 분석 단계에서는 SQL 쿼리가 추상 구문 트리(Abstract Syntax Tree, AST)로 변환됩니다. 모든 구문 검증이 이루어지고 나면 이 단계에서 분석기가 AST를 검증하고 모든 표, 열, 함수, 별칭, 네임스페이스를 확인하여 논리적 계획을 수립합니다. 모든 중복 표현식을 제거하는 등 이 계획이 최적화되고 나면 물리적 계획(Elasticsearch DSL 등)으로 변환되어 실행될 준비를 갖춥니다. 그러고 나면 쿼리 실행자가 실제 쿼리를 실행하고 필요에 따라 모든 유형과 표 형식의 변환(예: 집계 트리를 표로 변환)을 수행하여 결과를 클라이언트에게 스트리밍합니다.

연결 방법

SQL 솔루션 제공에 있어 현재 가장 중요한 고려 사항은 연결성 지원입니다. 일부 사용자에게는 REST 전용 인터페이스만으로도 충분하지만 대부분의 사용자는 JDBC와 ODBC 등의 표준 인터페이스를 통해 연결되기를 원하고 있습니다. ODBC 지원은 계획되어 있고 현재 적극적으로 개발 중에 있습니다. JDBC는 이 첫 번째 릴리스에서 이용할 수 있으며 다운로드가 가능합니다.

중요하게는, 이 드라이버와의 모든 통신이 REST 인터페이스를 통해 HTTP 상에서 유지됩니다. 이것은 몇 가지 매력적인 장점을 가집니다.

  1. 사용자에게 SQL 액세스를 허용하는 것은 Elasticsearch 포트를 보안과 고유하게 통합시켜개방하고 노출시키는 것과 다르지 않습니다. 따라서 기존 사용자가 액세스 제어 권한 OOTB와 결합시킬 수 있도록 하여 Elastic Cloud 상의 호스팅된 Elasticsearch Service에서 즉시 SQL을 지원할 수도 있습니다.
  2. 이것은 우리가 직접 REST 인터페이스를 통해 SQL을 활용할 수 있게 해주며 편리를 위해 추가적인 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

이 동일한 REST 요청/응답은 JDBC 드라이버와콘솔에 의해 소비되지만 사용자에게는 숨겨집니다.

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

demo.elastic.co에서 해보세요. (결과는 다양할 수 있습니다.)

언제든 존재하지 않는(대소문자 구분) 필드를 요청하는 경우, 표 형식의 의미구조과 유형 의존성이 강한 저장소는 오류가 반환될 것임을 의미합니다. 이것은 필드가 단순히 반환되지 않을 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 함수는 프레젠테이션에서 서버 사이드 플러그인이 처리합니다.

이용 가능한 함수 목록은 “함수 표시”에서 검색하실 수 있다는 점을 참고하세요.

demo.elastic.co에서 해보세요.

이것을 앞서 살펴본 수학 역량과 결합시키면 DSL에서 대부분의 사용자들에게는 명확히 나타내기에 복잡할 쿼리를 형식화하기 시작할 수 있습니다.

“월요일, 화요일, 수요일 오전 9시에서 오전 11시 사이에 출발하는, 500km 이상인 가장 빠른 항공편 2개(속도)의 거리와 평균 속도를 구하세요. 거리와 속도는 가장 가까운 정수로 반올림하세요. 속도가 같으면, 가장 먼 것을 먼저 표시하세요.”

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로 번역

누구나 SQL 쿼리를 Elasticsearch DSL에서 표현하려고 애썼던 적이 있거나 또는 과연 이것이 가능한 최적의 방식인지 궁금해했던 적이 있을 것입니다. 새로운 SQL 인터페이스의 가장 매력적인 기능 중 하나는 그러한 문제에서 Elasticsearch를 새로 채택한 사용자를 지원하는 능력입니다. REST 인터페이스를 사용해서 우리는 간단히 /translate를 “sql” 엔드포인트에 첨부하여 드라이버가 문제삼게 될 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에서 해보세요.

동일한 DSL은 숙련된 Elasticsearch 사용자에게는 꽤 분명하게 보일 것입니다.

{
  "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 구문은 기대한 대로 범위기간 쿼리로 변환됩니다. 하위 필드의 OriginCountry.keyword variant가 정확한 기간 매칭 vs 상위 OriginCountry(텍스트 유형)에 대해 어떻게 사용되는지 눈여겨 보세요. 사용자가 기본 매핑 동작에서 그 차이점들을 알아야 할 필요는 없습니다. 정확한 필드 유형은 자동으로 선택됩니다. 흥미로운 점은 예를 들어 활성화된 문서 값이 있는 정확한 유형(숫자, 날짜, 키워드)에 대해 사용 가능할 때 인터페이스가 _source보다 docvalue_fields를 사용함으로써 검색 성능을 최적화하려고 한다는 것입니다. 지정된 쿼리에 대해 가장 최적화된 DSL을 생성하기 위해 Elasticsearch SQL을 신뢰해도 됩니다.

이제 마지막으로 사용했던 가장 복잡한 쿼리를 살펴보겠습니다.

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문에 대해 가장 최적화된 번역을 나타내기는 하지만, 이것이 더 폭넓은 문제에 대해 가장 좋은 솔루션을 나타내는 것은 아닙니다. 실제로 우리는 일주일 중 하루, 하루 중 한 시간, 색인 시간에서 문서 상의 속도를 인코딩하려고 할 것이며, 따라서 우리가 간단한 범위 쿼리만 사용할 수 있게 해줄 것입니다. 이것은 이 특정 문제를 해결하는데는 Painless 스크립트를 사용하는 것보다 훨씬 더 성능이 좋을 가능성이 높습니다. 이것은 사용자들이 유의해야 하는 일반적인 주제입니다. 최적화된 번역을 제공하기 위해 Elasticsearch SQL 구현을 신뢰할 수는 있지만, 이것은 쿼리에서 지정된 필드를 활용할 수 있을 뿐입니다. 따라서 더 큰 규모의 문제에 대해 반드시 최적화된 솔루션을 제공할 수 있는 것은 아닙니다. 최적화된 접근을 얻기 위해서는 기본 플랫폼의 강점을 고려하는 것이 필요합니다. 그리고 _translate API는 이 프로세스에서 첫 단계가 될 수 있습니다.

다음으로는

실제적인 Elasticsearch 소개 II부에서는 더 복잡한 Elasticsearch SQL 기능 일부를 보여드리면서 계속해서 _translate API를 사용해 보겠습니다. 또한 첫 번째 릴리스에서 사용자가 직면하게 될 가능성이 높은 몇 가지 제한사항도 살펴보겠습니다. 그 후에는, 로드맵 내용을 살짝 알려드리겠습니다!