이 글에서 얻는 것
- Elasticsearch가 무엇이고, 언제 사용하는지 이해합니다.
- 인덱스를 생성하고 문서를 저장할 수 있습니다.
- 검색 쿼리를 작성하고 결과를 분석할 수 있습니다.
- Spring Data Elasticsearch로 통합할 수 있습니다.
0) Elasticsearch는 “검색 특화 데이터베이스"다
왜 Elasticsearch인가?
RDBMS의 LIKE 검색:
-- ❌ 느린 검색
SELECT * FROM products
WHERE name LIKE '%맥북%'
OR description LIKE '%맥북%';
-- 문제:
-- - Full Table Scan (인덱스 사용 불가)
-- - 대용량 데이터에서 매우 느림
-- - 형태소 분석 불가 ("맥북프로" 검색 시 "맥북 프로" 못 찾음)
Elasticsearch:
GET /products/_search
{
"query": {
"multi_match": {
"query": "맥북",
"fields": ["name", "description"]
}
}
}
// 장점:
// - 역인덱스로 빠른 검색 (ms 단위)
// - 형태소 분석으로 유연한 검색
// - 관련도 점수 (relevance score)
사용 사례
✅ 전문 검색: 상품 검색, 문서 검색
✅ 로그 분석: ELK Stack (Elasticsearch + Logstash + Kibana)
✅ 실시간 분석: 대시보드, 모니터링
✅ 자동완성: 검색어 추천
1) 핵심 개념
1-1) 인덱스 vs 도큐먼트
Elasticsearch RDBMS
───────────────── ──────────
Index Database
Document Row
Field Column
Mapping Schema
예시:
// Index: products
{
"_id": "1",
"_source": {
"name": "맥북 프로 M3",
"price": 2500000,
"category": "노트북",
"tags": ["Apple", "고성능"],
"created_at": "2025-12-16"
}
}
1-2) 역인덱스 (Inverted Index)
일반 인덱스:
문서 ID → 내용
역인덱스:
단어 → 문서 ID 목록
예시:
"맥북" → [1, 5, 10, 15]
"프로" → [1, 3, 5]
"맥북 프로" 검색 → 교집합 → [1, 5]
2) Elasticsearch 시작
2-1) Docker로 실행
# Elasticsearch + Kibana
docker-compose up -d
docker-compose.yml:
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
volumes:
- es-data:/usr/share/elasticsearch/data
kibana:
image: docker.elastic.co/kibana/kibana:8.11.0
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
depends_on:
- elasticsearch
volumes:
es-data:
확인:
curl http://localhost:9200
# {
# "name" : "...",
# "cluster_name" : "docker-cluster",
# "version" : { "number" : "8.11.0" }
# }
2-2) 인덱스 생성
# 인덱스 생성
PUT /products
{
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "standard"
},
"price": {
"type": "integer"
},
"category": {
"type": "keyword"
},
"tags": {
"type": "keyword"
},
"created_at": {
"type": "date"
}
}
}
}
필드 타입:
text: 전문 검색 (분석됨)
keyword: 정확히 일치 (분석 안 됨)
integer, long: 숫자
date: 날짜
boolean: true/false
3) CRUD 작업
3-1) 문서 추가
# 단일 문서
POST /products/_doc/1
{
"name": "맥북 프로 M3",
"price": 2500000,
"category": "노트북",
"tags": ["Apple", "고성능"],
"created_at": "2025-12-16"
}
# ID 자동 생성
POST /products/_doc
{
"name": "아이패드 프로",
"price": 1200000,
"category": "태블릿"
}
# 벌크 삽입
POST /_bulk
{ "index": { "_index": "products", "_id": "2" } }
{ "name": "맥북 에어 M2", "price": 1500000, "category": "노트북" }
{ "index": { "_index": "products", "_id": "3" } }
{ "name": "아이맥 M3", "price": 2000000, "category": "데스크톱" }
3-2) 문서 조회
# ID로 조회
GET /products/_doc/1
# 전체 조회
GET /products/_search
# 조건 검색
GET /products/_search
{
"query": {
"match": {
"name": "맥북"
}
}
}
3-3) 문서 수정
# 전체 교체
PUT /products/_doc/1
{
"name": "맥북 프로 M3 Max",
"price": 3000000
}
# 부분 수정
POST /products/_update/1
{
"doc": {
"price": 2800000
}
}
3-4) 문서 삭제
# 단일 삭제
DELETE /products/_doc/1
# 조건부 삭제
POST /products/_delete_by_query
{
"query": {
"range": {
"price": {
"lt": 1000000
}
}
}
}
4) 검색 쿼리
4-1) Match Query (전문 검색)
GET /products/_search
{
"query": {
"match": {
"name": "맥북 프로"
}
}
}
# 결과: "맥북", "프로" 중 하나라도 포함된 문서
4-2) Multi Match (여러 필드 검색)
GET /products/_search
{
"query": {
"multi_match": {
"query": "Apple 노트북",
"fields": ["name", "category", "tags"]
}
}
}
4-3) Term Query (정확히 일치)
GET /products/_search
{
"query": {
"term": {
"category": "노트북"
}
}
}
# keyword 타입 필드에 사용
4-4) Range Query (범위 검색)
GET /products/_search
{
"query": {
"range": {
"price": {
"gte": 1000000,
"lte": 2000000
}
}
}
}
# gte: >=, lte: <=, gt: >, lt: <
4-5) Bool Query (복합 조건)
GET /products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "맥북" } }
],
"filter": [
{ "range": { "price": { "gte": 1000000 } } }
],
"must_not": [
{ "term": { "category": "중고" } }
],
"should": [
{ "term": { "tags": "Apple" } }
]
}
}
}
# must: AND (점수 영향 O)
# filter: AND (점수 영향 X, 빠름)
# must_not: NOT
# should: OR (점수 가산)
5) 정렬과 페이징
5-1) 정렬
GET /products/_search
{
"query": { "match_all": {} },
"sort": [
{ "price": "desc" },
{ "created_at": "desc" }
]
}
5-2) 페이징
GET /products/_search
{
"query": { "match_all": {} },
"from": 0,
"size": 10
}
# from: 시작 위치 (0부터)
# size: 개수 (기본: 10, 최대: 10000)
6) Spring Data Elasticsearch
6-1) 의존성
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
}
6-2) 설정
spring:
elasticsearch:
uris: http://localhost:9200
6-3) Entity
@Document(indexName = "products")
@Setting(settingPath = "elasticsearch/product-settings.json")
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "standard")
private String name;
@Field(type = FieldType.Integer)
private Integer price;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Keyword)
private List<String> tags;
@Field(type = FieldType.Date, format = DateFormat.date)
private LocalDate createdAt;
}
6-4) Repository
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
// Query Method
List<Product> findByName(String name);
List<Product> findByPriceBetween(Integer min, Integer max);
List<Product> findByCategory(String category);
// @Query 사용
@Query("{\"match\": {\"name\": \"?0\"}}")
List<Product> searchByName(String name);
// Bool Query
@Query("""
{
"bool": {
"must": [
{ "match": { "name": "?0" } }
],
"filter": [
{ "range": { "price": { "gte": ?1, "lte": ?2 } } }
]
}
}
""")
List<Product> searchByNameAndPriceRange(String name, Integer minPrice, Integer maxPrice);
}
6-5) Service
@Service
public class ProductSearchService {
@Autowired
private ProductRepository productRepository;
@Autowired
private ElasticsearchOperations elasticsearchOperations;
// 단순 검색
public List<Product> search(String keyword) {
return productRepository.findByName(keyword);
}
// 복잡한 검색
public List<Product> advancedSearch(String keyword, Integer minPrice, Integer maxPrice) {
Criteria criteria = new Criteria("name").matches(keyword)
.and(new Criteria("price").between(minPrice, maxPrice));
Query query = new CriteriaQuery(criteria);
SearchHits<Product> searchHits = elasticsearchOperations.search(query, Product.class);
return searchHits.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
// 자동완성
public List<String> autocomplete(String prefix) {
Query query = new NativeQueryBuilder()
.withQuery(QueryBuilders.prefixQuery("name", prefix))
.withFields("name")
.withPageable(PageRequest.of(0, 10))
.build();
SearchHits<Product> searchHits = elasticsearchOperations.search(query, Product.class);
return searchHits.stream()
.map(hit -> hit.getContent().getName())
.distinct()
.collect(Collectors.toList());
}
}
7) 실전 패턴
7-1) 상품 검색
@RestController
@RequestMapping("/api/products")
public class ProductSearchController {
@Autowired
private ProductSearchService searchService;
@GetMapping("/search")
public ResponseEntity<SearchResult> search(
@RequestParam String keyword,
@RequestParam(required = false) Integer minPrice,
@RequestParam(required = false) Integer maxPrice,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
List<Product> products = searchService.advancedSearch(
keyword, minPrice, maxPrice
);
return ResponseEntity.ok(new SearchResult(products));
}
@GetMapping("/autocomplete")
public ResponseEntity<List<String>> autocomplete(@RequestParam String q) {
List<String> suggestions = searchService.autocomplete(q);
return ResponseEntity.ok(suggestions);
}
}
8) 베스트 프랙티스
✅ 1. 적절한 필드 타입 선택
// ✅ 전문 검색이 필요한 필드
@Field(type = FieldType.Text)
private String description;
// ✅ 정확히 일치 검색 (필터링)
@Field(type = FieldType.Keyword)
private String category;
✅ 2. 인덱스 매핑 사전 정의
PUT /products
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"name": { "type": "text" },
"price": { "type": "integer" }
}
}
}
✅ 3. 벌크 작업 사용
// ❌ 나쁜 예: 하나씩 저장
for (Product product : products) {
productRepository.save(product);
}
// ✅ 좋은 예: 벌크 저장
productRepository.saveAll(products);
요약
- Elasticsearch는 전문 검색에 특화된 NoSQL
- 역인덱스로 빠른 검색 제공
- text vs keyword 타입 구분 중요
- Spring Data Elasticsearch로 쉽게 통합
- 상품 검색, 로그 분석에 필수
다음 단계
- ELK Stack:
/learning/deep-dive/deep-dive-elk-stack/ - 로그 분석:
/learning/deep-dive/deep-dive-log-analysis/ - 검색 최적화:
/learning/deep-dive/deep-dive-search-optimization/
💬 댓글