Elastic Search (2) ES 데이터 처리, 검색과 쿼리 - Query DSL

rin·2020년 7월 16일
5

Elastic Search

목록 보기
2/4
post-thumbnail

ref. https://esbook.kimjmin.net/
위 링크의 내용을 요약, 정리합니다.

Elasticsearch 데이터 처리

데이터 저장 형식 뿐만 아니라 쿼리와 클러스터 설정 등 모든 정보를 JSON 형태로 주고받는다.

REST API

  • http 프로토콜로 접근 가능한 REST API를 지원한다.
  • 자원별로 고유 URL로 접근
  • PUT, POST, GET, DELETE를 이용해 자원을 처리한다.

유닉스 curl

Kibana Dev Tools

Kibana에는 ES에서 REST API를 간편하게 실행할 수 있는 Dev Tools라는 도구를 제공한다.

https://www.elastic.co/kr/downloads/kibana 에서 다운로드 한 뒤 bin/kibana를 실행시켜준다.

위와같이 http://localhost:5601 가 실행되고 있다는 로그가 출력되면 해당 서버로 접속한다.

curl 명령어로 요청한 값과 동일한 결과를 받아오는 것을 확인 할 수 있다.

CRUD

단일 도큐먼트별로 고유한 URL을 갖는다. 도큐먼트에 접근하는 URL의 구조는 다음과 같다.

http://<호스트>:<포트>/<인덱스>/_doc/<도큐먼트 id>

PUT

데이터 입력시 PUT 메서드를 사용한다.

동일한 URL에 다른 내용의 도큐먼트를 재입력하는 경우 기존 도큐먼트는 삭제되고 새로운 도큐먼트로 덮어씌워지게 된다. 이 때 응답의 result는 "created" 대신 "updated"가 표시된다.

실수로 기존 도큐먼트가 덮어씌워지는 것을 방지하기 위해서는 _doc 대신 _create를 사용한다.

이미 존재하는 도큐먼트라면 입력 오류가 난다.

GET

다양한 정보가 함께 표시되며 문서의 내용은 _source 항목에 나타난다.

DELETE

도큐먼트 또는 인덱스 단위의 삭제가 가능하다.

하나의 도큐먼트를 삭제하는 경우

전체 인덱스를 삭제하는 경우

전체 인덱스를 삭제하고 GET 요청을 보내면 다음처럼 "index_not_found_exception"이 발생한다.

POST

도큐먼트를 입력할 때 POST 메서드로 <인덱스>/_doc까지만 입력하게 되면 자동으로 임의의 도큐먼트id가 생성된다. 도큐먼트 id의 자동 생성은 PUT 메서드로는 동작하지 않는다.

🔎 _update
PUT을 사용하는 경우, 도큐먼트 전체가 새로운 값으로 대치된다.
이전 도큐먼트에서 원하는 필드만 선택적으로 변경하고 싶은 경우에는 다음의 URL을 사용하고, body에 "doc"이라는 지정자를 사용한다.

POST <인덱스>/_update/<도큐먼트 id>
이전에 저장한 내용_update 요청

다시 GET 요청을 보낸 결과이다.

_id 필드 하위에 _version을 보면 2로 (이전에는 1) 증가된 것을 확인 할 수 있다. 이는 단일 필드만 수정하는 경우에도 내부에서는 도큐먼트 전체 내용을 가져와 _doc 에서 지정한 내용으로 변경한 새 도큐먼트를 PUT으로 입력하는 작업을 진행하기 때문이다.

_bulk API

여러 명령을 배치로 수행하기 위해서 _bulk API의 사용이 가능하다.-index, create, update, delete

내용 입력이 필요없는 delete를 제외하고는 명령문과 데이터문을 한 줄 씩 순서대로 입력해야 한다.

요청 결과는 다음과 같다.

{
  "took" : 116,
  "errors" : false,
  "items" : [
    {
      "index" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "1",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 1,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "2",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 2,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "delete" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "2",
        "_version" : 2,
        "result" : "deleted",
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 3,
        "_primary_term" : 1,
        "status" : 200
      }
    },
    {
      "create" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "3",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 4,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "update" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "1",
        "_version" : 2,
        "result" : "updated",
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 5,
        "_primary_term" : 1,
        "status" : 200
      }
    }
  ]
}

모든 명령이 동일한 인덱스에서 수행되는 경우에는 아래와같은 형식으로도 사용가능하다.

POST test/_bulk
{"index":{"_id":"1"}}
{"field":"value one"}

대량의 데이터를 입력할 때는 _bulk API를 사용해야 불필요한 오버헤드가 없다.

❗️ NOTE
ES에는 커밋이나 롤백 등의 트랜잭션 개념이 없다. _bulk 작업 수행 중에 동작이 중단되면 어느 동작까지 실행되었는지 확인이 불가하다. 이런 경우 전체 인덱스를 삭제하고 처음부터 다시 하는 것이 안전하다.

파일에 저장 내용 실행

벌크 명령을 파일로 저장하고 curl 명령으로 실행시킬 수 있다. 저장한 명령 파일을 --data-binary로 지정하여 사용한다.

_search API ⭐️

검색은 인덱스 단위로 이뤄진다. GET <인덱스명>/_search 형식으로 사용하며 쿼리를 입력하지 않으면 전체 도큐먼트를 찾는 match_all 검색을 한다.

URI 검색

_search 뒤에 q 파라메터를 사용해 검색어를 입력할 수 있다. request url에 검색어를 넣어 검색하는 방식을 URI 검색이라고 한다.

hits.total.value : 검색 결과 전체에 해당되는 문서의 개수 표시
hits:[] : 배열로 가장 정확도가 높은 문서 "10개"

이 정확도를 relevancy라고 한다.

AND 조건으로 검색하는 경우

  • URI 쿼리에서는 AND, OR, NOT의 사용이 가능하며 반드시 모두 대문자로 입력해야 한다.
  • 특정 필드를 key, 검색어를 value로 사용하고 싶은 경우에는 key:value 꼴로 작성하면 된다.

루씬의 기본 쿼리 문법을 사용

좀 더 복잡한 검색을 위해서는 data body 검색을 이용해야 한다.

Data Body 검색

  • 검색 쿼리를 바디에 입력하는 방식
  • QueryDSL을 사용
  • Json 형식

Multitenancy

  • 여러 개의 인덱스를 한꺼번에 묶어서 검색할 수 있다.
  • 쉼표 , 로 나열하거나 와일드카드 * 문자로 묶는다.
// logs-2018-01, logs-2018-02, logs-2018-03 ... 으로 저장된 인덱스

// 쉼표로 나열해서 여러 인덱스 검색
GET logs-2018-01,2018-02,2018-03/_search

// 와일드카드 * 를 이용해서 여러 인덱스 검색
GET logs-2018-*/_search

❗️ NOTE
인덱스명 대신 _all 지정자를 사용하여 GET _all/_search와 같이 실행 시 클러스터 내 "모든 인덱스"를 대상으로 검색이 가능하다.
🙅🏻 단, _all 은 불필요한 데이터까지 접근해 작업 부하를 초래하므로 되도록 사용하지 않도록 한다.

검색과 쿼리

검색(Search)

수많은 대상 데이터 중에서 "조건"에 부합하는 데이터로 범위를 "축소"하는 행위

  • ES는 실제로 검색에 사용되는 "검색어"인 텀(Term)으로 분석 과정을 거쳐 저장
  • 검색 시 대소문자, 단수, 복수, 원형 여부와 상관 없이 검색이 가능하다.
  • 이러한 특징을 풀 텍스트 검색(Full Text Search, 전문 검색) 이라고도 한다.

Query DSL(Domain Specific Language)

  • ES의 Query DSL은 모두 json 형식으로 입력해야한다.

Full Text Query

Full Text Query는 이하 FTQ로 표시할 것이다. 🙋🏻

match_all

해당 인덱스의 모든 도큐먼트를 검색
검색 시 쿼리를 넣지않으면 자동으로 match_all을 적용한다.

match

FTQ에 사용되는 가장 일반적인 쿼리

GET my_index/_search
{
  "query": {
    "match": {
      "message": "dog"
    }
  }
}

👉 SQL: message like '%dog%';

여러 개의 검색어를 집어넣으면 디폴트로 OR 조건으로 검색한다.

GET my_index/_search
{
  "query": {
    "match": {
      "message": "quick dog"
    }
  }
}

👉 SQL: message like '%quick%' or message like '%dog%'

❗️ message like '%quick dog%'가 아닌 것에 유의한다.

검색어 조건을 AND로 바꾸려면 operator 옵션을 사용할 수 있다.
<필드명>:<검색어> 대신
<필드명>: {"query":<검색어>, "operator":<operator>} 로 입력한다.

GET my_index/_search
{
  "query": {
    "match": {
      "message": {
        "query": "quick dog",
        "operator": "and"
      }
    }
  }
}

👉 SQL: message like '%quick%' and message like '%dog%'

❗️ 이 경우도 message like '%quick dog%'와는 다르다.

match_phrase

SQL의 message like '%quick dog%' 와 같은 효과를 내기 위해선 어떻게 해야할까?
이 때 사용하는 것이 match_phrase이다.

  • 입력된 검색어를 순서까지 고려하여 검색을 수행한다.
GET my_index/_search
{
  "query": {
    "match_phrase": {
      "message": "quick dog"
    }
  }
}

  • slop 이라는 옵션을 이용해 slop에 지정된 값 만큼 단어 사이에 다른 "검색어"(=단어)가 끼어드는 것을 허용할 수 있다.

  • 단, slop이 너무 크면 검색 범위가 넓어져 관련이 없는 결과가 나타날 확률도 높아지므로 1이상 사용하지 않는 것을 권장한다.

query_string

  • URL의 q 파라메터와 동일함
GET my_index/_search
{
  "query": {
    "query_string": {
      "default_field": "message",
      "query": "(jumping AND lazy) OR \"quick dog\""
    }
  }
}

👉 SQL: message like '%jumping%' AND message like '%lazy' OR message like '%quick dog%'

❗️ 공백을 포함하는 문자열 "quick dog"을 검색하는 것에 유의하자.

Bool 복합 쿼리 - Bool Query

  • 앞의 query_string 쿼리는 여러 조건을 조합하기에는 용이한 문법이지만 옵션이 한정적이다.
  • 여러 쿼리를 조합하기 위해 Bool 쿼리를 사용할 수 있다.
GET <인덱스명>/_search
{
  "query": {
    "bool": {
      "must": [
        { <쿼리> },],
      "must_not": [
        { <쿼리> },],
      "should": [
        { <쿼리> },],
      "filter": [
        { <쿼리> },]
    }
  }
}

bool 쿼리는 4개의 인자를 가진다.

  1. must : 쿼리가 인 도큐먼트 검색
  2. must_not : 쿼리가 거짓인 도큐먼트 검색
  3. should : 검색 결과 중 이 쿼리에 해당하는 도큐먼트의 "점수"를 증대
  4. filter : 쿼리가 인 도큐먼트를 검색하나 "점수" 계산 X, must보다 검색 속도 빠르고 캐싱이 가능함.
GET my_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "message": "quick"
          }
        },
        {
          "match_phrase": {
            "message": "lazy dog"
          }
        }
      ]
    }
  }
}

must → 배열 내 모든 조건이 true여야 한다.
match → message 필드에 "quick"을 포함한다.
match_pharase → message 필드에 "lazy dog"을 포함한다.
👉 SQL: message like '%quick%' AND message like '%lazy dog%'

GET my_index/_search
{
  "query": {
    "bool": {
      "must_not": [
        {
          "match": {
            "message": "quick"
          }
        },
        {
          "match_phrase": {
            "message": "lazy dog"
          }
        }
      ]
    }
  }
}

must_not → 배열 내 모든 조건이 false여야 한다.
match → message 필드에 "quick"을 포함한다.
match_pharase → message 필드에 "lazy_dog"을 포함한다.
👉 SQL: message not like '%quick%' AND message not like '%lazy dog%'

🤔 must는 SQL의 AND 연산자와 "유사하게" 동작하나, SQL의 OR과 정확히 일치하게 동작하는 bool 쿼리는 없다. (should과 유사하지만 정확히 같지는 않다.)

✔️ 표준 SQL의 AND, OR 조건은 2개 조건값에 대한 이항 연산자

✔️ 반면 ES의 must, must_not, should 등은 내부에 있는 각 쿼리들에 대해 이 쿼리는 참/거짓으로 적용하는 단항 연산자

OR 조건은 match의 공백으로 표현하고, not을 must_not으로 대치한다.

정확도 - Relevancy

  • RDBMS는 단순히 참/거짓만 판단할 뿐 각 결과가 얼마나 정확한지에 대한 판단이 불가능하다.
  • ES와 같은 Full Text Search Engine은 검색 결과가 검색 조건과 얼마나 정확하게 일치하는 지를 계산하는 알고리즘을 가지고 있다. → 정확도(relevancy)를 기반으로 사용자가 가장 원하는 결과를 먼저 보여줄 수 있다.
  • 즉, 검색하여 찾은 결과 중 사용자가 입력한 검색어와의 연관성을 계산해 relevancy가 높은 순으로 출력한다.

Score

검색된 결과가 얼마나 검색 조건과 일치하는지를 나타낸다.

다음과 같은 요청을 보냈을 때 결과를 보도록 하자.

GET my_index/_search
{
  "query": {
    "match": {
      "message": "quick dog"
    }
  }
}

응답값 :

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 5,
      "relation" : "eq"
    },
    "max_score" : 1.1274881,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.1274881,
        "_source" : {
          "message" : "The quick brown fox jumps over the quick dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.8707045,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.7636937,
        "_source" : {
          "message" : "The quick brown fox"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 0.53332734,
        "_source" : {
          "message" : "Lazy jumping dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 0.4868012,
        "_source" : {
          "message" : "Brown fox brown dog"
        }
      }
    ]
  }
}

위 출력값에서 정렬된 기준이 무엇일까? 바로 "_score"이다. 최상단의 "_id"가 3인 인덱스의 "_score"는 1.125... 이며 갈수록 값이 작아지고 있다.

max_score에는 전체 결과 중 가장 높은 점수가 표시된다.

❗️ NOTE
ES는 이 점수를 계산하기 위해 BM25(Best Matching)라는 알고리즘을 이용한다.
ref. https://en.wikipedia.org/wiki/Okapi_BM25

[BM25에 사용되는 3요소]
TF(Term Frequency)
도큐먼트 내에 검색된 텀(term)이 많을수록 점수가 높아진다. (도큐먼트 내에서 중복되는 검색어)

IDF(Inverse Document Frequency)
여러 검색어 중에서 전체 검색 결과에 희소하게 나타나는 단어일수록 중요한 텀일 가능성이 높다. 따라서 검색한 텀을 포함하는 도큐먼트가 많을 수록 해당 텀(희소성이 떨어지는 단어)이 가지는 점수가 감소한다.

Field Length
필드 길이가 짧은 필드에 있는 텀의 비중이 크다.

Bool : Should

bool 쿼리의 should는 검색 점수를 조정하기 위해 사용할 수 있다.

GET my_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "message": "fox"
          }
        }
      ],
      "should": [
        {
          "match": {
            "message": "lazy"
          }
        }
      ]
    }
  }
}
  1. fox가 포함된 결과 중
  2. lazy 또한 포함하고 있는 결과에 가중치를 준다.

match 쿼리만 사용한 경우 :

should 쿼리를 추가한 경우 :

match만 사용한 경우에 0.339의 스코어를 기록했던 "_id=2"인 도큐먼트가 lazy를 포함하고 있기때문에 가중치를 얻어 가장 높은 스코어(1.129)를 가지게 되었다.

shouldmatch_pharse와 함께 유용하게 사용될 수 있다.

Exact Value Query

정확도를 고려하는 Full Text Search 외에도 참/거짓 여부만 판별해 결과로 가져오는 것이 가능하다. 이 특성을 Exact Value(정확값)이라고 한다.

term, range와 같은 쿼리들이 속하며, 스코어를 계산하지 않으므로 보통 bool 쿼리의 filter 내부에서 사용한다.

bool : filter

bool 쿼리의 filter 안에 하위 쿼리를 사용하면 스코어에 영향을 주지 않는다.
즉, 검색에 조건을 추가하지만 스코어에는 영향을 주지 않도록 제어할 때 사용한다.

filter "내부"에 must_not과 같은 다른 bool 쿼리를 포함하려면 filter 내부에 bool 쿼리가 먼저 포함되어야한다.

GET my_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "message": "fox"
          }
        }
      ],
      "filter": [
        {
          "bool": {
            "must_not": [
              {
                "match": {
                  "message": "dog"
                }
              }
            ]
          }
        }
      ]
    }
  }
}

위의 쿼리는 fox를 반드시 포함하면서 dog를 반드시 포함하지 않아야하나, 실제로 score에 영향을 끼치는 값은 fox 뿐이다.

keyword

문자열 데이터는 keyword 형식으로 저장해 정확한 "값" 검색이 가능하다.

GET my_index/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "match": {
            "message.keyword": "Brown fox brown dog"
          }
        }
      ]
    }
  }
}

위 쿼리는 message의 필드값이 "Brown fox brown dog" 문자열과 공백, 대소문자까지 정확하게 일치하는 데이터만을 결과로 리턴한다.
단, keyword 타입으로 저장된 필드는 스코어를 계산하지 않으므로 반드시 filter 구문안에 넣어서 사용하도록 한다.

❗️ NOTE
filter 내 검색 조건은 캐싱이 되므로 쿼리가 더 가볍고 빠르게 실행된다. 따라서 스코어 계산이 필요하지 않은 쿼리들은 모두 filter안에 넣어서 실행하는 것이 좋다.

Range Query

숫자나 날짜 형식의 저장이 가능하다. 이런 형식은 range 쿼리를 이용해 검색한다.

range query는 range: {<필드명>: {<파라메터>:<값>}} 으로 입력된다.
파라메터는 다음과 같다.

  • gte (Greater-Than or Equal to) : 같거나 큼
  • gt (Greater-Than) : 큼
  • lte (Less-Than or Equal to) : 같거나 작음
  • lt (Less-Than) : 작음

숫자 검색

GET phones/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 700,
        "lt": 900
      }
    }
  }
}

👉 SQL: price >= 700 AND price < 900

날짜 검색

JSON에 일반적으로 사용되는 ISO8601 형식을 사용한다.
(2016-01-01 or 2016-01-01T10:15:30)

GET phones/_search
{
  "query": {
    "range": {
      "date": {
        "gt": "2016-01-01"
      }
    }
  }
}

👉 2016년 1월 1일 (포함X) 이후의 데이터 검색

GET phones/_search
{
  "query": {
    "range": {
      "date": {
        "gt": "31/12/2015",
        "lt": "2018",
        "format": "dd/MM/yyyy||yyyy"
      }
    }
  }
}

format을 사용할 수 있으며 ||를 이용해 여러 포맷을 동시에 사용할 수 있다.
👉 2015년 12월 31일(포함 X) 이후, 2018년(포함 X) 이전의 데이터 검색

❗️ NOTE
사용가능한 예약어
now(현재시간), y(년), M(월), d(일), h(시), m(분), s(초), w(주) 등

GET phones/_search
{
  "query": {
    "range": {
      "date": {
        "gt": "2016-01-01||+6M",
        "lt": "now-365d"
      }
    }
  }
}

👉 date 값이 2016-01-01 의 6개월 이후부터 오늘의 365일 이전의 데이터 검색

range 쿼리는 기본적으로 정확도를 계산하지 않는다. range 쿼리에 기준점을 주고 이에 가깝거나 멀어질 수록 다른 스코어를 적용할 필요가 있는 경우에는 function_score 쿼리를 사용한다.

profile
🌱 😈💻 🌱

1개의 댓글

comment-user-thumbnail
2021년 3월 8일

와 정리 잘해두셨네요!
굉장히 도움받고 갑니다 ㅎㅎ

답글 달기