# Tipitaka Web Application Architecture (Turbovec Edition)
## Vite + FastAPI + Turbovec — "Book Sanctuary + Floating AI Consultant"
> **Version:** 2.5.0 (Turbovec Edition)
> **Date:** 2026-05-26
> **Status:** Production (local dev & HF Spaces using Turbovec backend)
> **Based on:** Code at commit `603ead8`
---
## 1. Executive Summary
### 1.1 Vision
Web Application สำหรับอ่านพระไตรปิฎก มจร. ที่ให้ประสบการณ์การอ่านแบบจิตวิเวก โดยมี AI เป็นผู้ช่วยลอยตัวที่ไม่รบกวนสมาธิ และใช้ Vector Backend ประสิทธิภาพสูงที่ทำงานแบบ In-process (ไม่ต้องใช้ Server แยก) เพื่อความรวดเร็วสูงสุดและประหยัดทรัพยากรบน CPU Basic Free Tier
### 1.2 Key Changes
#### V2.5.0 (2026-05-26) - Turbovec Edition
| Layer | V2.4.0 (Qdrant) | V2.5.0 (Turbovec) |
|-------|-----------------|-------------------|
| **Vector Backend** | Qdrant (Embedded snapshot / Server) | **Turbovec IdMapIndex** — In-process vector index file (`tipitaka_chunks.tvim` 4-bit SQ) |
| **Search Speed** | ~19.42 ms (Qdrant Server queries) | **~1.43 ms** — ~13.5x faster local search latency |
| **RAM Footprint** | ~1.2–1.5 GB (Qdrant process) | **~14 MB** — run in-process within Python runtime |
| **Index Size** | ~600+ MB (Snapshots & storage) | **13.2 MB** — quantized using 4-bit scalar quantization |
| **Database Enrichment**| Vector payload returned by Qdrant | **Dynamic SQLite Lookup** — content fetched from `pages` using decoded volume & page IDs |
| **Mobile Stability** | Framer Motion layout freezes on swipe | **`mode="popLayout"`** & standalone loading spinner outside of `AnimatePresence` |
| **ID casting** | Int / string uuid | **Native Python `int()` casting** — prevents `numpy.uint64` being treated as SQL binary BLOB |
| **Commit** | `f11cd44` | `603ead8` |
---
### 1.3 Tech Stack
```
┌──────────────────────────────────────────────────────────────────┐
│ CLIENT (Browser) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Vite 6 + React 18 + TypeScript 5 │ │
│ │ Tailwind CSS v4 + @tailwindcss/typography │ │
│ │ Framer Motion (animations) + Zustand (state) │ │
│ │ Lucide React (icons) + React Markdown + remark-gfm │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────┬───────────────────────────────────────┘
│ HTTP / SSE Streaming
┌──────────────────────────┴───────────────────────────────────────┐
│ SERVER (FastAPI Python 3.13) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ FastAPI │ │ Local │ │ Turbovec │ │ Embedding │ │
│ │ Routes │──│ Services │──│ (In-process) │──│ Provider │ │
│ │ │ │ │ │ Index (.tvim)│ └────────────┘ │
│ └──────────┘ └──────────┘ └──────────────┘ ┌────────────┐ │
│ │ │ │ SQLite │ │
│ │ ├── SQLite FTS5 ──────────│ (tipitaka) │ │
│ │ ├── ONNX Reranker (CPU) └────────────┘ │
│ │ └── DeepSeek API (streaming) │
│ └── Static files (Served Vite build in production) │
└──────────────────────────────────────────────────────────────────┘
```
**Embedding Providers:**
- **Ollama** (local dev): `jina-embeddings-v5-small-retrieval` GGUF Q6_K (596MB)
- **sentence-transformers** (HF Space / fallback): `jinaai/jina-embeddings-v5-text-small-retrieval` PyTorch
- Both produce 1024-dims vectors (`EMBED_DIMS = 1024`)
---
## 2. System Architecture
### 2.1 High-Level Component Diagram
```mermaid
graph TB
subgraph "Client Layer (Vite + React)"
UI[User Interface]
subgraph "Core Components"
APP[AppShell
flex h-screen layout]
ND[NavDrawer
320px sidebar]
RP[ReaderPanel
Main reading area]
RT[RightToolbar
44px controls strip]
AIP[AIPopup
Draggable + resizable chat]
SP[SelectionPopup
AI from selection]
AIF[AIFloatingMenu
Speed Dial Menu]
end
subgraph "State (Zustand)"
RS[ReaderStore
volume, page, content]
TS[ThemeStore
theme, fontSize]
US[UIStore
navOpen, activeTab]
AS[AIStore
messages, mode, useRag, viewMode, width, height, pos]
SS[SearchStore
query, results, breakdown]
end
end
subgraph "API Layer (FastAPI)"
subgraph "Routers"
R1[GET /api/pages/volumes]
R2[GET /api/pages/volumes/{id}/toc]
R3[GET /api/pages/{vol}/{page}]
R4[GET /api/search?q=...]
R5[GET /api/search/suggestions]
R6[POST /api/ask/stream (SSE)]
R7[POST /api/ask (non-stream)]
R8[GET /api/ask/rag-status]
R9[GET /health]
end
subgraph "Services"
S1[PageService
SQLite queries]
S2[SearchService
FTS5 + LIKE fallback]
S3[PaliUtils
autocorrect pipeline]
S4[RAGService
Turbovec + Embedding + Reranker]
S5[LLMService
DeepSeek streaming]
S6[ONNXReranker
jina-reranker-v2]
end
subgraph "Data"
DB[(SQLite
tipitaka_mcu.db)]
VDB[(Turbovec Index
tipitaka_chunks.tvim)]
end
end
UI --> APP
APP --> ND & RP & RT & AIP & SP & AIF
RP --> RS
ND --> SS & US
RT --> TS & RS
AIP --> AS
AIF --> AS
R1 & R2 & R3 --> S1 --> DB
R4 & R5 --> S2 --> DB
R4 --> S3
R6 & R7 --> S5
S5 --> S4 --> VDB
S4 --> DB
S5 --> DS[(DeepSeek API)]
S4 --> EMB[(Embedding
Ollama v5 / ST fallback)]
S4 --> RER[(ONNX Reranker)]
```
### 2.2 Data Flow — Hybrid Search (RAG)
```mermaid
sequenceDiagram
actor User
participant Browser
participant API as FastAPI
participant TV as Turbovec Index
participant DB as SQLite (tipitaka_mcu.db)
participant EMB as Embedding
participant ONNX as ONNX Reranker
participant LLM as DeepSeek API
User->>Browser: ถาม AI (RAG ON)
Browser->>API: POST /api/ask/stream {question, use_rag:true}
par Phase 1: FTS5
API->>DB: MATCH query (30 results)
DB-->>API: FTS rows + rank
and Phase 2: Vector (Turbovec)
API->>EMB: query_embedding()
alt Ollama available
EMB->>OLL: jina-embeddings-v5 GGUF
OLL-->>EMB: 1024-dims vector
else Fallback
EMB->>ST: sentence-transformers PyTorch
ST-->>EMB: 1024-dims vector
end
EMB-->>API: embedding vector
API->>TV: tv_index.search(vector, k=30)
TV-->>API: scores & ids (volume*100000+page)
end
API->>API: Decode UIDs to vol & page (cast to native int)
API->>DB: Enrich payload (Fetch content_text by vol & page)
DB-->>API: Content text for missing vector payloads
API->>API: Merge & deduplicate by (vol, page)
API->>ONNX: Rerank candidates
ONNX-->>API: Reordered scores
API->>LLM: context + question (streaming)
LLM-->>API: SSE chunks
API-->>Browser: data: {chunk: "..."}
```
### 2.3 Data Flow — Search
```mermaid
sequenceDiagram
actor User
participant Browser
participant API as FastAPI
participant DB as SQLite
User->>Browser: พิมพ์คำค้น
Browser->>Browser: Debounce 280ms → fetchSuggestions
Browser->>API: GET /api/search/suggestions?q=...
API->>DB: search_log LIKE + POPULAR_TERMS
DB-->>API: Suggestions
API-->>Browser: ["อริยสัจ", "อริยสัจ 4", ...]
User->>Browser: Enter
Browser->>API: GET /api/search?q=อริยสัจ&limit=50
par FTS5
API->>DB: pages_fts MATCH "อริยสัจ"
and LIKE Fallback
API->>DB: content_text LIKE '%อริยสัจ%'
end
API->>API: Pali autocorrect pipeline
API->>API: Merge & deduplicate
API->>API: Highlight
API-->>Browser: {total: 234, results: [...], breakdown: {...}}
Browser->>Browser: Render cards + volume bars
User->>Browser: คลิกผล → เล่ม 12 หน้า 45
Browser->>API: GET /api/pages/12/45
API-->>Browser: Page content
```
---
## 3. Frontend Architecture
### 3.1 Project Structure
*(ยังคงโครงสร้างไฟล์เช่นเดิมตามสถาปัตยกรรม V2.4.0)*
### 3.2 Key Components
#### ReaderPanel (Updated for Mobile Stability)
- การนำทางทีละหน้า (Page-by-page)
- Render ข้อความโดยจัดชิดขอบซ้ายขวาอย่างสม่ำเสมอ (`text-justify: inter-character`)
- **การแก้ไขบั๊กค้างบนมือถือ (Mobile Stability)**:
- แก้ไขปัญหาหน้าจอค้างสีขาวบนมือถือขณะปัดหน้า (swipe transitions)
- นำสถานะ `loading` spinner ออกนอก Framer Motion `AnimatePresence`
- ปรับการทำงานของ `AnimatePresence` ไปใช้ `mode="popLayout"` เพื่อให้สามารถนำทางและเลื่อนเปลี่ยนหน้าได้อย่างต่อเนื่องโดยที่ UI เก่าไม่ระงับการทำงานของ UI ใหม่
#### AIFloatingMenu
- ปุ่มวงกลม Sparkles/X ที่กางออกเป็น 5 คำสั่งย่อ (Speed Dial)
- คำสั่ง 1-4 จะดึงเนื้อหาหน้าปัจจุบันเพื่อส่งไปให้ AI ประมวลผลด่วนในหน้าต่าง frosted-glass (Answer Card)
- คำสั่ง 5 เปิดหน้าแชทหลัก (Chat viewMode)
#### AIPopup
- พาเนลลอยตัวสำหรับแชทกับ AI (รองรับการลาก drag และปรับขนาด resize และจดจำขนาดผ่าน localStorage)
- รองรับมุมมอง 2 แบบ: `chat` (สนทนาต่อเนื่อง) และ `answer` (ตอบคำถามสั้นจาก Speed Dial)
- แสดงคำเตือนกาลามสูตรใน Title Bar กลางหัวข้อเพื่อประหยัดพื้นที่แนวตั้ง
---
## 4. Backend Architecture
### 4.1 Project Structure
```
tipitaka-api/
├── app/
│ ├── main.py # FastAPI app + lifespan (background init)
│ ├── config.py # pydantic-settings
│ ├── schemas.py # Pydantic models
│ │
│ ├── routers/
│ │ ├── pages.py # /api/pages/*
│ │ ├── search.py # /api/search
│ │ └── ai.py # /api/ask
│ │
│ ├── services/
│ │ ├── page_service.py # Page retrieval & formatting
│ │ ├── search_service.py # FTS5 + LIKE + turbovec + rerank
│ │ ├── pali_utils.py # Autocorrect & Thai digit utilities
│ │ ├── rag_service.py # turbovec init + dual-mode embedding + rerank
│ │ ├── llm_service.py # DeepSeek streaming + context injection
│ │ └── onnx_reranker.py # jina-reranker-v2 ONNX CPU inference
│ │
│ └── database/
│ └── sqlite_db.py # SQLite connection & in-memory backup
│
├── models/
│ └── jina-v2-onnx/ # ONNX reranker model files (~267MB)
│
├── tests/ # Unit tests (35 tests - all passing)
│
├── download_assets.py # Downloads DB & turbovec index from HF dataset
├── startup.sh # HF Spaces entrypoint
├── requirements.txt
└── .env
```
### 4.2 API Endpoints
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/pages/volumes` | รายการเล่มทั้งหมด |
| GET | `/api/pages/volumes/{id}/toc` | สารบัญ |
| GET | `/api/pages/{vol}/{page}` | เนื้อหาหน้าพร้อมข้อมูลเชิงอรรถ |
| GET | `/api/search` | ค้นหาแบบไฮบริด (FTS5 + turbovec) และการทำไฮไลท์คำ |
| GET | `/api/search/suggestions` | คำแนะนำการค้นหาประวัติตามความนิยม |
| POST | `/api/ask/stream` | AI streaming (SSE) |
| POST | `/api/ask` | AI non-streaming |
| GET | `/api/ask/rag-status` | ตรวจสอบสถานะการเชื่อมต่อของดัชนี Turbovec |
| GET | `/health` | ตรวจสอบความพร้อมของเซิร์ฟเวอร์ |
### 4.3 Search Pipeline (SearchService)
```
query → Pali Autocorrect Pipeline
→ แปลงอักษรเลขไทย/เลขสากล
├─► FTS5 MATCH (pages_fts)
├─► turbovec vector search
│ └─ stack numpy array -> batch query -> tv_index.search()
→ RRF Score Fusion (Deduplicate & Merge)
→ SQLite Content Enrichment (ดึงข้อความจาก pages สำหรับ vector results)
→ ONNX Reranking (jina-reranker-v2 vs original query)
→ Snippet & Highlight ( tags)
```
### 4.4 RAG Pipeline (RAGService)
- เมื่อมีข้อสงสัยธรรมะ RAGService จะทำการค้นหาข้อมูลอ้างอิงผ่าน FTS5 และ Turbovec (In-process) จากนั้นดึงข้อความเต็มจาก SQLite มาเติมลงใน Payload แล้วทำการ Rerank ก่อนส่งให้ DeepSeek API คัดเลือกคำตอบที่ดีที่สุด
### 4.5 Database & Vector Index Details
#### SQLite (tipitaka_mcu.db)
- เก็บสารบัญ รายชื่อเล่ม และเนื้อหาพระไตรปิฎก 45 เล่ม (~238MB)
- โหลดขึ้นสู่หน่วยความจำแบบ In-memory (`:memory:`) ตอนสตาร์ทอัป เพื่อดึงข้อความได้รวดเร็วที่สุดระดับไมโครวินาที
- ใช้ virtual table FTS5 (`pages_fts`) ในการสืบค้นแบบ Lexical
#### Turbovec Index
- ไฟล์: `tipitaka_chunks.tvim` (~13.2 MB)
- ใช้จัดเก็บเวกเตอร์ขนาด 1024 มิติ (จาก Jina Embedding v5) ที่ผ่านการทำ Quantization แบบ 4-bit SQ
- ทำงานแบบ In-process โดยเรียกผ่านคลาส `IdMapIndex.load(self._tv_path)`
- **ID Encoding**: เข้ารหัส ID เวกเตอร์เป็นเลขจำนวนเต็ม (Integer) ด้วยสูตร `UID = volume_id * 100000 + page_number` เพื่อไม่ต้องเก็บข้อมูล payload ซับซ้อนในตัวเวกเตอร์
- **ID Casting Fix**: ผลลัพธ์ ID จากการค้นหา Turbovec จะมีชนิดข้อมูลเป็น `numpy.uint64` ซึ่งจำเป็นต้องแปลงเป็น native Python `int()` ก่อนนำไป Query ใน SQLite มิฉะนั้น `sqlite3` driver จะมองเป็น binary BLOB และไม่สามารถดึงข้อมูลเนื้อหาได้ (แสดงผลเป็นหน้าว่าง)
---
## 5. Dual-Mode Embedding System
- ใช้ `jina-embeddings-v5-small-retrieval` (1024 มิติ) ทั้งในระบบพัฒนาท้องถิ่น (Local) และระบบคลาวด์ (HF Space)
- **Local Dev**: ดึงเวกเตอร์ผ่าน Ollama API (`/api/embed` ด้วยรุ่น GGUF)
- **Hugging Face Fallback**: ประมวลผลเวกเตอร์ใน Process ด้วยไลบรารี `sentence-transformers` ผ่านโมเดล PyTorch
- **Cache**: ระบบเก็บเวกเตอร์ใน OrderedDict (LRU cache ขนาด 256 รายการ) ป้องกันการยิงประมวลผลซ้ำคำเดิม
---
## 6. Deployment
### 6.1 Hugging Face Spaces
- รันบน CPU Basic ฮาร์ดแวร์ฟรี (16GB RAM)
- ในขั้นตอนสตาร์ทอัป สคริปต์ `download_assets.py` จะดาวน์โหลดไฟล์สำคัญจาก dataset `dhammawatthumpra/tipitaka-storage` ลงมาที่เครื่อง ได้แก่:
- `tipitaka_mcu.db` (SQLite)
- `tipitaka_chunks.tvim` (ดัชนี Turbovec)
- โมเดล ONNX Reranker
- ด้วยการเปลี่ยนมาใช้ Turbovec ทำให้ไม่ต้องรัน Qdrant container บนเครื่อง ส่งผลให้ลดความซับซ้อนของ Dockerfile และลดโอกาสเกิดข้อผิดพลาดจาก Lock file (`.lock` file) ในรอบการเปิดเครื่องใหม่ได้อย่างเด็ดขาด
---
## 7. Key Design Decisions
### 7.1 Why Turbovec over Qdrant?
- **Speed**: ในการทดสอบสืบค้นเวกเตอร์ด้วย Random Queries พบว่า Turbovec มี Latency เฉลี่ยเพียง **1.43 ms** ขณะที่ Qdrant Server เฉลี่ย **19.42 ms** (~13.5x speedup)
- **RAM Overhead**: Qdrant Server ในลักษณะ container/embedded ต้องการแรมอย่างน้อย 150-300MB ขณะที่ Turbovec เป็นการโหลดตรงเข้า Process Python เพิ่มแรมเพียงแค่ **~14 MB** เท่านั้น
- **Disk Usage**: ตัวไฟล์ดัชนีของ Turbovec หลังย่อขนาด 4-bit SQ เหลือเพียง **13.2 MB** ประหยัดพื้นที่จัดเก็บข้อมูลมากกว่า Qdrant snapshots ที่มีขนาดรวมเกือบ 1 GB
- **Reliability**: ไม่พึ่งพา REST calls หรือการจัดการ socket connections ช่วยขจัดปัญหา network timeout หรือ Docker instance lock ค้าง
### 7.2 Why SQLite Enrichment?
- เพื่อให้ตัวไฟล์ดัชนี Turbovec (`.tvim`) มีขนาดเล็กที่สุด (13.2 MB) เราจึงไม่ได้เก็บข้อความพระไตรปิฎก (content_text) ไว้ในดัชนีเวกเตอร์ แต่ใช้วิธีจัดเก็บเฉพาะพิกัด ID เล่มและหน้า (volume/page)
- เมื่อค้นพบเวกเตอร์ที่เกี่ยวข้องแล้ว ค่อยไปดึงข้อความฉบับเต็มอย่างรวดเร็วจาก SQLite In-memory ในลักษณะ Lazy Load
### 7.3 Why Casting numpy.uint64 to Native int?
- ไดรเวอร์ `sqlite3` ของ Python ดำเนินการค้นหาพารามิเตอร์โดยยึดตามประเภทข้อมูลของ Python โดยตรง เมื่อส่งค่า `numpy.uint64` เข้าไป ตัวไดรเวอร์จะไม่แปลงเป็นค่าตัวเลขปกติ แต่จะบันทึกเป็นไบนารีบล็อบ (BLOB) ส่งผลให้ SQL Query ในตาราง `pages` ไม่พบแถวข้อมูลใดๆ (Empty Result)
- การใช้ `int(uid)` จึงเป็นหัวใจสำคัญที่ทำให้การดึงเนื้อหาทำได้อย่างสมบูรณ์
### 7.4 Why "popLayout" Reader Transitions?
- เพื่อป้องกันปัญหา UI ค้างขณะปัดเปลี่ยนหน้าบนอุปกรณ์มือถือ ซึ่ง Framer Motion มักจะมีปัญหากับการจัดระเบียบ layout ด้วยโหมด `mode="wait"` เมื่อส่วนประกอบใหม่เข้ามาเร็วกว่าที่ส่วนประกอบเดิมจะเฟดออกสำเร็จ การสลับมาใช้ `popLayout` ทำให้ส่วนประกอบเก่าถูกดีดออกตามพารามิเตอร์ขอบเขตจริง ช่วยให้ระบบสลับหน้าระหว่างการปัดได้อย่างเสถียรและราบรื่น