![[ElasticSearch] match, term, bool](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlqEIr%2FbtsOmXNdVMV%2FvL1osGM1sErjK9S7i6RLi0%2Fimg.png)
이 글은 인프런의 지식 공유자 박재성님의 강의를 듣고 개인적으로 정리하는 글임을 알립니다.
match
match
- 검색어가 포함된 유연한 데이터를 조회하고 싶을 때
- match 쿼리는 검색 키워드가 포함된 모든 도큐먼트를 조회한다.
- match 쿼리는 text 타입의 필드에서만 사용하는 쿼리이다.
- keyword를 포함한 long, integer와 같은 타입의 필드에서는 사용하지 않는다.
- text 타입의 필드는 match 쿼리를 활용해 검색을 자주 하는 편이다. 하지만 text 타입이 아닌 필드에서는 match 쿼리를 활용하면 안 된다.
- text를 제외한 모든 타입에선 term을 써야 한다.
인덱스 생성 및 매핑 정의
PUT /boards { "mappings": { "properties": { "title": { "type": "text" } } } } |
데이터 삽입
POST /boards/_doc { "title": "편의점 과자 내돈내산 후기" } |
도큐먼트 검색
GET /boards/_search { "query": { "match": { "title": "편의점 후기" } } } |
multi_match
- 여러 필드에서 검색어가 포함된 데이터를 조회하고 싶을 때
- 여러 필드에서 검색 키워드가 포함된 데이터를 조회할 때는 multi_match 키워드를 사용하면 된다.
구글에서 특정 키워드로 검색해보면 사이트의 제목(title) 뿐만 아니라 내용(content)을 포함해서 검색한다.
이와 같이 여러 필드에서 검색어(엘라스틱서치 적용 후기)가 포함된 데이터를 조회하고 싶을 때는 아래와 같다.
인덱스 생성
PUT /boards { "mappings": { "properties": { "title": { "type": "text", "analyzer": "nori" }, "content": { "type": "text", "analyzer": "nori" } } } } |
데이터 삽입
// title, content 둘 다에 키워드 포함 POST /boards/_doc { "title": "엘라스틱서치 적용 후기", "content": "회사 프로젝트에 엘라스틱서치를 적용한 후기를 공유합니다." } // title에만 키워드 포함 POST /boards/_doc { "title": "엘라스틱서치를 사용해보니", "content": "검색 엔진 도입 후 성능이 향상되었습니다." } // content에만 키워드 포함 POST /boards/_doc { "title": "검색엔진 도입 사례", "content": "이번 프로젝트에 엘라스틱서치를 적용한 후 많은 개선 효과가 있었습니다." } // title, content 둘 다 포함 안 됨 POST /boards/_doc { "title": "레디스 캐시 사용기", "content": "서비스 속도 개선을 위해 캐시 시스템을 사용했습니다." } |
검색
GET /boards/_search { "query": { "multi_match": { "query": "엘라스틱서치 적용 후기", "fields": ["title", "content"] } } } |
검색 응답값
{ "took": 33, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 3, "relation": "eq" }, "max_score": 3.79424, "hits": [ // title, content 둘 다에 키워드 포함 { "_index": "boards", "_id": "2k-SmJYBWhYNXJPWbr6u", "_score": 3.79424, "_source": { "title": "엘라스틱서치 적용 후기", "content": "회사 프로젝트에 엘라스틱서치를 적용한 후기를 공유합니다." } }, // content에만 키워드 포함 { "_index": "boards", "_id": "3E-SmJYBWhYNXJPWd740", "_score": 1.7749425, "_source": { "title": "검색엔진 도입 사례", "content": "이번 프로젝트에 엘라스틱서치를 적용한 후 많은 개선 효과가 있었습니다." } }, // title에만 키워드 포함 { "_index": "boards", "_id": "20-SmJYBWhYNXJPWc75z", "_score": 1.3862942, "_source": { "title": "엘라스틱서치를 사용해보니", "content": "검색 엔진 도입 후 성능이 향상되었습니다." } } ] } } |
- title 또는 content 필드에 검색 키워드가 포함된 데이터를 조회했다.
- title이랑 content 둘 다에 키워드가 포함되지 않은 데이터는 제외됐다.
- 검색 키워드와 데이터의 관련성을 score로 점수를 매겨 조회했다.
- 검색 키워드가 문서에서 자주 등장할 수록 점수가 높게 책정된다.
- 전체 문서 중 검색어가 희귀한 검색어가 일치할 수록 점수가 높게 책정된다.
- 필드 값의 길이가 작은데도 불구하고 키워드가 등장했다면 점수가 높게 책정된다.
특정 필드에 가중치 부여
내용(content)에만 키워드가 포함된 글보다 제목(title)에만 키워드가 포함된 글을 더 상위노출 시키고 싶을 수 있다.
즉, 제목(title)에 검색 키워드가 등장한다면 더 관련성 높은 데이터라고 판단하고 싶을 수 있다. 그럴 때 가중치를 활용한다.
이렇게 검색하면 아래와 같이 title에 검색어가 포함되어있으면 가중치가 부여되어 상위에 노출된다.
GET /boards/_search
{
"query": {
"multi_match": {
"query": "엘라스틱서치 적용 후기",
"fields": ["title^2", "content"] // title에 2배 더 높은 score를 부여
}
}
}
"hits": [
// title, content 둘 다에 키워드 포함
{
"_index": "boards",
"_id": "2k-SmJYBWhYNXJPWbr6u",
"_score": 7.58848,
"_source": {
"title": "엘라스틱서치 적용 후기",
"content": "회사 프로젝트에 엘라스틱서치를 적용한 후기를 공유합니다."
}
},
// title에만 키워드 포함
{
"_index": "boards",
"_id": "20-SmJYBWhYNXJPWc75z",
"_score": 2.7725885,
"_source": {
"title": "엘라스틱서치를 사용해보니",
"content": "검색 엔진 도입 후 성능이 향상되었습니다."
}
},
// content에만 키워드 포함
{
"_index": "boards",
"_id": "3E-SmJYBWhYNXJPWd740",
"_score": 1.7749425,
"_source": {
"title": "검색엔진 도입 사례",
"content": "이번 프로젝트에 엘라스틱서치를 적용한 후 많은 개선 효과가 있었습니다."
}
}
]
}
}
term, terms
term
- 특정 값과 정확하게 일치하는 데이터를 조회하고 싶을 때
- term 쿼리는 특정 값과 정확히 일치하는 모든 도큐먼트를 조회한다.
- term 쿼리는 text를 제외한 모든 타입에서 사용한다.
- text 타입에서도 사용은 가능하나 의도대로 작동하지 않을 수 있어서 사용하지 않는 걸 권장
인덱스 생성 및 매핑 정의
PUT /boards { "mappings": { "properties": { "board_id": { "type": "long" }, "category": { "type": "keyword" } } } } |
데이터 삽입
POST /boards/_doc { "board_id": 1, "category": "자유 게시판" } POST /boards/_doc { "board_id": 2, "category": "익명 게시판" } POST /boards/_doc { "board_id": 3, "category": "광고 게시판" } |
도큐먼트 검색
데이터가 가진 값과 정확히 일치하게 검색하지 않으면 도큐먼트가 조회되지 않는다.
정확한 값으로 검색하면 도큐먼트가 잘 조회된다.
GET /boards/_search { "query": { "term": { "category": "자유 게시판" } } } GET /boards/_search { "query": { "term": { "board_id": 1 } } } |
SQL문으로 표현하자면 아래와 같이 표현할 수 있다.
- SELECT * FROM boards WHERE category = "자유 게시판"
- SELECT * FROM boards WHERE board_id = 1
terms
- 여러 개의 값 중 하나라도 일치하는 도큐먼트 조회
- terms 쿼리는 여러 개의 값 중 하나라도 일치하는 모든 도큐먼트를 조회한다.
- SQL문의 IN과 비슷한 역할을 한다.
- term 쿼리는 하나의 필드에 대해서만 사용할 수 있다.
SELECT * FROM boards WHERE category IN ("자유 게시판", "익명 게시판")처럼 category가 자유 게시판 또는 익명 게시판인 모든 데이터를 조회하고 싶다고 가정
GET /boards/_search { "query": { "terms": { "category": ["자유 게시판", "익명 게시판"] } } } |
SELECT * FROM boards WHERE category = “자유 게시판” AND board_id = 1과 같이 2가지 이상의 조건을 활용해 검색하고 싶을 수도 있다.
2가지 이상의 조건을 만족시키는 데이터를 조회하고 싶을 때는 bool의 filter 또는 must를 사용하면 된다.
잘못 사용한 사례
이 쿼리가 잘못된 이유는 term 쿼리는 하나의 필드에 대해서만 사용할 수 있기 때문이다.
GET /boards/_search
{
"query": {
"term": {
"category": "자유 게시판",
"board_id": 1
}
}
}
bool
1가지 조건이 아닌 2가지 이상의 조건을 만족시키는 쿼리를 작성하려면 bool 쿼리를 활용해야 한다.
즉, bool 쿼리는 여러 쿼리를 조합하기 위해서 사용하는 개념이다. bool 쿼리에는 크게 4가지 기능이 있다.
- must: SQL의 WHERE 절에서 AND 조건(score 영향 있음)과 유사하다.
- filter: SQL의 WHERE 절에서 AND 조건(score 영향 없음)과 유사하다.
- must_not: SQL의 WHERE NOT 조건과 유사하다.
- should: SQL의 WHERE 절에서 OR 조건과 유사하다. (must나 filter와 함께 쓰일 경우 가중치(점수)에 영향을 줄 수 있음)
filter와 must
ilter와 must의 가장 큰 차이는 filter는 score(점수)에 영향을 주지 않고, must는 score(점수)에 영향을 준다는 점이다.
여기서 말하는 score(점수)는 검색을 할 때 관련도가 얼마나 높은지를 나타내는 수치이다.
score(점수)가 활용되는 대표적인 쿼리가 match이다. score(점수)를 활용해 관련도가 높은 도큐먼트를 우선적으로 조회해온다.
즉, 정확하게 일치하지 않아도 도큐먼트를 조회해온다. 반면, term 쿼리와 같이 조건을 정확하게 일치시키지 않으면 도큐먼트를 아예 조회해오지 않는다. 이 때문에 score(점수)를 사용하지 않는다.
filter와 must의 사용 용도는 score(점수)에 영향을 주는 쿼리인지 아닌지로 구분해서 사용해야 한다.
- filter : score(점수)와 상관없는 쿼리인 경우 → ex) term
- must : score(점수)와 상관있는 쿼리인 경우 → ex) match
filter와 must를 쉽게 구분하기
점수(score)를 활용하는 쿼리는 text 타입을 기반으로 검색을 유연하게 해주는 쿼리(match, match_phrase, multi_match 등) 밖에 없다.
즉, 정확하게 일치하지 않더라도 관련성이 있는 데이터까지 조회하는 쿼리에 대해서만 must를 사용하면 된다. 그 외에는 전부 filter를 쓰면 된다.
머릿속에 아래와 같이 연결을 시켜도 이해하기가 쉽다.
유연한 검색 필요
→ text 타입, match 쿼리
→ bool의 must
정확한 검색 필요 (정확한 값 비교)
→ text 이외의 타입, term 쿼리
→ bool의 filter
따라서 아래 예시에서의 term 쿼리는 score(점수)와 상관없는 쿼리이므로 must가 아닌 filter 내부에서 사용하는 것이 올바른 사용 방식이다.
// 올바른 사용 방식 GET /boards/_search { "query": { "bool": { "filter": [ { "term": { "category": "자유 게시판" } }, { "term": { "board_id": 1 } } ] } } } // 잘못된 방식 GET /boards/_search { "query": { "bool": { "must": [ { "term": { "category": "자유 게시판" } }, { "term": { "board_id": 1 } } ] } } } |
filter와 must 구분해서 사용하기
// 인덱스 생성 PUT /boards { "mappings": { "properties": { "board_id": { "type": "long" }, "title": { "type": "text", // 유연한 검색이 필요하므로 text 타입으로 선언 "analyzer": "nori" // 한글 데이터를 토큰으로 정확하게 나누기 위해 }, "category": { "type": "keyword" // 유연한 검색이 필요없으므로 keyword 타입으로 선언 }, "is_notice": { // (공지글 여부) "type": "boolean" }, "created_at": { "type": "date" } } } } // 데이터 삽입 POST /boards/_doc { "board_id": 1, "title": "엘라스틱서치는 정말 강력한 검색엔진이에요", "category": "자유 게시판", "is_notice": false, "created_at": "2025-05-01T12:00:00" } |
검색 쿼리 사용
자유 게시판의 게시글 중에서 검색엔진과 관련된 글을 찾고 싶다. 그런데 공지글이 아닌 게시글 중에서만 검색하고 싶다고 가정하자.
GET /boards/_search { "query": { "bool": { "must": [ { "match": { "title": "검색엔진" } } // 유연한 검색이 필요 (must) ], "filter": [ { "term": { "category": "자유 게시판" } }, // 정확한 검색이 필요 (filter) { "term": { "is_notice": false } } // 정확한 검색이 필요 (filter) ] } } } |
위와 같이 쿼리를 작성해야 용도에 맞게 잘 작성한 것이다. 실제 쿼리를 실행시켜봐도 원하는 값이 출력된다.
must_not
- 특정 조건을 만족하지 않는 데이터를 조회하고 싶을 때 사용
인덱스 생성 및 데이터 삽입
// 인덱스 생성 PUT /boards { "mappings": { "properties": { "board_id": { "type": "long" }, "title": { "type": "text", // 유연한 검색이 필요하므로 text 타입으로 선언 "analyzer": "nori" // 한글 데이터를 토큰으로 정확하게 나누기 위해 }, "category": { "type": "keyword" // 유연한 검색이 필요없으므로 keyword 타입으로 선언 }, "is_notice": { // (공지글 여부) "type": "boolean" }, "created_at": { "type": "date" } } } } // 데이터 삽입 POST /boards/_doc { "board_id": 1, "title": "엘라스틱서치는 정말 강력한 검색엔진이에요", "category": "자유 게시판", "is_notice": false, "created_at": "2025-05-01T12:00:00" } POST /boards/_doc { "board_id": 2, "title": "이벤트 참여 방법 안내드립니다", "category": "광고 게시판", "is_notice": false, "created_at": "2025-05-02T10:30:00" } POST /boards/_doc { "board_id": 3, "title": "익명으로 질문하고 답변 받을 수 있어요", "category": "익명 게시판", "is_notice": true, "created_at": "2025-05-03T08:20:00" } |
검색
광고 게시판의 글이 아니면서, 공지 글이 아니면서, 검색엔진의 키워드와 관련된 게시글을 조회하고 싶다고 가정
// 방법 1 GET /boards/_search { "query": { "bool": { "must": [ { "match": { "title": "검색엔진" } } // 유연한 검색이 필요 (must) ], "filter": [ { "term": { "is_notice": false } } // 정확한 검색이 필요 (filter) ], "must_not": [ { "term": { "category": "광고 게시판" } } ] } } } // 방법 2 GET /boards/_search { "query": { "bool": { "must": [ { "match": { "title": "검색엔진" } } // 유연한 검색이 필요 (must) ], "must_not": [ { "term": { "category": "광고 게시판" } }, { "term": { "is_notice": true } } ] } } } |
range
- 숫자/날짜의 값에 대해 범위 조건으로 데이터를 조회하고 싶을 때, range를 사용한다.
- range는 정확한 값 비교를 하는 쿼리이기 때문에 점수(score)를 계산할 필요가 없다.
- 예를 들어, 날짜, 가격, 숫자 범위 등은 관련도와 무관하게 조건을 만족하느냐만 중요하기 때문에 filter 에서 사용하는 것이 가장 효율적이다.
range 쿼리에서 사용하는 연산자
연산자 | 의미 | 예시 조건 | 의미 설명 |
---|---|---|---|
gte | 이상 (≥) | "gte": 30 | 30 이상인 값 (30, 31, 32...) 포함 |
lte | 이하 (≤) | "lte": 100 | 100 이하인 값 (100, 99, 98...) 포함 |
gt | 초과 (>) | "gt": 18 | 18 초과인 값 (19, 20...) 18은 제외 |
lt | 미만 (<) | "lt": 10 | 10 미만인 값 (9, 8...) 10은 제외 |
인덱스 생성 및 데이터 삽입
// 인덱스 생성 PUT /users { "mappings": { "properties": { "name": { "type": "keyword" }, "age": { "type": "integer" }, "created_at": { "type": "date" } } } } // 데이터 삽입 POST /users/_doc { "name": "kim_jisoo", "age": 28, "created_at": "2024-09-01" } POST /users/_doc { "name": "lee_joon", "age": 35, "created_at": "2024-12-15" } POST /users/_doc { "name": "park_saejin", "age": 32, "created_at": "2025-03-25" } |
검색
나이가 30살 이상이면서 회원가입 날짜가 2025년 1월 1일 이후인 사용자를 조회
GET /users/_search { "query": { "bool": { "filter": [ { "range": { "age": { "gte": 30 } } }, { "range": { "created_at": { "gte": "2025-01-01" } } } ] } } } |
should
bool 쿼리에서 must와 filter는 반드시 조건을 만족하는 데이터만 조회되지만, should는 조건을 만족하지 않는 데이터도 조회되기도 한다.
다만, should의 조건을 충족시키는 데이터일 경우 score(점수)에 가산점을 부여해 상위 노출될 가능성이 높아진다.
그래서 must, filter는 ‘무조건’의 느낌이지만 should는 ‘있으면 좋고, 아니면 말고’의 느낌이다.
사례를 통해서 조금 더 이해해보자.
[사례 1]
고객이 쇼핑몰에서 "노트북"을 검색했을 때, 우리는 다음 조건을 만족하는 상품을 상위에 노출시키고 싶은 경우
- 최근 출시된 제품
- 평점이 높은 제품
- 특정 브랜드(예: Apple)
하지만, 이 조건을 필수로 강제하진 않고 만족하면 가산점을 부여
[사례 2]
사용자가 “백엔드 개발자”를 검색했을 때, 우리는 다음 조건을 만족하는 공고를 더 상위에 노출시키고 싶은 경우
- 원격 근무 가능
- 연봉 6,000만 원 이상
- 유명 기업(예: 네이버, 카카오, 당근)
이 조건은 필수는 아니고, 만족하면 더 매력적인 공고이므로 가산점 부여
[사례 3]
사용자가 네이버에서 "챗GPT"로 블로그를 검색했을 때, 우리는 다음 조건을 만족하는 글을 더 상위에 노출시키고 싶은 경우
- 제목에 "챗GPT"가 포함된 글
- 작성일이 최근일수록
- 좋아요 수가 많은 글
이 조건들을 should로 설정해 더 좋은 글을 먼저 보여주는 전략
인덱스 생성 및 데이터 삽입
// 인덱스 생성 PUT /products { "mappings": { "properties": { "name": { "type": "text", "analyzer": "nori" }, "rating": { "type": "double" }, "likes": { "type": "integer" } } } } // 데이터 삽입 // 키워드("무선 이어폰")와 관련성은 적지만, 좋아요 수가 높고, 평점이 좋은 경우 POST /products/_doc { "name": "무선 충전기 C타입", "rating": 4.9, "likes": 300 } // 키워드("무선 이어폰")와 관련성은 높지만, 좋아요 수가 낮고, 평점도 낮은 경우 POST /products/_doc { "name": "소니 무선 이어폰 WF", "rating": 3.8, "likes": 15 } // 키워드("무선 이어폰")와 관련성도 높고, 좋아요 수도 높고, 평점도 높은 경우 POST /products/_doc { "name": "갤럭시 버즈2 무선 이어폰", "rating": 4.8, "likes": 310 } // 키워드("무선 이어폰")와 관련성이 아예 없는데, 좋아요 수는 높고, 평점도 높은 경우 POST /products/_doc { "name": "삼성 노트북 13인치", "rating": 5.0, "likes": 1000 } |
should를 활용한 검색
GET /products/_search { "query": { "bool": { "must": [ { "match": { "name": "무선 이어폰" } } ], "should": [ { "range": { "rating": { "gte": 4.5 // 4.5 이상의 평점의 상품일 경우 score에 가산점 부여 } } }, { "range": { "likes": { "gte": 100 // 좋아요 수가 100개 이상인 상품일 경우 score에 가산점 부여 } } } ] } } } |
- 1번째 데이터 : 검색 키워드와 관련성이 높고, 평점이 좋고, 좋아요 수가 높은 상품이 상위에 노출됐다.
- 2번째 데이터 : 검색 키워드와 관련성이 조금 낮더라도, 평점이 좋고, 좋아요 수가 높아서 검색 키워드와 관련성이 높은 3번째 상품보다 상단에 노출됐다.
- 3번째 데이터 : 검색 키워드와 관련성은 높았으나, 평점이 낮고 좋아요 수가 낮아서 하단에 배치됐다.
이와 같이 score에 가산점을 주는 bool 쿼리의 should를 활용해 원하는 조건을 갖추고 있는 데이터를 상단에 노출되게 만들 수 있다.
'Back-End > ElasticSearch' 카테고리의 다른 글
[ElasticSearch] 멀티 필드(Multi Field) (0) | 2025.06.02 |
---|---|
[ElasticSearch] 오타 허용 검색, 하이라이팅, 페이지네이션 및 정렬 (0) | 2025.06.01 |
[ElasticSearch] 한글 검색 최적화(Nori Analyzer) (0) | 2025.06.01 |
[ElasticSearch] 데이터 타입(data type)과 매핑(mapping) (0) | 2025.05.31 |
[ElasticSearch] 애널라이저(Analyzer) (0) | 2025.05.31 |