Spaces:
Running
Running
Commit ·
a43002d
1
Parent(s): 603ead8
docs: add Turbovec-specific architecture and tech stack documents
Browse files
SEARCH_ARCHITECTURE_TURBOVEC.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 📖 Search Architecture Specification (Turbovec Edition)
|
| 2 |
+
## Next-Gen Hybrid Search for Tipitaka Web Application
|
| 3 |
+
|
| 4 |
+
> **Version:** 3.5.0 (Turbovec Edition)
|
| 5 |
+
> **Updated:** 2026-05-26
|
| 6 |
+
> **Status:** Revised — ใช้ Turbovec เป็น Vector Search Engine หลัก และเพิ่ม SQLite Content Enrichment ใน RAG pipeline
|
| 7 |
+
|
| 8 |
+
เอกสารนี้ระบุการออกแบบสถาปัตยกรรมการค้นหา V3 ที่ถูกปรับปรุงเพื่อใช้งาน **Turbovec** (In-process Vector Search) แทน Qdrant โดยรวมเข้ากับ Query Understanding ด้วย Local LLM (Qwen2.5-0.5B-Instruct / Qwen3.5-0.8B) และ FTS5 + ONNX Reranker เพื่อความรวดเร็วและน้ำหนักที่เบาที่สุดบน Hugging Face Space
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
## 0. Model Name Reference & Comparison
|
| 13 |
+
|
| 14 |
+
| Model Role | Platform | ชื่อ model ที่ถูกต้อง | พารามิเตอร์ | จุดเด่น / การใช้งาน |
|
| 15 |
+
|------------|----------|----------------------|------------|-------------------|
|
| 16 |
+
| **Default** (Recommended) | HuggingFace | `Qwen/Qwen2.5-0.5B-Instruct` | 0.5B | โหลดเร็วมาก กินแรมต่ำสุด (~1GB ใน FP32) เหมาะกับ CPU Basic |
|
| 17 |
+
| | Ollama | `qwen2.5:0.5b` | 0.5B | รันในเครื่องได้รวดเร็วมาก |
|
| 18 |
+
| **Alternative** (Comparison) | HuggingFace | `Qwen/Qwen3.5-0.8B` | 0.8B | ตอบโครงสร้างลึกได้ดีกว่าเล็กน้อย แต่ใช้แรม ~1.6GB ใน FP32 และช้ากว่า |
|
| 19 |
+
| | Ollama | `qwen3.5:0.8b` | 0.8B | รันผ่าน Ollama daemon |
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## 1. System Dataflow
|
| 24 |
+
|
| 25 |
+
```
|
| 26 |
+
[ User Natural Query (ภาษาพูด) ]
|
| 27 |
+
│
|
| 28 |
+
▼
|
| 29 |
+
[ 1. Query Transformation ] ◄── SQLite Query Cache (exact match)
|
| 30 |
+
Qwen2.5-0.5B-Instruct (Default) / Qwen3.5-0.8B (Alternative)
|
| 31 |
+
Local: via Ollama HTTP API
|
| 32 |
+
HF Space: via Transformers (Preloaded on startup)
|
| 33 |
+
Output: {"fts_queries": [...], "vector_queries": [...]}
|
| 34 |
+
│
|
| 35 |
+
▼ (Fallback: original query)
|
| 36 |
+
│
|
| 37 |
+
├─────────────────────────────────────────┐
|
| 38 |
+
▼ asyncio.gather (parallel) ▼
|
| 39 |
+
[ 2A. Lexical Search (FTS5) ] [ 2B. Semantic Search (Turbovec) ]
|
| 40 |
+
SQLite pages_fts (Prefix matching) jina-embeddings-v5 (1024d)
|
| 41 |
+
e.g. "ธัมมทินนา"* Turbovec IdMapIndex batch search
|
| 42 |
+
asyncio.gather(*fts_tasks) tv_index.search(stacked_vectors)
|
| 43 |
+
│ │
|
| 44 |
+
└──────────────────┬──────────────────────┘
|
| 45 |
+
▼
|
| 46 |
+
[ 3. RRF Score Fusion ]
|
| 47 |
+
Deduplicate: (volume_id, page_number)
|
| 48 |
+
RRF(d) = Σ 1/(k=60 + rank)
|
| 49 |
+
Top 15 candidates
|
| 50 |
+
│
|
| 51 |
+
▼
|
| 52 |
+
[ 3.5 SQLite Content Enrichment ] ◄── ดึงเนื้อหาจาก pages_db
|
| 53 |
+
(เติมเนื้อหาฉบับเต็มลงใน payload ของ vector candidates ที่ไม่มีข้อความ)
|
| 54 |
+
│
|
| 55 |
+
▼
|
| 56 |
+
[ 4. ONNX Reranking ]
|
| 57 |
+
jina-reranker-v2-base-multilingual (max_length=256)
|
| 58 |
+
Re-score vs original user query
|
| 59 |
+
│
|
| 60 |
+
▼
|
| 61 |
+
[ 5. Post-Process & Highlight ]
|
| 62 |
+
Highlight keywords จาก fts_queries
|
| 63 |
+
│
|
| 64 |
+
┌──────────────────┴──────────────────┐
|
| 65 |
+
▼ ▼
|
| 66 |
+
[ UI Search Page ] [ AI Assistant (RAG) ]
|
| 67 |
+
(Paginated Reader Links) (DeepSeek / Gemma Generation)
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
## 2. Layer Specifications
|
| 73 |
+
|
| 74 |
+
### Layer 1: Query Transformation — Dual Runtime & Model Comparison
|
| 75 |
+
*(เหมือนรุ่น V3.4.0 — ใช้ Qwen ในการแปลงข้อความภาษาพูดให้กลายเป็น FTS และ Vector query)*
|
| 76 |
+
|
| 77 |
+
### Layer 2: Parallel Retrieval (FTS5 & Turbovec)
|
| 78 |
+
- FTS5 และ Vector Search ทำงานแบบขนานผ่าน `asyncio.gather`
|
| 79 |
+
- **FTS5**: ค้นหาคีย์เวิร์ดในตารางเสมือน `pages_fts`
|
| 80 |
+
- **Turbovec (Vector)**: ทำการแปลงคิวรีเป็นเวกเตอร์ 1024 มิติ จากนั้นส่งแบบกลุ่ม (Batch) โดย Stack เวกเตอร์เป็น NumPy Array ไปยัง `tv_index.search(vecs, k=limit)` การประมวลผลเกิดขึ้นในหน่วยความจำของ Process Python ทันที ไม่มี overhead จาก Network หรือ Client-Server serialization
|
| 81 |
+
|
| 82 |
+
### Layer 3: RRF Score Fusion & SQLite Enrichment
|
| 83 |
+
- หลังจากรวมคะแนนด้วย Reciprocal Rank Fusion (RRF) และกรองผู้เข้ารอบสูงสุด (เช่น Top 15)
|
| 84 |
+
- **SQLite Content Enrichment (ขั้นตอนสำคัญ)**: เนื่องจากดัชนี Turbovec (`.tvim`) เก็บเพียง ID ตัวเลขและค่าคะแนนเพื่อประหยัดแรมและลดขนาดไฟล์ เราจึงจำเป็นต้องทำการ Lazy Load ข้อความตัวเต็ม (`content_text`) ของผลลัพธ์เวกเตอร์ โดยเปิด Transaction ไปสืบค้นกับ SQLite In-memory อย่างรวดเร็วก่อนนำข้อมูลไปประมวลผลต่อ
|
| 85 |
+
|
| 86 |
+
### Layer 4: ONNX Reranking
|
| 87 |
+
- นำคู่คิวรีต้นฉบับของผู้ใช้และข้อความจากขั้นตอน Enrichment มา Rerank คะแนนใหม่ด้วย `jina-reranker-v2-base-multilingual` รันบน ONNX CPU ด้วยความยาว Tokenizer สูงสุด `max_length=256`
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## 3. RAM Budget Comparison
|
| 92 |
+
|
| 93 |
+
ด้วยการสลับจาก Qdrant มาใช้ Turbovec ทำให้ประหยัด RAM ได้มหาศาลเนื่องจากไม่ต้องมี Qdrant process รันค้าง
|
| 94 |
+
|
| 95 |
+
### Local (RTX Laptop / PC Dev)
|
| 96 |
+
|
| 97 |
+
| Component | RAM (Qdrant Backend) | RAM (Turbovec Backend) |
|
| 98 |
+
|-----------|--------------------|-----------------------|
|
| 99 |
+
| SQLite in-memory | ~238 MB | ~238 MB |
|
| 100 |
+
| Vector Engine | **~1.2–1.5 GB** (Qdrant Embedded) | **~14 MB** (Turbovec Index) |
|
| 101 |
+
| Jina-v5 (ST in-process) | ~1.2 GB | ~1.2 GB |
|
| 102 |
+
| ONNX Reranker | ~500 MB | ~500 MB |
|
| 103 |
+
| Qwen LLM (Ollama) | ~0.5 GB | ~0.5 GB |
|
| 104 |
+
| FastAPI + Python overhead | ~300 MB | ~300 MB |
|
| 105 |
+
| **รวม** | **~3.9–4.2 GB** | **~2.7–3.0 GB** ✅ |
|
| 106 |
+
|
| 107 |
+
### HF Space (CPU Basic = 16GB)
|
| 108 |
+
|
| 109 |
+
| Component | RAM (Qdrant Backend) | RAM (Turbovec Backend) |
|
| 110 |
+
|-----------|--------------------|-----------------------|
|
| 111 |
+
| SQLite in-memory | ~238 MB | ~238 MB |
|
| 112 |
+
| Vector Engine | **~1.2–1.5 GB** (Qdrant Embedded) | **~14 MB** (Turbovec Index) |
|
| 113 |
+
| Jina-v5 (ST in-process) | ~1.2 GB | ~1.2 GB |
|
| 114 |
+
| ONNX Reranker | ~500 MB | ~500 MB |
|
| 115 |
+
| Qwen LLM (in-process, FP32) | ~1.0 GB | ~1.0 GB |
|
| 116 |
+
| FastAPI + Python + Ubuntu OS | ~800 MB | ~800 MB |
|
| 117 |
+
| **รวม** | **~5.0–5.2 GB** | **~3.5–3.7 GB** ✅ |
|
| 118 |
+
| **Headroom ที่เหลือ** | **~10.8 GB** | **~12.3 GB** ✅ |
|
| 119 |
+
|
| 120 |
+
---
|
| 121 |
+
|
| 122 |
+
## 4. Latency & Performance Profile
|
| 123 |
+
|
| 124 |
+
จากการรัน Benchmark เปรียบเทียบความเร็วระหว่างการสืบค้นเวกเตอร์ด้วย Random Queries (k=30, 100 queries) พบว่า Turbovec มีประสิทธิภาพเหนือกว่าอย่างก้าวกระโดด:
|
| 125 |
+
|
| 126 |
+
| Metric | Turbovec (4-bit SQ) | Qdrant Local Server |
|
| 127 |
+
|--------|---------------------|---------------------|
|
| 128 |
+
| **Average Latency (Mean)** | **1.425 ms** | **19.418 ms** (ช้ากว่า ~13.5 เท่า) |
|
| 129 |
+
| **Median (p50)** | **1.353 ms** | **18.724 ms** |
|
| 130 |
+
| **90th Percentile** | **1.621 ms** | **23.115 ms** |
|
| 131 |
+
| **Throughput (QPS)** | **701.7 queries/sec** | **51.5 queries/sec** (ต่ำกว่า ~13.6 เท่า) |
|
| 132 |
+
| **Disk Storage** | **13.2 MB** (`.tvim` file) | **~600+ MB** (snapshots/data) |
|
| 133 |
+
| **RAM Idle Overhead** | **~14 MB** | **~150–300 MB** (Docker Server) |
|
| 134 |
+
|
| 135 |
+
---
|
| 136 |
+
|
| 137 |
+
## 5. Implementation Blueprint
|
| 138 |
+
|
| 139 |
+
### 5.1 SearchService Implementation (Batch Search & SQLite Enrichment)
|
| 140 |
+
|
| 141 |
+
```python
|
| 142 |
+
# app/services/search_service.py
|
| 143 |
+
import asyncio
|
| 144 |
+
from typing import List
|
| 145 |
+
import numpy as np
|
| 146 |
+
import anyio
|
| 147 |
+
from app.database.sqlite_db import get_db
|
| 148 |
+
|
| 149 |
+
class SearchService:
|
| 150 |
+
def __init__(self, db, rag_service=None, query_transform_service=None):
|
| 151 |
+
self.db = db
|
| 152 |
+
self.rag_service = rag_service
|
| 153 |
+
self.qts = query_transform_service
|
| 154 |
+
|
| 155 |
+
async def search_hybrid(self, query: str, limit: int = 10, offset: int = 0) -> dict:
|
| 156 |
+
# 1. Transform query (มี Cache ป้องกันการยิง LLM ซ้ำ)
|
| 157 |
+
transformed = await self.qts.transform(query)
|
| 158 |
+
fts_queries = transformed["fts_queries"]
|
| 159 |
+
vector_queries = transformed["vector_queries"]
|
| 160 |
+
|
| 161 |
+
# 2. ค้นหา FTS5 และ Vector คู่ขนานกันจริงๆ
|
| 162 |
+
fts_tasks = [self._get_fts_results(fq) for fq in fts_queries]
|
| 163 |
+
vector_tasks = [self._get_vector_batch_results(vector_queries, limit=30)]
|
| 164 |
+
|
| 165 |
+
# รันงานทั้งหมดแบบขนาน
|
| 166 |
+
fts_results_list, [vector_results_list] = await asyncio.gather(
|
| 167 |
+
asyncio.gather(*fts_tasks),
|
| 168 |
+
vector_tasks
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
# 3. รวมคะแนนด้วย RRF (Reciprocal Rank Fusion)
|
| 172 |
+
rrf_scores, candidate_data = {}, {}
|
| 173 |
+
k = 60
|
| 174 |
+
|
| 175 |
+
for results in fts_results_list:
|
| 176 |
+
for rank, item in enumerate(results):
|
| 177 |
+
key = (item["volume_id"], item["page_number"])
|
| 178 |
+
rrf_scores[key] = rrf_scores.get(key, 0.0) + 1.0 / (k + rank + 1)
|
| 179 |
+
candidate_data.setdefault(key, item)
|
| 180 |
+
|
| 181 |
+
for results in vector_results_list:
|
| 182 |
+
for rank, item in enumerate(results):
|
| 183 |
+
key = (item["volume_id"], item["page_number"])
|
| 184 |
+
rrf_scores[key] = rrf_scores.get(key, 0.0) + 1.0 / (k + rank + 1)
|
| 185 |
+
candidate_data.setdefault(key, item)
|
| 186 |
+
|
| 187 |
+
# คัดกรองผู้เข้ารอบ 15 อันดับแรกเพื่อส่งให้ Reranker
|
| 188 |
+
top_keys = sorted(rrf_scores, key=rrf_scores.get, reverse=True)[:15]
|
| 189 |
+
top_candidates = [candidate_data[ky] for ky in top_keys]
|
| 190 |
+
|
| 191 |
+
# 3.5 SQLite Content Enrichment (เติมข้อความเต็มที่ไม่มีในเวกเตอร์ดัชนี)
|
| 192 |
+
missing_content_cands = [c for c in top_candidates if not c.get("content_text")]
|
| 193 |
+
if missing_content_cands:
|
| 194 |
+
try:
|
| 195 |
+
with self.db.get_connection() as conn:
|
| 196 |
+
cursor = conn.cursor()
|
| 197 |
+
for c in missing_content_cands:
|
| 198 |
+
cursor.execute(
|
| 199 |
+
"SELECT content_text FROM pages WHERE volume_id = ? AND page_number = ?",
|
| 200 |
+
(c["volume_id"], c["page_number"])
|
| 201 |
+
)
|
| 202 |
+
row = cursor.fetchone()
|
| 203 |
+
if row:
|
| 204 |
+
# ป้องกันปัญหารับค่า float/int จาก SQLite
|
| 205 |
+
c["content_text"] = row["content_text"]
|
| 206 |
+
except Exception as e:
|
| 207 |
+
import logging
|
| 208 |
+
logging.getLogger(__name__).warning(f"Enriching candidates content failed: {e}")
|
| 209 |
+
|
| 210 |
+
# 4. Reranking ด้วยโมเดล ONNX Reranker กับประโยคคำถามเดิม
|
| 211 |
+
if top_candidates and self.rag_service and self.rag_service.reranker and self.rag_service.reranker != "error":
|
| 212 |
+
final_results = await self._rerank_candidates(query, top_candidates)
|
| 213 |
+
else:
|
| 214 |
+
final_results = top_candidates
|
| 215 |
+
|
| 216 |
+
# 5. จัดรูปแบบและทำ Highlight คีย์เวิร์ด
|
| 217 |
+
return self._format_response(final_results, limit, offset, fts_queries)
|
| 218 |
+
|
| 219 |
+
async def _get_vector_batch_results(self, query_texts: List[str], limit: int = 30) -> List[List[dict]]:
|
| 220 |
+
"""ทำ Batch Search บน Turbovec โดยประมวลผลรวดเดียว"""
|
| 221 |
+
if not self.rag_service or not self.rag_service.tv_index or not query_texts:
|
| 222 |
+
return [[] for _ in query_texts]
|
| 223 |
+
|
| 224 |
+
tv = self.rag_service.tv_index
|
| 225 |
+
|
| 226 |
+
def _blocking():
|
| 227 |
+
# 1. แปลงคำค้นทั้งหมดเป็นเวกเตอร์
|
| 228 |
+
query_vectors = []
|
| 229 |
+
valid_indices = []
|
| 230 |
+
for idx, text in enumerate(query_texts):
|
| 231 |
+
vec = self.rag_service._get_embedding(text)
|
| 232 |
+
if vec:
|
| 233 |
+
query_vectors.append(vec)
|
| 234 |
+
valid_indices.append(idx)
|
| 235 |
+
|
| 236 |
+
if not query_vectors:
|
| 237 |
+
return [[] for _ in query_texts]
|
| 238 |
+
|
| 239 |
+
# 2. ซ้อน NumPy Array เพื่อทำค้นหาแบบกลุ่มใน Turbovec
|
| 240 |
+
vecs = np.array(query_vectors, dtype=np.float32)
|
| 241 |
+
all_scores, all_ids = tv.search(vecs, k=limit)
|
| 242 |
+
|
| 243 |
+
final_results = [[] for _ in query_texts]
|
| 244 |
+
for req_pos, req_idx in enumerate(valid_indices):
|
| 245 |
+
vec_results = []
|
| 246 |
+
for score, uid in zip(all_scores[req_pos], all_ids[req_pos]):
|
| 247 |
+
if float(score) < 0.2:
|
| 248 |
+
continue
|
| 249 |
+
# ถอดรหัส ID: UID = volume * 100,000 + page
|
| 250 |
+
# และแปลงเป็น int ป้องกันปัญหา numpy.uint64 ใน SQLite
|
| 251 |
+
vol = int(uid) // 100_000
|
| 252 |
+
page = int(uid) % 100_000
|
| 253 |
+
vec_results.append({
|
| 254 |
+
"volume_id": int(vol),
|
| 255 |
+
"page_number": int(page),
|
| 256 |
+
"content_text": "", # โหลดภายหลังในขั้นตอน Enrichment
|
| 257 |
+
"score": float(score)
|
| 258 |
+
})
|
| 259 |
+
final_results[req_idx] = vec_results
|
| 260 |
+
return final_results
|
| 261 |
+
|
| 262 |
+
return await anyio.to_thread.run_sync(_blocking)
|
| 263 |
+
```
|
TIPITAKA_WEB_ARCHITECTURE_TURBOVEC.md
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Tipitaka Web Application Architecture (Turbovec Edition)
|
| 2 |
+
## Vite + FastAPI + Turbovec — "Book Sanctuary + Floating AI Consultant"
|
| 3 |
+
|
| 4 |
+
> **Version:** 2.5.0 (Turbovec Edition)
|
| 5 |
+
> **Date:** 2026-05-26
|
| 6 |
+
> **Status:** Production (local dev & HF Spaces using Turbovec backend)
|
| 7 |
+
> **Based on:** Code at commit `603ead8`
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 1. Executive Summary
|
| 12 |
+
|
| 13 |
+
### 1.1 Vision
|
| 14 |
+
Web Application สำหรับอ่านพระไตรปิฎก มจร. ที่ให้ประสบการณ์การอ่านแบบจิตวิเวก โดยมี AI เป็นผู้ช่วยลอยตัวที่ไม่รบกวนสมาธิ และใช้ Vector Backend ประสิทธิภาพสูงที่ทำงานแบบ In-process (ไม่ต้องใช้ Server แยก) เพื่อความรวดเร็วสูงสุดและประหยัดทรัพยากรบน CPU Basic Free Tier
|
| 15 |
+
|
| 16 |
+
### 1.2 Key Changes
|
| 17 |
+
|
| 18 |
+
#### V2.5.0 (2026-05-26) - Turbovec Edition
|
| 19 |
+
|
| 20 |
+
| Layer | V2.4.0 (Qdrant) | V2.5.0 (Turbovec) |
|
| 21 |
+
|-------|-----------------|-------------------|
|
| 22 |
+
| **Vector Backend** | Qdrant (Embedded snapshot / Server) | **Turbovec IdMapIndex** — In-process vector index file (`tipitaka_chunks.tvim` 4-bit SQ) |
|
| 23 |
+
| **Search Speed** | ~19.42 ms (Qdrant Server queries) | **~1.43 ms** — ~13.5x faster local search latency |
|
| 24 |
+
| **RAM Footprint** | ~1.2–1.5 GB (Qdrant process) | **~14 MB** — run in-process within Python runtime |
|
| 25 |
+
| **Index Size** | ~600+ MB (Snapshots & storage) | **13.2 MB** — quantized using 4-bit scalar quantization |
|
| 26 |
+
| **Database Enrichment**| Vector payload returned by Qdrant | **Dynamic SQLite Lookup** — content fetched from `pages` using decoded volume & page IDs |
|
| 27 |
+
| **Mobile Stability** | Framer Motion layout freezes on swipe | **`mode="popLayout"`** & standalone loading spinner outside of `AnimatePresence` |
|
| 28 |
+
| **ID casting** | Int / string uuid | **Native Python `int()` casting** — prevents `numpy.uint64` being treated as SQL binary BLOB |
|
| 29 |
+
| **Commit** | `f11cd44` | `603ead8` |
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
### 1.3 Tech Stack
|
| 34 |
+
|
| 35 |
+
```
|
| 36 |
+
┌──────────────────────────────────────────────────────────────────┐
|
| 37 |
+
│ CLIENT (Browser) │
|
| 38 |
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
| 39 |
+
│ │ Vite 6 + React 18 + TypeScript 5 │ │
|
| 40 |
+
│ │ Tailwind CSS v4 + @tailwindcss/typography │ │
|
| 41 |
+
│ │ Framer Motion (animations) + Zustand (state) │ │
|
| 42 |
+
│ │ Lucide React (icons) + React Markdown + remark-gfm │ │
|
| 43 |
+
│ └────────────────────────────────────────────────────────────┘ │
|
| 44 |
+
└──────────────────────────┬───────────────────────────────────────┘
|
| 45 |
+
│ HTTP / SSE Streaming
|
| 46 |
+
┌──────────────────────────┴───────────────────────────────────────┐
|
| 47 |
+
│ SERVER (FastAPI Python 3.13) │
|
| 48 |
+
│ │
|
| 49 |
+
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌────────────┐ │
|
| 50 |
+
│ │ FastAPI │ │ Local │ │ Turbovec │ │ Embedding │ │
|
| 51 |
+
│ │ Routes │──│ Services │──│ (In-process) │──│ Provider │ │
|
| 52 |
+
│ │ │ │ │ │ Index (.tvim)│ └────────────┘ │
|
| 53 |
+
│ └──────────┘ └──────────┘ └──────────────┘ ┌────────────┐ │
|
| 54 |
+
│ │ │ │ SQLite │ │
|
| 55 |
+
│ │ ├── SQLite FTS5 ──────────│ (tipitaka) │ │
|
| 56 |
+
│ │ ├── ONNX Reranker (CPU) └────────────┘ │
|
| 57 |
+
│ │ └── DeepSeek API (streaming) │
|
| 58 |
+
│ └── Static files (Served Vite build in production) │
|
| 59 |
+
└──────────────────────────────────────────────────────────────────┘
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
**Embedding Providers:**
|
| 63 |
+
- **Ollama** (local dev): `jina-embeddings-v5-small-retrieval` GGUF Q6_K (596MB)
|
| 64 |
+
- **sentence-transformers** (HF Space / fallback): `jinaai/jina-embeddings-v5-text-small-retrieval` PyTorch
|
| 65 |
+
- Both produce 1024-dims vectors (`EMBED_DIMS = 1024`)
|
| 66 |
+
|
| 67 |
+
---
|
| 68 |
+
|
| 69 |
+
## 2. System Architecture
|
| 70 |
+
|
| 71 |
+
### 2.1 High-Level Component Diagram
|
| 72 |
+
|
| 73 |
+
```mermaid
|
| 74 |
+
graph TB
|
| 75 |
+
subgraph "Client Layer (Vite + React)"
|
| 76 |
+
UI[User Interface]
|
| 77 |
+
subgraph "Core Components"
|
| 78 |
+
APP[AppShell<br/>flex h-screen layout]
|
| 79 |
+
ND[NavDrawer<br/>320px sidebar]
|
| 80 |
+
RP[ReaderPanel<br/>Main reading area]
|
| 81 |
+
RT[RightToolbar<br/>44px controls strip]
|
| 82 |
+
AIP[AIPopup<br/>Draggable + resizable chat]
|
| 83 |
+
SP[SelectionPopup<br/>AI from selection]
|
| 84 |
+
AIF[AIFloatingMenu<br/>Speed Dial Menu]
|
| 85 |
+
end
|
| 86 |
+
subgraph "State (Zustand)"
|
| 87 |
+
RS[ReaderStore<br/>volume, page, content]
|
| 88 |
+
TS[ThemeStore<br/>theme, fontSize]
|
| 89 |
+
US[UIStore<br/>navOpen, activeTab]
|
| 90 |
+
AS[AIStore<br/>messages, mode, useRag, viewMode, width, height, pos]
|
| 91 |
+
SS[SearchStore<br/>query, results, breakdown]
|
| 92 |
+
end
|
| 93 |
+
end
|
| 94 |
+
|
| 95 |
+
subgraph "API Layer (FastAPI)"
|
| 96 |
+
subgraph "Routers"
|
| 97 |
+
R1[GET /api/pages/volumes]
|
| 98 |
+
R2[GET /api/pages/volumes/{id}/toc]
|
| 99 |
+
R3[GET /api/pages/{vol}/{page}]
|
| 100 |
+
R4[GET /api/search?q=...]
|
| 101 |
+
R5[GET /api/search/suggestions]
|
| 102 |
+
R6[POST /api/ask/stream (SSE)]
|
| 103 |
+
R7[POST /api/ask (non-stream)]
|
| 104 |
+
R8[GET /api/ask/rag-status]
|
| 105 |
+
R9[GET /health]
|
| 106 |
+
end
|
| 107 |
+
subgraph "Services"
|
| 108 |
+
S1[PageService<br/>SQLite queries]
|
| 109 |
+
S2[SearchService<br/>FTS5 + LIKE fallback]
|
| 110 |
+
S3[PaliUtils<br/>autocorrect pipeline]
|
| 111 |
+
S4[RAGService<br/>Turbovec + Embedding + Reranker]
|
| 112 |
+
S5[LLMService<br/>DeepSeek streaming]
|
| 113 |
+
S6[ONNXReranker<br/>jina-reranker-v2]
|
| 114 |
+
end
|
| 115 |
+
subgraph "Data"
|
| 116 |
+
DB[(SQLite<br/>tipitaka_mcu.db)]
|
| 117 |
+
VDB[(Turbovec Index<br/>tipitaka_chunks.tvim)]
|
| 118 |
+
end
|
| 119 |
+
end
|
| 120 |
+
|
| 121 |
+
UI --> APP
|
| 122 |
+
APP --> ND & RP & RT & AIP & SP & AIF
|
| 123 |
+
RP --> RS
|
| 124 |
+
ND --> SS & US
|
| 125 |
+
RT --> TS & RS
|
| 126 |
+
AIP --> AS
|
| 127 |
+
AIF --> AS
|
| 128 |
+
R1 & R2 & R3 --> S1 --> DB
|
| 129 |
+
R4 & R5 --> S2 --> DB
|
| 130 |
+
R4 --> S3
|
| 131 |
+
R6 & R7 --> S5
|
| 132 |
+
S5 --> S4 --> VDB
|
| 133 |
+
S4 --> DB
|
| 134 |
+
S5 --> DS[(DeepSeek API)]
|
| 135 |
+
S4 --> EMB[(Embedding<br/>Ollama v5 / ST fallback)]
|
| 136 |
+
S4 --> RER[(ONNX Reranker)]
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
### 2.2 Data Flow — Hybrid Search (RAG)
|
| 140 |
+
|
| 141 |
+
```mermaid
|
| 142 |
+
sequenceDiagram
|
| 143 |
+
actor User
|
| 144 |
+
participant Browser
|
| 145 |
+
participant API as FastAPI
|
| 146 |
+
participant TV as Turbovec Index
|
| 147 |
+
participant DB as SQLite (tipitaka_mcu.db)
|
| 148 |
+
participant EMB as Embedding
|
| 149 |
+
participant ONNX as ONNX Reranker
|
| 150 |
+
participant LLM as DeepSeek API
|
| 151 |
+
|
| 152 |
+
User->>Browser: ถาม AI (RAG ON)
|
| 153 |
+
Browser->>API: POST /api/ask/stream {question, use_rag:true}
|
| 154 |
+
|
| 155 |
+
par Phase 1: FTS5
|
| 156 |
+
API->>DB: MATCH query (30 results)
|
| 157 |
+
DB-->>API: FTS rows + rank
|
| 158 |
+
and Phase 2: Vector (Turbovec)
|
| 159 |
+
API->>EMB: query_embedding()
|
| 160 |
+
alt Ollama available
|
| 161 |
+
EMB->>OLL: jina-embeddings-v5 GGUF
|
| 162 |
+
OLL-->>EMB: 1024-dims vector
|
| 163 |
+
else Fallback
|
| 164 |
+
EMB->>ST: sentence-transformers PyTorch
|
| 165 |
+
ST-->>EMB: 1024-dims vector
|
| 166 |
+
end
|
| 167 |
+
EMB-->>API: embedding vector
|
| 168 |
+
API->>TV: tv_index.search(vector, k=30)
|
| 169 |
+
TV-->>API: scores & ids (volume*100000+page)
|
| 170 |
+
end
|
| 171 |
+
|
| 172 |
+
API->>API: Decode UIDs to vol & page (cast to native int)
|
| 173 |
+
API->>DB: Enrich payload (Fetch content_text by vol & page)
|
| 174 |
+
DB-->>API: Content text for missing vector payloads
|
| 175 |
+
|
| 176 |
+
API->>API: Merge & deduplicate by (vol, page)
|
| 177 |
+
API->>ONNX: Rerank candidates
|
| 178 |
+
ONNX-->>API: Reordered scores
|
| 179 |
+
|
| 180 |
+
API->>LLM: context + question (streaming)
|
| 181 |
+
LLM-->>API: SSE chunks
|
| 182 |
+
API-->>Browser: data: {chunk: "..."}
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
### 2.3 Data Flow — Search
|
| 186 |
+
|
| 187 |
+
```mermaid
|
| 188 |
+
sequenceDiagram
|
| 189 |
+
actor User
|
| 190 |
+
participant Browser
|
| 191 |
+
participant API as FastAPI
|
| 192 |
+
participant DB as SQLite
|
| 193 |
+
|
| 194 |
+
User->>Browser: พิมพ์คำค้น
|
| 195 |
+
Browser->>Browser: Debounce 280ms → fetchSuggestions
|
| 196 |
+
Browser->>API: GET /api/search/suggestions?q=...
|
| 197 |
+
API->>DB: search_log LIKE + POPULAR_TERMS
|
| 198 |
+
DB-->>API: Suggestions
|
| 199 |
+
API-->>Browser: ["อริยสัจ", "อริยสัจ 4", ...]
|
| 200 |
+
|
| 201 |
+
User->>Browser: Enter
|
| 202 |
+
Browser->>API: GET /api/search?q=อริยสัจ&limit=50
|
| 203 |
+
|
| 204 |
+
par FTS5
|
| 205 |
+
API->>DB: pages_fts MATCH "อริยสัจ"
|
| 206 |
+
and LIKE Fallback
|
| 207 |
+
API->>DB: content_text LIKE '%อริยสัจ%'
|
| 208 |
+
end
|
| 209 |
+
|
| 210 |
+
API->>API: Pali autocorrect pipeline
|
| 211 |
+
API->>API: Merge & deduplicate
|
| 212 |
+
API->>API: Highlight <mark>
|
| 213 |
+
API-->>Browser: {total: 234, results: [...], breakdown: {...}}
|
| 214 |
+
Browser->>Browser: Render cards + volume bars
|
| 215 |
+
|
| 216 |
+
User->>Browser: คลิกผล → เล่ม 12 หน้า 45
|
| 217 |
+
Browser->>API: GET /api/pages/12/45
|
| 218 |
+
API-->>Browser: Page content
|
| 219 |
+
```
|
| 220 |
+
|
| 221 |
+
---
|
| 222 |
+
|
| 223 |
+
## 3. Frontend Architecture
|
| 224 |
+
|
| 225 |
+
### 3.1 Project Structure
|
| 226 |
+
*(ยังคงโครงสร้างไฟล์เช่นเดิมตามสถาปัตยกรรม V2.4.0)*
|
| 227 |
+
|
| 228 |
+
### 3.2 Key Components
|
| 229 |
+
|
| 230 |
+
#### ReaderPanel (Updated for Mobile Stability)
|
| 231 |
+
- การนำทางทีละหน้า (Page-by-page)
|
| 232 |
+
- Render ข้อความโดยจัดชิดขอบซ้ายขวาอย่างสม่ำเสมอ (`text-justify: inter-character`)
|
| 233 |
+
- **การแก้ไขบั๊กค้างบนมือถือ (Mobile Stability)**:
|
| 234 |
+
- แก้ไขปัญหาหน้าจอค้างสีขาวบนมือถือขณะปัดหน้า (swipe transitions)
|
| 235 |
+
- นำสถานะ `loading` spinner ออกนอก Framer Motion `AnimatePresence`
|
| 236 |
+
- ปรับการทำงานของ `AnimatePresence` ไปใช้ `mode="popLayout"` เพื่อให้สามารถนำทางและเลื่อนเปลี่ยนหน้าได้อย่างต่อเนื่องโดยที่ UI เก่าไม่ระงับการทำงานของ UI ใหม่
|
| 237 |
+
|
| 238 |
+
#### AIFloatingMenu
|
| 239 |
+
- ปุ่มวงกลม Sparkles/X ที่กางออกเป็น 5 คำสั่งย่อ (Speed Dial)
|
| 240 |
+
- คำสั่ง 1-4 จะดึงเนื้อหาหน้าปัจจุบันเพื่อส่งไปให้ AI ประมวลผลด่วนในหน้าต่าง frosted-glass (Answer Card)
|
| 241 |
+
- คำสั่ง 5 เปิดหน้าแชทหลัก (Chat viewMode)
|
| 242 |
+
|
| 243 |
+
#### AIPopup
|
| 244 |
+
- พาเนลลอยตัวสำหรับแชทกับ AI (รองรับการลาก drag และปรับขนาด resize และจดจำขนาดผ่าน localStorage)
|
| 245 |
+
- รองรับมุมมอง 2 แบบ: `chat` (สนทนาต่อเนื่อง) และ `answer` (ตอบคำถามสั้นจาก Speed Dial)
|
| 246 |
+
- แสดงคำเตือนกาลามสูตรใน Title Bar กลางหัวข้อเพื่อประหยัดพื้นที่แนวตั้ง
|
| 247 |
+
|
| 248 |
+
---
|
| 249 |
+
|
| 250 |
+
## 4. Backend Architecture
|
| 251 |
+
|
| 252 |
+
### 4.1 Project Structure
|
| 253 |
+
|
| 254 |
+
```
|
| 255 |
+
tipitaka-api/
|
| 256 |
+
├── app/
|
| 257 |
+
│ ├── main.py # FastAPI app + lifespan (background init)
|
| 258 |
+
│ ├── config.py # pydantic-settings
|
| 259 |
+
│ ├── schemas.py # Pydantic models
|
| 260 |
+
│ │
|
| 261 |
+
│ ├── routers/
|
| 262 |
+
│ │ ├── pages.py # /api/pages/*
|
| 263 |
+
│ │ ├── search.py # /api/search
|
| 264 |
+
│ │ └── ai.py # /api/ask
|
| 265 |
+
│ │
|
| 266 |
+
│ ├── services/
|
| 267 |
+
│ │ ├── page_service.py # Page retrieval & formatting
|
| 268 |
+
│ │ ├── search_service.py # FTS5 + LIKE + turbovec + rerank
|
| 269 |
+
│ │ ├── pali_utils.py # Autocorrect & Thai digit utilities
|
| 270 |
+
│ │ ├── rag_service.py # turbovec init + dual-mode embedding + rerank
|
| 271 |
+
│ │ ├── llm_service.py # DeepSeek streaming + context injection
|
| 272 |
+
│ │ └── onnx_reranker.py # jina-reranker-v2 ONNX CPU inference
|
| 273 |
+
│ │
|
| 274 |
+
│ └── database/
|
| 275 |
+
│ └── sqlite_db.py # SQLite connection & in-memory backup
|
| 276 |
+
│
|
| 277 |
+
├── models/
|
| 278 |
+
│ └── jina-v2-onnx/ # ONNX reranker model files (~267MB)
|
| 279 |
+
│
|
| 280 |
+
├── tests/ # Unit tests (35 tests - all passing)
|
| 281 |
+
│
|
| 282 |
+
├── download_assets.py # Downloads DB & turbovec index from HF dataset
|
| 283 |
+
├── startup.sh # HF Spaces entrypoint
|
| 284 |
+
├── requirements.txt
|
| 285 |
+
└── .env
|
| 286 |
+
```
|
| 287 |
+
|
| 288 |
+
### 4.2 API Endpoints
|
| 289 |
+
|
| 290 |
+
| Method | Path | Description |
|
| 291 |
+
|--------|------|-------------|
|
| 292 |
+
| GET | `/api/pages/volumes` | รายการเล่มทั้งหมด |
|
| 293 |
+
| GET | `/api/pages/volumes/{id}/toc` | สารบัญ |
|
| 294 |
+
| GET | `/api/pages/{vol}/{page}` | เนื้อหาหน้าพร้อมข้อมูลเชิงอรรถ |
|
| 295 |
+
| GET | `/api/search` | ค้นหาแบบไฮบริด (FTS5 + turbovec) และการทำไฮไลท์คำ |
|
| 296 |
+
| GET | `/api/search/suggestions` | คำแนะนำการค้นหาประวัติตามความนิยม |
|
| 297 |
+
| POST | `/api/ask/stream` | AI streaming (SSE) |
|
| 298 |
+
| POST | `/api/ask` | AI non-streaming |
|
| 299 |
+
| GET | `/api/ask/rag-status` | ตรวจสอบสถานะการเชื่อมต่อของดัชนี Turbovec |
|
| 300 |
+
| GET | `/health` | ตรวจสอบความพร้อมของเซิร์ฟเวอร์ |
|
| 301 |
+
|
| 302 |
+
### 4.3 Search Pipeline (SearchService)
|
| 303 |
+
|
| 304 |
+
```
|
| 305 |
+
query → Pali Autocorrect Pipeline
|
| 306 |
+
→ แปลงอักษรเลขไทย/เลขสากล
|
| 307 |
+
├─► FTS5 MATCH (pages_fts)
|
| 308 |
+
├─► turbovec vector search
|
| 309 |
+
│ └─ stack numpy array -> batch query -> tv_index.search()
|
| 310 |
+
→ RRF Score Fusion (Deduplicate & Merge)
|
| 311 |
+
→ SQLite Content Enrichment (ดึงข้อความจาก pages สำหรับ vector results)
|
| 312 |
+
→ ONNX Reranking (jina-reranker-v2 vs original query)
|
| 313 |
+
→ Snippet & Highlight (<mark> tags)
|
| 314 |
+
```
|
| 315 |
+
|
| 316 |
+
### 4.4 RAG Pipeline (RAGService)
|
| 317 |
+
- เมื่อมีข้อสงสัยธรรมะ RAGService จะทำการค้นหาข้อมูลอ้างอิงผ่าน FTS5 และ Turbovec (In-process) จากนั้นดึงข้อความเต็มจาก SQLite มาเติมลงใน Payload แล้วทำการ Rerank ก่อนส่งให้ DeepSeek API คัดเลือกคำตอบที่ดีที่สุด
|
| 318 |
+
|
| 319 |
+
### 4.5 Database & Vector Index Details
|
| 320 |
+
|
| 321 |
+
#### SQLite (tipitaka_mcu.db)
|
| 322 |
+
- เก็บสารบัญ รายชื่อเล่ม และเนื้อหาพระไตรปิฎก 45 เล่ม (~238MB)
|
| 323 |
+
- โหลดขึ้นสู่หน่วยความจำแบบ In-memory (`:memory:`) ตอนสตาร์ทอัป เพื่อดึงข้อความได้รวดเร็วที่สุดระดับไมโครวินาที
|
| 324 |
+
- ใช้ virtual table FTS5 (`pages_fts`) ในการสืบค้นแบบ Lexical
|
| 325 |
+
|
| 326 |
+
#### Turbovec Index
|
| 327 |
+
- ไฟล์: `tipitaka_chunks.tvim` (~13.2 MB)
|
| 328 |
+
- ใช้จัดเก็บเวกเตอร์ขนาด 1024 มิติ (จาก Jina Embedding v5) ที่ผ่านการทำ Quantization แบบ 4-bit SQ
|
| 329 |
+
- ทำงานแบบ In-process โดยเรียกผ่านคลาส `IdMapIndex.load(self._tv_path)`
|
| 330 |
+
- **ID Encoding**: เข้ารหัส ID เวกเตอร์เป็นเลขจำนวนเต็ม (Integer) ด้วยสูตร `UID = volume_id * 100000 + page_number` เพื่อไม่ต้องเก็บข้อมูล payload ซับซ้อนในตัวเวกเตอร์
|
| 331 |
+
- **ID Casting Fix**: ผลลัพธ์ ID จากการค้นหา Turbovec จะมีชนิดข้อมูลเป็น `numpy.uint64` ซึ่งจำเป็นต้องแปลงเป็น native Python `int()` ก่อนนำไป Query ใน SQLite มิฉะนั้น `sqlite3` driver จะมองเป็น binary BLOB และไม่สามารถดึงข้อมูลเนื้อหาได้ (แสดงผลเป็นหน้าว่าง)
|
| 332 |
+
|
| 333 |
+
---
|
| 334 |
+
|
| 335 |
+
## 5. Dual-Mode Embedding System
|
| 336 |
+
|
| 337 |
+
- ใช้ `jina-embeddings-v5-small-retrieval` (1024 มิติ) ทั้งในระบบพัฒนาท้องถิ่น (Local) และระบบคลาวด์ (HF Space)
|
| 338 |
+
- **Local Dev**: ดึงเวกเตอร์ผ่าน Ollama API (`/api/embed` ด้วยรุ่น GGUF)
|
| 339 |
+
- **Hugging Face Fallback**: ประมวลผลเวกเตอร์ใน Process ด้วยไลบรารี `sentence-transformers` ผ่านโมเดล PyTorch
|
| 340 |
+
- **Cache**: ระบบเก็บเวกเตอร์ใน OrderedDict (LRU cache ขนาด 256 รายการ) ป้องกันการยิงประมวลผลซ้ำคำเดิม
|
| 341 |
+
|
| 342 |
+
---
|
| 343 |
+
|
| 344 |
+
## 6. Deployment
|
| 345 |
+
|
| 346 |
+
### 6.1 Hugging Face Spaces
|
| 347 |
+
- รันบน CPU Basic ฮาร์ดแวร์ฟรี (16GB RAM)
|
| 348 |
+
- ในขั้นตอนสตาร์ทอัป สคริปต์ `download_assets.py` จะดาวน์โหลดไฟล์สำคัญจาก dataset `dhammawatthumpra/tipitaka-storage` ลงมาที่เครื่อง ได้แก่:
|
| 349 |
+
- `tipitaka_mcu.db` (SQLite)
|
| 350 |
+
- `tipitaka_chunks.tvim` (ดัชนี Turbovec)
|
| 351 |
+
- โมเดล ONNX Reranker
|
| 352 |
+
- ด้วยการเปลี่ยนมาใช้ Turbovec ทำให้ไม่ต้องรัน Qdrant container บนเครื่อง ส่งผลให้ลดความซับซ้อนของ Dockerfile และลดโอกาสเกิดข้อผิดพลาดจาก Lock file (`.lock` file) ในรอบการเปิดเครื่องใหม่ได้อย่างเด็ดขาด
|
| 353 |
+
|
| 354 |
+
---
|
| 355 |
+
|
| 356 |
+
## 7. Key Design Decisions
|
| 357 |
+
|
| 358 |
+
### 7.1 Why Turbovec over Qdrant?
|
| 359 |
+
- **Speed**: ในการทดสอบสืบค้นเวกเตอร์ด้วย Random Queries พบว่า Turbovec มี Latency เฉลี่ยเพียง **1.43 ms** ขณะที่ Qdrant Server เฉลี่ย **19.42 ms** (~13.5x speedup)
|
| 360 |
+
- **RAM Overhead**: Qdrant Server ในลักษณะ container/embedded ต้องการแรมอย่างน้อย 150-300MB ขณะที่ Turbovec เป็นการโหลดตรงเข้า Process Python เพิ่มแรมเพียงแค่ **~14 MB** เท่านั้น
|
| 361 |
+
- **Disk Usage**: ตัวไฟล์ดัชนีของ Turbovec หลังย่อขนาด 4-bit SQ เหลือเพียง **13.2 MB** ประหยัดพื้นที่จัดเก็บข้อมูลมากกว่า Qdrant snapshots ที่มีขนาดรวมเกือบ 1 GB
|
| 362 |
+
- **Reliability**: ไม่พึ่งพา REST calls หรือการจัดการ socket connections ช่วยขจัดปัญหา network timeout หรือ Docker instance lock ค้าง
|
| 363 |
+
|
| 364 |
+
### 7.2 Why SQLite Enrichment?
|
| 365 |
+
- เพื่อให้ตัวไฟล์ดัชนี Turbovec (`.tvim`) มีขนาดเล็กที่สุด (13.2 MB) เราจึงไม่ได้เก็บข้อความพระไตรปิฎก (content_text) ไว้ในดัชนีเวกเตอร์ แต่ใช้วิธีจัดเก็บเฉพาะพิกัด ID เล่มและหน้า (volume/page)
|
| 366 |
+
- เมื่อค้นพบเวกเตอร์ที่เกี่ยวข้องแล้ว ค่อยไปดึงข้อความฉบับเต็มอย่างรวดเร็วจาก SQLite In-memory ในลักษณะ Lazy Load
|
| 367 |
+
|
| 368 |
+
### 7.3 Why Casting numpy.uint64 to Native int?
|
| 369 |
+
- ไดรเวอร์ `sqlite3` ของ Python ดำเนินการค้นหาพารามิเตอร์โดยยึดตามประเภทข้อมูลของ Python โดยตรง เมื่อส่งค่า `numpy.uint64` เข้าไป ตัวไดรเวอร์จะไม่แปลงเป็นค่าตัวเลขปกติ แต่จะบันทึกเป็นไบนารีบล็อบ (BLOB) ส่งผลให้ SQL Query ในตาราง `pages` ไม่พบแถวข้อมูลใดๆ (Empty Result)
|
| 370 |
+
- การใช้ `int(uid)` จึงเป็นหัวใจสำคัญที่ทำให้การดึงเนื้อหาทำได้อย่างสมบูรณ์
|
| 371 |
+
|
| 372 |
+
### 7.4 Why "popLayout" Reader Transitions?
|
| 373 |
+
- เพื่อป้องกันปัญหา UI ค้างขณะปัดเปลี่ยนหน้าบนอุปกรณ์มือถือ ซึ่ง Framer Motion มักจะมีปัญหากับการจัดระเบียบ layout ด้วยโหมด `mode="wait"` เมื่อส่วนประกอบใหม่เข้ามาเร็วกว่าที่ส่วนประกอบเดิมจะเฟดออกสำเร็จ การสลับมาใช้ `popLayout` ทำให้ส่วนประกอบเก่าถูกดีดออกตามพารามิเตอร์ขอบเขตจริง ช่วยให้ระบบสลับหน้าระหว่างการปัดได้อย่างเสถียรและราบรื่น
|
Tipitaka-Web-Application-Tech-Stack-TURBOVEC-HF.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Tech Stack Specification: Tipitaka Web Application (Turbovec Edition)
|
| 2 |
+
## Phase 1 — All-on-Hugging-Face with Turbovec Backend
|
| 3 |
+
|
| 4 |
+
> **Version:** 3.1.0 (Turbovec Edition)
|
| 5 |
+
> **Date:** 2026-05-26
|
| 6 |
+
> **Status:** Production — Live at [dhammadassana-tipitaka.hf.space](https://dhammadassana-tipitaka.hf.space/)
|
| 7 |
+
> **Based on Architecture:** V2.5.0 (commit `603ead8`)
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 1. Executive Summary
|
| 12 |
+
|
| 13 |
+
Phase 1 ออกแบบให้ทุกอย่างรันอยู่บน **Hugging Face Spaces** ในลักษณะการทำงานใน Process เดียวกัน (In-process) เพื่อความเรียบง่าย เสถียรภาพ และประหยัดค่าใช้จ่าย โดยใช้ **Turbovec** เป็น Vector Database หลัก ซึ่งเป็นชุดค้นหาเวกเตอร์น้ำหนักเบาและประมวลผลเร็วระดับเสี้ยววินาที ระบบสามารถทำงานบน HF CPU Basic (Free Tier) ได้อย่างลื่นไหลโดยไม่ต้องใช้ RAM ปริมาณมหาศาล
|
| 14 |
+
|
| 15 |
+
**สิ่งที่ระบบ Turbovec ยืนยันแล้ว:**
|
| 16 |
+
- การสืบค้นเวกเตอร์เฉลี่ยอยู่ที่ **~1.43 ms** (เร็วกว่า Qdrant 13.5 เท่า)
|
| 17 |
+
- RAG Pipeline ทำงานแบบ In-process ผ่าน `sentence-transformers` -> `Turbovec` -> `SQLite Enrichment` -> `ONNX Reranker` -> `DeepSeek`
|
| 18 |
+
- ประหยัดหน่วยความจำ RAM ของเวกเตอร์เอนจินเหลือเพียง **~14 MB** (ลดลงจาก Qdrant 100 เท่า)
|
| 19 |
+
- ขนาดไฟล์ดัชนีเวกเตอร์ที่ต้องจัดเก็บลดลงเหลือ **13.2 MB** (ผ่านการ Quantize 4-bit SQ)
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## 2. Architecture Overview
|
| 24 |
+
|
| 25 |
+
```
|
| 26 |
+
┌─────────────────────────────────────────────────────────┐
|
| 27 |
+
│ Hugging Face Space (CPU Basic) │
|
| 28 |
+
│ │
|
| 29 |
+
│ ┌─────────────────────────────────────────────────┐ │
|
| 30 |
+
│ │ Vite + React 18 + TypeScript 5 │ │
|
| 31 |
+
│ │ (Static Build — Served by FastAPI) │ │
|
| 32 |
+
│ └────────────────────┬────────────────────────────┘ │
|
| 33 |
+
│ │ HTTP / SSE │
|
| 34 |
+
│ ┌────────────────────▼────────────────────────────┐ │
|
| 35 |
+
│ │ FastAPI (Python 3.13) │ │
|
| 36 |
+
│ │ │ │
|
| 37 |
+
│ │ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ │
|
| 38 |
+
│ │ │ SQLite │ │ Turbovec │ │ Sentence- │ │ │
|
| 39 |
+
│ │ │ FTS5 │ │ Index │ │ Transformers │ │ │
|
| 40 |
+
│ │ │(in-mem) │ │ (.tvim) │ │ (in-process) │ │ │
|
| 41 |
+
│ │ └──────────┘ └──────────┘ └───────────────┘ │ │
|
| 42 |
+
│ │ ┌──────────────────────────────────────────┐ │ │
|
| 43 |
+
│ │ │ ONNX Reranker (jina-reranker-v2) │ │ │
|
| 44 |
+
│ │ └──────────────────────────────────────────┘ │ │
|
| 45 |
+
│ └──────────────────────────────────────────────────┘ │
|
| 46 |
+
│ │ │
|
| 47 |
+
│ ▼ External API │
|
| 48 |
+
│ DeepSeek API (Pay-as-you-go) │
|
| 49 |
+
└─────────────────────────────────────────────────────────┘
|
| 50 |
+
|
| 51 |
+
Assets: ดาวน์โหลดจาก HF Dataset (dhammawatthumpra/tipitaka-storage) ที่ Startup
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
## 3. Component Specification
|
| 57 |
+
|
| 58 |
+
### 3.1 Presentation Layer
|
| 59 |
+
|
| 60 |
+
| รายการ | Detail |
|
| 61 |
+
|--------|--------|
|
| 62 |
+
| **Framework** | Vite 6 + React 18 + TypeScript 5 |
|
| 63 |
+
| **Styling** | Tailwind CSS v4 + Framer Motion + Lucide React |
|
| 64 |
+
| **State** | Zustand (5 stores: Reader, Theme, UI, AI, Search) |
|
| 65 |
+
| **PWA** | manifest.json + Dharma wheel PWA icons + Safari app touch icons |
|
| 66 |
+
| **Transitions** | ReaderPanel ปรับใช้ `mode="popLayout"` พร้อมควบคุม loading spinner ด้านนอก เพื่อแก้ปัญหาจอนิ่งขาวบนอุปกรณ์พกพาขณะปัดเปลี่ยนหน้า |
|
| 67 |
+
|
| 68 |
+
---
|
| 69 |
+
|
| 70 |
+
### 3.2 API Layer
|
| 71 |
+
|
| 72 |
+
| รายการ | Detail |
|
| 73 |
+
|--------|--------|
|
| 74 |
+
| **Framework** | FastAPI (Python 3.13) + Uvicorn |
|
| 75 |
+
| **Workers** | Single worker (ช่วยให้การโหลดโมเดลตัวแปลงและเวกเตอร์ทำงานร่วมกันได้ประหยัดแรมสูงสุด) |
|
| 76 |
+
| **Deployment** | HF Space — port 7860 |
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
### 3.3 Data & Retrieval Layer
|
| 81 |
+
|
| 82 |
+
#### SQLite FTS5 (Lexical Search)
|
| 83 |
+
- ไฟล์: `tipitaka_mcu.db` (~238MB)
|
| 84 |
+
- โหลดเข้า Memory 100% ผ่าน `sqlite3.backup()` ที่ startup
|
| 85 |
+
- Pipeline: FTS5 MATCH -> LIKE fallback -> Pali Autocorrect -> dedup -> highlight -> snippet
|
| 86 |
+
|
| 87 |
+
#### Turbovec Index (Vector Search)
|
| 88 |
+
- ไฟล์ดัชนี: `tipitaka_chunks.tvim` (~13.2 MB)
|
| 89 |
+
- รูปแบบ: โหลดขึ้นบน Memory ใน Python process โดยตรง ผ่านโมเดล `turbovec.IdMapIndex` (ไม่ใช่ server แยก)
|
| 90 |
+
- เวกเตอร์ถูกย่อขนาดลงผ่าน 4-bit scalar quantization เหลือ 13.2 MB ทำให้โหลดไวและประหยัดพื้นที่คลาวด์
|
| 91 |
+
- การเข้ารหัสพิกัด: แปลงพิกัดของเวกเตอร์เป็น ID ตัวเลขจำนวนเต็มด้วยสูตร `UID = volume_id * 100_000 + page_number` เพื่อไม่ต้องเก็บข้อมูล Payload อื่นๆ บนเวกเตอร์โดยไม่จำเป็น
|
| 92 |
+
- การแก้ไขพารามิเตอร์ SQLite: แปลงประเภทผลลัพธ์เวกเตอร์ `numpy.uint64` ให้เป็น native `int()` ก่อนค้นหากับฐานข้อมูล `tipitaka_mcu.db` เสมอ เพื่อป้องกันข้อผิดพลาดที่ SQLite จะมองพารามิเตอร์ดังกล่าวเป็นข้อมูลชนิด BLOB
|
| 93 |
+
|
| 94 |
+
#### ONNX Reranker
|
| 95 |
+
- Model: `jina-reranker-v2-base-multilingual` (ONNX CPU, ~267MB)
|
| 96 |
+
- Sequence Length: ปรับลดความยาวสูงสุดใน tokenization เหลือ `max_length=256` ช่วยเร่งความเร็วในการประเมินคะแนนบน CPU พื้นฐานได้มากกว่าเดิม 4 เท่า
|
| 97 |
+
|
| 98 |
+
---
|
| 99 |
+
|
| 100 |
+
### 3.4 Embedding Layer
|
| 101 |
+
|
| 102 |
+
**ใช้ sentence-transformers in-process เท่านั้น** (ไม่มี Ollama ในระบบ HF Space)
|
| 103 |
+
|
| 104 |
+
| รายการ | Detail |
|
| 105 |
+
|--------|--------|
|
| 106 |
+
| **Model** | `jinaai/jina-embeddings-v5-text-small-retrieval` |
|
| 107 |
+
| **Runtime** | sentence-transformers (PyTorch, in-process) |
|
| 108 |
+
| **Dimensions** | 1024 dims |
|
| 109 |
+
| **RAM** | ~1.2 GB |
|
| 110 |
+
| **Cache** | LRU 256 entries (OrderedDict) เพื่อสกัดการประมวลผลคำคิวรีซ้ำ |
|
| 111 |
+
|
| 112 |
+
---
|
| 113 |
+
|
| 114 |
+
### 3.5 Generative AI Layer
|
| 115 |
+
*(เหมือนเดิม — DeepSeek streaming API ผ่านโมเดล fast/reasoner)*
|
| 116 |
+
|
| 117 |
+
---
|
| 118 |
+
|
| 119 |
+
## 4. Data Flow
|
| 120 |
+
|
| 121 |
+
### 4.1 RAG Pipeline
|
| 122 |
+
|
| 123 |
+
```
|
| 124 |
+
User Query
|
| 125 |
+
│
|
| 126 |
+
├─► FTS5 MATCH (30 results) [Lexical]
|
| 127 |
+
│
|
| 128 |
+
├─► ST Embedding → Turbovec Search (30) [Semantic]
|
| 129 |
+
│ ├─ ตรวจสอบ LRU cache
|
| 130 |
+
│ └─ tv_index.search() -> return vol & page IDs
|
| 131 |
+
│
|
| 132 |
+
├─► SQLite Payload Enrichment (ดึง content_text จาก db ด้วย vol & page)
|
| 133 |
+
├─► Merge + Deduplicate by (vol, page)
|
| 134 |
+
│
|
| 135 |
+
├─► ONNX Reranker → sort by score
|
| 136 |
+
│
|
| 137 |
+
└─► Top N context → DeepSeek API → SSE Stream → Browser
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
---
|
| 141 |
+
|
| 142 |
+
## 5. Deployment
|
| 143 |
+
|
| 144 |
+
### 5.1 HF Spaces Configuration
|
| 145 |
+
- **Visibility**: Protected (ซ่อน Source Code และ Assets)
|
| 146 |
+
- **Build**: Dockerfile (multi-stage)
|
| 147 |
+
- **Port**: 7860
|
| 148 |
+
|
| 149 |
+
### 5.2 Startup Sequence
|
| 150 |
+
|
| 151 |
+
```
|
| 152 |
+
1. Dockerfile build: npm ci → vite build → pip install
|
| 153 |
+
2. startup.sh:
|
| 154 |
+
a. download_assets.py → ดึงไฟล์จาก HF Dataset
|
| 155 |
+
- tipitaka_mcu.db (~238MB)
|
| 156 |
+
- tipitaka_chunks.tvim (~13.2MB - โหลดแทน snapshot ดั้งเดิม)
|
| 157 |
+
- ONNX reranker via huggingface_hub (~267MB)
|
| 158 |
+
b. ลบ Qdrant .lock file (กรณีมีการเปิดฟังก์ชัน Qdrant ไว้ร่วมกัน)
|
| 159 |
+
c. uvicorn app.main:app --port 7860
|
| 160 |
+
3. FastAPI lifespan:
|
| 161 |
+
a. SQLite → โหลดเข้า :memory:
|
| 162 |
+
b. Turbovec Index → โหลดเข้า���ลาส tv_index ในหน่วยความจำ (~14MB)
|
| 163 |
+
c. ONNX reranker → โหลดโมเดล
|
| 164 |
+
d. ST model → โหลด lazy (ครั้งแรกที่มี query)
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
**เวลา Cold Start ของเครื่อง**: ลดลงเหลือเพียง **~1.5–3 นาที** (เนื่องจากไม่ต้องดาวน์โหลดไฟล์ขนาด 1 GB ของ Qdrant snapshot และข้ามขั้นตอนการดึง snapshot restore ขนาดใหญ่)
|
| 168 |
+
|
| 169 |
+
### 5.3 RAM Budget (CPU Basic = 16GB)
|
| 170 |
+
|
| 171 |
+
ด้วยการใช้ Turbovec ทำให้ขยายขนาดพื้นที่ว่างบน RAM ได้มากถึง 12 GB ซึ่งช่วยเพิ่มเสถียรภาพสูงสุดในการประมวลผลคำตอบ RAG
|
| 172 |
+
|
| 173 |
+
| Component | RAM (Qdrant Embedded) | RAM (Turbovec Edition) |
|
| 174 |
+
|-----------|--------------------|------------------------|
|
| 175 |
+
| SQLite in-memory | ~238 MB | ~238 MB |
|
| 176 |
+
| Vector Index Engine | **~1.2–1.5 GB** | **~14 MB** ✅ |
|
| 177 |
+
| ST Model (Jina-v5) | ~1.2 GB | ~1.2 GB |
|
| 178 |
+
| ONNX Reranker | ~500 MB | ~500 MB |
|
| 179 |
+
| FastAPI + Python overhead | ~300 MB | ~300 MB |
|
| 180 |
+
| Ubuntu OS | ~500 MB | ~500 MB |
|
| 181 |
+
| **รวม** | **~4.0–4.2 GB** | **~2.7–2.8 GB** ✅ |
|
| 182 |
+
| **เหลือ Headroom บนแรม**| **~11.8 GB** | **~13.2 GB** ✅ |
|
| 183 |
+
|
| 184 |
+
---
|
| 185 |
+
|
| 186 |
+
## 6. Environment Variables
|
| 187 |
+
|
| 188 |
+
| Variable | Default | Description |
|
| 189 |
+
|----------|---------|-------------|
|
| 190 |
+
| `LLM_API_KEY` | — | คีย์ DeepSeek API |
|
| 191 |
+
| `DATA_DIR` | Auto-detect | โฟลเดอร์เก็บข้อมูล SQLite & Turbovec Index |
|
| 192 |
+
| `DATABASE_PATH` | Auto-detect | ไฟล์ที่อยู่ของ SQLite |
|
| 193 |
+
| `SERVE_STATIC` | `true` | ให้ FastAPI ทำหน้าที่เสิร์ฟ Frontend ของ Vite |
|
| 194 |
+
| `ST_EMBED_MODEL` | `jinaai/jina-embeddings-v5-text-small-retrieval` | โมเดลทำเวกเตอร์บน Hugging Face |
|
| 195 |
+
| `PORT` | `7860` | พอร์ตของ HF Space |
|
| 196 |
+
|
| 197 |
+
---
|
| 198 |
+
|
| 199 |
+
## 7. Cost Summary
|
| 200 |
+
- **HF PRO Account**: $9.00/เดือน (ประมาณ 315 บาท) เพื่อสิทธิ์การเข้าถึงแบบ Protected
|
| 201 |
+
- **HF Space Hardware (CPU Basic)**: ฟรี
|
| 202 |
+
- **DeepSeek API**: จ่ายตามปริมาณประมวลผลโทเค็นจริง
|
| 203 |
+
- **รวมค่าบริการคงที่**: **$9.00/เดือน**
|
| 204 |
+
|
| 205 |
+
---
|
| 206 |
+
|
| 207 |
+
## 8. ข้อจำกัดและแนวทางปรับปรุง
|
| 208 |
+
|
| 209 |
+
| ข้อจำกัด | ผลกระทบ | แนวทางแก้ไข |
|
| 210 |
+
|---------|---------|-------------|
|
| 211 |
+
| Cold start ในรอบการเปิดเครื่องใหม่ | ระบบหยุดทำงานชั่วคราวเมื่อรีสตาร์ทหลังไม่มีการใช้งาน 48 ชม. | ใช้ HF Persistent Storage หรือ VPS โฮสต์ส่วนตัว |
|
| 212 |
+
| SQLite FTS5 และ Turbovec ค้นแยก | ข้อมูลเวกเตอร์อาจแตกต่างเล็กน้อยจากคำดั้งเดิมก่อน enrichment | ทำ Cache เอนทิตีที่ซับซ้อนขึ้น |
|
| 213 |
+
|
| 214 |
+
---
|
| 215 |
+
|
| 216 |
+
*เอกสารนี้ได้รับการปรับปรุงเพื่อสะท้อนประสิทธิภาพจริงของดัชนี Turbovec ที่รันอยู่บนระบบของแอปพลิเคชัน*
|