μ€μκ° λ΄μ€ λ° μμ λ―Έλμ΄ λ°μ΄ν°λ₯Ό μμ§, μ μ , μμΈννμ¬ νΈλ λ© ν€μλλ₯Ό μ 곡νλ νλ«νΌ
μ΄ νλ«νΌμ λ€μν μμ€(Wikipedia, YNA λ΄μ€, YouTube)μμ μ€μκ°μΌλ‘ λ°μ΄ν°λ₯Ό μμ§νκ³ , μ΄λ₯Ό μ μ λ° μμΈννμ¬ νΈλ λ© ν€μλλ₯Ό μΆμΆνκ³ μ 곡ν©λλ€.
- μ€μκ° λ°μ΄ν° μμ§: Wikipedia, μ°ν©λ΄μ€(YNA), YouTubeμμ λ°μ΄ν° μμ§
- μλ μ μ : HTML/JSON/Wikitext νμ± λ° ν μ€νΈ μΆμΆ
- νκ΅μ΄ ν€μλ μΆμΆ: Elasticsearch Nori νλ¬κ·ΈμΈ κΈ°λ° ννμ λΆμ
- κ°μ€μΉ κΈ°λ° λνΉ: μμ€λ³, μκ°λ³ κ°μ€μΉ μ μ©ν ν€μλ μ μ κ³μ°
- μ€μκ° API: νΈλ λ© ν€μλ μ‘°ν REST API μ 곡
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Realtime Search Platform Architecture β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββββββββ βββββββββββββββββ βββββββββββββββββ ββββββββββββββββββ
β β Collector β β Refine β β Index β β Serving ββ
β β System ββββββ>β System ββββββ>β System ββββββ>β System ββ
β β (9080) β β (9082) β β (9083) β β (9086) ββ
β βββββββββ¬ββββββββ βββββββββ¬ββββββββ βββββββββ¬ββββββββ βββββββββ¬βββββββββ
β β β β β β
β β β β β β
β βββββββββΌββββββββ βββββββββΌββββββββ βββββββββΌββββββββ βββββββββΌβββββββββ
β β MinIO β β MongoDB β β Elasticsearch β β Redis ββ
β β (μλ³Έ μ μ₯) β β (μ μ μ μ₯) β β (μμΈ μ μ₯) β β (μΊμ±) ββ
β βββββββββββββββββ βββββββββββββββββ βββββββββββββββββ ββββββββββββββββββ
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β Apache Kafka (λ©μμ§ λ°±λ³Έ) ββ
β β raw.* topics ββββββββββββββββββ> refined.* topics βββββββββββββββββ> API Response ββ
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β MySQL (λ©νλ°μ΄ν° μ μ₯) ββ
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| κ³μΈ΅ | μμ€ν | μν | μ μ₯μ |
|---|---|---|---|
| μμ§ | Collector | μΈλΆ μμ€μμ μλ³Έ λ°μ΄ν° μμ§ | MinIO |
| μ μ | Refine | HTML/ν μ€νΈ νμ± λ° μ μ | MongoDB |
| μμΈ | Index | ν€μλ μΆμΆ λ° κ²μ μμΈ | Elasticsearch |
| μλΉ | Serving | API μ 곡 λ° λνΉ κ³μ° | Redis + MySQL |
| ν ν½ | μμ€ | μ€λͺ |
|---|---|---|
raw.docs.wikipedia |
Collector | Wikipedia μλ³Έ λ°μ΄ν° |
raw.news.yna |
Collector | YNA λ΄μ€ μλ³Έ λ°μ΄ν° |
raw.sns.youtube |
Collector | YouTube μλ³Έ λ°μ΄ν° |
refined.docs.wikipedia |
Refine | Wikipedia μ μ λ°μ΄ν° |
refined.news.yna |
Refine | YNA λ΄μ€ μ μ λ°μ΄ν° |
refined.sns.youtube |
Refine | YouTube μ μ λ°μ΄ν° |
*.dlq |
κ° μμ€ν | Dead Letter Queue (μ€λ₯ μ²λ¦¬) |
CollectMessage (μμ§ β μ μ )
{
"schemaVersion": "1",
"collectionId": "YNA_20250902_001",
"source": "NEWS_YNA",
"occurredAt": "2025-09-02T10:30:00Z",
"rawDataUrl": "minio://raw-news-yna/2025-09-02/article_123.html",
"recordCount": 1
}RefineMessage (μ μ β μμΈ)
{
"schemaVersion": "1",
"collectionId": "YNA_123",
"source": "NEWS_YNA",
"occurredAt": "2025-09-02T10:35:00Z",
"refinedId": "refined_abc123def456"
}ν¬νΈ: 9080 | μν : μΈλΆ μμ€μμ λ°μ΄ν° μμ§ β Kafka λ©μμ§ λ°μ‘
| μμ€ν | μꡬμ¬ν | μν | ꡬν λ°©μ |
|---|---|---|---|
| μμ§ | λμ©λ νμΌ μ²λ¦¬ (1GB+) | β μΆ©μ‘± | StAX μ€νΈλ¦¬λ° + BZip2 μμΆ ν΄μ |
| μμ§ | λ©μμ§ λΆν (1MB μ΄ν) | β μΆ©μ‘± | NDJSON + GZIP μ€λ (500νμ΄μ§/μ€λ) |
| μμ§ | HTML ν¬λ‘€λ§ (νλ£¨μΉ μμ§) | β μΆ©μ‘± | RSS νΌλ κΈ°λ° λ³λ ¬ ν¬λ‘€λ§ |
| μμ§ | RSS νμ© | β μΆ©μ‘± | Rome RSS Parser + ν΄λ°± νμ± |
| μμ§ | YouTube API μ°λ | β μΆ©μ‘± | YouTube Data API v3 |
| μμ§ | API νΈμΆ μ₯μ λμ | β μΆ©μ‘± | Exponential Backoff + μ¬μλ |
μꡬμ¬ν: 1GB μ΄μ νμΌ νΈλ€λ§, λ©μμ§ 1MB μ΄νλ‘ λΆν
ꡬν λ°©μ:
Wikipedia XML λ€ν (1GB+)
β BZip2 μμΆ ν΄μ (μ€νΈλ¦¬λ°)
β StAX νμ (XMLStreamReader)
β 500νμ΄μ§ λ¨μ μ€λ λΆν
β NDJSON + GZIP μμΆ (μ½ 500KB~1MB)
β MinIO μ
λ‘λ + Kafka μ΄λ²€νΈ λ°ν
μ€μ κ°:
| μ€μ | κ° | μ€λͺ |
|---|---|---|
pages-per-shard |
500 | μ€λλΉ νμ΄μ§ μ |
| μμΆ λ°©μ | GZIP | μ€λ μμΆ |
| νμ | NDJSON | μ€νΈλ¦¬λ° νμ± μ©μ΄ |
- Kafka λ©μμ§ ν¬κΈ° μ ν: Kafka κΈ°λ³Έ μ΅λ λ©μμ§ ν¬κΈ°λ 1MB, 500νμ΄μ§ Γ GZIP μμΆ β 500KB~1MBλ‘ μ μ
- λ©λͺ¨λ¦¬ ν¨μ¨: ν λ²μ λ무 λ§μ νμ΄μ§λ₯Ό λ©λͺ¨λ¦¬μ μ¬λ¦¬λ©΄ OOM μν
- μ€ν¨ 볡ꡬ λ¨μ: μ€λ μ²λ¦¬ μ€ν¨ μ 500νμ΄μ§λ§ μ¬μ²λ¦¬νλ©΄ λ¨ (μ 체 μ¬μ²λ¦¬ λ°©μ§)
- λ³λ ¬ μ²λ¦¬ ν¨μ¨: μ€λκ° λ무 ν¬λ©΄ Consumer κ° λΆν λΆκ· ν, λ무 μμΌλ©΄ μ€λ²ν€λ μ¦κ°
- Newline Delimited JSON: ν μ€μ νλμ JSON κ°μ²΄, μ€λ°κΏ(
\n)μΌλ‘ κ΅¬λΆ - μ€νΈλ¦¬λ° νμ± κ°λ₯: μ 체 νμΌμ λ©λͺ¨λ¦¬μ μ¬λ¦¬μ§ μκ³ ν μ€μ© μ½μ΄μ μ²λ¦¬
- μΌλ° JSON λ°°μ΄κ³Όμ μ°¨μ΄:
# μΌλ° JSON λ°°μ΄ (μ 체λ₯Ό νμ±ν΄μΌ ν¨) [{"id": 1}, {"id": 2}, {"id": 3}] # NDJSON (ν μ€μ© λ 립μ μΌλ‘ νμ± κ°λ₯) {"id": 1} {"id": 2} {"id": 3} - λμ©λ μ²λ¦¬μ μ ν©: Wikipedia λ€νμ²λΌ μμλ§ κ°μ λ¬Έμλ₯Ό μ²λ¦¬ν λ λ©λͺ¨λ¦¬ μ¬μ©λ μ΅μν
μꡬμ¬ν: νλ£¨μΉ μμ§, RSS νμ©, νμ΄μ§ λ§ν¬ μλ νμ Bot
ꡬν λ°©μ:
RSS νΌλ λ€μ΄λ‘λ
β Rome RSS Parserλ‘ κΈ°μ¬ λͺ©λ‘ μΆμΆ
β μ€λ³΅ μ κ±° (articleId κΈ°μ€)
β Semaphore κΈ°λ° λ³λ ¬ ν¬λ‘€λ§ (λμ 4κ°)
β μμ² κ° μ§μ° (250ms + jitter 150ms)
β MinIO μ μ₯ + Kafka μ΄λ²€νΈ λ°ν
μ€μ κ°:
| μ€μ | κ° | μ€λͺ |
|---|---|---|
concurrency |
4 | λμ μμ² μ |
inter-request-delay-ms |
250 | μμ² κ° μ§μ° |
inter-request-jitter-ms |
150 | μ§μ° λλ€ν |
connect-timeout-ms |
2000 | μ°κ²° νμμμ |
response-timeout-ms |
5000 | μλ΅ νμμμ |
μꡬμ¬ν: YouTube Developer API νμ©, API νΈμΆ μ₯μ λμ
ꡬν λ°©μ:
YouTube Data API v3 νΈμΆ
β mostPopular λΉλμ€ λͺ©λ‘ μ‘°ν
β μ₯μ μ Exponential Backoff μ¬μλ
β 429/5xx μλ¬ μλ μ¬μλ
β μ€ν¨ μ DLQ μ μ‘
μ€μ κ°:
| μ€μ | κ° | μ€λͺ |
|---|---|---|
max-attempts |
2 | μ΅λ μ¬μλ νμ |
base-backoff-ms |
200 | κΈ°λ³Έ λ°±μ€ν μκ° |
batch-size |
1000 | λ°°μΉ ν¬κΈ° |
ν¬νΈ: 9082 | μν : Kafka λ©μμ§ μμ β λ°μ΄ν° μ μ β Kafka λ©μμ§ λ°μ‘
| μμ€ν | μꡬμ¬ν | μν | ꡬν λ°©μ |
|---|---|---|---|
| μ μ | λλ λ°μ΄ν° μμ (μ΄λΉ 1000건) | λ°°μΉ(50) Γ λμμ±(10) Γ 2 = ~1000건/μ΄ | |
| μ μ | μΉ΄νμΉ΄ 컨μλ¨Έ νλ | β μΆ©μ‘± | 11κ° νλ ν¬μΈνΈ μ μ© |
| μ μ | λ°μ΄ν° λλ²λ§ (PK λΆμ¬) | β μΆ©μ‘± | refined_<SHA256(source+id)> λ°©μ |
| μ μ | λ©νλ°μ΄ν° μμ± | β μΆ©μ‘± | μμ€λ³ μμΈ λ©νλ°μ΄ν° μμ± |
| μ μ | FullText μΆμΆ | β μΆ©μ‘± | Wikitext/HTML/JSON νμ± |
μꡬμ¬ν: μ΄λΉ 1000건 μ²λ¦¬
ꡬν λ°©μ:
Kafka Consumer (concurrency=10, νν°μ
μμ λμΌ)
β λ°°μΉ μμ (max-poll-records=50)
β λ³λ ¬ μ²λ¦¬ (ThreadPool: core=10, max=20)
β μλ μ»€λ° (ack-mode=manual)
μ²λ¦¬λ κ³μ°:
μ΄λ‘ μ μ²λ¦¬λ = λ°°μΉν¬κΈ°(50) Γ λμμ±(10) Γ μ΄λΉλ°°μΉμ
= 50 Γ 10 Γ ~2 = ~1000건/μ΄
- Kafka concurrencyμ λ§€μΉ: Consumer μ€λ λ μ(10)μ λμΌνκ² μ€μ νμ¬ λ³λͺ© λ°©μ§
- CPU λ°μ΄λ μμ νΉμ±: μ μ μμ μ HTML/Wikitext νμ±μΌλ‘ CPUλ₯Ό λ§μ΄ μ¬μ©
- max = core Γ 2 μμΉ: λ²μ€νΈ νΈλν½ μ 2λ°°κΉμ§ νμ₯, I/O λκΈ° μ€ μΆκ° μ²λ¦¬ κ°λ₯
- queue-capacity=500: μ€λ λκ° λ°μ λ λκΈ° ν, λ무 ν¬λ©΄ λ©λͺ¨λ¦¬ λλΉ
μ μ©λ νλ ν¬μΈνΈ:
| μ€μ | κ° | λͺ©μ |
|---|---|---|
max-poll-records |
50 | LAG λͺ¨λν°λ§ μ΅μ ν |
fetch.min.bytes |
1MB | λ€νΈμν¬ ν¨μ¨ν |
fetch.max.wait.ms |
50ms | μ§μ°/μ²λ¦¬λ κ· ν |
concurrency |
10 | νν°μ μμ λ§€μΉ |
ack-mode |
MANUAL | μ νν μ²λ¦¬ 보μ₯ |
enable-auto-commit |
false | μλ μ»€λ° |
session.timeout.ms |
45000 | μΈμ μ μ§ |
heartbeat.interval.ms |
15000 | ννΈλΉνΈ κ°κ²© |
isolation.level |
read_committed | 컀λ°λ λ°μ΄ν°λ§ |
max.poll.interval.ms |
600000 | λ°°μΉ μ²λ¦¬ μ΅λ μκ° |
- LAG λͺ¨λν°λ§ μ νλ: κ°μ΄ λ무 ν¬λ©΄(300+) Consumer LAG μ§νκ° μ€μ λ³΄λ€ λΆνλ €μ Έ 보μ
- μ²λ¦¬ μ€ν¨ μ μ¬μ²λ¦¬ λ²μ μ΅μν: λ°°μΉ μ€κ°μ μ€ν¨νλ©΄ μ 체 λ°°μΉλ₯Ό μ¬μ²λ¦¬ν΄μΌ νλ―λ‘, μμ λ°°μΉκ° 볡ꡬμ μ 리
- λ©λͺ¨λ¦¬ μ¬μ©λ μ μ΄: 50κ° Γ λ©μμ§ ν¬κΈ°λ‘ ν λ©λͺ¨λ¦¬ μ¬μ©λ μμΈ‘ κ°λ₯
- λ€νΈμν¬ μ볡 μ΅μν: μμ λ©μμ§κ° λ§μ λ 1MBκ° λ λκΉμ§ λΈλ‘μ»€κ° λκΈ° ν μΌκ΄ μ μ‘
- λΈλ‘컀 λΆν κ°μ: μ¦μ fetch μμ²μΌλ‘ μΈν λΈλ‘컀 CPU μ¬μ©λ μ κ°
- μ²λ¦¬λ ν₯μ: ν λ²μ λ λ§μ λ°μ΄ν°λ₯Ό κ°μ Έμ λ°°μΉ μ²λ¦¬ ν¨μ¨ μ¦κ°
- 1:1 λ§€μΉ μμΉ: Kafkaμμ νλμ νν°μ μ Consumer Group λ΄ λ¨ νλμ Consumerλ§ μλΉ κ°λ₯
- 리μμ€ λλΉ λ°©μ§: concurrency > νν°μ μμ΄λ©΄ μ΄κ³Ό μ€λ λλ μ ν΄ μνλ‘ λ©λͺ¨λ¦¬λ§ λλΉ
- λ³λ ¬μ± μ΅λν: concurrency < νν°μ μμ΄λ©΄ μΌλΆ νν°μ μ΄ νλμ μ€λ λμ λͺ°λ € LAG μ¦κ°
- νμ¬ κΆμ₯: νν°μ
10κ° νκ²½μμλ
concurrency: 10μ΄ μ΅μ
- μ νν ν λ²(Exactly-Once) μ²λ¦¬ 보μ₯: λ©μμ§ μ²λ¦¬κ° μμ ν λλ νμλ§ μ»€λ°
- λ°μ΄ν° μ μ€ λ°©μ§: auto-commitμ μ²λ¦¬ μ μ 컀λ°λ μ μμ΄, μ₯μ μ λ©μμ§ μ μ€ κ°λ₯
- μ¬μ²λ¦¬ κ°λ₯: μ²λ¦¬ μ€ μμΈ λ°μ μ 컀λ°νμ§ μμ μ¬μμ ν λμΌ λ©μμ§ μ¬μ²λ¦¬
- 리밸λ°μ± μ§μ° λ°©μ§: κ°μ΄ λ무 μμΌλ©΄ GCλ μΌμμ λ€νΈμν¬ μ§μ°μλ Consumerκ° μ£½μ κ²μΌλ‘ νλ¨
- μ₯μ κ°μ§ μλ κ· ν: κ°μ΄ λ무 ν¬λ©΄ μ€μ μ₯μ μ 리밸λ°μ±μ΄ λ¦μ΄μ Έ μ²λ¦¬ μ§μ°
- heartbeat.interval.msμ 3λ°° μμΉ: session.timeout(45s) = heartbeat(15s) Γ 3, 3λ²μ ννΈλΉνΈ μ€ν¨ ν μ μΈ
- λμ©λ λ°°μΉ μ²λ¦¬ νμ©: Wikipedia μ€λ(500νμ΄μ§)λ λλ λ΄μ€ μ²λ¦¬ μ μΆ©λΆν μκ° ν보
- μΈλΆ μλΉμ€ μ§μ° λλΉ: MongoDB, MinIO λ± μΈλΆ μ μ₯μ μλ΅ μ§μ° μμλ νμμμ λ°©μ§
- 리밸λ°μ± νΈλ¦¬κ±° λ°©μ§: μ΄ μκ° λ΄μ poll()μ νΈμΆνμ§ μμΌλ©΄ Consumerκ° κ·Έλ£Ήμμ μ μΈλ¨
λλ²λ§ (PK λΆμ¬):
// κ²°μ λ‘ μ ID μμ± - λ©±λ±μ± 보μ₯
String refinedId = "refined_" + HashUtils.sha256Hex(source + pageId);
// μ: refined_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6FullText μΆμΆ:
| μμ€ | νμ | μ²λ¦¬ |
|---|---|---|
| Wikipedia | Bliki (Wikitext) | Wikitext β HTML β PlainText |
| YNA News | JSoup (HTML) | HTML β λ³Έλ¬Έ μ ν β κ΄κ³ /UI μ κ±° β PlainText |
| YouTube | Jackson (JSON) | JSON β ꡬ쑰νλ νλ μΆμΆ |
ν¬νΈ: 9083 | μν : Kafka λ©μμ§ μμ β Elasticsearchλ‘ λ°μ΄ν° μμΈ
| μμ€ν | μꡬμ¬ν | μν | ꡬν λ°©μ |
|---|---|---|---|
| μμΈ | λλ λ°μ΄ν° μμ (μ΄λΉ 1000건) | λ°°μΉ(100) Γ λμμ±(10) = 1000건/μ΄. κ·Έλ¬λ νμ¬ 600건/μ΄ | |
| μμΈ | μΉ΄νμΉ΄ 컨μλ¨Έ νλ | β μΆ©μ‘± | 11κ° νλ ν¬μΈνΈ μ μ© |
| μμΈ | ννμ λΆμ μμΈ | β μΆ©μ‘± | Elasticsearch Nori λΆμκΈ° |
| μμΈ | μμ λ§μ ννμ λΆμκΈ° | Nori + μ체 λΆμ©μ΄ νν°λ§ |
μ²λ¦¬λ:
μ²λ¦¬λ = λ°°μΉν¬κΈ°(100) Γ λμμ±(10) Γ μ΄λΉλ°°μΉμ β 1000~2000건/μ΄
μ€μ :
| μ€μ | κ° | μ€λͺ |
|---|---|---|
max-poll-records |
100 | λ°°μΉ ν¬κΈ° |
concurrency |
10 | νν°μ μμ λ§€μΉ |
BULK_SIZE |
500 | ES Bulk ν¬κΈ° |
- Elasticsearch Bulk API μ΅μ ν: ESλ κ°λ³ λ¬Έμ μμΈλ³΄λ€ Bulk μμΈμ΄ 10λ°° μ΄μ λΉ λ¦
- μ μ μμ€ν λ³΄λ€ ν° λ°°μΉ: μμΈ μμ μ μ μ (νμ±/λ³ν)λ³΄λ€ I/O λ°μ΄λμ΄λ―λ‘ ν° λ°°μΉκ° μ 리
- BULK_SIZE(500)μμ κ΄κ³: 100κ°μ© μ¬λ¬ λ² pollνμ¬ 500κ°κ° λͺ¨μ΄λ©΄ Bulk μμ² μ μ‘
- ES μ΅μ λ°°μΉ ν¬κΈ°: Elasticsearch 곡μ κΆμ₯μ 5-15MB λλ 1000-5000 λ¬Έμ
- λ©λͺ¨λ¦¬ μ¬μ©λ κ· ν: 500 Γ νκ· λ¬Έμ ν¬κΈ°(~10KB) = ~5MBλ‘ μ μ μμ€
- μ€ν¨ μ μ¬μλ λ²μ: Bulk μ€ν¨ μ 500κ° λ¨μλ‘ μ¬μλ, λ무 ν¬λ©΄ μ¬μλ λΆλ΄ μ¦κ°
- λ€νΈμν¬ ν¨μ¨: λ무 μμΌλ©΄ HTTP μ€λ²ν€λ μ¦κ°, λ무 ν¬λ©΄ νμμμ μν
- I/O λ°μ΄λ μμ νΉμ±: ES μμΈμ λ€νΈμν¬ I/O λκΈ°κ° λ§μ CPU μ½μ΄ μλ³΄λ€ λ§μ μ€λ λ νμ
- core-pool-size=10: νμμ μ μ§ν μ€λ λ μ, ES μ°κ²° ν ν¬κΈ°μ μ μ¬νκ² μ€μ
- max-pool-size=20: λ²μ€νΈ νΈλν½ μ μμ νμ₯, λ무 ν¬λ©΄ ES μ°κ²° κ³ κ° μν
- queue-capacity=500: μ€λ λκ° λͺ¨λ λ°μ λ λκΈ° ν, BULK_SIZEμ λμΌνκ² μ€μ
Elasticsearch λΆμκΈ° μ€μ (elasticsearch-index-settings.json):
{
"analyzer": {
"korean_analyzer": {
"type": "custom",
"tokenizer": "nori_tokenizer",
"filter": [
"lowercase",
"nori_part_of_speech",
"korean_stopwords",
"nori_readingform"
]
}
}
}"nori_part_of_speech": {
"stoptags": [
"E", "IC", "J", "MAG", "MAJ", "MM", "SP", "SSC", "SSO",
"SC", "SE", "XPN", "XSA", "XSN", "XSV", "UNA", "NA", "VSV"
]
}- μ κ±°: μ’ λ£λΆνΈ(E), κ°νμ¬(IC), μ‘°μ¬(J), μμ μ(MM) λ±
- μ μ§: λͺ μ¬(N*), μ£Όμ λμ¬/νμ©μ¬
Java λ 벨 μΆκ° νν°λ§(ElasticsearchKeywordExtractor.java):
private static final Set<String> STOPWORDS = Set.of(
// νκ΅μ΄ μ‘°μ¬/μ΄λ―Έ
"μλ€", "μλ", "νλ€", "λλ€", "μ΄λ€", "κ²", "μ", "λ±", "κ·Έ", "λ°",
"μ΄", "κ°", "μ", "λ₯Ό", "μ", "μ", "μ", "κ³Ό", "λ", "λ§",
// Wikipedia λ©νλ°μ΄ν°
"νμ", "μ¬λ§", "κΈ°λ
", "μ¬κ±΄", "λΆλ₯", "μ°νΈ", "μμ‘°", "μΆμ"
);
// ν€μλ μΆμΆ λ‘μ§
Map<String, Long> tokenFrequency = response.tokens().stream()
.filter(token -> token.length() >= 2) // μ΅μ κΈΈμ΄
.filter(token -> !isStopword(token)) // λΆμ©μ΄ μ κ±°
.collect(groupingBy(identity(), counting())); // λΉλ κ³μ°| μ€μ | κ° | μ€λͺ |
|---|---|---|
| μ€λ μ | 3 | λΆμ° μ²λ¦¬ |
| λ ν리카 μ | 1 | κ³ κ°μ©μ± |
| 리νλ μ κ°κ²© | 5μ΄ | μ€μκ° κ²μ |
| Bulk ν¬κΈ° | 500 λ¬Έμ | λ°°μΉ μμΈ |
| JVM λ©λͺ¨λ¦¬ | 1GB | ν λ©λͺ¨λ¦¬ |
ν¬νΈ: 9086 | μν : Elasticsearch 쿼리 λ° Redis μΊμ±
| μμ€ν | μꡬμ¬ν | μν | ꡬν λ°©μ |
|---|---|---|---|
| μλΉ | λͺ μ¬ μΆμΆ | Nori POS νκ·Έ νν°λ§ | |
| μλΉ | λΉλμ κ³μ° + κ°μ€μΉ | β μΆ©μ‘± | μμ€/μκ° κ°μ€μΉ μ μ© |
| μλΉ | λ°μ΄ν° μ μ₯ (ES) | β μΆ©μ‘± | Elasticsearchμ ν€μλ μ μ₯ |
| μλΉ | λ°μ΄ν° μΊμ (μμ 10건) | β μΆ©μ‘± | Redis μΊμ± (ν루 1ν κ°±μ , 24μκ° TTL) |
KeywordRankingScheduler (λ§€μΌ μμ + μμ μ 10μ΄ ν)
β Elasticsearch ν€μλ μ§κ³ (μ΅κ·Ό 24μκ°)
β μμ€λ³ κ°μ€μΉ μ μ©
β μκ°λ³ κ°μ€μΉ μ μ© (ν루 κΈ°μ€)
β μ μ κ³μ° λ° λνΉ
β Redis μΊμ μ μ₯ (TTL: 24μκ°)
- μμ€λ³ κ°μ€μΉ
| μμ€ | κ°μ€μΉ | μ€λͺ |
|---|---|---|
| NEWS_YNA | 100.0 | μ€μκ° λ΄μ€ (μ΅κ³ κ°μ€μΉ) |
| SNS_YOUTUBE | 5.0 | SNS νΈλ λ |
| DOCS_WIKIPEDIA | 0.1 | λ°°κ²½ μ§μ |
- μκ°λ³ κ°μ€μΉ (ν루 κΈ°μ€)
| μκ° λ²μ | κ°μ€μΉ | μ€λͺ |
|---|---|---|
| λΉμΌ | 10.0 | μ€λ νΈλ λ |
| μ μΌ | 5.0 | μ΄μ νΈλ λ |
| κ·Έ μ΄μ | 1.0 | κΈ°λ³Έκ° |
μ μ = docCount Γ sourceWeight Γ timeWeight
μμ: "νλν" ν€μλ
YNAμμ 15건 (2μκ° μ ): 15 Γ 100.0 Γ 5.0 = 7,500.0
YouTubeμμ 3건 (30λΆ μ ): 3 Γ 5.0 Γ 10.0 = 150.0
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
μ΅μ’
μ μ: 7,650.0
Redis μΊμ±(KeywordRankingCache.java)
private static final String RANKING_KEY = "trending:keywords:full";
private static final long CACHE_TTL_HOURS = 24;
// μΊμ μ μ₯
public void saveRanking(List<RankedKeyword> rankedKeywords) {
String json = objectMapper.writeValueAsString(rankedKeywords);
redisTemplate.opsForValue().set(RANKING_KEY, json,
CACHE_TTL_HOURS, TimeUnit.HOURS);
}
// μμ Nκ° μ‘°ν
public List<RankedKeyword> getTopKeywords(int limit) {
return allKeywords.stream().limit(limit).toList();
}κ°±μ μ€μΌμ€λ¬(KeywordRankingScheduler.java)
@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") // λ§€μΌ μμ
public void updateKeywordRanking() {
// 1. μ΅κ·Ό 24μκ° ν€μλ μ§κ³
Map<ContentSource, List<RawKeyword>> keywordsBySource =
aggregationService.aggregateAllKeywords(
LocalDateTime.now().minusHours(24));
// 2. μ€μ½μ΄λ§ λ° λνΉ
List<RankedKeyword> ranked =
scoringService.calculateRanking(keywordsBySource);
// 3. Redis μ μ₯
rankingCache.saveRanking(ranked);
}| κΈ°μ | λ²μ | μ©λ |
|---|---|---|
| Java | 21 | λ°νμ |
| Spring Boot | 3.3.1 | νλ μμν¬ |
| Gradle | 8.x | λΉλ λꡬ |
| κΈ°μ | λ²μ | μ©λ |
|---|---|---|
| MySQL | 8.0 | λ©νλ°μ΄ν° μ μ₯ |
| Elasticsearch | 8.11.0 | κ²μ μΈλ±μ€ |
| MongoDB | 7.0 | μ μ λ°μ΄ν° μ μ₯ |
| Redis | 7.2 | μΊμ± |
| MinIO | latest | μλ³Έ κ°μ²΄ μ μ₯ |
| κΈ°μ | λ²μ | μ©λ |
|---|---|---|
| Apache Kafka | 7.4.0 | μ΄λ²€νΈ μ€νΈλ¦¬λ° |
| κΈ°μ | λ²μ | μ©λ |
|---|---|---|
| Selenium | 4.16.1 | λμ ν¬λ‘€λ§ |
| JSoup | 1.18.1 | HTML νμ± |
| Apache Tika | 2.9.2 | λ¬Έμ νμ± |
| Bliki | 3.1.0 | Wikitext νμ± |
| Rome | 1.18.0 | RSS νμ± |
| κΈ°μ | μ©λ |
|---|---|
| Zstandard (zstd) | Kafka λ©μμ§ μμΆ |
| Snappy | λΉ λ₯Έ μμΆ |
| Commons Compress | bz2 μμΆ ν΄μ |
| κΈ°μ | μ©λ |
|---|---|
| Prometheus | λ©νΈλ¦ μμ§ |
| Grafana | λμ보λ |
| Kibana | Elasticsearch UI |
| μλΉμ€ | ν¬νΈ | μ©λ |
|---|---|---|
| μ ν리μΌμ΄μ | ||
| Collector System | 9080 | λ°μ΄ν° μμ§ API |
| Refine System | 9082 | λ°μ΄ν° μ μ API |
| Index System | 9083 | κ²μ μμΈ API |
| Serving System | 9086 | μ€μκ° ν€μλ API |
| λ°μ΄ν°λ² μ΄μ€ | ||
| MySQL | 3306 | λ©νλ°μ΄ν° |
| Elasticsearch | 9200 | κ²μ μμ§ |
| MongoDB | 27017 | λ¬Έμ μ μ₯μ |
| Redis | 6379 | μΊμ± |
| MinIO API | 9000 | κ°μ²΄ μ€ν λ¦¬μ§ |
| MinIO Console | 19000 | MinIO κ΄λ¦¬ UI |
| λ©μμ§ | ||
| Kafka | 9092 | λ©μμ§ λΈλ‘컀 |
| λͺ¨λν°λ§ | ||
| Grafana | 18083 | λμ보λ |
| Prometheus | 19090 | λ©νΈλ¦ |
| Kibana | 15601 | ES λͺ¨λν°λ§ |
| κ°λ° λꡬ | ||
| Kafka UI | 18080 | Kafka κ΄λ¦¬ |
| Mongo Express | 18081 | MongoDB UI |
| Redis Commander | 18082 | Redis UI |
| νλ‘νμΌ | μλΉμ€ | μ©λ |
|---|---|---|
collector |
MySQL, Kafka, MinIO | μμ§ μμ€ν |
refine |
MySQL, Kafka, MinIO, MongoDB, Redis | μ μ μμ€ν |
index |
MySQL, Kafka, Elasticsearch, MongoDB | μμΈ μμ€ν |
serving |
MySQL, Elasticsearch, Redis | μλΉ μμ€ν |
dev |
Kafka UI, Mongo Express, Redis Commander | κ°λ° λꡬ |
FROM docker.elastic.co/elasticsearch/elasticsearch:8.11.0
RUN bin/elasticsearch-plugin install --batch analysis-noriνκ΅μ΄ ννμ λΆμμ μν΄ Nori νλ¬κ·ΈμΈμ΄ ν¬ν¨λ 컀μ€ν μ΄λ―Έμ§ μ¬μ©
- Java 21+
- Docker & Docker Compose
- Gradle 8.x
git clone <repository-url>
cd realtime-ju
# νκ²½ λ³μ μ€μ
cp .env.example .env
# .env νμΌμ μ΄μ΄ νμν κ° μμ # μ 체 μΈνλΌ + λͺ¨λν°λ§ μμ
./scripts/compose-up-all.sh
# λλ κ°λ³ μμ€ν
λ³ μ€ν
./scripts/compose-up-collector.sh # μμ§ μμ€ν
μ©
./scripts/compose-up-refine.sh # μ μ μμ€ν
μ©
./scripts/compose-up-index.sh # μμΈ μμ€ν
μ©
./scripts/compose-up-serving.sh # μλΉ μμ€ν
μ©
# μ 체 μ€μ§ λ° λ³Όλ₯¨ μ 리
./scripts/compose-down.sh# μ 체 νλ‘μ νΈ λΉλ
./gradlew clean build
# κ³΅ν΅ λΌμ΄λΈλ¬λ¦¬ λ°ν
./gradlew :common-lib:publishToMavenLocal# κ° μμ€ν
κ°λ³ μ€ν
./gradlew :collector-system:bootRun
./gradlew :refine-system:bootRun
./gradlew :index-system:bootRun
./gradlew :serving-system:bootRun# μ 체 ν
μ€νΈ
./gradlew test
# νΉμ λͺ¨λ ν
μ€νΈ
./gradlew :collector-system:test
./gradlew :refine-system:test
./gradlew :index-system:test
./gradlew :serving-system:test