Docker & Linux ์ค์น, Nori ํ๊ธ ํํ์ ๋ถ์๊ธฐ, ์ธ๋ฑ์ค ๋งคํ, CRUD & Bulk API, Query DSL(Full Text, Term, Bool, Range), ์ง๊ณ(Aggregations), ํด๋ฌ์คํฐ ํ๋ ๋ฐ Spring/Python ์ฐ๋ ์ค๋ฌด ๊ฐ์ด๋
## 1. Elasticsearch ํต์ฌ ์ํคํ
์ฒ & ๋์ ์๋ฆฌ (Architecture)
```mermaid
graph LR
subgraph Client ["Client / App"]
Req[REST API Request<br/>JSON Query DSL]
end
subgraph Cluster ["Elasticsearch Cluster"]
subgraph Node1 ["Master-Eligible / Data Node 1"]
P0["Index: products<br/>Primary Shard 0"]
R1["Index: products<br/>Replica Shard 1"]
end
subgraph Node2 ["Data Node 2"]
P1["Index: products<br/>Primary Shard 1"]
R0["Index: products<br/>Replica Shard 0"]
end
end
Req --> Node1
Req --> Node2
P0 -.->|Replication| R0
P1 -.->|Replication| R1
```
```text
โ RDBMS vs Elasticsearch ํต์ฌ ๊ฐ๋
๋งคํ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
RDBMS Elasticsearch ์ค๋ช
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Database (DB) -> Index ๋ฐ์ดํฐ ๋
ผ๋ฆฌ์ ์ ์ฅ์ ๋จ์
Table -> Index (๊ณผ๊ฑฐ Type ํ์ง) 7.x+๋ถํฐ ์ธ๋ฑ์ค ๋น 1๊ฐ์ ๋งคํ ํ์
๋ง ํ์ฉ
Row -> Document JSON ํฌ๋งท ๋จ์ผ ๋ฐ์ดํฐ ํ
Column -> Field JSON ์์ฑ (text, keyword, integer ๋ฑ)
Schema -> Mapping ํ๋ ๋ฐ์ดํฐ ํ์
๊ณผ ๋ถ์๊ธฐ(Analyzer) ์ ์
SQL -> Query DSL JSON ๊ธฐ๋ฐ ๊ฒ์ ๋ฐ ์ง๊ณ ์ง์ ์ธ์ด
B-Tree Index -> Inverted Index (์ญ์์ธ) ๋จ์ด(Term)๊ฐ ์์นํ ๋ฌธ์ ID๋ฅผ ๋งคํํ๋ ๊ตฌ์กฐ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โข ์ญ์์ธ(Inverted Index): ํ
์คํธ๋ฅผ ํ ํฐํํ์ฌ ๋จ์ด ์ฌ์ ์ ๋ง๋ค๊ณ , ํด๋น ๋จ์ด๊ฐ ํฌํจ๋ Document ID ๋ชฉ๋ก์ ๋ณด๊ดํ์ฌ ์ด๊ณ ์ ๊ฒ์ ์ ๊ณต
โข Shard: ์ธ๋ฑ์ค๋ฅผ ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ถํ ํ ๋จ์ (Primary Shard: ์๋ณธ ๋ฐ์ดํฐ, Replica Shard: ์ฅ์ ๋ณต๊ตฌ ๋ฐ ๊ฒ์ ๋ถ์ฐ์ฉ ๋ณต์ ๋ณธ)
```
---
## 2. Elasticsearch & Kibana ์ค์น & ์คํ ๊ฐ์ด๋ (Installation)
```bash
# ๐ณ [1] Docker ๋จ์ผ ์ปจํ
์ด๋ ๋น ๋ฅธ ์คํ (๊ฐ๋ฐ & ๋ก์ปฌ ํ
์คํธ์ฉ)
# Elasticsearch 8.x๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ณด์(HTTPS/๋น๋ฐ๋ฒํธ)์ด ๊ฐ์ ๋๋ฏ๋ก, ๋ก์ปฌ ํ
์คํธ ์ xpack.security.enabled=false ์ต์
์ผ๋ก ๊ฐํธ ์คํ ๊ฐ๋ฅ
docker run -d \
--name elasticsearch \
-p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
-e "ES_JAVA_OPTS=-Xms1g -Xmx1g" \
-v es_data:/usr/share/elasticsearch/data \
docker.elastic.co/elasticsearch/elasticsearch:8.13.0
```
```yaml
# ๐ณ [2] Docker Compose: Elasticsearch + Kibana + Nori ํ๊ธ ๋ถ์๊ธฐ
# docker-compose.yml
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
container_name: elasticsearch
environment:
- node.name=es01
- cluster.name=es-docker-cluster
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
volumes:
- es_data:/usr/share/elasticsearch/data
ports:
- "9200:9200"
- "9300:9300"
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
kibana:
image: docker.elastic.co/kibana/kibana:8.13.0
container_name: kibana
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
- I18N_LOCALE=ko-KR
ports:
- "5601:5601"
depends_on:
- elasticsearch
volumes:
es_data:
```
```bash
# Nori ํ๊ธ ํํ์ ๋ถ์๊ธฐ ํ๋ฌ๊ทธ์ธ ์ค์น (์ปจํ
์ด๋ ์คํ ํ 1ํ ์คํ)
docker exec -it elasticsearch bin/elasticsearch-plugin install analysis-nori
docker restart elasticsearch
# ๐ง [3] Linux (Ubuntu / Debian) APT ํจํค์ง ์ค์น
# GPG Key ๋ฑ๋ก ๋ฐ APT Repository ์ถ๊ฐ
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /usr/share/keyrings/elasticsearch-keyring.gpg
sudo apt install -y apt-transport-https
echo "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-8.x.list
sudo apt update && sudo apt install -y elasticsearch
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-nori
# ์๋น์ค ๋ฑ๋ก ๋ฐ ์์
sudo systemctl daemon-reload
sudo systemctl enable --now elasticsearch
sudo systemctl status elasticsearch
# ๐ [4] macOS (Homebrew ์ค์น)
brew tap elastic/tap
brew install elastic/tap/elasticsearch-full
brew services start elastic/tap/elasticsearch-full
# ๐ช [5] Windows ํ๊ฒฝ
# WSL2(Ubuntu) ํฐ๋ฏธ๋์์ ์ Linux APT ๋ฐฉ์์ผ๋ก ์ค์นํ๊ฑฐ๋ Docker Desktop ์คํ์ ์ ๊ทน ๊ถ์ฅํฉ๋๋ค.
```
```bash
# ๐ [6] ํด๋ฌ์คํฐ ์ํ ํ์ธ & ํฌ์ค์ฒดํฌ (Cluster Health & Cat APIs)
# 1. ํด๋ฌ์คํฐ ํฌ์ค (status: green / yellow / red)
curl -X GET "http://localhost:9200/_cluster/health?pretty"
# 2. ๋
ธ๋ ๋ชฉ๋ก ๋ฐ ๋ฆฌ์์ค ์ ์ ์ํ
curl -X GET "http://localhost:9200/_cat/nodes?v&h=ip,name,role,master,heap.percent,cpu"
# 3. ์ ์ฒด ์ธ๋ฑ์ค ๋ชฉ๋ก ๋ฐ ๋ฌธ์ ์/์ฉ๋ ํ์ธ
curl -X GET "http://localhost:9200/_cat/indices?v&s=index"
# 4. ์ค๋ ๋ถ์ฐ ์ํ ์ ๊ฒ
curl -X GET "http://localhost:9200/_cat/shards?v"
```
---
## 3. ์ธ๋ฑ์ค ์์ฑ, ๋งคํ & Nori ํ๊ธ ํํ์ ๋ถ์๊ธฐ (Index & Mapping)
```bash
# [1] Nori ํํ์ ๋ถ์๊ธฐ ๊ธฐ๋ฐ ์ธ๋ฑ์ค ๋ฐ ๋งคํ ์์ฑ (products ์ธ๋ฑ์ค)
curl -X PUT "http://localhost:9200/products" -H 'Content-Type: application/json' -d'
{
"settings": {
"number_of_shards": 2,
"number_of_replicas": 1,
"index.refresh_interval": "1s",
"analysis": {
"tokenizer": {
"nori_user_dict_tokenizer": {
"type": "nori_tokenizer",
"decompound_mode": "mixed",
"user_dictionary_rules": [
"๋งฅ๋ถํ๋ก",
"์์ดํฐ"
]
}
},
"analyzer": {
"korean_analyzer": {
"type": "custom",
"tokenizer": "nori_user_dict_tokenizer",
"filter": [
"nori_readingform",
"lowercase",
"nori_part_of_speech_filter"
]
}
},
"filter": {
"nori_part_of_speech_filter": {
"type": "nori_part_of_speech",
"stoptags": [
"E", "IC", "J", "MAG", "MAJ", "MM", "SP", "SSC", "SSO", "SC", "SE", "XPN", "XSA", "XSN", "XSV", "UNA", "NA", "VSV"
]
}
}
}
},
"mappings": {
"properties": {
"id": { "type": "keyword" },
"title": {
"type": "text",
"analyzer": "korean_analyzer",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 }
}
},
"description": {
"type": "text",
"analyzer": "korean_analyzer"
},
"category": { "type": "keyword" },
"tags": { "type": "keyword" },
"price": { "type": "long" },
"stock": { "type": "integer" },
"is_active": { "type": "boolean" },
"created_at": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
}
}
}'
```
```bash
# [2] ํํ์ ๋ถ์๊ธฐ ๋์ ํ
์คํธ (_analyze API)
curl -X POST "http://localhost:9200/products/_analyze" -H 'Content-Type: application/json' -d'
{
"analyzer": "korean_analyzer",
"text": "์ต์ ๋งฅ๋ถํ๋ก์ ์ ํ ๋ฌด์ ์ด์ดํฐ์ ๊ตฌ๋งคํ์ต๋๋ค."
}
'
# [3] ์ธ๋ฑ์ค ๋งคํ ํ์ธ ๋ฐ ์ธ๋ฑ์ค ์ญ์
curl -X GET "http://localhost:9200/products/_mapping?pretty"
curl -X DELETE "http://localhost:9200/products"
```
> [!TIP]
> **`text` vs `keyword` ํ์
์ ์ฐจ์ด**:
> - `text`: ํํ์ ๋ถ์๊ธฐ(Analyzer)์ ์ํด ํ ํฐ์ผ๋ก ์ชผ๊ฐ์ ธ ์ ๋ฌธ ๊ฒ์(Full-text Search, match ์ฟผ๋ฆฌ)์ ์ฌ์ฉ๋ฉ๋๋ค.
> - `keyword`: ์ชผ๊ฐ์ง ์๊ณ ์๋ฌธ ๊ทธ๋๋ก ์ญ์์ธ๋์ด ์์ ์ผ์น ๊ฒ์(term ์ฟผ๋ฆฌ), ์ ๋ ฌ(sort), ์ง๊ณ(aggregations, group by)์ ์ฌ์ฉ๋ฉ๋๋ค.
---
## 4. ๋ํ๋จผํธ CRUD & Bulk ๋๋ ์ฒ๋ฆฌ (Document CRUD & Bulk)
```bash
# [1] ๋จ๊ฑด ๋ํ๋จผํธ ๋ฑ๋ก (POST: ID ์๋์์ฑ / PUT: ID ์ง์ )
# ID 1๋ฒ ๋ํ๋จผํธ ๋ช
์์ ์์ฑ
curl -X PUT "http://localhost:9200/products/_doc/1" -H 'Content-Type: application/json' -d'
{
"id": "PROD-001",
"title": "์ ํ M3 ๋งฅ๋ถํ๋ก 16์ธ์น ์คํ์ด์ค ๋ธ๋",
"description": "์ต๊ณ ์ ์ฑ๋ฅ์ ์๋ํ๋ ์ ๋ฌธ ์์
์ฉ ๋งฅ๋ถํ๋ก ๋
ธํธ๋ถ์
๋๋ค.",
"category": "๋
ธํธ๋ถ",
"tags": ["์ ํ", "๋
ธํธ๋ถ", "M3", "๋งฅ๋ถ"],
"price": 3690000,
"stock": 25,
"is_active": true,
"created_at": "2026-10-04 10:00:00"
}'
# [2] ๋ํ๋จผํธ ๋จ๊ฑด ์กฐํ (GET)
curl -X GET "http://localhost:9200/products/_doc/1?pretty"
# [3] ๋ํ๋จผํธ ๋ถ๋ถ ์์ (_update API)
curl -X POST "http://localhost:9200/products/_update/1" -H 'Content-Type: application/json' -d'
{
"doc": {
"price": 3590000,
"stock": 20
}
}'
# [4] ๋ํ๋จผํธ ์ญ์ (DELETE)
curl -X DELETE "http://localhost:9200/products/_doc/1"
```
```bash
# [5] Bulk API ๋๋ ์ธ๋ฑ์ฑ (NDJSON ํฌ๋งท - ๋ช
๋ นํ๊ณผ ๋ฐ์ดํฐํ์ด ๋ฒ๊ฐ์ ๋ฐฐ์น๋๋ฉฐ ๋์ ๋ฐ๋์ ๊ฐํ ํฌํจ)
curl -X POST "http://localhost:9200/_bulk" -H 'Content-Type: application/x-ndjson' -d'
{ "index" : { "_index" : "products", "_id" : "2" } }
{ "id": "PROD-002", "title": "์ผ์ฑ ๊ฐค๋ญ์๋ถ4 ํ๋ก 14์ธ์น", "description": "๊ฐ๋ณ๊ณ ํด๋์ฑ์ด ๋ฐ์ด๋ ๊ณ ์ฑ๋ฅ ์ธํ
์ธํธ๋ผ ๋
ธํธ๋ถ", "category": "๋
ธํธ๋ถ", "tags": ["์ผ์ฑ", "๋
ธํธ๋ถ", "๊ฐค๋ญ์"], "price": 1850000, "stock": 40, "is_active": true, "created_at": "2026-10-04 11:00:00" }
{ "index" : { "_index" : "products", "_id" : "3" } }
{ "id": "PROD-003", "title": "LG ๊ทธ๋จ ํ๋ก 16 ์ธํธ๋ผ", "description": "์ด๊ฒฝ๋ ๋ํ๋ฉด ์ด์ฌ๋ฆผ ๋
ธํธ๋ถ", "category": "๋
ธํธ๋ถ", "tags": ["LG", "๋
ธํธ๋ถ", "๊ทธ๋จ"], "price": 2100000, "stock": 15, "is_active": true, "created_at": "2026-10-04 12:00:00" }
{ "index" : { "_index" : "products", "_id" : "4" } }
{ "id": "PROD-004", "title": "์ ํ ๋งค์ง ๋ง์ฐ์ค ํ์ดํธ", "description": "๋ธ๋ฃจํฌ์ค ๋ฉํฐ ํฐ์น ์ ์ค์ฒ ์ง์ ๋ฌด์ ๋ง์ฐ์ค", "category": "์ฃผ๋ณ๊ธฐ๊ธฐ", "tags": ["์ ํ", "๋ง์ฐ์ค", "๋ฌด์ "], "price": 119000, "stock": 100, "is_active": true, "created_at": "2026-10-04 13:00:00" }
{ "index" : { "_index" : "products", "_id" : "5" } }
{ "id": "PROD-005", "title": "๋ก์งํ
MX Master 3S ๋ง์ฐ์ค", "description": "๋ฌด์์ ํด๋ฆญ๊ณผ ์ด๊ณ ์ ํ ์ ํ์ฌํ ์
๋ฌด์ฉ ๋ํ์ ๋ง์ฐ์ค", "category": "์ฃผ๋ณ๊ธฐ๊ธฐ", "tags": ["๋ก์งํ
", "๋ง์ฐ์ค", "๋ธ๋ฃจํฌ์ค"], "price": 139000, "stock": 80, "is_active": true, "created_at": "2026-10-04 14:00:00" }
'
```
---
## 5. ์ฟผ๋ฆฌ DSL ํต์ฌ ๊ฒ์ (Query DSL: Match, Term, Bool, Range)
```bash
# [1] match ๊ฒ์ (์ ๋ฌธ ํํ์ ๋ถ์ ๊ฒ์: '๋งฅ๋ถ' ๋๋ '๋
ธํธ๋ถ' ๊ฒ์)
curl -X GET "http://localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"match": {
"title": "๋งฅ๋ถ ๋
ธํธ๋ถ"
}
}
}'
# [2] term ๊ฒ์ (์ ํํ ์์ ์ผ์น ๊ฒ์ - keyword ํ๋์ ์ฃผ๋ก ์ฌ์ฉ)
curl -X GET "http://localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"term": {
"category": "๋
ธํธ๋ถ"
}
}
}'
# [3] multi_match ๊ฒ์ (์ฌ๋ฌ ํ๋์ ๊ฑธ์ณ ๊ฒ์ + ๊ฐ์ค์น boost ๋ถ์ฌ)
# title ํ๋ ์ผ์น ์ ์ ์๋ฅผ 3๋ฐฐ(^3) ๊ฐ์ค์น ๋ถ์ฌ
curl -X GET "http://localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"multi_match": {
"query": "์ ํ",
"fields": ["title^3", "description", "tags"]
}
}
}'
```
```bash
# [4] ์ค๋ฌด ํ์ค: bool ์ฟผ๋ฆฌ (must, filter, should, must_not ์กฐํฉ)
# โข must: ์ ์ ๊ณ์ฐ ๋ฐ์ + ํ์ ์ผ์น
# โข filter: ์ ์ ๊ณ์ฐ ์ ํจ (์บ์ฑ๋์ด ์ฑ๋ฅ ๋งค์ฐ ๋น ๋ฆ) + ์กฐ๊ฑด ํํฐ๋ง
# โข should: ํ์ ์๋ (๋งค์นญ ์ ๊ฒ์ ์์ ์ ์ ์์น)
# โข must_not: ๋ฐ๋์ ์ ์ธ
curl -X GET "http://localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"bool": {
"must": [
{ "match": { "title": "๋
ธํธ๋ถ" } }
],
"filter": [
{ "term": { "is_active": true } },
{ "range": { "price": { "gte": 1500000, "lte": 4000000 } } }
],
"should": [
{ "match_phrase": { "title": "๋งฅ๋ถํ๋ก" } }
],
"must_not": [
{ "term": { "category": "์ค๊ณ " } }
]
}
},
"sort": [
{ "price": { "order": "desc" } },
{ "_score": { "order": "desc" } }
],
"from": 0,
"size": 10,
"highlight": {
"pre_tags": ["<mark class=\"hl\">"],
"post_tags": ["</mark>"],
"fields": {
"title": {}
}
}
}'
```
```bash
# [5] ๋์ฉ๋ ๋ฅ ํ์ด์ง (Search After ๊ธฐ๋ฒ: from/size 10,000๊ฑด ์ด๊ณผ ์ ๊ถ์ฅ)
# ์ฒซ ํ์ด์ง ํธ์ถ (์ ๋ ฌ ๊ธฐ์ค ๊ฐ _sort ๊ฒฐ๊ณผ๋ฅผ ํ์ธ)
curl -X GET "http://localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"size": 10,
"query": { "match_all": {} },
"sort": [
{ "created_at": "desc" },
{ "id": "asc" }
]
}'
# ๋ค์ ํ์ด์ง ํธ์ถ (์ด์ ํ์ด์ง ๋ง์ง๋ง ํญ๋ชฉ์ sort ๋ฐฐ์ด ๊ฐ์ search_after์ ์ ๋ฌ)
curl -X GET "http://localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"size": 10,
"query": { "match_all": {} },
"search_after": [1791111600000, "PROD-004"],
"sort": [
{ "created_at": "desc" },
{ "id": "asc" }
]
}'
```
---
## 6. ํต๊ณ ๋ฐ ๋ฐ์ดํฐ ๋ถ์ ์ง๊ณ (Aggregations)
```bash
# [1] ๋ฉํธ๋ฆญ ์ง๊ณ (Metrics: ํ๊ท , ์ต์, ์ต๋, ํฉ๊ณ, ๊ณ ์ ๊ฐ ์นด์ดํธ)
curl -X GET "http://localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"size": 0,
"aggs": {
"avg_product_price": {
"avg": { "field": "price" }
},
"price_stats": {
"stats": { "field": "price" }
},
"unique_category_count": {
"cardinality": { "field": "category" }
}
}
}'
# [2] ๋ฒํท ์ง๊ณ & ์๋ธ ์ง๊ณ (Category๋ณ ์ํ ์ ๊ทธ๋ฃนํ + ์นดํ
๊ณ ๋ฆฌ๋ณ ํ๊ท ๊ฐ๊ฒฉ)
curl -X GET "http://localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"size": 0,
"aggs": {
"by_category": {
"terms": {
"field": "category",
"size": 10,
"order": { "_count": "desc" }
},
"aggs": {
"avg_price": {
"avg": { "field": "price" }
},
"total_stock": {
"sum": { "field": "stock" }
}
}
}
}
}'
# [3] ์๊ณ์ด ๋ ์ง ํ์คํ ๊ทธ๋จ ์ง๊ณ (Date Histogram: ์ผ์๋ณ ๋ฑ๋ก ์ถ์ด)
curl -X GET "http://localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"size": 0,
"aggs": {
"daily_trends": {
"date_histogram": {
"field": "created_at",
"calendar_interval": "1d",
"format": "yyyy-MM-dd"
}
}
}
}'
```
---
## 7. ์ค๋ฌด ์ ํ๋ฆฌ์ผ์ด์
์ฐ๋ ์์ (Python & Spring Boot)
```python
# ๐ [1] Python (elasticsearch ๊ณต์ ํด๋ผ์ด์ธํธ)
# pip install elasticsearch
from elasticsearch import Elasticsearch
es = Elasticsearch("http://localhost:9200")
# 1. ํ ํ
์คํธ
if not es.ping():
raise ValueError("Elasticsearch ์๋ฒ ์ฐ๊ฒฐ ์คํจ")
# 2. ๋ํ๋จผํธ ์์ธ
doc = {
"id": "PROD-010",
"title": "์ ํ ์์ดํ ํ๋ก 2์ธ๋ USB-C",
"category": "์ํฅ๊ธฐ๊ธฐ",
"price": 359000,
"stock": 50,
"is_active": True
}
res = es.index(index="products", id="10", document=doc)
print(f"์ธ๋ฑ์ฑ ๊ฒฐ๊ณผ: {res['result']}")
# 3. ๋ณตํฉ ๊ฒ์ ์ฟผ๋ฆฌ ์คํ
query = {
"bool": {
"must": [{"match": {"title": "์์ดํ"}}],
"filter": [{"term": {"is_active": True}}]
}
}
response = es.search(index="products", query=query, size=5)
for hit in response["hits"]["hits"]:
source = hit["_source"]
print(f"[{hit['_score']:.2f}] {source['title']} - {source['price']:,}์")
```
```java
// ๐ [2] Spring Boot (Spring Data Elasticsearch)
// build.gradle: implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
@Document(indexName = "products")
@Setting(replicas = 1, shards = 2)
public class ProductDocument {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "korean_analyzer")
private String title;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Long)
private Long price;
@Field(type = FieldType.Boolean)
private Boolean isActive;
// getters and setters...
}
public interface ProductRepository extends ElasticsearchRepository<ProductDocument, String> {
// ๋ฉ์๋ ์ฟผ๋ฆฌ ์์ฑ
List<ProductDocument> findByCategoryAndIsActiveTrue(String category);
// ๋ค์ดํฐ๋ธ ๊ฒ์ ์ฟผ๋ฆฌ
@Query("{\"bool\": {\"must\": [{\"match\": {\"title\": \"?0\"}}], \"filter\": [{\"term\": {\"isActive\": true}}]}}")
List<ProductDocument> searchByTitle(String keyword);
}
```
---
## 8. ์ค๋ฌด ์ด์ ํ๋ & ํด๋ฌ์คํฐ ํธ๋ฌ๋ธ์ํ
(Performance & Ops)
```text
โ JVM Heap Memory ์ค์ ์์น
1. ๋ฌผ๋ฆฌ ์์คํ
RAM์ 50%๋ฅผ ์ด๊ณผํ์ง ๋ง ๊ฒ:
- ๋๋จธ์ง 50% ๋ฉ๋ชจ๋ฆฌ๋ OS์ ํ์ผ ์์คํ
๋ฃจ์ฌ ์บ์(Page Cache)๋ก ํ์ ํ์ฉ๋ฉ๋๋ค.
2. ์ต๋ 31GB ์ดํ๋ก ์ ํํ ๊ฒ:
- 32GB ์ด์ ์ค์ ์ JVM์ด ์์ถ ๊ฐ์ฒด ํฌ์ธํฐ(Compressed OOPs)๋ฅผ ๋นํ์ฑํํ์ฌ ๋ฉ๋ชจ๋ฆฌ ๋ญ๋น์ GC ์ค๋ฒํค๋๊ฐ ๊ธ์ฆํฉ๋๋ค.
- ์: /etc/elasticsearch/jvm.options -> -Xms16g -Xmx16g
```
```bash
# [1] ๋๋ ๋ฐ์ดํฐ(Bulk) ์ ์ฌ ์ ์๋ ๊ทน๋ํ ์ค์
# ์์๋ก ๋ฆฌํ๋ ์์ ๋ณต์ ๋ณธ์ ๋นํ์ฑํํ ๋ค ๋๋ ์ ์ฌ -> ์๋ฃ ํ ๋ณต๊ตฌ
curl -X PUT "http://localhost:9200/products/_settings" -H 'Content-Type: application/json' -d'
{
"index": {
"refresh_interval": "-1",
"number_of_replicas": 0
}
}'
# ๋๋ ์ ์ฌ ์๋ฃ ํ ์๋ณต ๋ฐ ๊ฐ์ ๋ณํฉ(Force Merge)
curl -X PUT "http://localhost:9200/products/_settings" -H 'Content-Type: application/json' -d'
{
"index": {
"refresh_interval": "1s",
"number_of_replicas": 1
}
}'
curl -X POST "http://localhost:9200/products/_forcemerge?max_num_segments=1"
```
```bash
# [2] ํด๋ฌ์คํฐ ์ํ๊ฐ Yellow ๋๋ Red์ผ ๋ ์์ธ ๋ถ์ (Allocation Explain API)
# ์ค๋๊ฐ ํน์ ๋
ธ๋์ ํ ๋น๋์ง ๋ชปํ๋ ์ด์ ๋ฅผ ์ ํํ ์ง๋จ
curl -X GET "http://localhost:9200/_cluster/allocation/explain?pretty"
# [3] ๋์คํฌ ์ฉ๋ ํ๋(Disk Watermark) ์ด๊ณผ๋ก ์ฝ๊ธฐ ์ ์ฉ ๋ฝ์ด ๊ฑธ๋ ธ์ ๋ ํด์
# ๊ธฐ๋ณธ ์๊ณ์น: Low 85%, High 90%, Flood Stage 95%
curl -X PUT "http://localhost:9200/products/_settings" -H 'Content-Type: application/json' -d'
{
"index.blocks.read_only_allow_delete": null
}
'
```
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์