✅ match
match
쿼리는 text
타입의 필드에서만 사용하는 쿼리match
쿼리는 검색 키워드가 포함된 모든 도큐먼트를 조회EX)
PUT /boards // 인덱스 생성 및 매핑 정의
{
"mappings": {
"properties": {
"title": {
"type": "text"
}
}
}
}
POST /boards/_doc // 데이터 삽입
{
"title": "벨로그 짱짱 멋짐"
}
GET /boards/_search // 도큐먼트로 검색 (아래와 같이 조회하면 결과값이 조회됨)
{
"query": {
"match": {
"title": "벨로그 멋짐"
}
}
}
✅ term
term
쿼리는 특정 값과 정확히
일치하는 모든 도큐먼트를 조회term
쿼리는 text
를 제외한 모든 타입에서 사용EX)
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": "익명 게시판"
}
---------------------------
GET /boards/_search // 조회1
{
"query": {
"term": {
"category": "자유"
}
}
}
GET /boards/_search // 조회2
{
"query": {
"term": {
"category": "자유게시판"
}
}
}
GET /boards/_search // 조회3
{
"query": {
"term": {
"category": "자유 게시판"
}
}
}
위와같이 조회를 3번 해봤을 때 어떤것이 조회될까?
조회3만 도큐먼트가 조회된다
조회1 -> 자유 (데이터가 정확하게 일치하지 않음)
조회2 -> 자유게시판 (공백이 있어서 데이터가 일치하지 않음)
즉 데이터가 가진 값과 정확히 일치하게 검색하지 않으면 도큐먼트가 조회되지 않는다
SQL문으로 표현하면 SELECT * FROM boards WHERE category = "자유 게시판"
와 동일하다
그럼 여러개 값중 하나라도 일치하면 도큐먼트를 조회하고 싶을 때 어떻게 해야할까?
✅ terms
terms
쿼리는 여러 개의 값 중 하나라도 일치하는 모든 도큐먼트를 조회IN
과 비슷한 역할EX)
GET /boards/_search
{
"query": {
"terms": {
"category": ["자유 게시판", "익명 게시판"]
}
}
}
위와 같이 조회를 하면 자유 게시판
익명 게시판
둘다 조회되는것을 확인할 수 있다
이 문장은 SELECT * FROM boards WHERE category IN ("자유 게시판", "익명 게시판")
와 동일하다
그러면 여러개의 조건을 동시에 만족하는 데이터를 조회할 때는 어떻게 해야할까?
SELECT * FROM boards WHERE category = “자유 게시판” AND board_id = 1
와 같이
2가지 이상의 조건을 활용해서 검색하려 한다
아래와 같이 조회하면 오류가 발생한다
즉 term
쿼리에서는 2개이상 필드를 사용하는 것을 막아놨다
GET /boards/_search
{
"query": {
"term": {
"category": "자유 게시판",
"board_id": 1
}
}
}
✅ 2가지 이상의 조건을 만족시키는 데이터를 조회하고 싶을 때 사용하는 쿼리
bool
쿼리에서 must
or filter
를 사용하면 된다
즉 SQL에서 AND 역할을 한다
filter
와 must
의 큰 차이점은
filter
는 Score(점수) 영향을 주지않고, must
는 Score(점수) 영향을 준다
Score(점수)는 사용자가 입력한 값이 얼마나 잘 일치하는지 수치로 표현한 관련도 점수이며
역인덱스를 이용해서 계산된 관련도 점수이다
역인덱스는 필드 값을 단어마다 쪼개서 찾기 쉽게 정리해놓은 목록이다
여기서 전에 배운 text
타입과 역인덱스와 Score(점수) 는 밀접한 관련이 있다
text
타입으로 필드를 만든 후 데이터를 넣으면 Analyzer
에 의해서 역인덱스
가 생성된다
역인덱스
에 저장된 단어들을 토큰
이라고 부른다. 그리고 검색 쿼리가 오면 역인덱스
를 통해 데이터를 찾고 Score(점수)를 계산한다
결론은 text
타입으로만 Score(점수) 가 사용되며 계산되며
text
타입의 경우 match
쿼리를 사용하기 때문에 Score(점수)와 상관있는 쿼리의 경우 match
사용
text 이외의 타입의 경우는 Score(점수)와 상관없기 때문에 term
을 사용
💡 정리
유연한 검색이 필요하면
-> text
타입, match
쿼리, bool
의 must
사용
정확한 검색이 필요하면
-> text 이외의 타입, term
쿼리, bool
의 filter
사용
예시를 보면서 이해해보자
board_id : long 타입
title : text 타입
category : keyword 타입
is_notice : boolean 타입
created_at : date 타입
위처럼 boards라는 인덱스를 생성하고 매핑까지 했다고 가정하자
그 후 데이터를 삽입
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)
]
}
}
}
자유 게시판의 게시글 중에서라는 뜻 -> 자유 게시판 이라고 정확한 검색이 필요 (filter 사용)
검색엔진과 관련된 글 뜻 -> 유연한 검색이 가능함 (must 사용)
공지글이 아닌 게시글 뜻 -> 공지글이 아닌것은 false 로 정확히 구분 (filter 사용)
✅ 특정 조건을 만족하지 않는 데이터를 조회하고 싶을 때
bool
쿼리에서 must_not
을 사용하면 된다
즉 SQL에서 NOT 역할을 한다
직전 예시를 그대로 구현해보자
EX) 광고 게시판
의 글이 아니면서, 공지 글
이 아니면서, 검색엔진
의 키워드와 관련된 게시글을 조회
GET /boards/_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "검색엔진" } } // 유연한 검색이 필요 (must)
],
"filter": [
{ "term": { "is_notice": false } } // 정확한 검색이 필요 (filter)
],
"must_not": [
{ "term": { "category": "광고 게시판" } }
]
}
}
}
광고 게시판의 글이 아니라는 뜻 -> 광고 게시판이 아니여야 한다는 정확한 검색이 필요 (mustnot 사용)
공지 글이 아니면서 뜻 -> 공지글이 아니여야 한다는 정확한 검색 필요 (filter 사용)
검색엔진 의 키워드와 관련된 뜻 -> (must 사용)
💡 must_not
은 term
과 match
를 둘 다 사용할 수 있지만 조건에 따라 사용이 다르다
ex) 어떤 필드값이 광고가 아니여야 할 때 -> term
사용
ex) 어떤 필드값이 '광고’ 관련 내용을 유사하게라도 포함한 글이 아니여야할 때 -> match
사용
✅ 숫자/날짜의 값에 대한 범위 조건으로 데이터를 조회하고 싶을 때
range
쿼리를 사용하면 된다
range
쿼리에서 사용하는 연산자
gte
: 이상 lte
: 이하 gt
초과 lt
미만gte
= Greater Than or Equal to
(크거나 같음)lte
= Less-than or equal to
(작거나 같음)ex) 나이가 30살 이상이면서 회원가입 날짜가 2025년 1월 1일 이후인 사용자를 조회
GET /users/_search
{
"query": {
"bool": { // 2가지 이상의 조건이 있기때문에 bool 쿼리 사용
"filter": [ // 정확한 검색을 해야하기때문에 filter 사용
{
"range": {
"age": {
"gte": 30
}
}
},
{
"range": {
"created_at": {
"gte": "2025-01-01"
}
}
}
]
}
}
}
✅ 특정 조건을 만족하는 데이터 위주로 상위 노출시키고 싶을 때
boole
쿼리에서 should
를 사용하면 된다
위에서 배운 must
와 filter
는 반드시 조건을 만족하는 데이터만 조회하지만
should
는 조건을 만족하지 않는 데이터도 조회된다
그 이유는 should
는 조건을 추가해서 그 조건이 맞을경우 score(점수)
에 가산점을 부여해서
데이터가 상위로 노출될 가능성이 높아진다
ex) 검색 결과 중 평점이 높고
좋아요 수가 많은 글
을 상위에 노출시키고 싶은 경우
인덱스를 다음과 같이 생성했다고 가정하자
PUT /products
{
"mappings": {
"properties": {
"name": { // 제품이름
"type": "text",
"analyzer": "nori"
},
"rating": { "type": "double" }, // 평점
"likes": { "type": "integer" } // 좋아요 수
}
}
}
데이터는 아래와 같이 넣었다고 가정하자
"name": "무선 충전기 C타입", "rating": 4.9, "likes": 300
"name": "소니 무선 이어폰 WF", "rating": 3.8, "likes": 15
"name": "갤럭시 버즈2 무선 이어폰", "rating": 4.8, "likes": 310
"name": "삼성 노트북 13인치", "rating": 5.0, "likes": 1000
should
조건 없이 데이터를 조회하면 무선 이어폰
키워드가 없는 데이터는 아예 조회되지도 않고
평점이 낮거나 좋아요 수가 적은 상품이 상위에 노출되는 것을 확인할 수 있다
아래처럼 should
를 사용해서 조회를 해보자
GET /products/_search
{
"query": {
"bool": {
"must": [ // 특정키워드로 연관성이 있는 것 즉 score를 매겨야하기 때문에 must 사용
{
"match": {
"name": "무선 이어폰"
}
}
],
"should": [
{
"range": {
"rating": {
"gte": 4.5 // 4.5 이상의 평점의 상품일 경우 score에 가산점 부여
}
}
},
{
"range": {
"likes": {
"gte": 100 // 좋아요 수가 100개 이상인 상품일 경우 score에 가산점 부여
}
}
}
]
}
}
}
결과는 어떻게 나올까?
1 순위 : 갤럭시 버즈2 무선 이어폰 -> 검색 키워드도 관련성 높으면서 평점도 좋고 좋아요 수도 많아서 1위
2 순위 : 무선 충전기 C타입 -> 검색 키워드는 '무선' 만 일치하지만 평점과 좋아요 수가 높아서 2위
3 순위 : 소니 무선 이어폰 WF -> 검색 키워드는 관련성이 높지만 평점과 좋아요 수가 낮아서 3위
삼성 노트북 13인치는 관련 키워드가 아예 없기 때문에 조회되지 않음
✅ 오타가 있더라도 유사한 단어를 포함한 데이터를 조회하고 싶을 때
fuzziness
를 사용하면 된다
데이터에 name 필드가 => 메이플 캐논슈터 일때
아래와 같이 조회하면 데이터가 잘 조회되는 것을 확인할 수 있다
GET /boards/_search
{
"query": {
"match": {
"title": "메이플"
}
}
}
하지만 마이플 이라고 검색하면 데이터가 조회되지 않는다
이럴 때 오타가 어느정도 있어도 검색되게 만들기 위해서 fuzziness
를 사용한다
GET /boards/_search
{
"query": {
"match": {
"title": {
"query": "마이플",
"fuzziness": "AUTO"
}
}
}
}
fuzziness : AUTO
: 단어 길이에 따라 오타 허용 개수를 자동으로 설정
✅ 여러 필드에서 검색 필드가 포함된 데이터를 조회하고 싶을 때 (ex : 제목, 내용)
multi_match
를 사용하면 된다
예시로 데이터를 4개 삽입했다고 가정하자
POST /boards/_doc // (1) title, content 둘 다에 키워드 포함
{
"title": "엘라스틱서치 적용 후기",
"content": "회사 프로젝트에 엘라스틱서치를 적용한 후기를 공유합니다."
}
POST /boards/_doc // (2) title 에만 키워드 포함
{
"title": "엘라스틱서치를 사용해보니",
"content": "검색 엔진 도입 후 성능이 향상되었습니다."
}
POST /boards/_doc // (3) content에만 키워드 포함
{
"title": "검색엔진 도입 사례",
"content": "이번 프로젝트에 엘라스틱서치를 적용한 후 많은 개선 효과가 있었습니다."
}
POST /boards/_doc // (4) title, content 둘 다 포함 안 됨
{
"title": "레디스 캐시 사용기",
"content": "서비스 속도 개선을 위해 캐시 시스템을 사용했습니다."
}
여기서 아래와 같이 multi_match
를 이용해서 조회하면
GET /boards/_search
{
"query": {
"multi_match": { // 두가지 이상 필드에서는 multi_match 사용
"query": "엘라스틱서치 적용 후기",
"fields": ["title", "content"] // 찾고자하는 여러개의 필드를 배열로 지정
}
}
}
(1) <- (3) <- (2) 순으로 상위노출되서 조회되는것을 확인할 수 있다
(4)는 title과 content에 둘 다 키워드가 없어서 조회되지 않는다
왜 위와 같은 순서로 상위노출 조회 됬을까?
다음과 같은 조건으로 score
로 점수를 매긴다
(1) 은 title과 content에 키워드가 둘 다 들어있기 때문에 score
를 높게 받아 제일 먼저 조회된다
그러면 내가 원하는 필드에 검색어가 포함될 때 점수를 더 높게 주려면 어떻게 해야할까? (가중치)
다음과 같이 작성하면 된다
GET /boards/_search
{
"query": {
"multi_match": {
"query": "엘라스틱서치 적용 후기",
"fields": ["title^2", "content"] // title에 2배 더 높은 score를 부여
}
}
}
위와 같이 조회한다면 (1) <- (2) <- (3) 순으로 조회되는 것을 확인할 수 있다
✅ 검색한 키워드를 하이라이팅 처리하고 싶을 때
highlight
를 사용하면 된다
바로 위 예시로 사용해서 작성해보자
GET /boards/_search
{
"query": {
"multi_match": {
"query": "엘라스틱서치 적용 후기",
"fields": ["title", "content"]
}
},
"highlight": {
"fields": {
"title": {
"pre_tags": ["<mark>"],
"post_tags": ["</mark>"]
},
"content": {
"pre_tags": ["<b>"],
"post_tags": ["</b>"]
}
}
}
}
위처럼 title에서 특정 키워드가 일치하면은
pre_tags
(키워드 앞에는) mark 태그를 붙이고 post_tags
(키워드 뒤에는) /mark 태그를 붙인다
또 content에서 키워드가 일치하면
pre_tags
(키워드 앞에는) b 태그를 붙이고 post_tags
(키워드 뒤에는) /b 태그를 붙인다
조회 결과는 아래와 같이 HTML 태그로 감싸져서 결과 값이 반환된다
"title": [ "<mark>엘라스틱</mark><mark>서치</mark> <mark>적용</mark> <mark>후기</mark>" ],
"content": [ "회사 프로젝트에 <b>엘라스틱</b><b>서치</b>를 <b>적용</b>한 <b>후기</b>를 공유합니다." ]
✅ 페이징과 정렬
우선 페이징은 size
와 from
을 사용하면 된다
데이터를 총 7개 (1번글 ~ 7번글) 을 생성했다고 가정하자
POST /boards/_doc
{
"title": "1번 글",
"likes": 12
}
POST /boards/_doc
{
"title": "2번 글",
"likes": 35
}
..... ~ 7번글까지
여기서 페이징 처리를 적용시켜서 데이터를 조회해보자
GET /boards/_search
{
"query": {
"match": {
"title": "글"
}
},
"size": 3, // 3개만 불러오기
"from": 0 // 첫번째부터
}
size
: 한 페이지에 불러올 데이터 개수 (SQL문의 LIMIT과 동일)
from
: 몇 번째 데이터부터 불러올 지 (SQL문의 OFFSET과 동일, 0부터 시작)
그럼 2페이지는 어떻게 조회하는가?
size
는 그대로 3이고 from
을 3으로 바꾸면 된다
다음은 정렬을 해보자
likes(좋아요) 필드를 기준으로 정렬해보자
GET /boards/_search
{
"query": {
"match": {
"title": "글"
}
},
"sort": [
{
"likes": {
"order": "desc" // 내림차순
}
}
]
}
✅ 하나의 필드에 text
와 keyword
타입을 동시에 사용하고 싶을 때
text
는 유연한 검색, keyword
는 정확한 검색이 필요할 때 사용한다
유연한 검색과 정확한 검색을 둘 다 하고 싶을 때 사용 한다
아래 예시는 category 필드에 두 가지 타입을 사용한 경우이다
PUT /products
{
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "nori"
},
"category": {
"type": "text", // text 타입 선언
"analyzer": "nori",
"fields": {
"raw": { // 서브 필드명 (다른 이름으로 설정해도 됨)
"type": "keyword" // keyword 타입 선언
}
}
}
}
}
}
위처럼 인덱스를 생성하고 매핑 한 후 데이터를 다음과 같이 삽입하면
POST /products/_doc
{
"name": "삼성 세탁기",
"category": "특수 가전제품"
}
text 타입은 토큰으로 분리해서 저장한다 -> 특수
가전
제품
keyword 타입은 값 자체를 통째로 저장한다 -> 특수 가전제품
이 상태에서 유연하게 검색을 하면
POST /products/_search
{
"query": {
"multi_match": {
"query": "가전",
"fields": ["name", "category"] // name필드나 category 필드에 가전이 들어가면 조회됨
}
}
}
이번엔 특수 가전제품 카테고리의 상품만 검색하고 싶으면
POST /products/_search
{
"query": {
"term": { // 정확하게 일치하는 데이터를 찾기위해서 term 사용
"category.raw": "특수 가전제품"
}
}
}
✅ 검색어를 추천해주는 기능 ( 자동 완성 기능)
여러가지 방법이 있지만 자주 사용하고 가성비 좋은 search_as_you_type
을 활용해보자
search_as_you_type
데이터 타입
_2gram
, _3gram
이라는 멀티 필드(Multi Field)도 같이 만든다._2gram
: 두 단어씩 묶어서 토큰을 만든다
_3gram
: 세 단어씩 묶어서 토큰을 만든다
예를 들어 아래와 같이 search_as_you_type
타입으로 인덱스를 생성하고
PUT /products
{
"mappings": {
"properties": {
"name": {
"type": "search_as_you_type",
"analyzer": "nori"
}
}
}
}
Analyze로 토큰을 분석해보자
GET /products/_analyze
{
"field": "name",
"text": "You have the big banana"
// 일반 text 타입처럼 동일한 방식으로 분리됨 -> 'you' 'have' 'the' 'big' 'banana'
}
GET /products/_analyze
{
"field": "name._2gram",
"text": "you have the big banana"
// 두 단어씩 분리됨 -> 'you have' 'have the' 'the big' 'big banana'
}
GET /products/_analyze
{
"field": "name._3gram",
"text": "you have the big banana"
// 세 단어씩 분리됨 -> 'you have the' 'have the big' 'the big banana'
}
이제 자동완성 기능을 사용해보자
데이터를 곱창 돌김생김
구운 돌김
완도 곱창 돌김 100매
삼성 노트북
Nike 신발
을 삽입했다고 가정
위 데이터를 바탕으로 아래처럼 조회를 하면 데이터가 자동완성 된다
GET /products/_search
{
"query": {
"multi_match": {
"query": "돌김",
"type": "bool_prefix",
"fields": [
"name",
"name._2gram",
"name._3gram"
]
}
}.
"size" : 5 // 5개까지만
}
여기서 bool_prefix
앞쪽 단어는 match, 마지막 단어는 prefix match로 처리한다
ex) you have the
라고 검색하면 you
have
는 연인덱스에 저장된 토큰과 일치하는 데이터를 찾고
마지막 단어인 the
로 시작하는 데이터를 조회하는 것이다