RAG(Retrieval-Augmented Generation) νμ΄νλΌμΈμ μν λ²‘ν° DB κ΄λ¦¬ μλΉμ€.
Qdrant κΈ°λ° νμ΄λΈλ¦¬λ κ²μ(Dense + Sparse + RRF Fusion)κ³Ό μΈλΆ 리λ컀 μ°λ,
MongoDB λ¨λ½ μλ¬Έ μ μ₯, RabbitMQ λΉλκΈ° μλ² λ© νμ΄νλΌμΈμ μ 곡νλ€.
- μν€ν μ²
- μμ‘΄ μλΉμ€
- μλ² λ© λͺ¨λΈ
- νκ²½ μ€μ
- μ€ν λ°©λ²
- API μλν¬μΈνΈ
- λ°μ΄ν° λͺ¨λΈ
- κ²μ νμ΄νλΌμΈ
- μλ² λ© νμ΄νλΌμΈ (MQ)
- νλ‘μ νΈ κ΅¬μ‘°
ν΄λΌμ΄μΈνΈ
β
βΌ
FastAPI (kdb_manager.py)
βββ routes/collection.py 컬λ μ
CRUD, λ¬Έμ μ‘°ν
βββ routes/document.py μ
μνΈ, μμ , MongoDB μ μ₯
βββ routes/search.py νμ΄λΈλ¦¬λ κ²μ, 리λνΉ, λ¨λ½ 볡μ
βββ routes/feedback.py κ²μ νΌλλ°±
βββ routes/query_cache.py 쿼리 μΊμ±
β
βββ Qdrant λ²‘ν° DB (Dense + Sparse λ²‘ν° μ μ₯/κ²μ)
βββ MongoDB λ¨λ½ μλ¬Έ μ μ₯ (rag-data.documents, chunks.*)
βββ RabbitMQ λΉλκΈ° μλ² λ© μμ
ν
βββ Redis μλ² λ© μμ
λ°μ΄ν° μμ μ μ₯
βββ Reranker API μΈλΆ 리λνΉ μλΉμ€
| μλΉμ€ | μ©λ | κΈ°λ³Έ ν¬νΈ |
|---|---|---|
| Qdrant | λ²‘ν° μ μ₯ λ° νμ΄λΈλ¦¬λ κ²μ | 6333 |
| MongoDB | λ¨λ½/μ²ν¬ μλ¬Έ μ μ₯ | 27017 |
| RabbitMQ | λΉλκΈ° μλ² λ© μμ ν | 5672 |
| Redis | μλ² λ© μμ² λ°μ΄ν° μμ μ μ₯ | 6379 |
| Reranker API | κ²μ κ²°κ³Ό μ¬μμν | - |
| μ’ λ₯ | λͺ¨λΈλͺ | μν |
|---|---|---|
| Dense | Qwen/Qwen3-Embedding-4B |
μλ―Έ κΈ°λ° λ²‘ν° κ²μ |
| Sparse | Qdrant/bm42-all-minilm-l6-v2-attentions |
BM25 κ³μ΄ ν€μλ κ²μ |
| Reranker | Qwen/Qwen3-Reranker-4B |
κ²μ κ²°κ³Ό μ¬μμν (μΈλΆ μλΉμ€ μ¬μ©) |
λͺ¨λΈ νμΌμ ./models λλ ν 리μ μ μ₯νλ€. μμΌλ©΄ μλΉμ€ μμ μ μλ λ€μ΄λ‘λ.
루νΈμ .env νμΌλ‘ μ€μ νλ€. env.exampleμ 볡μ¬ν΄ μ¬μ©.
| ν€ | μ€λͺ | μμ |
|---|---|---|
APP__PORT |
μλΉμ€ ν¬νΈ | 28101 |
APP__SERVICE_NAME |
μλΉμ€ μ΄λ¦ (Eureka λ±λ‘μ©) | kdb-manager |
APP__EUREKA_SERVER |
Eureka μλ² μ£Όμ | http://host:8761/eureka |
| ν€ | μ€λͺ | κΈ°λ³Έκ° |
|---|---|---|
LOG__LEVEL |
λ‘κ·Έ λ 벨 (DEBUG | INFO | WARNING | ERROR) |
INFO |
LOG__FORMAT |
μΆλ ₯ νμ (json | plain) |
plain |
LOG__OUTPUT_TARGET |
μΆλ ₯ λμ (console | file | both) |
both |
LOG__DIR |
λ‘κ·Έ νμΌ λλ ν 리 | ./logs |
LOG__FILENAME |
λ‘κ·Έ νμΌ μ΄λ¦ | app.log |
LOG__FILE_MAX_BYTES |
νμΌ μ΅λ ν¬κΈ° (bytes) | 10485760 (10MB) |
LOG__FILE_BACKUP_COUNT |
λ°±μ νμΌ μ | 5 |
| ν€ | μ€λͺ | κΈ°λ³Έκ° |
|---|---|---|
REDIS__HOST |
νΈμ€νΈ | 127.0.0.1 |
REDIS__PORT |
ν¬νΈ | 6379 |
REDIS__DB |
DB λ²νΈ | 0 |
REDIS__USERNAME |
μΈμ¦ μ¬μ©μ (ACL) | - |
REDIS__PASSWORD |
μΈμ¦ λΉλ°λ²νΈ | - |
REDIS__SSL |
SSL μ¬μ© μ¬λΆ | false |
| ν€ | μ€λͺ | κΈ°λ³Έκ° |
|---|---|---|
DB__HOST |
νΈμ€νΈ | 127.0.0.1 |
DB__PORT |
ν¬νΈ | 27017 |
DB__USERNAME |
μ¬μ©μ μ΄λ¦ | - |
DB__PASSWORD |
λΉλ°λ²νΈ | - |
DB__AUTH_SOURCE |
μΈμ¦ DB | - |
| ν€ | μ€λͺ | κΈ°λ³Έκ° |
|---|---|---|
MQ__HOST |
νΈμ€νΈ | localhost |
MQ__PORT |
ν¬νΈ | 5672 |
MQ__USERNAME |
μ¬μ©μ μ΄λ¦ | guest |
MQ__PASSWORD |
λΉλ°λ²νΈ | guest |
MQ__VIRTUAL_HOST |
κ°μ νΈμ€νΈ | / |
MQ__HEARTBEAT |
ννΈλΉνΈ κ°κ²© (μ΄) | 600 |
| ν€ | μ€λͺ |
|---|---|
MODEL__DENSE_NAME |
Dense λͺ¨λΈλͺ (HuggingFace ID λλ λ‘컬 κ²½λ‘) |
MODEL__DENSE_PATH |
Dense λͺ¨λΈ λ‘컬 μΊμ λλ ν 리 |
MODEL__SPARSE_NAME |
Sparse λͺ¨λΈλͺ |
MODEL__SPARSE_PATH |
Sparse λͺ¨λΈ λ‘컬 μΊμ λλ ν 리 |
MODEL__MODEL_DEVICE |
μ€ν μ₯μΉ (cpu | cuda | mps) |
| ν€ | μ€λͺ |
|---|---|
QDRANT__URL |
Qdrant HTTP μ£Όμ |
QDRANT__API_KEY |
API ν€ (보μ μ€μ μ) |
| ν€ | μ€λͺ |
|---|---|
MS__RERANKER |
μΈλΆ 리λ컀 μλΉμ€ Base URL |
# μμ‘΄μ± μ€μΉ (uv μ¬μ©)
uv sync
# μλ² μμ
uvicorn kdb_manager:app --port 28101 --host 0.0.0.0
# ν¬νΈ μ€λ²λΌμ΄λ
python kdb_manager.py --port 28200# μ΄λ―Έμ§ λΉλ
docker build -t whitebearhands/kdb-manager:2.1.1 .
# Docker Compose μ€ν (GPU ν¬ν¨)
docker compose up -d
docker-compose.ymlμnetwork_mode: host+ NVIDIA GPU ν¨μ€μ€λ£¨λ‘ ꡬμ±λμ΄ μλ€.
.env,./models,./logsλλ ν 리λ₯Ό 컨ν μ΄λμ λ§μ΄νΈνλ€.
Swagger UI: http://host:port/docs
λͺ¨λ Qdrant 컬λ μ λͺ©λ‘μ λ°ννλ€.
μλ΅
[
{ "name": "my-collection", "status": "green" }
]컬λ μ μ μμ±νλ€. μ΄λ―Έ μ‘΄μ¬νλ©΄ 무μνλ€.
μμ² λ°λ
{ "collection_name": "my-collection" }컬λ μ μ€μ
- Dense 벑ν°: COSINE 거리, HNSW (m=64, ef_construct=1000)
- Sparse 벑ν°: BM42 κΈ°λ° (on_disk=false)
- μΈκ·Έλ¨ΌνΈ 10κ°, μ΅μ ν μ€λ λ 8κ°
μλ΅
{ "result": "Create collection successfully." }컬λ μ κ³Ό λ΄λΆ λͺ¨λ λ°μ΄ν°λ₯Ό μꡬ μμ νλ€.
collection_nameμ HTML μΈμ½λ©λ λ¬Έμμ΄λ‘ μ λ¬νλ€.
μλ΅
{ "result": "Delete collection successfully." }컬λ μ μ ν¬μΈνΈ(μ²ν¬)λ₯Ό νμ΄μ§μΌλ‘ μ‘°ννλ€.
쿼리 νλΌλ―Έν°
| νλΌλ―Έν° | νμ | κΈ°λ³Έκ° | μ€λͺ |
|---|---|---|---|
page |
int | 1 | νμ΄μ§ λ²νΈ |
page_size |
int | 10 | νμ΄μ§ ν¬κΈ° (μ΅λ 1000) |
μλ΅
{
"page": [
{
"context": "μ²ν¬ ν
μ€νΈ",
"ids": "doc-id",
"metadatas": { "file_name": "νμΌ.pdf", "doc_id": "..." }
}
],
"page_info": {
"total_elements": 1500,
"total_pages": 150,
"page": 1,
"first": true,
"last": false,
"empty": false
}
}MongoDBμμ νΉμ λ¨λ½μ μλ¬Έμ λ¨κ±΄ μ‘°ννλ€.
μλ΅
{
"collection_id": "my-collection",
"paragraph_id": "para-001",
"context": "λ¨λ½ μλ¬Έ ν
μ€νΈ...",
"metadatas": { "doc_id": "...", "file_name": "..." }
}λ¬Έμ μ²ν¬ λͺ©λ‘μ μλ² λ© ν Qdrantμ upsertνλ€.
컬λ μ μ΄ μμΌλ©΄ μλ μμ±νλ€.
μμ² λ°λ
{
"collection_name": "my-collection",
"documents": [
{
"context": "μ²ν¬ λ³Έλ¬Έ ν
μ€νΈ",
"ids": "μλ³Έ λ¬Έμ ID",
"page_number": 3,
"size": 512,
"metadatas": {
"file_name": "λ¬Έμ.pdf",
"doc_id": "doc-001",
"collection_id": "my-collection",
"paragraph_id": "para-001"
}
}
]
}μ²λ¦¬ κ³Όμ
- Dense + Sparse μλ² λ© λ³λ ¬ μμ± (asyncio.gather)
- μ μν λ°°μΉ upsert (μ΄κΈ° 100건, OOM μ μ λ°μ© κ°μ)
- 10% μν λ¬΄κ²°μ± κ²μ¦
- μ€ν¨ λ¬Έμ μλ μ¬μλ (50건 λ―Έλ§ μ)
μλ΅
{
"result": "95/100 items added successfully",
"details": {
"total_requested": 100,
"successful": 95,
"failed": 5,
"integrity_verified": true,
"integrity_rate": 0.97
}
}λ¨λ½ λͺ©λ‘μ MongoDB rag-data.documentsμ μ§μ μ μ₯νλ€.
μμ² λ°λ
{
"pages": [
{
"collection_id": "my-collection",
"paragraph_id": "para-001",
"context": "λ¨λ½ μλ¬Έ...",
"metadatas": { "doc_id": "doc-001", "file_name": "νμΌ.pdf" }
}
]
}νμΌ μ΄λ¦μΌλ‘ ν΄λΉ νμΌμ λͺ¨λ μ²ν¬λ₯Ό Qdrantμμ μμ νλ€.
μμ² λ°λ
{
"file_name": "μμ ν νμΌ.pdf",
"collection_name": "my-collection"
}λ¬Έμ ID(ids νλ)λ‘ ν¬μΈνΈλ₯Ό Qdrantμμ μμ νλ€.
collection_nameμ HTML μΈμ½λ©λ λ¬Έμμ΄λ‘ μ λ¬νλ€.
νμ΄λΈλ¦¬λ κ²μ β 리λνΉ β (μ ν) λ¨λ½ 볡μ νμ΄νλΌμΈ.
μμ² λ°λ
| νλ | νμ | νμ | μ€λͺ |
|---|---|---|---|
collection_name |
string | β | κ²μν 컬λ μ |
query |
string | β | κ²μ 쿼리 |
top_k |
int | - | κ²μ ν보 μ (κΈ°λ³Έ 100) |
use_paragraph |
bool | - | trueλ©΄ MongoDB λ¨λ½ μλ¬Έ λ°ν (κΈ°λ³Έ false) |
metadata_filter_key |
string | - | λ©νλ°μ΄ν° νν° ν€ (μ: "doc_id") |
match_values |
string[] | - | νν° κ° λͺ©λ‘ |
room_id |
string | - | μ±ν λ°© λ¨μ κ²μ λ²μ μ ν |
μμ² μμ
{
"collection_name": "my-collection",
"query": "RAG νμ΄νλΌμΈ κ΅¬μ± λ°©λ²",
"top_k": 50,
"use_paragraph": true,
"metadata_filter_key": "doc_id",
"match_values": ["doc-001", "doc-002"]
}μλ΅ (use_paragraph=false)
[
{
"context": "μ²ν¬ ν
μ€νΈ",
"ids": "doc-id",
"metadatas": { "file_name": "...", "doc_id": "..." },
"reranked_score": 0.92
}
]μλ΅ (use_paragraph=true)
[
{
"collection_id": "my-collection",
"paragraph_id": "para-001",
"context": "λ¨λ½ μλ¬Έ ν
μ€νΈ (μ²ν¬λ³΄λ€ λ κΈ΄ λ§₯λ½)",
"metadatas": { "doc_id": "doc-001", "file_name": "νμΌ.pdf" },
"rerank_score": 0.92
}
]κ²μ + 리λνΉ ν paragraph_idμ μ μλ§ λ°ννλ€.
λ¨λ½ λ΄μ© μμ΄ ID/μ μλ§ νμν κ²½μ° (νλ‘ νΈ lazy-load λ±) μ¬μ©νλ€.
μμ² λ°λ: search_rerankμ λμΌ
μλ΅
[
{ "paragraph_id": "para-001", "score": 0.92 },
{ "paragraph_id": "para-003", "score": 0.87 }
]리λνΉ μλ μμ νμ΄λΈλ¦¬λ κ²μ. Qdrant RRF ν¨μ κ²°κ³Όλ₯Ό κ·Έλλ‘ λ°ννλ€.
μμ² λ°λ: search_rerankμ λμΌ (use_paragraph, room_id λ―Έμ μ©)
μλ΅: Qdrant ν¬μΈνΈ λͺ©λ‘ (score_threshold=0.11 μ μ©)
class Document:
context: str # μ²ν¬ λ³Έλ¬Έ ν
μ€νΈ
ids: str # μλ³Έ λ¬Έμ ID
page_number: int = -1 # μλ³Έ λ¬Έμ νμ΄μ§ λ²νΈ (-1: μ μ μμ)
size: int # μ²ν¬ κΈμ μ
metadatas: Dict # λΆκ° μ 보 (μλ μ°Έμ‘°)metadatas κΆμ₯ νλ
| νλ | μ€λͺ |
|---|---|
file_name |
μλ³Έ νμΌλͺ |
doc_id |
λ¬Έμ κ³ μ ID |
collection_id |
컬λ μ ID |
paragraph_id |
λ¨λ½ ID (MongoDB μ‘°ν ν€) |
paragraph_type |
λ¨λ½ μ ν ("faq" μ΄λ©΄ MongoDB μ‘°ν μλ΅) |
room_id |
μ±ν λ°© ID (room_id νν° μ¬μ© μ) |
bbox |
μλ³Έ λ¬Έμ λ΄ μμΉ μ 보 |
Qdrantμ μ μ₯λλ ν¬μΈνΈμ payload ꡬ쑰:
{
"context": "μ²ν¬ ν
μ€νΈ",
"ids": "μλ³Έ λ¬Έμ ID",
"page_number": 3,
"size": 512,
"metadatas": {
"file_name": "λ¬Έμ.pdf",
"doc_id": "doc-001",
"collection_id": "my-collection",
"paragraph_id": "para-001"
}
}rag-data.documents β λ¨λ½ μλ¬Έ μ μ₯
{
"collection_id": "my-collection",
"paragraph_id": "para-001",
"context": "λ¨λ½ μ 체 μλ¬Έ",
"metadatas": {
"doc_id": "doc-001",
"file_name": "λ¬Έμ.pdf",
"bbox": [...]
}
}μΈλ±μ€: (collection_id, metadatas.doc_id, paragraph_id) λ³΅ν© μΈλ±μ€
chunks.<collection_name> β μ²ν¬ μλ¬Έ μ μ₯ (컬λ μ
λ³ λΆλ¦¬)
쿼리 μ
λ ₯
β
ββ Dense μλ² λ© (Qwen3-Embedding-8B)
ββ Sparse μλ² λ© (BM42)
β
βΌ
Qdrant Prefetch (κ°κ° top_k ν보 μμ§)
β
βΌ
RRF Fusion (λ κ²°κ³Ό ν΅ν© β 50건)
β
βΌ
μΈλΆ 리λ컀 (Qwen3-Reranker-4B) β top 5
β
βΌ
[use_paragraph=true]
MongoDB rag-data.documents μμ λ¨λ½ μλ¬Έ μ‘°ν
β
βΌ
μ΅μ’
κ²°κ³Ό λ°ν
RabbitMQ κΈ°λ° λΉλκΈ° μλ² λ© μ²λ¦¬ νλ¦:
μΈλΆ μλΉμ€
β rag.embedding.request νμ λ©μμ§ λ°ν
βΌ
EmbeddingConsumer (kdb_manager.py)
β
ββ 1. Redisμμ μ²ν¬/λ¨λ½ λ°μ΄ν° λ‘λ
β redis_chunk_key β {"data": [...chunks]}
β redis_para_key β {"data": [...paragraphs]}
β
ββ 2. POST /api/v1/documents (Qdrant upsert)
β
ββ 3. chunk_to_mongo() β MongoDB chunks.<collection_id>
β
ββ 4. paragraph_to_mongo() β MongoDB rag-data.documents
β
ββ 5. MQ μ΄λ²€νΈ λ°ν
μ±κ³΅: rag.embedding.completed
μ€ν¨: rag.embedding.failed
μμ λ©μμ§ νμ
{
"job_id": "unique-job-id",
"file_id": "file-storage-id",
"doc_id": "document-id",
"collection_id": "my-collection",
"redis_chunk_key": "rag.document.{doc_id}.chunk",
"redis_para_key": "rag.document.{doc_id}.para",
"redis_img_key": "rag.document.{doc_id}.img",
"redis_tbl_key": "rag.document.{doc_id}.tbl"
}kdb-manager/
βββ kdb_manager.py μ§μ
μ (μ±, λΌμ΄νμ¬μ΄ν΄, MQ Consumer)
βββ routes/
β βββ collection.py 컬λ μ
CRUD, λ¬Έμ μ‘°ν
β βββ document.py μ
μνΈ, μμ , MongoDB μ μ₯
β βββ search.py νμ΄λΈλ¦¬λ κ²μ, 리λνΉ
βββ modules/
β βββ dependencies.py λͺ¨λΈ/ν΄λΌμ΄μΈνΈ μ±κΈν€
β βββ redis.py RedisManager
β βββ singleton_meta.py μ±κΈν€ λ©νν΄λμ€
βββ config/
β βββ __init__.py pydantic-settings μ€μ λͺ¨λΈ
βββ wrapper/
β βββ logger_wrapper.py λ‘κ±° μ€μ
β βββ redis_wrapper.py Redis ν΄λΌμ΄μΈνΈ
β βββ rabbitmq_wrapper.py RabbitMQ ν΄λΌμ΄μΈνΈ
β βββ rabbitmq_wrapper_for_rag.py RAG μ μ© Producer/Consumer
βββ models/ μλ² λ© λͺ¨λΈ νμΌ (git λ―Έν¬ν¨)
βββ logs/ λ‘κ·Έ νμΌ (git λ―Έν¬ν¨)
βββ pyproject.toml
βββ docker-compose.yml
βββ Dockerfile
βββ .env νκ²½ λ³μ (git λ―Έν¬ν¨)