1. 동기
나는 스타트업에서 next 풀스택 개발을 한다. 요즘은 검색엔진 작업을 주로 한다
자꾸 뭐가 뭐였는지 기본적인 개념들을 정리 안한 상태에서 개발하다 보니 헷갈리고 까먹고 뒤죽박죽 멍텅구리다
그래도 검색엔진 사용해 현재 서비스까지 배포하면서 이거는 알면 좋아가 아니라
이건 외워야해 하는 거 정리할거다. 검색엔진은 일종의 네이버라고 생각하면 된다. 네이버도 결국 어떤 키워드로 검색을 하고, 우리의 디비에서 어떤 필드에서 어떤 키워드로 검색해서 추출해 데이터들을 가져올지 결정한다.
작은 필드 타입, 값들 하나가 검색엔진에서 보여지는 화면에 데이터의 수를 가름한다ㅠ 눈물의 검색엔진 개발이었음 ㅠㅠ
그래도 해야지 ?
2. 구조
POST my_index/_search
{
"size": 50,
"from": 0,
"_source": ["title", "content", "Date"],
"query": {
"bool": {
"must": [
{ "match": { "title": "토마토" } }
],
"filter": [
{ "range": { "postDate": { "gte": 20250101, "lte": 20250131 } } }
]
}
},
"sort": [
{ "Date": "desc" },
{ "timestamp": "desc" }
],
"aggs": {
"by_mart": {
"terms": { "field": "supermarket.keyword", "size": 10 }
}
}
}
보통은 엘라스틱 서버에 이렇게 쿼리 요청을 보내게 된다. 그럼 이 구조에 맞게 응답을 주게 된다. ( 워워 진정하자 구조만 보자 )
내부 구조를 크게 나누면
- top-level 파라미터: size, from, _source, sort, track_total_hits, timeout, collapse...등등
- query DSL (검색 조건): query 안의 bool, must, filter, should, must_not, match, term, range 등 : 키워드가 어떤 방식으로 조회할 것인지 볼 수 있다(띄어쓰기를 포함할것인지, 정확히 매칭할 것인지, 이 키워드가 포함되면서 어떤 필드에 필터를 적용할 것인지 등을 매칭해주는 것이다)
- aggs (집계): aggs 안의 terms, date_histogram, cardinality … : 일종의 제일 많이 언급된 것 이런 집계적인 조회를 할 수 있다
- 필드와 매핑: text, keyword, date, long 등의 타입과 .keyword 서브필드
요렇게 나누고 뜯어보자
3. 개념
3.1. 문서 / 필드 / 매핑 개념
- index : RDBMS로 치면 테이블 비슷한 단위 (디비에 테이블을 만들어야 조회를 하는거 처럼 이게 인덱스)
- document(문서) : 테이블의 row 느낌. JSON 하나가 문서.
- field(필드) : row의 column 같은 것.
매핑 : 각 필드가 어떤 타입인지 정의를 하는 설정이라고 보면 된다
- text : 풀텍스트 검색용 (문장 검색), 분석기(analyzer)로 잘게 쪼개서 단어 단위로 색인, 그래서 부분검색이 가능, match, multi_match, query_string, bool_prefix 등에서 검색할 때 사용 (나중에 설명) , 한국어 최적화된 nori(나중에 설명) 를 쓸때 사용한다고 함
- keyword : 정확값 / 집계 / 정렬용, text랑 다르게 쪼개지 않고 전체 문자열 그대로 저장 (term 쿼리, terms 쿼리, terms aggregation, sort에 사용)
- date : 날짜/시간 (range 쿼리 필수)
- long, integer, double : 숫자형
- boolean
좀 헷갈렸던 부분 !!!!!!!
보통은 엘라스틱에서 문자열 필드를 만들면 이런 매핑을 볼 수 있다.
GET 귀욤둥이의인덱스/_mapping : 이렇게 인덱스를 만들어서 이 명령어하면 매핑 정보 볼 수 있뚬!
"product": {
"type": "text",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 }
}
}
- product : 이 자체의 type: text (nori 같은 한글분석기로 토큰을 쪼개서 풀텍스트 검색용으로 쓰는 필드) , match, multi_match 등에 사용된다.
- product.keyword : 이것의 타입은 type: keyword
(문자열 전체를 쪼개지 않고 그대로 저장한 것), (정확한 비교, 집계, 정렬에 사용된다)
"terms": {
"product.keyword": ["냉장고", "에어컨"]
}
product 배열 필드가 사실 디비에 저장이 되어 있을때 엘라스틱에서는 product는 text라서 집계, 정렬에 바로 못쓴다. 그래서 product.keyword로 바꿔서 써야하는 것이다.
왜 .keyword가 필요?
만약 content: "토마토 주스보다 토마토 케찹이 좋아 멋쟁이 토마토"
1. content 는 text로 토마토, 주스보다, 케찹 뭐 이렇게 단어들로 분석된다. (그래서 주스가 포함된 글을 검색하기에 좋다(match, multi_match)
2. content.keyword 는 keyword로 "토마토 주스보다 토마토 케찹이 좋아 멋쟁이 토마토" 통째로 한 덩어리로 저장된다.
이것을 term, terms로 검색을 하면 문장이 완전히 똑같은 경우에만 매칭된다.
- 일반적으로 제목, 본문 같은 것보다 제품명, 위치 같은 필드에 .keyword가 유용할 거 같다.
- 특징으로는 분석기에 적용이 안되기 때문에 오타, 띄어쓰기, 대소문자 그대로 본다(그래서 집계, 정렬, 정확 필터에 사용하기 좋은 것이다. 너무 많이 언급했다 힘들다)
3.2. top-level 파라미터
- size : 한번에 몇개 가져올지 (사과가 들어간 문서를 몇개 가져올지 엘라스틱에서는 1만개를 최대로 권장함 )
- from : 페이지네이션 (앞에를 몇개를 건너뛸지 ) - page 1: from: 0, size: 20/ page 2: from: 20, size: 20 (만약에 1만개의 카드리스트를 뿌린다고 할때 1만개를 보여주는게 아니라 페이지네이션을 결정해도 좋다는 것이다. 몇페이지까지만 부르고 뭐 이런 방식 -> 이거 나도 프론트에서 데이터를 몇 만개를 뿌릴떄 애용했다 ㅎㅎ)
- sort : 정렬 (위에서부터 우선순위다) - text 타입 필드는 정렬 /집게에 직접 사용안됌 (반드시 creadAt.keyword 처럼 .keyword 필드를 사용해야한다-> 아래 예시를 들어보겠음)
"sort": [
{ "date": "desc" },
{ "timestamp": "desc" },
{ "createdAt.keyword": "asc" }
]
- _source : 어떤 필드를 가져올지 ( 응답에 포함할 필드를 지정해서 네트워크 비용을 줄일 수 있다)
- track_total_hits : total count 정확도 (기본적으로 1만개 이상이면 "relation": "gte" 로만 알려준다) , 정확한 전체 개수가 필요할 때 true 또는 충분히 큰 숫자를 주어서 해당 키워드로 검색된 문서 수를 알자
- timeout : "2s" -> 2초 넘으면 중간까지만 결과 반환한다.
- terminate_after : 샤드에서 각 몇건까지 검색할지 제한( 성능/속도 튜닝용임) 아 샤드 개념 정리는 따로 작성해야징
3.3. Query DSL !! (쿼리라는 도메인에 특화된 dsl - 도메인 특화 언어)
엘라스틱 검색 조건을 표현하기 위해 만든 json 기반 언어라고 생각하며 됌. (검색조건을 전달하기 위해 사용한 '검색 전용 json 문법 체계'
이게 찐이다 여기를 일단 잘 알아야한다.
- bool? : 여러 개의 쿼리를 논리적으로 조합하는 컨테이너
and, or, not 같은 논리(부울) 연산을 수행하기 위한 쿼리 컴바이너(조합기) 이다. 그래서 이름도 불쿼리!
엘라스틱에서는 기본적으로 단일쿼리는 하나의 조건만 검색하지만 실제로는 항상 여러 조건을 붙여져서 검색하게 된다. 예를 들어 제품 필드에 '토마토' 가 포함되면서 date 는 최근 일주일 범위이고, 장소 필드는 서울이라고 하면 이런 조합들을 만들기 위해서는 공식 컨테이너 bool 이 필요한 것이다.
그래서 bool 이 없이도 검색을 할 수도 있지만 이러면 단일 쿼리 검색이 되니까 노잼 쿼리가 되는 것 ~ query 아래에는 bool 만 오는게 아니라 엘라스틱의 모든 쿼리 타입이 올 수 있다 하지만 bool을 쓰면 여러 조건을 조합할 수 있다. match/mulit_match/term/range도 단독으로 올 수 있긴 한데 걍 같이 쓰는게 편한거 같다
3.3.1. bool 의 내부 구조 (핵심만 4가지 타입)
타입 더 많음 ;;
"bool": {
"must": [],
"filter": [],
"should": [],
"must_not": []
}
각각의 불리언 논리연산을 수행한다고 보면 됌.
(1) must
- and 조건 ( 점수 존재 - 가중치가 있다 어떤 것이 더 매칭률이 높은 문서인지 라고 생각하면 될듯)
- 텍스트를 검색할 때 (match, mulit_match - 아래 설명) 이런 조건을 넣는데 이때 점수 계산이 필요한 조건은 여기에 들어간다.
(위에 3.1. 매핑 타입에서 설명했듯이 텍스트는 정확한 쿼리 검색이 아니라 전체 텍스트에서 검색하려는 단어를 찾는 것이기 때문에 점수 매칭에 적용된다고 보면 된다. 반대로 .keyword 는 텍스트와 다르게 검색한 단어와 정확한 단어를 찾아서 보여준다.)
- 캐시를 적게 사용한다.
- 텍스트 검색에 좋고, 날짜/정확한 값은 별로임
(2) filter
- and 조건( 점수 없음 : must 에 검색할 단어를 넣었고, 필터는 검색한 단어로 찾은 문서들 중에 내가 넣고 싶은 조건들을 넣는 부분이라고 보면 된다. 그래서 점수가 없고 반드시 만족해야할 조건이 된다.)
- 캐시를 많이 사용되서 빠르고, 성능 최적화의 핵심이다.
- 텍스트 검색 안됌
- 날짜/ 정확한 값에 좋음
(3) shold
- OR 조건 ( 이 조건을 만족하면 좋다) 라는 뜻이고
- 예를 들어 제품 필드에 토마토가 들어가는 것은 must에 넣고 type필드에 싱싱함 이라는 키워드가 포함되면 더 좋다 이런 느낌임
"should": [
{ "match": { "type": "싱싱" }},
{ "match": { "type": "국내산" }}
],
"minimum_should_match": 1
- minimum_should_match 옵션 ! : or 조건이 여러 개 있을때 그 중 최소한 '몇 개' 를 만족해야 문서를 검색 결과에 포함시킬 지 결정한다. should는 기본적으로 있으면 점수는 올려주지만 맞지 않아도 된다. 선택조건이기 때문에 (하지만 should 안에 조건을 최소 1개는 만족해야 문서를 포함하기 위해서는 저렇게 1을 포함해주면 된다!) -> 숫자 말고 퍼센트도 됨 ㅎㅎ 후뚜루 마뚜루 조건에 쓰기 좋음
"query": {
"bool": {
"must": [
{ "match": { "product": "냉장고" } }
],
"should": [
{ "match": { "content": "저렴" } },
{ "match": { "content": "가성비" } }
],
"minimum_should_match": 1
}
}
제품에 냉장고는 반드시 있어야 하고 본문에는 저렴, 가성비가 하나라도 나와야 한다.
(4) must_not
- 말 그대로 not 조건 (제외조건)
"must_not": [
{ "term": { "유통기한": "지남" } }
]
+ bool은 조합기 일 뿐이다. bool 은 직접 검색을 하지 않는다. bool 내부에 match, term, range, query_string 등이 실제 쿼리를 넣어 조합하는역할이다
"query": {
"bool": {
"must": [
{ "multi_match": { "query": "대파", "fields": ["냉장고","냉동고"] } }
],
"filter": [
{ "range": { "Date": { "gte": 20250101, "lte": 20251231 } } },
{ "terms": { "source": ["국내산","중국산"] } }
]
}
}
bool(상자), must/filter (칸), match/term (조건) : bool은 and 조합(must + filter), or조함(should), not 조합(must_not)
3.3.3. term/terms 타입
: 정확한 값을 찾는 쿼리라고 보면된다 "정확 매칭 쿼리" (Nori 같은 분석기없이 원본 문자열 그대로 완벽히 일치하는 문서만 매칭한다)
term 는 "토마토 주스보다 토마토 케찹이 좋아 멋쟁이 토마토" 이 전체 문자열 그대로 비교하는데 사용된다.
대소문자, 띄어쓰기, 중간에 오타 이런것도 다 매칭이 안된다.
그래서 keyword 타입에 써야한다 !!!!
!!!!!!!!!!!!! text 필드는 분석되서 조각나기 때문에 term으로 비교하면 절대 거의 안맞음 !!!!!!!1
{ "term": { "product.keyword": "생수 500ml" } }
terms는 복수 형태로 여러 값 중에 하나라도 같으면 매칭이 된다.
{
"terms": {
"sesson": ["spring", "winter"]
}
}
구분 term terms
| 의미 |
term(정확히 하나의 값) |
terms(여러 값 중 하나) |
| SQL 대응 |
= |
IN (...) |
| 속성 |
exact match |
OR 조건 |
| 사용 예 |
특정 제품, 특정 위치 |
여러 제품, 여러 위치 |
{ "terms": { "products.keyword": ["냉장고", "에어컨"] } }
products: ["선풍기", "전자렌지", "냉장고"] -> 하나라도 있으면 매칭이 된다.
- term/terms 는 점수 계산을 안하기 때문에 필터로 사용해야한다.
- term/terms는 query 단게 (문서 검색시 ) , 필터링 하는 단계에서 사용한다. rdb에서 where 조건 역할
- term/terms는 집계(agg) 할때 사용된다. (문서 그룹핑하고 그 갯수를 세는 단계, sql 의 group by 역할임)