✅ 역 인덱스
✅ 역 인덱스의 원리
PUT /products // products 인덱스(테이블)을생성하면서 매핑(스키마) 하기
{
"mappings": {
"properties": {
"name" : {
"type": "text"
}
}
}
}
POST /products/_create/1 // id : 1
{
"name": "Apple 2025 맥북 에어 13 M4 10코어"
}
POST /products/_create/2 // id : 2
{
"name": "Apple 2024 에어팟 4세대"
}
POST /products/_create/3 // id : 3
{
"name": "Apple 2024 아이패드 mini A17 Pro"
}
GET /products/_search // 특정 단어(문장)을 포함한 도큐먼트를 조회하기
{
"query": {
"match": {
"name" : "Apple 2024 아이패드"
}
}
}
위와 같이 3개의 도큐먼트를 엘라스틱 서치에 저장한다고 가정해보자
그러면 내부적으로 데이터가 단어단위로 잘라지고 역인덱스
로 저장이 된다
💡 필드값에서 추출되어 역 인덱스에 저장된 단어를 토큰
이라고 부름
💡 생성된 역 인덱스는 시스템 내부적으로만 생성이 되어서 눈으로 확인 불가능
역 인덱스로 저장
검색을 할 경우 역 인덱스 활용
EX) Apple 2024 아이패드
를 검색하면 역 인덱스를 활용하여 일치하는 단어가 많은
도큐먼트를 우선적으로 조회함
id가 1 도큐먼트는 토큰이 1개 일치
id가 2 도큐먼트는 토큰이 2개 일치
id가 3 도큐먼트는 토큰이 3개 일치
즉 id가 3인 도큐먼트가 제일 먼저 조회됨
ElasticSearch 가 자체적인 로직으로 점수를 매겨서 높은순으로 도큐먼트를 조회한다
대략적으로 점수를 계산하는 로직에는 3가지 정도로 판단된다
1. 문서 내에서 검색어가 얼마나 자주 등장하는지? -> 많이 등장할수록 점수 UP
2. 검색어가 전체 문서 중 얼마나 희귀하는지? -> 희귀할수록 점수 UP
3. 문서(필드)가 짧을수록 점수 UP
ElasticSearch는 역 인덱스의 기능을 가지고 있기 때문에, 단어의 순서랑 상관없이 도큐먼트를 조회 가능함
참고로 이러한 구조는 데이터 타입의 text
타입 한해서만 해당한다
✅ 애널라이저 (Analyzer)
문자열 -> 토큰으로 만들어주는 도구
캐릭터 필터, 토크나이저, 토큰필터 이렇게 3가지 전부 합쳐서 애널라이저라 함
캐릭터 필터
: 문자열을 토큰으로 자르기전에 문자열을 다듬는 역할을 함
다양한 종류의 필터가 존재하며, 여러 개의 필터를 적용시킬 수 있음
💡 기본값으로는 캐릭터 필터가 설정되어 있지 않음
ex) html_strip
필터 적용 (html 태그 제거)
<h1>아이폰 15 사용 후기</h1>
-> 아이폰 15 사용 후기
토크나이저
: 문자열을 토큰으로 자르는 역할을 함
💡 기본값으로 standard 토크나이저
가 적용되어 있으며
공백 ,
.
!
?
와 같은 문장 부호를 기준으로 자름
ex) The Brown-Foxes jumped over the roof.
→ [The
, Brown
, Foxes
, jumped
, over
, the
, roof
]
토큰 필터
: 잘린 토큰을 최종적으로 다듬는 역할을 함
다양한 종류의 필터가 존재하며, 여러 개의 필터를 적용시킬 수 있음
💡 기본값으로는 lowercase
필터가 적용되어 있음
ex) lowercase 필터 적용 (소문자로 변환)
[The
, Brown
, Foxes
, jumped
, over
, the
, roof
]
→ [the
, brown
, foxes
, jumped
, over
, the
, roof
]
ex) stop 필터 적용 (a
, the
, is
와 같은 특별한 의미를 가지지 않는 단어 제거)
[the
, brown
, foxes
, jumped
, over
, the
, roof
]
→ [brown
, foxes
, jumped
, roof
]
ex) stemmer 필터 적용 (단어의 원래 형태로 변환)
[brown
, foxes
, jumped
, roof
]
→ [brown
, fox
, jump
, roof
]
✅ 애널라이저가 토큰을 나누는 방식
방법 1 : "analyzer": "standard"
-> analyzer 를 standart 기본값으로 명시하는 방법
방법 2
"char_filter": [],
"tokenizer": "standard",
"filter": ["lowercase"]
-> standard analyer의 구성을 직접 명시하는 방법
아래와 같이 입력하면 토큰이 나눠지는 것을 볼 수 있다
GET /_analyze
란?
Elasticsearch에서 텍스트가 Analyzer에 의해 어떻게 처리되는지 직접 확인할 수 있게 해주는 분석 도구 API
// 방법 1
GET /_analyze
{
"text": "Apple 2025 맥북 에어 13 M4 10코어",
"analyzer": "standard"
}
// 방법 2
GET /_analyze
{
"text": "Apple 2025 맥북 에어 13 M4 10코어",
"char_filter": [],
"tokenizer": "standard",
"filter": ["lowercase"]
}
✅ 대소문자 구분없이 검색하는 방법 (예시)
PUT /products // 인덱스 생성 + 매핑 정의 + Custom Analyzer 적용
{
"settings": {
"analysis": {
"analyzer": {
"products_name_analyzer": {
"char_filter": [],
"tokenizer": "standard",
"filter": ["lowercase"] // lowercase token filter 추가
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "products_name_analyzer"
}
}
}
}
POST /products/_create/1 // id가 1값인 데이터 삽입
{
"name": "Apple 2025 맥북 에어 13 M4 10코어"
}
이런식으로 데이터를 삽입한 후 조회해보도록 하겠다
GET /products/_search // 조회 잘됨
{
"query": {
"match": {
"name": "apple"
}
}
}
GET /products/_search // 조회 잘됨
{
"query": {
"match": {
"name": "Apple"
}
}
}
둘다 조회가 잘 되는것을 확인할 수 있다
이제 Analyze API 사용해서 분석을 해보자
GET /products/_analyze
{
"field": "name"
"text": "Apple 2025 맥북 에어 13 M4 10코어"
}
위 사진처럼 도큐먼트를 생성할 때 Analyzer가 문자열을 분리해서 역인덱스를 생성하기 때문에
토큰이 나눠지는것을 확인할 수 있다
그러면 토큰에는 소문자로 apple 이 저장되있어서 text를 apple로 검색했을 때 나오는 것은 알겠는데
왜 대문자로 Apple 을 검색했을 때는 조회가 잘 되는가?
💡 그 이유는 검색을 할때의 Text 값도 Analyzer을 사용하여 토큰을 분리시킨 다음
실제 역인덱스 값이랑 비교하기 때문에 조회할 수 있는 것이다
그래서 사용자는 대소문자를 신경쓰지 않고 데이터를 조회할 수 있다
✅ html_strip
(HTML 태그 제거)
💡 전체적인 예시는 위에서 Custom Analyzer 적용할 때
character filter
값만 바꿔주면 되기때문에 생략하겠다 => character filter : ["html_strip"]
게시글 서비스에는 굵게, 기울임, 링크 등을 포함해서 글을 작성하기 때문에
HTML태그를 포함해서 DB에 저장하는 경우가 있다
POST /boards/_doc
{
"content": "<h1>이 물품 팝니다/h1>"
}
위처럼 HTML 태그가 포함되있는 상태에서 데이터가 저장된 상태에서 h1을 검색하면
GET /boards/_search
{
"query": {
"match": {
"content": "h1"
}
}
}
데이터가 조회되는것을 확인할 수 있다
하지만 실제 글과 상관없는 h1이라는 키워드로 게시글이 조회된다면 검색 품질이 떨어지는 것이다
실제로 어떤식으로 토큰이 저장되어있는지 확인해보자
GET /boards/_analyze
{
"field": "content",
"text": "<h1>Running cats, jumping quickly — over the lazy dogs!</h1>"
}
결과값으로 앞뒤로 h1 태그들도 역 인덱스에 저장되는것을 확인할 수 있어서 디스크 공간을 낭비한다
이러한 이유로 html_strip
를 사용하여 HTML 태그를 제거하고 저장을 해야한다
✅ 검색할 때 필요없는 불용어 제거하기 = stop
사용자가 검색할 때 그 사과는 맛있다
라고 입력하면
사실 검색에서 중요한 단어는 사과
와 맛있다
고 그
는 필요없는 단어이기 때문에 제거하는게 좋다
필요없는 단어를 제거해야 역인덱스의 저장공간을 절약할 수 있다
영어: "a", "the", "is", "at", "which", "on", "in"
등
한글: "이", "그", "저", "그리고", "하지만", "또는", "은", "는", "이", "가", "에", "의"
등
적용방법은 filter
부분에 stop
을 추가하면 된다
filter": ["lowercase", "stop"]
✅ 단어의 형태에 상관없이 검색하는 방법 = stemmer
stemmer
: 어미, 접미사 등을 제거해서 어근만 남기는 것
ex) 영어
running, ran, runs
-> run
playing, played, player
-> play
ex) 한글
먹었다, 먹고, 먹는
→ 먹다
사랑한다, 사랑해서, 사랑할
→ 사랑
이렇게 형태는 달라도 같은 의미인 단어들을 하나의 형태로 통일시켜서
검색 시 더 정확한 매칭이 되도록 도움을 준다
적용방법은 filter
부분에 stemmer
를 추가하면 된다
stemmer
를 적용하면 영단어나 한글을 기본형으로 변환한 후 토큰에 저장한다
그러면 토큰에는 사랑
이라고 저장되있는데 왜 사랑한다
를 검색하면 조회될까?
아까 위에서 말한 예시와 동일하다
사랑한다
라고 검색했을 때, stemmer
에 의해 기본형인 사랑
으로 바뀐채로 검색을 하기 때문이다
✅ 동의어로 검색하는 방법 = synonym
synonym
필터는 동의어 필터로 동일하거나 유사한 의미의 단어들을 하나처럼 취급한다
적용방법은 filter
에 synonym
추가하고나서 세부적으로 매칭을 추가시켜야함
ex) 나는 랩탑을 새로 샀다
에서 노트북
이라고 검색했을 때, 인식을 시키고 싶으면?
PUT /korean_synonym_test
{
"settings": {
"analysis": {
"filter": {
"korean_synonym_filter": {
"type": "synonym",
"synonyms": [
"노트북, 랩탑", // 노트북, 랩탑 입력시 다 매칭
"핸드폰, 휴대폰, 스마트폰", // 핸드폰, 휴대폰, 스마트폰 입력시 다 매칭
"자동차, 차량, 차" // 자동차, 차량, 차 입력시 다 매칭
]
}
},
"analyzer": {
"korean_synonym_analyzer": {
"tokenizer": "standard",
"filter": ["lowercase", "korean_synonym_filter"]
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "korean_synonym_analyzer"
}
}
}
}
다음과 같이 저장했을 때, 토큰을 분석하면 사진과 같이 동의어들이 토큰에 저장되는것을 확인할 수 있다
GET /products/_analyze
{
"field": "name",
"text": "노트북 스마트폰 차량"
}
✅ Nori Analyzer
"filter": ["lowercase", "stop", "stemmer"]
로 인덱스를 생성했을 때
영어와 같은 경우 띄어쓰기로 단어가 명확하게 구분되어 토큰으로 저장이 된다
ex) "text": "I like bananas"
-> i
like
banana
하지만 한글과 같은 경우 ex) "text": "백화점에서 쇼핑을 하다가 친구를 만났다."
-> 백화점에서
쇼핑을
하다가
친구를
만났다
단순히 띄어쓰기로 토큰이 저장된다
이럴경우 사용자가 백화점
쇼핑
친구
라는 키워드로 검색하면 조회되지가 않는 문제가 발생한다
이러한 문제점을 해결하기 위해 한글에 맞는 Nori Analyzer
를 써야 한다.
Nori Analyzer
를 사용하려면 플러그인을 설치해야한다
도커 예시
# Dockerfile
FROM docker.elastic.co/elasticsearch/elasticsearch:8.17.4
# Nori Analyzer 플러그인 설치
RUN bin/elasticsearch-plugin install analysis-nori
services:
elastic:
build:
context: .
dockerfile: Dockerfile # 위에 저장한 Dockerfile 넣기
ports:
- 9200:9200 # 9200번 포트에서 Elasticsearch 실행
environment:
# 아래 설정은 개발/테스트 환경에서 간단하게 테스트하기 위한 옵션 (운영 환경에서는 설정하면 안 됨)
- discovery.type=single-node # 단일 노드
- xpack.security.enabled=false # 보안 설정
- xpack.security.http.ssl.enabled=false # 보안 설정
kibana:
image: docker.elastic.co/kibana/kibana:8.17.4 # 8.17.4 버전
ports:
- 5601:5601 # 5601번 포트에서 kibana 실행
environment:
- ELASTICSEARCH_HOSTS=http://elastic:9200 # kibana에게 통신할 Elasticsearch 주소 알려주기
이런식으로 바꿔서 컨테이너를 띄우고 Analyze API 활용해 디버깅 해보면
GET /_analyze
{
"text": "백화점에서 쇼핑을 하다가 친구를 만났다.",
"analyzer": "nori"
}
## 위 아래 동일한 방법이다 (아래 방법은 Nori Analyze 와 동일한 애널라이저이다)
GET /_analyze
{
"text": "백화점에서 쇼핑을 하다가 친구를 만났다.",
"char_filter": [],
"tokenizer": "nori_tokenizer",
"filter": ["nori_part_of_speech", "nori_readingform", "lowercase"]
}
# `nori_part_of_speech` : 의미 없는 조사(`을`, `의` 등), 접속사 등을 제거
# `nori_readingform` : 한자를 한글로 바꿔서 토큰으로 저장
백화
점
쇼핑
하
친구
만나
로 토큰이 저장되어 있다
이제 백화점
쇼핑
친구
키워드로 검새하면 잘 조회되는것을 확인할 수 있다
그러면 한글과 영어를 섞어서 검색하려면 어떻게 해야할까?
정답은 Nori analyzer
를 사용하고 필드값의 특징에 따라
character filter
나 token filter
를 추가해주면 된다
ex) Nori analyzer
에 불용어 제거와 단어의 기본형태로 저장하기
// Nori analyzer의 구성을 직접 명시
GET /_analyze
{
"text": "오늘 영어 책에서 'It depends on the results.'이라는 문구를 봤다.",
"char_filter": [],
"tokenizer": "nori_tokenizer",
"filter": ["nori_part_of_speech", "nori_readingform", "lowercase", "stop", "stemmer"]
}