Elasticsearch 의 Search 쿼리
Elasticsearch 검색의 핵심 쿼리 3대장
Elasticsearch 에 analyzer 들을 정의하여 index 를 생성하는 것까지 살펴봤고, 이제는 elasticsearch의 꽃인 검색 쿼리에 대해서 살펴보자. 기본적으로 ES에게 특정 index 내 검색을 요청하는 쿼리는 다음과 같다
$ GET /{index_name}/_search
위와 같이 날리면 index 내의 모든 필드를 조회해준다. 기본 Pagination 이 적용되어 offset 0, limit 10 으로 적용된다. 하지만 우리는 쿼리 형태의 요청을 날릴 것이기 때문에, 다음 쿼리들에 대해서 알아둬야 한다.
match 쿼리 ⭐
원하는 내용이 포함된 모든 데이터를 조회하는, Analyzer 를 사용하는 유연한 검색 쿼리이다. match 쿼리는 "text" 타입의 필드에 대해서 적용을 해야 원하는대로 동작하며, 검색어를 "검색 Analyzer"로 토큰화하여, input token 들을 사용하여 해당 token 들과 관련된 Document 들을 찾아준다. 또한, match 쿼리는 이 찾은 Document 들에 대해 Score 을 해주는 시스템을 적용한다 (관련도가 높을 수록 점수가 높다. 내부적으로 BM25 함수를 사용한다. 사용했던 모습은 (ES기본1편) 의 Analzyer 를 소개하는 부분에서 간단히 소개되었다.
term 쿼리
term 쿼리는 LIKE 나 = 를 활용한 쿼리라고 생각하면 된다. text 외 모든 필드에 대해 "정확히 일치하는 값"을 검색하게 된다. 따라서 문자열에 대해서 유연한 검색을 제공할 필요가 없는 값들 (ex: 비밀번호, 상품코드 등) 은 keyword type 으로 선언을 하게되고, 이 keyword 타입들은 term 쿼리를 사용해서 검색할 수 있는 것이다.
참고로 term 쿼리는 keyword 만을 위함이 아닌, 'text 타입 외 모든 타입'의 검색에 사용된다. boolean 필드의 true/false, integer 필드의 숫자 일치 값 등등 모두 사용된다. boards 라는 게시판 index 안에 isActive 란 필드가 있다고 해보자. 다음과 같이 쿼리를 날려볼 수 있다
$ GET /boards/_search
{
"query": {
"term":{
"isActive": false
}
}
}
---
$ GET /boards/_search ------ X 틀림
{
"query": {
"term":{
"writer": "mooncake",
"isActive": false
}
}
}
위에서 1번과 같이, 이 쿼리는 게시판 index 내 "비활성화된" (보이지 않는) 게시물들을 찾아보는 쿼리로 term 쿼리를 사용할 수 있다. 만약 여러개의 값 중 하나라도 일치하는게 있는지 조회하는 IN절 조회는, term 이 아닌 terms 를 사용해서 array 를 전달하면 된다. 하지만, 2 번처럼 여러 필드에 대한 조건을 나열할 수는 없을까? 정답은 안 된다. 한 가지 이상의 조건을 적용하는 AND절 활용은, 다음 bool 쿼리를 사용해야 한다.
bool 쿼리
Bool 쿼리는 거의 필수적으로 사용되는 쿼리이다. 여러가지 조건을 나열하게 해주며, 내부적으로 여러 동작성을 제공해 줄 수 있는 다양한 부가 조건절들을 사용할 수 있다.
Bool 쿼리
bool 쿼리는 한 검색 쿼리 내에서 여러 조건들을 조합하기 위해서 사용한다. 한 개 이상의 조건이 필요할시 바로 bool 쿼리를 생각하면 되고, 정말 99% 상황에서 사용할 쿼리이다. bool 쿼리 안에는 사용할 수 있는 4가지 조건절이 있다.
filter 절
$ GET /boards/_search
{
"query": {
"bool" : {
"filter": [
{
"term": { "category" : "자유게시판" }
},
{
"range": { "created_at" : { "gte": "2024-01-01" } }
}
]
}
}
}
filter 절은 반드시 만족해야 하는 조건을 나열하기 위해 사용된다. filter 는 말 그대로 "정확한 해당 값"을 필터링 하는 역할이며, 내부 캐싱이 동작하기 때문에 성능 최적화에 도움을 준다. 내부적으로 term 쿼리를 많이 사용한다. filter 절은 "유연한 검색"에 제공되는 점수에 아무 영향을 주지 않는다. 따라서, 유연한 검색을 위한 text 타입 필드들에 대해서는 filter 절을 사용하지 않는다.
must 절
$ GET /boards/_search
{
"query": {
"bool" : {
"must": [
{
"match": { "content" : "통신 기기 매장" }
},
{
"match": { "title" : "Apple 2025" }
}
]
}
}
}
must 절은 filter 절과 같이 쿼리와 "반드시 만족"하는 데이터를 찾아주지만, 점수 계산에 사용된다. 즉, 유연한 검색에 사용되는 쿼리임이 가장 큰 차이가 있으며, text 타입을 위한 조건절임을 알 수 있다. "text"타입, "match" 쿼리, "bool-must" 쿼리는 형제들이라고 생각하면 된다. filter 절과 must 절은 합쳐서도 많이 사용한다.
예시를 한 번 들어보면, 자유게시판 내에서 "검색 엔진"과 관련된 글을 찾고 싶다. 이 때, 카테고리는 완전한 일치 조건으로, 제목은 유연한 검색으로, 그 중 공지에서 내려간 글에서 찾고 싶은 경우이다. 한 가지 이상의 조건이므로 bool 쿼리, 유연한 검색과 정확한 일치 검색을 같이 사용해야 하니까 must & filter 절을 통해 다음과 같이 쿼리를 구성할 수 있다.
$ GET /boards/_search
{
"query": {
"bool": {
"must": [
{
"match":{
"title":"검색엔진"
}
}
],
"filter": [
{
"term": {
"category":"자유 게시판"
}
},
{
"term": {
"is_notice": false
}
}
]
}
}
}
must_not 절
$GET /boards/_search
{
"query": {
"bool": {
"must_not": [
{
"term": {
"category": "광고 게시판"
}
}
],
"filter": [
"term" : {
"is_notice": true
}
]
}
}
}
must_not 절은 filter 의 반대이다. 명시된 조건만 정확히 "제외"한 필드들을 검색한다. 이름 때문에 헷갈릴 수 있는데, 유연한 검색 및 점수와 아무 상관 없고, must 와도 먼 사이인 것을 확실히 알자. 위처럼 검색하면, 광고 게시판이 아니면서 공지된 모든 게시물을 가져오는 쿼리이고, 유연한 검색을 전혀 사용하지 않는 일치 여부 검색 쿼리이다.
should 절
should 절은 위 세가지 절들과 성격이 조금 다른데, 한마디로 "있으면 좋고 없으면 말고"의 느낌이다. should 절은 유연한 검색을 위한 절이며, score 에 가산점을 부여하기 위한 조건이다. 가령, 최근 출시된 제품, 평점이 좋은 제품, 광고 브랜드 제품 등을 상위에 노출시키는 요구사항을 해결할 때 좋은 조건이다. 내가 원하는 조건에 가산점을 주고 싶을 때, should 쿼리를 생각하면 된다. 따라서, 다양한 조건을 score system 과 연계시켜주기 위한 절이기도 하며, text 타입 외 타입들을 사용할 수 있는 절이다.
사용자가 상품을 검색하는데, 키워드로 관련 데이터를 조회한다고 해보자. 이 때, 평점이 높고 좋아요 수가 많은 상품이 우선적으로 노출되게끔 해보자. 다음과 같이 4.5점 이상, 100개의 좋아요 이상에 가산점을 주는 쿼리를 짜볼 수 있다.
$ GET /products/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "무선 이어폰"
}
}
],
"should": [ ------------- 평점이 4.5이상, 좋아요 100 이상이면 가산점, 없어도 괜찮다
{
"range": {
"rating": {
"gte": 4.5
}
}
},
{
"range": {
"likes": {
"gte": 100
}
}
}
]
}
}
}
결과를 살펴본다면, 4.5 이상의 평점과 100개의 좋아요 수를 넘는 상품들이 상단에 많이 배치되어 있는 것을 알 수 있다. 지금까지의 절들과의 차이는, 4.5이하와 평점이 100개가 안되어도 검색이 되며, score 에 가산점을 얻지 못한채 검색된다는 차이가 있다.
참고로 위에서 사용된 쿼리는 range 쿼리이다. 기간이나 시간 등 범위에 대해 BETWEEN 절을 넣을 수 있다. 위처럼 should 절에 넣을 수 있지만, 기본적으론 당연히 filter 절에서 동작하며, gte / lte / gt / lt 로 사용 가능하다.
이런식으로 서브 쿼리들은 정말 많지만, 주된 쿼리들만 소개하셨다. 실제 서비스 구현하면서 훨씬 복잡한 쿼리들을 제어해야 하는데, 모두 다 외울 수 없으니 그 때 그 때 가능한 쿼리들을 알아보고 조합해보면 된다.
Elasticsearch 에서 활용 빈도가 높은 다양한 기능들
Fuzziness
우리가 일상생활에서 검색을 하다보면 오타가 나도 잘 검색이 되는 모습을 볼 수 있다. fuzziness 는 이를 지원해주는 elasticsearch의 유용한 기능 중 하나이다. 이 부분은 딥하게 다루지는 않지만, 다음과 같이 fuzziness 옵션을 두면, 오타가 발생해도 elasticsearch 란 키워드에 대해서 정상적으로 검색해준다.
$ GET /boards/_search
{
"query": {
"match": {
"title": {
"query": "elastiksearch", ----------- 오타가 났어도 fuzziness 옵션 덕분에 검색된다
"fuzziness": "AUTO"
}
}
}
}
multi_match
여러 필드에 대해 match 쿼리를 사용하고 싶을 때 사용하는 기능이다. 처음 봤을 때 "그럼 bool 쿼리에 match 두 개 넣으면 된다"고 생각할 수 있지만, 차이가 있다. bool 쿼리에 match 두 개를 넣는 것은, 각기 다른 조건으로 사용하는 것이다. A 필드에 "hello", B 필드에 "yellow" 를 요청하는 것이다. 하지만 multi_match 는 A, B 필드에 모두 "hello" 를 검색하도록 요청하는 것이다. 따라서 점수를 선정하는 방식도 다를 것이다. 다음과 같이 데이터가 들어가 있다고 해보자.
- (1) title: 엘라스틱 서치 적용 후기 / content: 엘라스틱 서치 후기 공유합니다
- (2) title: 검색 엔진 사례 / content: 엘라스틱 서치가 너무 좋아요
- (3) title: 레디스 캐시 사용기 / content: 캐시 시스템도 쓸 수 있습니다
$ GET /boards/_search
{
"query": {
"multi_match": {
"query": "엘라스틱서치 적용 후기",
"fields": ["title", "content"] ---------- 위 검색어를 여러 필드에서 한 번에 검색
}
}
}
이와 같이 쿼리를 날리면, (1)과 (2)이 순서대로 검색이 되고, 3번은 검색되지 않을 것이다. 위와 같이 두 text 타입 필드에 대해 모두 query 문에 대한 탐색을 허용해준다. multi_match 쿼리는 가중치도 부여할 수 있다. 만약 위 사례에서 content 에 대해 두 배의 점수를 부여하고 싶다면, "fields":[ "title", "content^2"] 로 명시하면 되고, 이렇게 하면 순서에 영향을 줄 수도 있다.
highlight 기능
많은 검색 사이트를 보면 검색한 키워드에 bold 처리가 되거나 노란색으로 하이라이트 되어서 '이 컨텐츠가 왜 검색되었는지'를 알려주는 역할을 해준다. 이런 기능도 Elasticsearch의 highlight 기능을 사용해서 구현할 수 있다.
$ GET /boards/_search
{
"query": {
"multi_match": {
"query": "엘라스틱 서치 적용 후기",
"fields": ["title", "content"]
}
},
"highlight": {
"fields": {
"title":{
"pre_tags": ["<mark>"], ---------- title 필드에서 검색된 영역 앞뒤에 넣을 tag 들을 나열한다
"post_tags": ["</mark>"]
},
"content": {
"pre_tags": ["<b>"],
"post_tags": ["</b>"]
}
}
}
}
-------- 결과 예시
{
...
"_source": {
"title": "엘라스틱서치 적용 후기",
"content": "회사 프로젝트에 엘라스틱서치를 적용한 후기를 공유합니다."
},
"highlight": {
"title": [ -------- 각 토큰당 tag 를 달아주는 모습 확인
"<mark>엘라스틱</mark><mark>서치</mark> <mark>적용</mark> <mark>후기</mark>"
],
...
}
이처럼 웹 프런트에 바로 html 랜더리을 해줘야 하거나, 다른 서버에서 "검색 결과"를 인지할 수 있는 pointer 를 넣어줘야 할 때 유용하게 사용될 수 있다. 물론 위 응답 예시처럼 원문이 오리지날로 전달되고, highlight 필드에 적용되어 전달된다.
Pagination & Sorting
페이지네이션은 서버 부하를 막기 위해 매우 중요한 기능이며, Elasticsearch 에서도 당연히 이를 지원한다. 또한, 유연한 검색은 기본적으로 "점수"로 sorting 이 되지만, 이 조건을 원하는대로 바꿀 수 있다.
$ GET /boards/_search
{
"query": {
"match": {
"title": "글"
}
},
"size": 3,
"from": 6,
"sort":[
{
"likes": {
"order":"desc"
}
}
]
}
size 는 limit 이고, from 은 offset 이다. 기본값은 각각 10, 0 이다. sort 는 위처럼 특정 필드에 대해 asc / desc 를 정의해주면 된다. 다음과 같이 네 가지 사항만 알아두자.
- likes 처럼 필드명을 명시하지 않고 ["likes","hello"] 로 명시하면, 기본 asc 정렬을 사용한다
- 위 쿼리처럼 match 같이 유연한 검색을 사용하는 쿼리는 기본적으로 score 을 사용하여 내림차순 정렬한다. 위와 같이 sort 조건을 명시하면 score 은 무시하게 된다.
- score 이 같은 경우 likes 로 정렬하라 하고 싶으면, "_score" 필드와 "likes" 필드를 둘다 desc 로 정의하면 된다
- score 이 같은 경우는 잘 없다. 따라서, 다른 필드를 score 에 반영하라 하고 싶으면, 기본적으로 should 를 쓰면 된다. 하지만, should 는 정확한 점수 제어가 어렵기 때문에, 세부 control 을 위해선 functional score query 를 사용해야 한다 (알고만 있자)
multi-field 로 여러 타입 저장
어떤 필드에 대해서는 유연한 검색에도 사용하고 싶고, 정확한 일치에도 사용하고 싶다. 이런 경우가 꽤나 흔하다 (변형 데이터를 같이 저장해야 하는 경우). 이럴 경우 Multi-Field 선언을 해줄 수 있다.
// mapping 정의시
...
"category":{
"type": "text",
"analyzer": "nori",
"fields": { ------- multi-field 선언 (이름과 타입 등을 정의)
"raw": {
"type": "keyword"
}
}
}
...
위와 같이 선언을 해두면, category 는 기본적으로 text 타입 필드에 토큰화되어서 저장되지만, keyword 타입으로도 "category.raw" 라는 다른 필드에 함께 저장된다. 따라서, category 는 "text" 타입임에도 불구하고, 다음과 같이 term 쿼리에 사용할 수 있다.
$ GET /products/_search
{
"query": {
"term": {
"category.raw": "특수 가전제품"
}
}
}
multi-field 기능은 정말 유용하고 데이터 수집을 위해 정말 많이 사용되는 기능이다.
Search as you type, 검색어 추천
쿠팡 같은 곳에서 검색을 하면, 검색을 할 때마다 아래 추천 검색어들이 여러 개 조회되는 것을 확인할 수 있다. 당연히 이 정도 상용 서비스에는 훨씬 깊은 레벨의 구현이 있겠지만, elasticsearch 의 searcy_as_you_type 이라는 필드를 사용해서 적용해볼 수도 있다. 이 타입은 자동완성을 위해서 구현된 타입으로, text 타입처럼 Analyzer를 거쳐 토큰으로 분리된다. 이 타입은 multi field 로 _2gram, _3gram 을 같이 자동으로 만들어준다. 이는 토큰화 결과에서 2, 3개의 토큰을 묶어서 멀티 필드로 저장하는 기능을 말한다. 만약 nori analyzer 가 적용된 필드에 search_as_you_type 을 선언하고, "프리미엄 감귤 선물 세트"를 저장한다면, 다음과 같이 토큰화해서 각 필드에 저장한다.
- "name" : "프리미엄" / "감귤"/ "선물" / "세트"
- "name._2gram" : "프리미엄 감귤" / "감귤 선물" / "선물 세트"
- "name._3gram" : "프리미엄 감귤 선물" / "감귤 선물 세트"
따라서 유저가 검색어를 입력함에 따라서 (미입력 0.5초 단위), 다음과 같이 쿼리를 계속 날리고, 연관된 데이터들을 계~속 렌더링 해주는 방식으로 자동 완성을 구현해볼 수 있다.
$ GET /products/_search
{
"query": {
"multi_match": {
"query": "___",
"type": "bool_prefix",
"fields": [ ----------- 검색하고 있는 내용에 대해 준비해둔 완성 조합으로 연관 검색어를 모두 조회
"name",
"name._2gram",
"name._3gram"
]
}
}
}
위에서 배웠던 multi_match 쿼리와, multi_field에 대한 쿼리를 모두 사용하고 있다. bool_prefix(🚨) 라는 것은 앞 쪽 단어는 match 조건, 마지막 단어는 입력 중인 prefix match 조건을 뜻한다 (이에 대해 딥하게 들어가진 않음). 가령, "you have th" 라고 검색 중이라면 앞쪽 단어인 you / have 는 토큰 검색을 하고 (기존 text 검색), 뒤쪽 단어인 th는 th 로 시작하는 토큰을 찾는다 (일단 이 형태로 외우고 써도 된다. 예외가 발생하는 경우들도 생각나긴 하는데 일단 무시하자).
위 쿼리는 실제로 실무에서 많이 구현단에서 사용하기도 하는 쿼리이며, 당연히 서비스별로 최적화가 보통 진행되지만 기본적인 구현을 위한 뼈대이다.
📖 참고 - n_gram 과 shingle 에 대해
shingle 은 단어 기반으로, 연속된 단어를 묶어서 멀티 필드에 대해 토큰들을 저장하는 것이다. 즉, search_as_you_type 은 _2gram, _3gram 이라는 멀티 필드를 사용하지만, 사실 내부적으로 shingle filter 를 사용한다. 그리고 shingle filter 만 사용하면, 멀티 필드를 자동으로 만들어 주지 않기 때문에, multi_field 생성 기능까지 포함된 기능이라 볼 수 있다. shingle filter 를 원래 사용한다면 다음과 같이 할 수 있다.
$ PUT /{index_name}
{
"settings": {
"analysis": { ------- analyzer 에 적용할 필터 정의
"filter": {
"custom_shingle_filter": {
"type": "shingle",
"min_shingle_size": 2, ---- 두 단어의 조합부터
"max_shingle_size": 3, ---- 세 단어의 조합까지 만든다
"output_unigrams": false
}
},
..... --------- 이후 analyzer 및 필드에 적용
해당 analyzer 를 쓰는 필드에 "hello banana world" 가 전달되었다고 하면, ["hello banana", "banana world", "hello banana world"] 토큰들이 같이 저장되며, 이게 shingle의 기능이다. output_unigram 을 true 로 하면 "hello", "banana", "world"세 가지 토큰도 함께 추가된다. 만약 search_as_you_type 처럼 원본이 아닌 멀티 필드에 shingle 저장을 하고 싶으면, 당연히 멀티 필드 선언 후 해당 필드 analyzer 로 적용하면 된다.
그렇다면 search_as_you_type 에서 등장한 gram 은 무엇일까? n_gram 은 원래 문자 기반이다. 즉, banana 에 대한 2_gram 저장은, [ba, na, na] 로 저장하는 것이다. 2gram, 3gram 을 모두 저장한다 하면 [ban, ana, nan, ana] 가 추가될 것이다.
{
"settings": {
"analysis": {
"filter": {
"custom_ngram_filter": {
"type": "nGram",
"min_gram": 2,
"max_gram": 3
}
},
........ --------- 이후 analyzer 및 필드에 적용
n_gram 역시 shingle 과 똑같이 filter 로, analyzer 단에 적용하면 된다. 원본 필드 외 멀티 필드에 적용하려면, shingle 과 동일하게 멀티 필드를 선언 후 해당 필드의 analyzer 로 지정하면 된다. Search as you type 필드에서 사용하는 명칭들 때문에 조금 헷갈렸지만, 두 필터의 의미 차이는 알아두자!
정리하며
Elasticsearch 에 대해서 간략하지만 어느 정도 심도 있게 알아볼 수 있었다. 강의를 넘어서 궁금한 것들을 계속 찾아보는게 학습에 도움이 되는 것 같다. Elasticsearch 에는 정말 강력한 Analyzer 들이 플러그인으로 많이 들어가 있다. 정말 찾으면 찾을 수록 정말 많다. 하지만 도메인이 특화된 분야에서는 직접 Analyzer 를 개발하는 일이 필요하기도 할 것 같고, 나는 이 쪽을 목적으로 하고 있다.
백엔드 분야에서 개발을 하면 정말 쉽게 들어오는 요청이 바로 검색과 추천이다. 클라이언트들은 당연하게 생각하지만 맨땅으로 구현하기엔 굉장히 복잡하고 난이도가 있는 검색과 추천, 어찌보면 Elasticsearch 는 이제 백엔드 분야에서 정말 선택이 아닌 반드시 해야 하는 필수 소양인 것 같다. 꼭 앞으로도 병행하며 공부해야 할 것이다.
출처
[Elasticsearch 기본]으로 엮인 모든 포스트들은 교육 사이트 인프런의 지식공유자이신 박재성님의 [실전에서 바로 써먹는 Elasticsearch 입문 (검색 최적화편)] 강의를 기반으로 작성되었습니다. 열심히 정리하고 스스로 공부하기 위해 만든 포스트이지만, 제대로 공부하고 싶으시면 해당 강의를 꼭 들으시는 것을 추천드립니다.
실전에서 바로 써먹는 Elasticsearch 입문 (검색 최적화편) 강의 | JSCODE 박재성 - 인프런
JSCODE 박재성 | 비전공자 입장에서도 쉽게 이해할 수 있고, 실전에서 바로 적용 가능한 'Elasticsearch 입문' 강의를 만들어봤습니다!, 🤬 Elasticsearch는 혼자서 공부하기 왜 이렇게 어려운거야?!비전공
www.inflearn.com
'인프라 기술 > Elasticsearch' 카테고리의 다른 글
[Elasticsearch 기본] - 1. 기본 개념, 동작 원리와 Analyzer (5) | 2025.06.27 |
---|