# 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` ทำให้ส่วนประกอบเก่าถูกดีดออกตามพารามิเตอร์ขอบเขตจริง ช่วยให้ระบบสลับหน้าระหว่างการปัดได้อย่างเสถียรและราบรื่น