ref. https://esbook.kimjmin.net/
위 링크의 내용을 요약, 정리합니다.
데이터 저장 형식 뿐만 아니라 쿼리와 클러스터 설정 등 모든 정보를 JSON 형태로 주고받는다.
Kibana에는 ES에서 REST API를 간편하게 실행할 수 있는 Dev Tools라는 도구를 제공한다.
https://www.elastic.co/kr/downloads/kibana 에서 다운로드 한 뒤 bin/kibana
를 실행시켜준다.
위와같이 http://localhost:5601 가 실행되고 있다는 로그가 출력되면 해당 서버로 접속한다.
curl 명령어로 요청한 값과 동일한 결과를 받아오는 것을 확인 할 수 있다.
단일 도큐먼트별로 고유한 URL을 갖는다. 도큐먼트에 접근하는 URL의 구조는 다음과 같다.
http://<호스트>:<포트>/<인덱스>/_doc/<도큐먼트 id>
데이터 입력시 PUT 메서드를 사용한다.
동일한 URL에 다른 내용의 도큐먼트를 재입력하는 경우 기존 도큐먼트는 삭제되고 새로운 도큐먼트로 덮어씌워지게 된다. 이 때 응답의 result는 "created" 대신 "updated"가 표시된다.
실수로 기존 도큐먼트가 덮어씌워지는 것을 방지하기 위해서는 _doc 대신 _create를 사용한다.
이미 존재하는 도큐먼트라면 입력 오류가 난다.
다양한 정보가 함께 표시되며 문서의 내용은 _source 항목에 나타난다.
도큐먼트 또는 인덱스 단위의 삭제가 가능하다.
하나의 도큐먼트를 삭제하는 경우
전체 인덱스를 삭제하는 경우
전체 인덱스를 삭제하고 GET 요청을 보내면 다음처럼 "index_not_found_exception"이 발생한다.
도큐먼트를 입력할 때 POST 메서드로 <인덱스>/_doc
까지만 입력하게 되면 자동으로 임의의 도큐먼트id가 생성된다. 도큐먼트 id의 자동 생성은 PUT 메서드로는 동작하지 않는다.
🔎 _update
PUT을 사용하는 경우, 도큐먼트 전체가 새로운 값으로 대치된다.
이전 도큐먼트에서 원하는 필드만 선택적으로 변경하고 싶은 경우에는 다음의 URL을 사용하고, body에 "doc"이라는 지정자를 사용한다.
POST <인덱스>/_update/<도큐먼트 id>
이전에 저장한 내용 | _update 요청 |
---|---|
다시 GET 요청을 보낸 결과이다.
_id
필드 하위에 _version
을 보면 2로 (이전에는 1) 증가된 것을 확인 할 수 있다. 이는 단일 필드만 수정하는 경우에도 내부에서는 도큐먼트 전체 내용을 가져와 _doc
에서 지정한 내용으로 변경한 새 도큐먼트를 PUT으로 입력하는 작업을 진행하기 때문이다.
여러 명령을 배치로 수행하기 위해서 _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
로 지정하여 사용한다.
검색은 인덱스 단위로 이뤄진다. GET <인덱스명>/_search
형식으로 사용하며 쿼리를 입력하지 않으면 전체 도큐먼트를 찾는 match_all 검색을 한다.
_search 뒤에 q
파라메터를 사용해 검색어를 입력할 수 있다. request url에 검색어를 넣어 검색하는 방식을 URI 검색이라고 한다.
hits.total.value
: 검색 결과 전체에 해당되는 문서의 개수 표시
hits:[]
: 배열로 가장 정확도
가 높은 문서 "10개"
이 정확도를 relevancy라고 한다.
AND 조건으로 검색하는 경우
AND
, OR
, NOT
의 사용이 가능하며 반드시 모두 대문자로 입력해야 한다.루씬의 기본 쿼리 문법을 사용
좀 더 복잡한 검색을 위해서는 data body 검색을 이용해야 한다.
,
로 나열하거나 와일드카드 *
문자로 묶는다.// 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 은 불필요한 데이터까지 접근해 작업 부하를 초래하므로 되도록 사용하지 않도록 한다.
수많은 대상 데이터 중에서 "조건"에 부합하는 데이터로 범위를 "축소"하는 행위
Full Text Query는 이하 FTQ로 표시할 것이다. 🙋🏻
해당 인덱스의 모든 도큐먼트를 검색
검색 시 쿼리를 넣지않으면 자동으로 match_all을 적용한다.
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%'와는 다르다.
SQL의 message like '%quick dog%' 와 같은 효과를 내기 위해선 어떻게 해야할까?
이 때 사용하는 것이 match_phrase이다.
GET my_index/_search
{
"query": {
"match_phrase": {
"message": "quick dog"
}
}
}
slop
이라는 옵션을 이용해 slop
에 지정된 값 만큼 단어 사이에 다른 "검색어"(=단어)가 끼어드는 것을 허용할 수 있다.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"을 검색하는 것에 유의하자.
GET <인덱스명>/_search
{
"query": {
"bool": {
"must": [
{ <쿼리> }, …
],
"must_not": [
{ <쿼리> }, …
],
"should": [
{ <쿼리> }, …
],
"filter": [
{ <쿼리> }, …
]
}
}
}
bool 쿼리는 4개의 인자를 가진다.
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으로 대치한다.
검색된 결과가 얼마나 검색 조건과 일치하는지를 나타낸다.
다음과 같은 요청을 보냈을 때 결과를 보도록 하자.
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는 검색 점수를 조정하기 위해 사용할 수 있다.
GET my_index/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"message": "fox"
}
}
],
"should": [
{
"match": {
"message": "lazy"
}
}
]
}
}
}
match 쿼리만 사용한 경우 :
should 쿼리를 추가한 경우 :
match만 사용한 경우에 0.339의 스코어를 기록했던 "_id=2"인 도큐먼트가 lazy
를 포함하고 있기때문에 가중치를 얻어 가장 높은 스코어(1.129)를 가지게 되었다.
should는 match_pharse와 함께 유용하게 사용될 수 있다.
정확도를 고려하는 Full Text Search 외에도 참/거짓 여부만 판별해 결과로 가져오는 것이 가능하다. 이 특성을 Exact Value(정확값)이라고 한다.
term, range와 같은 쿼리들이 속하며, 스코어를 계산하지 않으므로 보통 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 형식으로 저장해 정확한 "값" 검색이 가능하다.
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 쿼리를 이용해 검색한다.
range query는 range: {<필드명>: {<파라메터>:<값>}}
으로 입력된다.
파라메터는 다음과 같다.
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 쿼리를 사용한다.
와 정리 잘해두셨네요!
굉장히 도움받고 갑니다 ㅎㅎ