Elasticsearch에도 SQL을 쓰고 싶어요!Opendistro for Elasticsearch SQL 시작

이 글은 ZOZO 기술 #2Advent Calendar 2020 19일째 글입니다.
SQL 좋아하세요?
Elasticsearch 서예가의 첫 번째 관문으로 혼자 검색하는 데 익숙하지 않은 글이 있나요?또 Elasticsearch 검색어를 쓸 수는 없지만 SQL로 쓰는 사람도 많을 것 같아요.
이번 기고문에서는 이들을 위해 Elasticsearch에 SQL을 발매해 데이터를 읽는 방법을 소개했다.
또한 이번 검증에는 키바나를 시작할 때 기본적으로 설정할 수 있는 Sample Commerce orders 데이터가 사용되었습니다.
이번 검증은 Apache License에서 사용 가능한 Open Distro for Elasticsearch SQL(이하odfe)를 사용합니다.로컬 그룹의 시작은 docker-compose를 사용합니다. 같은 기간 @ke-bo 에 쓴 여기. 의 글을 보십시오.

Kibana에서 면접을 보면서 SQL을 두드리고 있어요.


odfe 플러그인이 적용된 Kibana를 시작하면 사이드바에서Query Workbench를 선택할 수 있습니다.
kibana
Workbench를 열면 질의를 입력하는 화면이 바로 표시됩니다.
workbench RUN에 따라 초기값 입력을 실행하면 그룹 내의 모든 인덱스를 표 형식으로 얻을 수 있습니다.
index_list
또 옆Explain 버튼을 누르면 내부가 어떤 조회로 전환됐는지 확인할 수 있다.
다음 조회를 변환해 보세요.
SELECT * FROM kibana_sample_data_ecommerce;
explain

실천하다


여기서부터 실제 조작을 구상한 조회를 실제 집행하면서 어떻게 해석했는지 확인한다.

order_Top10 - id 내림차순


SELECT * FROM kibana_sample_data_ecommerce ORDER BY order_id DESC LIMIT 10;
{
  "from": 0,
  "size": 10,
  "sort": [
    {
      "order_id": {
        "order": "desc"
      }
    }
  ]
}
top10

customer_first_반복해서 가져오기name


SELECT DISTINCT customer_first_name from kibana_sample_data_ecommerce;
{
  "from": 0,
  "size": 0,
  "_source": {
    "includes": ["customer_first_name"],
    "excludes": []
  },
  "stored_fields": "customer_first_name",
  "aggregations": {
    "customer_first_name": {
      "terms": {
        "field": "customer_first_name",
        "size": 200,
        "min_doc_count": 1,
        "shard_min_doc_count": 0,
        "show_term_doc_count_error": false,
        "order": [
          {
            "_count": "desc"
          },
          {
            "_key": "asc"
          }
        ]
      }
    }
  }
}
실제로 실행하면customerfirst_네임이 text type이라서 욕을 먹었네요.정확한 조회는 아래와 같다.
{
  "from": 0,
  "size": 0,
  "_source": {
    "includes": ["customer_first_name"],
    "excludes": []
  },
  "stored_fields": "customer_first_name",
  "aggregations": {
    "customer_first_name": {
      "terms": {
        "field": "customer_first_name.keyword",
        "size": 200,
        "min_doc_count": 1,
        "shard_min_doc_count": 0,
        "show_term_doc_count_error": false,
        "order": [
          {
            "_count": "desc"
          },
          {
            "_key": "asc"
          }
        ]
      }
    }
  }
}
키워드를 SQL로 명확하게 지정해도 대응할 수 있다.
SELECT DISTINCT customer_first_name.keyword from kibana_sample_data_ecommerce;
distinct

taxless_total_100 이상 200 이하의 기록을 얻다


SELECT * FROM kibana_sample_data_ecommerce WHERE taxless_total_price BETWEEN 100 AND 200;
{
  "from": 0,
  "size": 200,
  "query": {
    "bool": {
      "filter": [
        {
          "bool": {
            "must": [
              {
                "range": {
                  "taxless_total_price": {
                    "from": 100,
                    "to": 200,
                    "include_lower": true,
                    "include_upper": true,
                    "boost": 1
                  }
                }
              }
            ],
            "adjust_pure_negative": true,
            "boost": 1
          }
        }
      ],
      "adjust_pure_negative": true,
      "boost": 1
    }
  }
}
where

각 고객명의 주문서 수를 얻다


이 부근의 복잡한 부분에서부터 워크벤치 측은 격리할 수 없어 오류가 발생하기 시작했다.그러나 Dev Tools에서 Explain에 나타난 질의를 던지면 결과를 얻을 수 있지만 키워드형을 지정하는 것을 의식해서 피할 수도 있다.(SQL의 특성이 좀 연해졌네요)
SELECT COUNT(customer_full_name.keyword), customer_full_name.keyword FROM kibana_sample_data_ecommerce GROUP BY customer_full_name.keyword;
{
  "from": 0,
  "size": 0,
  "_source": {
    "includes": ["customer_full_name.keyword", "COUNT"],
    "excludes": []
  },
  "stored_fields": "customer_full_name.keyword",
  "aggregations": {
    "customer_full_name#keyword": {
      "terms": {
        "field": "customer_full_name.keyword",
        "size": 200,
        "min_doc_count": 1,
        "shard_min_doc_count": 0,
        "show_term_doc_count_error": false,
        "order": [
          {
            "_count": "desc"
          },
          {
            "_key": "asc"
          }
        ]
      },
      "aggregations": {
        "COUNT_0": {
          "value_count": {
            "field": "customer_full_name.keyword"
          }
        }
      }
    }
  }
}
grpup by
Aggregation으로 GRUPBY를 재현했기 때문에 내림차순으로 배열하는 것이 재미있다.

보다 복잡한 질의 구문으로 이동


여기서부터 JOIN을 사용해 보세요.실제로 이 기능들은 집필할 때 ElasticLicense의 SQL 기능에서 이루어지지 않았고odfe의 SQL 플러그인만의 기능이다.

JOIN


데이터 준비


JOIN 대상 테이블을 작성하려면 다음 정의를 사용하여 색인을 작성합니다.
PUT /ecommerce_customer_info
{
  "mappings": {
    "properties": {
      "customer_id": {
        "type": "integer",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "customer_full_name": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      }
    }
  }
}
임시로 몇 개의 데이터를 투입한다.
POST /_bulk
{"index": {"_index": "ecommerce_customer_info"}}
{"customer_id": 1, "customer_full_name": "Eddie Underwood"}
{"index": {"_index": "ecommerce_customer_info"}}
{"customer_id": 2, "customer_full_name": "Mary Bailey"}
{"index": {"_index": "ecommerce_customer_info"}}
{"customer_id": 3, "customer_full_name": "Gwen Butler"}
{"index": {"_index": "ecommerce_customer_info"}}
{"customer_id": 4, "customer_full_name": "Diane Chandler"}

질의 실행


여기서부터 현재의 Query Workbench가 대응하지 않기 때문에 Dev Tools부터 실행합니다.
SELECT e.customer_id
FROM kibana_sample_data_ecommerce k
INNER JOIN ecommerce_customer_info e
  ON k.customer_full_name = e.customer_full_name
청원
POST _opendistro/_sql
{
  "query": """
  SELECT e.customer_id
  FROM kibana_sample_data_ecommerce k
  INNER JOIN ecommerce_customer_info e
    ON k.customer_full_name = e.customer_full_name
  """
}
반환값
{
  "schema": [{
    "name": "e.customer_id",
    "type": "keyword"
  }],
  "total": 8,
  "datarows": [
    [1],
    [2],
    [2],
    [2],
    [3],
    [3],
    [4],
    [4]
  ],
  "size": 8,
  "status": 200
}
Explain API를 사용해서 실제로 어떤 요구 사항이 있는지 알아보도록 하겠습니다.
청원
POST _opendistro/_sql/_explain
{
  "query": """
  SELECT e.customer_id
  FROM kibana_sample_data_ecommerce k
  INNER JOIN ecommerce_customer_info e
    ON k.customer_full_name = e.customer_full_name
  """
}
반환값
{
  "Physical Plan" : {
    "Project [ columns=[e.customer_id] ]" : {
      "Top [ count=200 ]" : {
        "BlockHashJoin[ conditions=( k.customer_full_name = e.customer_full_name ), type=INNER_JOIN, blockSize=[FixedBlockSize with size=10000] ]" : {
          "Scroll [ kibana_sample_data_ecommerce as k, pageSize=10000 ]" : {
            "request" : {
              "size" : 200,
              "from" : 0
            }
          },
          "useTermsFilterOptimization" : false,
          "Scroll [ ecommerce_customer_info as e, pageSize=10000 ]" : {
            "request" : {
              "size" : 200,
              "from" : 0,
              "_source" : {
                "excludes" : [ ],
                "includes" : [
                  "customer_id",
                  "customer_full_name"
                ]
              }
            }
          }
        }
      }
    }
  },
  "description" : "Hash Join algorithm builds hash table based on result of first query, and then probes hash table to find matched rows for each row returned by second query",
  "Logical Plan" : {
    "Project [ columns=[e.customer_id] ]" : {
      "Top [ count=200 ]" : {
        "Join [ conditions=( k.customer_full_name = e.customer_full_name ) type=INNER_JOIN ]" : {
          "Group" : [
            {
              "Project [ columns=[k.customer_full_name] ]" : {
                "TableScan" : {
                  "tableAlias" : "k",
                  "tableName" : "kibana_sample_data_ecommerce"
                }
              }
            },
            {
              "Project [ columns=[e.customer_full_name, e.customer_id] ]" : {
                "TableScan" : {
                  "tableAlias" : "e",
                  "tableName" : "ecommerce_customer_info"
                }
              }
            }
          ]
        }
      }
    }
  }
}
Elasticsearch가 아닌 검색어가 나왔어요.참조공식 문서는 다음과 같습니다.
Inner join creates a new result set by combining columns of two indices based on your join predicates. It iterates the two indices and compares each document to find the ones that satisfy the join predicates. You can optionally precede the JOIN clause with an INNER keyword.
대충 번역하면 인너 조인의 결과를 내기 위해 두 색인에서 조인 키가 된 열을 스캔하여 대조한다.대단하다

전체 텍스트 찾기


지금까지 일반 SQL의 조회 문법을 보았지만odfe의 SQL 플러그인에서는 Match, Match Phrase 등일부 조회도 지원한다.예를 봅시다.

고객의 기록을 얻고, 이름이 smith에 부합


SELECT * FROM kibana_sample_data_ecommerce WHERE MATCH_QUERY(customer_full_name, 'smith');
{
  "from": 0,
  "size": 200,
  "query": {
    "bool": {
      "filter": [
        {
          "bool": {
            "must": [
              {
                "match": {
                  "customer_full_name": {
                    "query": "smith",
                    "operator": "OR",
                    "prefix_length": 0,
                    "max_expansions": 50,
                    "fuzzy_transpositions": true,
                    "lenient": false,
                    "zero_terms_query": "NONE",
                    "auto_generate_synonyms_phrase_query": true,
                    "boost": 1
                  }
                }
              }
            ],
            "adjust_pure_negative": true,
            "boost": 1
          }
        }
      ],
      "adjust_pure_negative": true,
      "boost": 1
    }
  }
}
match_result
예쁘다full_name에 smith가 포함된 기록을 얻을 수 있습니다.

최후


이번 기고문에서는 Open Distro for Elasticseach의 SQL 플러그인 중 일부를 활용할 수 있는 기능을 소개했다.
실제 사용 후 SQL에서 요청할 수 있을 뿐만 아니라 이 조회가 어떻게 변환되는지 관찰함으로써 Elasticsearch 조회에 대한 이해를 깊이 있게 할 수 있다.

참고 자료


공식 문서
Docker를 사용하여 다중 노드에서 Open Distro for Elasticsearch의 로컬 개발 환경 구축

좋은 웹페이지 즐겨찾기