7.7 KiB
ADR-022: Retrieval-Augmented Generation (RAG) System
Status: Accepted Date: 2026-04-19 Decision Makers: Development Team, System Architect Related Documents:
- RAG Implementation Guide v1.1.2
- Implementation Plan
- Tasks
- ADR-018: AI Boundary
- ADR-020: AI Intelligence Integration
🎯 Gap Analysis & Purpose
ปิด Gap จากเอกสาร:
- ADR-020 AI Integration — ระบุว่า DMS ต้องรองรับ Document Q&A แต่ไม่มี retrieval mechanism
- เหตุผล: LLM อย่างเดียวไม่รู้ข้อมูลเอกสารของโครงการ — ต้องมี context injection จากเอกสารจริง
- 01-03-modules — ผู้ใช้ต้องการค้นหาข้อมูลจากเอกสาร Correspondence/RFA/Drawing แบบ natural language
- เหตุผล: Full-text search ให้แค่ keyword match — ไม่เข้าใจ semantic และภาษาไทย
แก้ไขความขัดแย้ง:
- ADR-018 AI Isolation vs RAG Cloud API: Typhoon API ใช้ cloud LLM ซึ่งขัดกับ on-premises policy
- การตัดสินใจ: CONFIDENTIAL documents → local Ollama เท่านั้น; PUBLIC/INTERNAL → Typhoon primary + Ollama failover
Context and Problem Statement
LCBP3-DMS เก็บเอกสารโครงการก่อสร้างขนาดใหญ่ (Correspondence, RFA, Drawing, Contract) ทั้งหมดในภาษาไทยและอังกฤษ ผู้ใช้ต้องการค้นหาและถามคำถามจากเอกสารเหล่านี้แบบ conversational Q&A โดยได้คำตอบพร้อม citation ที่อ้างอิงได้
Key Problems
- Semantic Gap: Full-text search หา keyword ได้แต่ไม่เข้าใจความหมาย เช่น "ปัญหาเรื่องการส่งมอบ" ≠ "delay in delivery"
- Thai Language: MariaDB FULLTEXT ไม่รองรับ tokenization ภาษาไทยอย่างถูกต้อง
- Hallucination Risk: LLM ที่ไม่มี context จะตอบจาก training data — ไม่ใช่เอกสารโครงการจริง
- Security Isolation: เอกสาร CONFIDENTIAL ห้ามส่งไป cloud AI ใดๆ (ADR-018)
- Multi-tenancy: เอกสารของแต่ละโครงการต้องแยก query scope อย่างเด็ดขาด
Decision Drivers
- Accuracy: คำตอบต้องมา from เอกสารจริง พร้อม citation ที่ตรวจสอบได้
- Security (ADR-018): CONFIDENTIAL → local Ollama only; ห้าม cloud API
- Thai Language: ต้องรองรับ PyThaiNLP tokenization ก่อน embed
- Multi-tenancy: Qdrant
project_public_idpayload filter — non-negotiable - Performance SLO: Typhoon primary p95 < 3s; Ollama fallback p95 < 10s
Considered Options
Option 1: Full-text Search Only (MariaDB FULLTEXT)
แนวทาง: ใช้ MariaDB FULLTEXT ที่มีอยู่แล้ว + prompt LLM ด้วย raw search results
ข้อดี: ไม่ต้อง infra เพิ่ม, deploy ง่าย
ข้อเสีย: Thai tokenization แย่, ไม่มี semantic understanding, recall ต่ำ
Option 2: Vector Search Only (Qdrant + Ollama)
แนวทาง: Embed ทุก chunk → เก็บใน Qdrant → query ด้วย vector similarity เท่านั้น
ข้อดี: Semantic search ดี, รองรับภาษาไทย
ข้อเสีย: Keyword exact match แย่กว่า FULLTEXT, miss เลขที่เอกสารที่พิมพ์ตรงๆ
Option 3: Hybrid RAG — Vector + Keyword + Re-rank ✅ SELECTED
แนวทาง: Hybrid search (0.7 vector + 0.3 keyword) → score-based re-rank → context build → LLM generate
ข้อดี:
- ดีที่สุดทั้ง semantic และ exact match
- PyThaiNLP preprocessing ก่อน embed → Thai language accuracy สูง
- Qdrant tiered multitenancy → tenant isolation สมบูรณ์
- Typhoon primary + Ollama failover → availability สูง
ข้อเสีย: Complex infrastructure (Qdrant + Redis + BullMQ + PyThaiNLP microservice)
Decision Outcome
เลือก Option 3: Hybrid RAG
Core Architecture
| Component | Technology | Role |
|---|---|---|
| Vector Store | Qdrant v1.16+ | Tiered multitenancy (project_public_id is_tenant=true) |
| Embedding | nomic-embed-text (Ollama) | 768-dim Thai+English vectors |
| Thai NLP | PyThaiNLP microservice | Tokenize + normalize ก่อน embed |
| Queue | BullMQ (rag:ocr, rag:thai-preprocess, rag:embedding) | Async ingestion pipeline |
| LLM Primary | Typhoon API | PUBLIC + INTERNAL (p95 < 3s) |
| LLM Fallback | Ollama local | CONFIDENTIAL + Typhoon down (p95 < 10s) |
| Cache | Redis | Query cache key: SHA256(question+projectPublicId+classificationCeiling) |
Key Decisions (Clarified 2026-04-19)
| # | Decision | Rationale |
|---|---|---|
| Q1 | Tiered multitenancy (single collection + is_tenant=true) | Qdrant v1.16+ native; no collection-per-project overhead |
| Q2 | Auto-failover Typhoon→Ollama | Availability > consistency for non-CONFIDENTIAL |
| Q3 | PyThaiNLP preprocessing | Thai tokenization critical for embed quality |
| Q4 | rag_status in attachments table |
Single source of truth per file (ADR-009) |
| Q5 | Split SLO: Typhoon < 3s / Ollama < 10s | Realistic targets per LLM path |
Edge Cases Enforced (Red Team 2026-04-19)
| EC | Rule |
|---|---|
| EC-RAG-001 | BullMQ jobId = attachmentId (native dedup — ป้องกัน concurrent ingestion) |
| EC-RAG-002 | reIngest() cleanup: DELETE chunks → DELETE Qdrant → PENDING (ordered) |
| EC-RAG-003 | QdrantService.OnModuleInit: auto-create collection; collectionReady flag + 503 |
| EC-RAG-004 | maxClassification ห้ามรับจาก client — server-derived จาก user role เท่านั้น |
| EC-RAG-005 | Cache key = SHA256(question+projectPublicId+classificationCeiling); CONFIDENTIAL bypass cache |
Consequences
Positive
- ✅ Q&A ภาษาไทยจากเอกสารโครงการพร้อม citation ที่ตรวจสอบได้
- ✅ ADR-018 compliant — CONFIDENTIAL ไม่ออกนอก on-premises
- ✅ Multi-tenant isolation ระดับ Qdrant payload filter
- ✅ Auto-failover → high availability
Negative / Trade-offs
- ⚠️ Infrastructure complexity เพิ่มขึ้น (Qdrant + PyThaiNLP microservice)
- ⚠️ Initial indexing ใช้เวลา — existing documents ต้อง batch re-index
- ⚠️ Ollama fallback p95 < 10s อาจ UX ไม่ดีสำหรับ CONFIDENTIAL users
Risks
- Qdrant volume loss → vectors ต้อง re-index ทั้งหมด (mitigated: Qdrant snapshot backup §19.7)
- PyThaiNLP microservice down → ingestion queue stuck (mitigated: 3-retry DLQ + FAILED status)