dhammawatthumpra commited on
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 ที่รันอยู่บนระบบของแอปพลิเคชัน*