JSCODE Logo
JSCODE 박재성JSCODE 제이온JSCODE 시니
무료 강의온라인 강의프로그래밍 과외책/전자책
유튜브블로그
후기
완강 후기 이벤트블로그 리뷰 이벤트AWS SAA 합격 후기 이벤트
회사명 : JSCODE대표 : 박재성사업자 등록번호 : 244-22-01557통신판매업 : 제 2023-인천미추홀-0381 호

서울특별시 구로구 경인로 20가길 11(오류동, 아델리아)

Copyright ⓒ 2026 JSCODE - 최상위 현업 개발자들의 프로그래밍 교육 All rights reserved.

이용약관개인정보처리방침

실전에서 바로 써먹는 Elasticsearch 입문 (검색 최적화편)

Elasticsearch 기본 개념

Elasticsearch란? / Elasticsearch 주요 활용 사례
Elasticsearch 설치하기
Elasticsearch 작동 방식 / GUI 툴
GUI 툴로 Elasticsearch 훨씬 쉽게 조작하기 (feat. Kibana)
Elasticsearch의 기본 용어 정리 (인덱스, 도큐먼트, 매핑, 필드)
인덱스 생성하기 / 매핑 정의하기 / 도큐먼트 삽입하기
도큐먼트 저장, 조회, 수정, 삭제하기

Spring Boot, Elasitcsearch를 활용해 CRUD 구현하기

[실습] Spring Boot에 Elasitcsearch 연결하기 (feat. Spring Data Elasticsearch)
[실습] Elasticsearch를 활용해 CRUD API 만들기

기본 검색 기능 및 작동 원리

단어의 순서가 바뀌어도 검색이 가능하다 ?!
[실습] Elasitcsearch의 검색 기능 테스트해보기
역인덱스(Inverted Index)란?
[실습] 역인덱스(Inverted Index)를 활용한 데이터 조회
애널라이저(Analyzer)란?
Elasticsearch에 기본값으로 설정되어 있는 애널라이저(Analyzer)
[실습] 애널라이저(Analyzer)가 토큰을 어떻게 나누는 지 확인하는 방법
[실습] 대소문자 구분없이 검색하는 방법 (lowercase)
[실습] 검색할 때 필요없는 HTML 태그 제거하기 (html_strip)
[실습] 검색할 때 필요없는 불용어(a, an, the, or, but) 제거하기 (stop)
[실습] 단어의 형태(-ed, -ing, -s 등)에 상관없이 검색하는 방법 (stemmer)
[실습] 동의어로 검색하는 방법 (synonym)

한글에 최적화된 검색 기능

한글(korean)이 제대로 검색되지 않는 현상
[실습] Nori Analyzer를 활용해 한글(korean)이 제대로 검색되게 만들기
[실습] 한글(korean)과 영어(english)가 섞인 글을 검색 가능하게 만들기

데이터의 저장 타입을 결정하는 매핑(Mapping)

매핑(mapping)이란? / 데이터 타입(data type)
매핑(mapping)의 특이한 특징 (null 허용, Array 허용)
[실습] 저장할 데이터에 맞게 매핑 정의하기

자주 사용하는 검색 기능

검색 키워드가 포함된 데이터를 조회하고 싶을 때 (match)
특정 값과 정확하게 일치하는 데이터를 조회하고 싶을 때 (term, terms)
2가지 이상의 조건을 만족시키는 데이터를 조회하고 싶을 때 (bool : filter, must)
[예제] filter와 must 구분해서 사용하기
특정 조건을 만족하지 않는 데이터를 조회하고 싶을 때 (bool : must_not)
숫자/날짜의 값에 대해 범위 조건으로 데이터를 조회하고 싶을 때 (range)
특정 조건을 만족하는 데이터 위주로 상위 노출 시키고 싶을 때 (bool : should)
[실습] 검색 결과 중 평점이 높고 좋아요 수가 많은 글을 상위에 노출시키고 싶은 경우
오타가 있더라도 유사한 단어를 포함한 데이터를 조회하고 싶을 때 (fuzziness)
여러 필드(ex. 제목, 내용)에서 검색 키워드가 포함된 데이터를 조회하고 싶을 때 (multi_match)
검색한 키워드를 하이라이팅 처리하고 싶을 때 (highlight)
페이지네이션 (Pagination), 정렬(Sorting)
하나의 필드에 text와 keyword 타입을 동시에 사용하고 싶을 때 (Multi Field)
검색 키워드를 일부 입력했을 때 검색어를 추천해주는 기능 (자동 완성 기능)

[프로젝트] Elasticsearch로 쿠팡의 상품 검색 기능 구현하기

프로젝트 설명 / 요구 사항 (상품 검색 기능, 자동 완성 기능)
기존 프로젝트 구조 살펴보기
인프라 아키텍처 설계하기
[실습] 인덱스 생성 / 매핑 정의 / 여러 필드에 걸쳐 검색하기
[실습] 대소문자 구분 없이 검색 / 단어 순서 상관없이 검색 / HTML 태그 섞이지 않게 검색
[실습] 한글, 영어 검색 / 동의어 기반 검색 / 필터링
[실습] 특정 조건 상위 노출 / 오타 허용 검색 / 하이라이팅 처리 / 페이지네이션
[실습] 자동 완성 기능
최종 쿼리 / 적용 전략 및 순서
[실습] Spring Boot에서 인덱스에 맞게 Document 정의하기
[실습] MySQL에 데이터 삽입/삭제 시 Elasticsearch에도 같이 반영되게 만들기
[실습] Spring Boot에서 Elasticsearch 활용해 자동완성 API 만들기
[실습] Spring Boot에서 Elasticsearch 활용해 검색 API 만들기
[참고 자료] 프로젝트 완성본 (Github)
현업에서는 Elasticsearch를 직접 구축해서 쓰진 않나요?
AWS Opensearch vs Elastic Cloud
[참고 자료] 실습에서 발생하는 Elastic Cloud 비용
[실습] Elastic Cloud에서 Elasticsearch 생성하기 (+ 가입하기)
[실습] Spring Boot에 Elasitc Cloud의 Elasticsearch 연결하기
[보충 강의] 비용 나가지 않게 Elastic Cloud 리소스 정리하기
← 블로그 목록으로 돌아가기

[실습] 한글, 영어 검색 / 동의어 기반 검색 / 필터링

JSCODE 박재성
JSCODE 박재성
2026. 04. 04.
author
JSCODE 박재성
category
Elasticsearch
createdAt
Dec 6, 2025 04:21 AM
isPublic
isPublic
series
실전에서 바로 써먹는 Elasticsearch 입문 (검색 최적화편)
slug
exercise-korean-english-synonym-filter-search
type
post
updatedAt
Apr 4, 2026 10:00

✅ 요구사항 반영하기

한글, 영어 둘 다 검색이 잘 돼야 한다.
상품명에 대해서는 아래 값을 동의어로 인식할 수 있어야 한다.
samsung, 삼성 apple, 애플 노트북, 랩탑, 컴퓨터, computer, laptop, notebook 전화기, 휴대폰, 핸드폰, 스마트폰, 휴대전화, phone, smartphone, mobile phone, cell phone 아이폰, iphone 맥북, 맥, macbook, mac
카테고리(category), 가격(price)으로 필터링을 할 수 있어야 한다.
  • 특정 카테고리 내에서만 검색할 수 있어야 한다.
  • 특정 가격 범위 내에서만 검색할 수 있어야 한다.
 
  1. 한글, 영어 둘 다 검색이 잘 되도록 만들기
    1. 상품명, 상품 설명, 카테고리 필드에 들어가는 한글로 이루어진 값도 적절하게 토큰을 분리하려면 기본으로 설정되는 standard Analyzer가 아닌 Nori Analyzer를 활용해야 한다. Nori Analyzer는 다음과 같이 설정할 수 있다고 했다.
      // 방법 1 "analyzer": "nori" // 방법 2 (nori analyzer의 구성을 직접 명시) "char_filter": [], "tokenizer": "nori_tokenizer", "filter": ["nori_part_of_speech", "nori_readingform", "lowercase"]
      기존 Nori Analyzer의 구성을 요구사항에 맞게 커스텀해서 사용하려면 2번째 방법을 사용해야 한다. 2번째 방법을 활용해 매핑을 다시 정의해서 인덱스를 재생성하자.
       
      DELETE /products PUT /products { "settings": { "analysis": { // 커스텀 애널라이저 정의 "analyzer": { "products_name_analyzer": { "char_filter": [], "tokenizer": "nori_tokenizer", "filter": ["nori_part_of_speech", "nori_readingform", "lowercase"] }, "products_description_analyzer": { "char_filter": ["html_strip"], "tokenizer": "nori_tokenizer", "filter": ["nori_part_of_speech", "nori_readingform", "lowercase"] }, "products_category_analyzer": { "char_filter": [], "tokenizer": "nori_tokenizer", "filter": ["nori_part_of_speech", "nori_readingform", "lowercase"] } } } }, "mappings": { "properties": { "id": { "type": "long" }, "name": { "type": "text", // 유연한 검색 필요 "analyzer": "products_name_analyzer" }, "description": { "type": "text", // 유연한 검색 필요 "analyzer": "products_description_analyzer" }, "price": { "type": "integer" // 10억 이하의 정수 }, "rating": { "type": "double" // 실수(소수점을 가진 숫자 포함) }, "category": { "type": "text", // 유연한 검색 필요 "analyzer": "products_category_analyzer" } } } }
       
      잘 작동하는 지 Analyze API로 테스트해보자.
      GET /products/_analyze { "field": "name", "text": "특수가전제품 Samsung TV" } GET /products/_analyze { "field": "description", "text": "<h1>특수가전제품 Samsung TV</h1>" } GET /products/_analyze { "field": "category", "text": "특수가전제품 Samsung TV" }
       
  1. 동의어로 검색할 수 있게 만들기
    1. DELETE /products PUT /products { "settings": { "analysis": { // 필터 정의 "filter": { "product_synonyms": { "type": "synonym", "synonyms": [ "samsung, 삼성", "apple, 애플", "노트북, 랩탑, 컴퓨터, computer, laptop, notebook", "전화기, 휴대폰, 핸드폰, 스마트폰, 휴대전화, phone, smartphone, mobile phone, cell phone", "아이폰, iphone", "맥북, 맥, macbook, mac" ] } }, // 커스텀 애널라이저 정의 "analyzer": { "products_name_analyzer": { "char_filter": [], "tokenizer": "nori_tokenizer", "filter": [ "nori_part_of_speech", "nori_readingform", "lowercase", "product_synonyms" ] }, "products_description_analyzer": { "char_filter": ["html_strip"], "tokenizer": "nori_tokenizer", "filter": [ "nori_part_of_speech", "nori_readingform", "lowercase" ] }, "products_category_analyzer": { "char_filter": [], "tokenizer": "nori_tokenizer", "filter": [ "nori_part_of_speech", "nori_readingform", "lowercase" ] } } } }, "mappings": { "properties": { "id": { "type": "long" }, "name": { "type": "text", // 유연한 검색 필요 "analyzer": "products_name_analyzer" }, "description": { "type": "text", // 유연한 검색 필요 "analyzer": "products_description_analyzer" }, "price": { "type": "integer" // 10억 이하의 정수 }, "rating": { "type": "double" // 실수(소수점을 가진 숫자 포함) }, "category": { "type": "text", // 유연한 검색 필요 "analyzer": "products_category_analyzer" } } } }
잘 작동하는 지 Analyze API로 테스트해보자.
GET /products/_analyze { "field": "name", "text": "삼성 애플 노트북 전화기 아이폰 맥북" }
 
  1. 가격으로 필터링 할 수 있게 만들기
    1. [기존 검색 쿼리]
      POST /products/_search { "query": { "multi_match": { "query": "lg", "fields": [ "name^3", "description^1", "category^2" ] } } }
       
      기존 검색 쿼리에서 가격(price)로 필터링 하려면 아래와 같이 쿼리를 수정하면 된다.
      POST /products/_search { "query": { "bool": { // 2가지 이상의 조건을 적용시켜야 할 때 "must": { // score와 관련 있는 쿼리 (유연한 검색 필요) "multi_match": { "query": "lg", "fields": [ "name^3", "description^1", "category^2" ] } }, "filter": { // score와 관련 없는 쿼리 (정확한 값 비교 필요) "range": { "price": { "gte": 10000, "lte": 50000 } } } } } }
       
  1. 카테고리로 필터링하기
    1. category를 기준으로 필터링하려면 타입이 keyword 타입이어야 한다. 왜냐하면 category를 기준으로 필터링하려면 정확히 일치하는 값으로 비교해서 필터링을 해야 하기 때문이다.
      하지만 category 필드는 관련된 상품을 유연하게 검색할 때도 사용하기 때문에 text 타입으로도 활용할 수 있어야 한다.
      이러한 특징 때문에 category 필드에 text와 keyword 타입을 동시에 적용시켜야 한다.
      DELETE /products PUT /products { "settings": { "analysis": { // 필터 정의 "filter": { "product_synonyms": { "type": "synonym", "synonyms": [ "samsung, 삼성", "apple, 애플", "노트북, 랩탑, 컴퓨터, computer, laptop, notebook", "전화기, 휴대폰, 핸드폰, 스마트폰, 휴대전화, phone, smartphone, mobile phone, cell phone", "아이폰, iphone", "맥북, 맥, macbook, mac" ] } }, // 커스텀 애널라이저 정의 "analyzer": { "products_name_analyzer": { "char_filter": [], "tokenizer": "nori_tokenizer", "filter": [ "nori_part_of_speech", "nori_readingform", "lowercase", "product_synonyms" ] }, "products_description_analyzer": { "char_filter": ["html_strip"], "tokenizer": "nori_tokenizer", "filter": [ "nori_part_of_speech", "nori_readingform", "lowercase" ] }, "products_category_analyzer": { "char_filter": [], "tokenizer": "nori_tokenizer", "filter": [ "nori_part_of_speech", "nori_readingform", "lowercase" ] } } } }, "mappings": { "properties": { "id": { "type": "long" }, "name": { "type": "text", // 유연한 검색 필요 "analyzer": "products_name_analyzer" }, "description": { "type": "text", // 유연한 검색 필요 "analyzer": "products_description_analyzer" }, "price": { "type": "integer" // 10억 이하의 정수 }, "rating": { "type": "double" // 실수(소수점을 가진 숫자 포함) }, "category": { "type": "text", // 유연한 검색 필요 "analyzer": "products_category_analyzer", // 멀티 필드로 keyword 타입을 추가 "fields": { "raw": { "type": "keyword" } } } } } }
       
      테스트를 하기 위해 더미 데이터를 먼저 넣자.
      POST /products/_doc { "id": 1, "name": "LG OLED TV 42인치", "description": "<p>선명한 화질의 LG OLED TV입니다.</p>", "price": 3000, "rating": 4.8, "category": "가전제품" } POST /products/_doc { "id": 2, "name": "LG 냉장고 300L", "description": "<p>신선함을 오래 유지하는 LG 냉장고</p>", "price": 49000, "rating": 4.5, "category": "가전제품" } POST /products/_doc { "id": 3, "name": "삼성 냉장고", "description": "<p>삼성의 최신 기술이 적용된 냉장고</p>", "price": 20000, "rating": 4.7, "category": "가전제품" } POST /products/_doc { "id": 4, "name": "LG 스마트폰", "description": "<p>LG의 최신 스마트폰 모델</p>", "price": 25000, "rating": 4.2, "category": "휴대전화" }
       
      필터링 기능이 잘 작동하는 지 확인하기 위해 아래 쿼리를 실행시켜보자.
      POST /products/_search { "query": { "bool": { "must": { "multi_match": { "query": "LG", "fields": [ "name^3", "description^1", "category^2" ] } }, "filter": [ { "term": { "category.raw": "가전제품" } }, { "range": { "price": { "gte": 10000, "lte": 50000 } } } ] } } }
       
       
       
📎
이 글은 실전에서 바로 써먹는 Elasticsearch 입문 (검색 최적화편) 강의의 수업 자료 중 일부입니다.