dhammawatthumpra commited on
Commit
351a621
·
1 Parent(s): abb8b56

docs: update V2 arch doc to 2.2.0 — jina-v5, ST fallback, diagonal resize

Browse files
.socraticodecontextartifacts.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "artifacts": [
3
+ {
4
+ "name": "architecture-v2",
5
+ "path": "./TIPITAKA_WEB_ARCHITECTURE_V2.md",
6
+ "description": "Full V2 architecture — React + FastAPI + Qdrant + ONNX Reranker + DeepSeek"
7
+ },
8
+ {
9
+ "name": "design-system",
10
+ "path": "./DESIGN.md",
11
+ "description": "Design system — colors, typography, components, themes, spacing"
12
+ },
13
+ {
14
+ "name": "product-vision",
15
+ "path": "./PRODUCT.md",
16
+ "description": "Product vision, target users (monks/scholars), brand personality, design principles"
17
+ },
18
+ {
19
+ "name": "api-schemas",
20
+ "path": "./webapp/tipitaka-api/app/schemas.py",
21
+ "description": "Pydantic API schemas — PageResponse, SearchResult, TOCItem, VolumeInfo"
22
+ },
23
+ {
24
+ "name": "app-config",
25
+ "path": "./webapp/tipitaka-api/app/config.py",
26
+ "description": "App settings — LLM provider, paths, CORS, Qdrant config"
27
+ },
28
+ {
29
+ "name": "rag-service",
30
+ "path": "./webapp/tipitaka-api/app/services/rag_service.py",
31
+ "description": "RAG Service — hybrid search (FTS5 + Qdrant vector + ONNX reranker), embedding with Ollama/sentence-transformers fallback"
32
+ },
33
+ {
34
+ "name": "docker-deployment",
35
+ "path": "./Dockerfile",
36
+ "description": "Multi-stage Docker build: React frontend + Python FastAPI backend, target HF Space port 7860"
37
+ },
38
+ {
39
+ "name": "download-assets",
40
+ "path": "./webapp/tipitaka-api/download_assets.py",
41
+ "description": "Downloads DB + Qdrant snapshots + ONNX reranker from HF dataset at startup"
42
+ }
43
+ ]
44
+ }
TIPITAKA_WEB_ARCHITECTURE_V2.md CHANGED
@@ -1,10 +1,10 @@
1
  # Tipitaka Web Application Architecture V2
2
  ## Vite + FastAPI + Qdrant — "Book Sanctuary + Floating AI Consultant"
3
 
4
- > **Version:** 2.1.0
5
- > **Date:** 2026-05-07
6
  > **Status:** Production (HF Spaces)
7
- > **Based on:** TIPITAKA_WEB_ARCHITECTURE.md (V1), actual code base at commit `e04d706`
8
 
9
  ---
10
 
@@ -13,22 +13,14 @@
13
  ### 1.1 Vision
14
  Web Application สำหรับอ่านพระไตรปิฎก มจร. ที่ให้ประสบการณ์การอ่านแบบจิตวิเวก โดยมี AI เป็นผู้ช่วยลอยตัวที่ไม่รบกวนสมาธิ
15
 
16
- ### 1.2 Key Changes from V1
17
-
18
- | Layer | V1 (Planning) | V2 (Actual) |
19
- |-------|---------------|-------------|
20
- | **Vector Store** | ChromaDB | **Qdrant** (local/server + snapshot restore) |
21
- | **Search** | FTS5 + ChromaDB | **Hybrid: FTS5 + Qdrant Vector + ONNX Reranker** |
22
- | **Pali Processing** | Basic | **Full pipeline:** dict correction PyThaiNLP similarity guard |
23
- | **Embedding Model** | Qwen3-Embedding-0.6B | **jina-embeddings-v3** (Q4_K_M GGUF via Ollama) |
24
- | **Reranker** | None | **jina-reranker-v2-base-multilingual** (ONNX, CPU) |
25
- | **Frontend CSS** | Tailwind v3 + shadcn/ui | **Tailwind CSS v4 + `@tailwindcss/typography`** |
26
- | **State Management** | Zustand + React Query | **Zustand only** (lightweight) |
27
- | **PWA / Offline** | Planned | **Not implemented** (server-dependant) |
28
- | **Continuous Scroll** | Planned | **Page-by-page** (prev/next/swipe) |
29
- | **Deployment** | Local server | **HF Spaces** (Docker multi-stage) |
30
- | **AI Provider** | DeepSeek / Local | **DeepSeek only** (provider-agnostic via OpenAI client) |
31
- | **Embedding Cache** | None | **LRU cache** (256 entries, OrderedDict) |
32
 
33
  ### 1.3 Tech Stack
34
 
@@ -44,20 +36,25 @@ Web Application สำหรับอ่านพระไตรปิฎก ม
44
  └──────────────────────────┬───────────────────────────────────────┘
45
  │ HTTP / SSE Streaming
46
  ┌──────────────────────────┴───────────────────────────────────────┐
47
- │ SERVER (FastAPI Python 3.11) │
48
  │ │
49
- │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │
50
- │ │ FastAPI │ │ Local │ │ Qdrant │ │ Ollama Server │ │
51
- │ │ Routes │──│ Services │──│ (Vector) │──│ (jina-emb-v3) │ │
52
- │ └──────────┘ └──────────┘ └──────────┘ └────────────────┘ │
53
- │ │ │
54
- │ │ ├── SQLite FTS5 (tipitaka_mcu.db)
55
- │ │ ├── ONNX Reranker (CPU, jina-reranker-v2) │
56
- │ │ └── DeepSeek API (streaming)
57
  │ └── Static files (Served Vite build in production) │
58
  └──────────────────────────────────────────────────────────────────┘
59
  ```
60
 
 
 
 
 
 
61
  ---
62
 
63
  ## 2. System Architecture
@@ -73,14 +70,14 @@ graph TB
73
  ND[NavDrawer<br/>320px sidebar]
74
  RP[ReaderPanel<br/>Main reading area]
75
  RT[RightToolbar<br/>44px controls strip]
76
- AIP[AIPopup<br/>Draggable chat]
77
  SP[SelectionPopup<br/>AI from selection]
78
  end
79
  subgraph "State (Zustand)"
80
  RS[ReaderStore<br/>volume, page, content]
81
  TS[ThemeStore<br/>theme, fontSize]
82
  US[UIStore<br/>navOpen, activeTab]
83
- AS[AIStore<br/>messages, mode, width, pos]
84
  SS[SearchStore<br/>query, results, breakdown]
85
  end
86
  end
@@ -107,7 +104,7 @@ graph TB
107
  end
108
  subgraph "Data"
109
  DB[(SQLite<br/>tipitaka_mcu.db)]
110
- VDB[(Qdrant<br/>tipitaka_chunks)]
111
  SN[(Snapshots<br/>.snapshot files)]
112
  end
113
  end
@@ -124,7 +121,7 @@ graph TB
124
  R6 & R7 --> S5
125
  S5 --> S4 --> VDB
126
  S5 --> DS[(DeepSeek API)]
127
- S4 --> OLL[(Ollama jina-embeddings-v3)]
128
  S4 --> RER[(ONNX Reranker)]
129
  ```
130
 
@@ -136,6 +133,7 @@ sequenceDiagram
136
  participant Browser
137
  participant API as FastAPI
138
  participant DB as SQLite FTS5
 
139
  participant QD as Qdrant
140
  participant ONNX as ONNX Reranker
141
  participant LLM as DeepSeek API
@@ -147,8 +145,17 @@ sequenceDiagram
147
  API->>DB: MATCH query (30 results)
148
  DB-->>API: FTS rows + rank
149
  and Phase 2: Vector
150
- API->>QD: query_embedding(jina-embeddings-v3)
151
- QD-->>API: semantic hits (30 results)
 
 
 
 
 
 
 
 
 
152
  end
153
 
154
  API->>API: Merge & deduplicate by (vol, page)
@@ -229,7 +236,7 @@ tipitaka-web/
229
  │ │ └── RightToolbar.tsx # 44px fixed right: page nav, font size, theme
230
  │ │
231
  │ └── ai/
232
- │ └── AIPopup.tsx # Draggable + resizable chat with focus mode
233
 
234
  ├── hooks/
235
  │ ├── useKeyboardNav.ts # ← → PgUp PgDown Space
@@ -237,7 +244,7 @@ tipitaka-web/
237
 
238
  ├── stores/
239
  │ ├── appStore.ts # readerStore + themeStore + uiStore
240
- │ ├── aiStore.ts # AI messages, streaming, drag+resize state
241
  │ └── searchStore.ts # Search queries, results, suggestions
242
 
243
  └── lib/
@@ -294,13 +301,15 @@ tipitaka-web/
294
 
295
  #### AIPopup
296
  ```tsx
297
- // Floating, draggable + right-edge resizable chat panel
298
- // Two modes: fast (direct) / reasoner (deepseek-reasoner)
 
 
299
  // RAG toggle pill (green/yellow/red dot indicator)
300
  // Focus mode: Maximize button → full-screen overlay (markdown rendering)
301
  // Prose-invert for dark mode readability
302
  // Disclaimer: 10s initial popup with Kalama Sutta reference
303
- // Persisted: drag position + panel width (localStorage)
304
  // Quick prompts: อธิบาย / สรุป / วิเคราะห์ธรรม / ประยุกต์ใช้
305
  // Input + Send button
306
  // Width: 300px–80vw, default 400px
@@ -313,7 +322,7 @@ tipitaka-web/
313
  | **ReaderStore** | currentVolume, currentPage, currentContent, totalPages | No |
314
  | **ThemeStore** | theme (dark/light/classic), fontSize (12-36) | No |
315
  | **UIStore** | isNavOpen, activeTab (toc/search/overview) | No |
316
- | **AIStore** | messages, isStreaming, isOpen, mode, useRag, ragReady, dragPos, panelWidth | dragPos + panelWidth → localStorage |
317
  | **SearchStore** | query, results, totalResults, isLoading, breakdown, timeTaken, suggestions | No |
318
 
319
  ### 3.4 Theming System
@@ -351,7 +360,7 @@ tipitaka-api/
351
  │ │ ├── page_service.py # Page retrieval + TOC dedup + blank detection + format_content_html() pipeline
352
  │ │ ├── search_service.py # FTS5 + LIKE + autocorrect + snippet + highlight
353
  │ │ ├── pali_utils.py # Pali autocorrect dict + Thai digit utils + PyThaiNLP
354
- │ │ ├── rag_service.py # Qdrant init + embedding + hybrid query + rerank
355
  │ │ ├── llm_service.py # OpenAI-compatible streaming + RAG context injection
356
  │ │ └── onnx_reranker.py # jina-reranker-v2 ONNX CPU inference
357
  │ │
@@ -359,14 +368,14 @@ tipitaka-api/
359
  │ └── sqlite_db.py # SQLite connection + in-memory load + FTS5 + search_log
360
 
361
  ├── models/
362
- ── jina-reranker-v2-base-multilingual-Q4_K_M.gguf # Or ONNX dir
363
- │ └── jina-v2-onnx/ # ONNX model files
364
 
365
  ├── tests/
366
  │ ├── test_pali_utils.py # 90 assertions (autocorrect, Thai digits, etc.)
367
  │ ├── test_search_service.py # Search pipeline, merge, suggestions
368
  │ └── test_rag_service.py # Embedding cache test
369
 
 
370
  ├── startup.sh # HF Spaces entrypoint
371
  ├── requirements.txt
372
  └── .env / .env.example
@@ -407,7 +416,9 @@ query → Pali Autocorrect Pipeline:
407
 
408
  ```
409
  query → Phase 1: FTS5 (30 results)
410
- → Phase 2: Qdrant Vector (30 results, jina-embeddings-v3)
 
 
411
  → Merge & deduplicate by (volume, page)
412
  → Phase 3: ONNX Reranker (jina-reranker-v2)
413
  → Sort by rerank_score
@@ -463,25 +474,82 @@ raw content_html
463
  - FTS5: `pages_fts` (virtual table on `content_text`, `unicode61` tokenizer)
464
  - In-memory: Full DB loaded to `:memory:` via `sqlite3.backup()` at startup
465
  - Search log: Disk-persisted for autocomplete (frequency-ranked)
 
466
 
467
  #### Qdrant
468
  - Two modes: **Local** (embedded, snapshot restore) or **Server** (remote connection)
469
- - Collections: `tipitaka_chunks` (scripture chunks with volume/page metadata)
 
 
470
  - Autodetect: Checks `localhost:6333` if not configured
471
  - Snapshot restore: Extracts `.snapshot` files to storage directory on first run
472
- - Embedding: **jina-embeddings-v3** (Q4_K_M GGUF, 1024 dims via Ollama)
473
  - LRU cache: 256 entries (OrderedDict) — avoids re-embedding identical queries
474
 
475
  #### ONNX Reranker
476
  - Model: **jina-reranker-v2-base-multilingual** (ONNX export, CPU)
477
- - Location: `models/jina-v2-onnx/`
478
  - Fallback: Graceful if unavailable (skips reranking step)
479
 
480
  ---
481
 
482
- ## 5. Deployment
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
 
484
- ### 5.1 Hugging Face Spaces
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
 
486
  ```dockerfile
487
  # Dockerfile — multi-stage build
@@ -493,9 +561,12 @@ Stage 2: python:3.11-slim → pip install → startup.sh
493
  ```
494
 
495
  **startup.sh** downloads:
496
- - `tipitaka_mcu.db` (SQLite corpus, ~250MB)
497
- - `qdrant_storage/` (vector index snapshots, ~300MB)
498
- - `models/` (ONNX reranker binary, ~100MB)
 
 
 
499
 
500
  **Qdrant lock cleanup:** ก่อน start uvicorn, startup.sh ลบ Qdrant lock file ที่ค้างจาก session ก่อนหน้า:
501
  ```bash
@@ -503,7 +574,7 @@ rm -f "$QDRANT_PATH/.lock" 2>/dev/null || true
503
  ```
504
  ป้องกัน error `"already accessed by another instance"` หลัง HF Space restart
505
 
506
- ### 5.2 Environment Variables (.env)
507
 
508
  | Variable | Default | Description |
509
  |----------|---------|-------------|
@@ -516,12 +587,13 @@ rm -f "$QDRANT_PATH/.lock" 2>/dev/null || true
516
  | `QDRANT_PATH` | Auto-detect | Qdrant storage path |
517
  | `QDRANT_URL` | `None` | Remote Qdrant URL (optional) |
518
  | `SNAPSHOT_DIR` | Auto-detect | Snapshot restore directory |
 
519
  | `CORS_ORIGINS` | `"*"` | CORS allowed origins |
520
  | `SERVE_STATIC` | `false` | Serve built frontend (HF Spaces) |
521
  | `PORT` | `8000` | Server port |
522
  | `DEBUG` | `true` | Debug mode |
523
 
524
- ### 5.3 Path Auto-Detection
525
 
526
  Config intelligently resolves paths based on environment:
527
 
@@ -534,43 +606,52 @@ Config intelligently resolves paths based on environment:
534
 
535
  ---
536
 
537
- ## 6. Key Design Decisions
538
 
539
- ### 6.1 Why Qdrant over ChromaDB?
540
  - **Persistence**: Qdrant stores on disk reliably; ChromaDB had data-loss issues
541
  - **Dual mode**: Local (embedded, no Docker) + Server (production) — same API
542
  - **Snapshot restore**: `.snapshot` files enable deployment without reindexing
543
  - **Performance**: Faster vector search with tunable `score_threshold`
544
 
545
- ### 6.2 Why jina-embeddings-v3 over Qwen3-Embedding?
546
- - **Thai accuracy**: Confirmed better on Thai/Pali content (scores 0.75 vs lower)
547
- - **GGUF via Ollama**: No Python dependency, reusable across services
548
- - **LRU cache**: 256 entries mitigate Ollama latency
549
-
550
- ### 6.3 Why Page-by-Page over Continuous Scroll?
 
 
 
 
 
 
 
 
 
551
  - **Simplicity**: Avoids virtual scrolling complexity
552
  - **Performance**: One page loaded at a time (no memory buildup)
553
  - **Navigation**: Clear page markers, prev/next predictable
554
  - **Swipe**: Mobile gesture ← → works naturally with page boundaries
555
 
556
- ### 6.4 Why No PWA/Offline?
557
  - **Content is dynamic**: Pages rendered with AI context and search results
558
  - **Token-based auth**: Not applicable (public content, but still server-dependant)
559
  - **Snapshot data too large**: 250MB+ not practical for offline storage
560
 
561
- ### 6.5 Why Split First Page at "ขอนอบน้อม"?
562
  - **Universal**: Works for all 45 volumes — the homage line is the only consistent structural element on page 1
563
  - **Volume 34 edge case**: L0 section header appears *before* the homage (unique among all volumes) — splitting at L0 would break. Splitting at homage handles this seamlessly
564
  - **Visual hierarchy**: Homage belongs to the top "ceremonial" zone (centered, clean); actual content starts below with normal styling
565
 
566
- ### 6.6 Why text-justify for Body Text?
567
  - **Reading experience**: Justified text mimics printed Tipitaka books, creating a familiar sacred-text feel
568
  - **CSS**: `text-justify: inter-character` ensures even spacing in Thai script (where inter-word gaps would look uneven due to lack of explicit word boundaries in Thai)
569
  - **User preference**: ผู้ใช้ request ให้จัด justify (2026-05-07)
570
 
571
  ---
572
 
573
- ## 7. Test Coverage
574
 
575
  | Module | Tests | What it covers |
576
  |--------|-------|----------------|
 
1
  # Tipitaka Web Application Architecture V2
2
  ## Vite + FastAPI + Qdrant — "Book Sanctuary + Floating AI Consultant"
3
 
4
+ > **Version:** 2.2.0
5
+ > **Date:** 2026-05-15
6
  > **Status:** Production (HF Spaces)
7
+ > **Based on:** TIPITAKA_WEB_ARCHITECTURE_V2.md (2.1.0), actual code base at commit `abb8b56`
8
 
9
  ---
10
 
 
13
  ### 1.1 Vision
14
  Web Application สำหรับอ่านพระไตรปิฎก มจร. ที่ให้ประสบการณ์การอ่านแบบจิตวิเวก โดยมี AI เป็นผู้ช่วยลอยตัวที่ไม่รบกวนสมาธิ
15
 
16
+ ### 1.2 Key Changes from V2.1.0
17
+
18
+ | Layer | V2.1.0 | V2.2.0 |
19
+ |-------|--------|--------|
20
+ | **Embedding Model** | jina-embeddings-v3 (GGUF, Q4_K_M) | **jina-embeddings-v5-small-retrieval** (GGUF Q6_K / PyTorch) |
21
+ | **Embedding Mode** | Ollama only | **Dual-mode:** Ollama (local) sentence-transformers fallback (HF Space) |
22
+ | **AI Popup Resize** | Right-edge only | **Vertical + Diagonal + Right-edge** resize, **persisted** panel size (localStorage) |
23
+ | **Python Version** | 3.11 | 3.13 (ใหม่หลัง reinstall) |
 
 
 
 
 
 
 
 
24
 
25
  ### 1.3 Tech Stack
26
 
 
36
  └──────────────────────────┬───────────────────────────────────────┘
37
  │ HTTP / SSE Streaming
38
  ┌──────────────────────────┴───────────────────────────────────────┐
39
+ │ SERVER (FastAPI Python 3.13) │
40
  │ │
41
+ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌────────────┐ │
42
+ │ │ FastAPI │ │ Local │ │ Qdrant │ │ Embedding │ │
43
+ │ │ Routes │──│ Services │──│ (Vector) │──│ Provider │ │
44
+ │ └──────────┘ └──────────┘ └──────────────┘ └────────────┘ │
45
+ │ │ │ ┌────────────┐
46
+ │ │ ├── SQLite FTS5 (tipitaka_mcu.db)
47
+ │ │ ├── ONNX Reranker (CPU, jina-reranker-v2)
48
+ │ │ └── DeepSeek API (streaming)
49
  │ └── Static files (Served Vite build in production) │
50
  └──────────────────────────────────────────────────────────────────┘
51
  ```
52
 
53
+ **Embedding Providers:**
54
+ - **Ollama** (local dev): `jina-embeddings-v5-small-retrieval` GGUF Q6_K (596MB)
55
+ - **sentence-transformers** (HF Space / fallback): `jinaai/jina-embeddings-v5-text-small-retrieval` PyTorch
56
+ - ทั้งคู่ produce 1024-dims vectors ค่า `EMBED_DIMS = 1024`
57
+
58
  ---
59
 
60
  ## 2. System Architecture
 
70
  ND[NavDrawer<br/>320px sidebar]
71
  RP[ReaderPanel<br/>Main reading area]
72
  RT[RightToolbar<br/>44px controls strip]
73
+ AIP[AIPopup<br/>Draggable + resizable chat]
74
  SP[SelectionPopup<br/>AI from selection]
75
  end
76
  subgraph "State (Zustand)"
77
  RS[ReaderStore<br/>volume, page, content]
78
  TS[ThemeStore<br/>theme, fontSize]
79
  US[UIStore<br/>navOpen, activeTab]
80
+ AS[AIStore<br/>messages, mode, width, pos, panelSize]
81
  SS[SearchStore<br/>query, results, breakdown]
82
  end
83
  end
 
104
  end
105
  subgraph "Data"
106
  DB[(SQLite<br/>tipitaka_mcu.db)]
107
+ VDB[(Qdrant<br/>tipitaka_chunks<br/>tipitaka_scripture)]
108
  SN[(Snapshots<br/>.snapshot files)]
109
  end
110
  end
 
121
  R6 & R7 --> S5
122
  S5 --> S4 --> VDB
123
  S5 --> DS[(DeepSeek API)]
124
+ S4 --> EMB[(Embedding<br/>Ollama v5 / ST fallback)]
125
  S4 --> RER[(ONNX Reranker)]
126
  ```
127
 
 
133
  participant Browser
134
  participant API as FastAPI
135
  participant DB as SQLite FTS5
136
+ participant EMB as Embedding
137
  participant QD as Qdrant
138
  participant ONNX as ONNX Reranker
139
  participant LLM as DeepSeek API
 
145
  API->>DB: MATCH query (30 results)
146
  DB-->>API: FTS rows + rank
147
  and Phase 2: Vector
148
+ API->>EMB: query_embedding()
149
+ alt Ollama available
150
+ EMB->>OLL: jina-embeddings-v5 GGUF
151
+ OLL-->>EMB: 1024-dims vector
152
+ else Fallback
153
+ EMB->>ST: sentence-transformers PyTorch
154
+ ST-->>EMB: 1024-dims vector
155
+ end
156
+ EMB-->>API: embedding vector
157
+ API->>QD: search with vector (30 results)
158
+ QD-->>API: semantic hits
159
  end
160
 
161
  API->>API: Merge & deduplicate by (vol, page)
 
236
  │ │ └── RightToolbar.tsx # 44px fixed right: page nav, font size, theme
237
  │ │
238
  │ └── ai/
239
+ │ └── AIPopup.tsx # Draggable + vertically/diagonally resizable chat
240
 
241
  ├── hooks/
242
  │ ├── useKeyboardNav.ts # ← → PgUp PgDown Space
 
244
 
245
  ├── stores/
246
  │ ├── appStore.ts # readerStore + themeStore + uiStore
247
+ │ ├── aiStore.ts # AI messages, streaming, drag+resize state, panelSize
248
  │ └── searchStore.ts # Search queries, results, suggestions
249
 
250
  └── lib/
 
301
 
302
  #### AIPopup
303
  ```tsx
304
+ // Floating, draggable chat panel
305
+ // Three resize modes: right-edge drag (horizontal), bottom-edge drag (vertical), corner drag (diagonal)
306
+ // Panel size persisted in localStorage
307
+ // Two modes: fast (deepseek-chat) / reasoner (deepseek-reasoner)
308
  // RAG toggle pill (green/yellow/red dot indicator)
309
  // Focus mode: Maximize button → full-screen overlay (markdown rendering)
310
  // Prose-invert for dark mode readability
311
  // Disclaimer: 10s initial popup with Kalama Sutta reference
312
+ // Persisted: drag position + panel width + panel height (localStorage)
313
  // Quick prompts: อธิบาย / สรุป / วิเคราะห์ธรรม / ประยุกต์ใช้
314
  // Input + Send button
315
  // Width: 300px–80vw, default 400px
 
322
  | **ReaderStore** | currentVolume, currentPage, currentContent, totalPages | No |
323
  | **ThemeStore** | theme (dark/light/classic), fontSize (12-36) | No |
324
  | **UIStore** | isNavOpen, activeTab (toc/search/overview) | No |
325
+ | **AIStore** | messages, isStreaming, isOpen, mode, useRag, ragReady, dragPos, panelWidth, panelHeight | dragPos + panelWidth + panelHeight → localStorage |
326
  | **SearchStore** | query, results, totalResults, isLoading, breakdown, timeTaken, suggestions | No |
327
 
328
  ### 3.4 Theming System
 
360
  │ │ ├── page_service.py # Page retrieval + TOC dedup + blank detection + format_content_html() pipeline
361
  │ │ ├── search_service.py # FTS5 + LIKE + autocorrect + snippet + highlight
362
  │ │ ├── pali_utils.py # Pali autocorrect dict + Thai digit utils + PyThaiNLP
363
+ │ │ ├── rag_service.py # Qdrant init + dual-mode embedding + hybrid query + rerank
364
  │ │ ├── llm_service.py # OpenAI-compatible streaming + RAG context injection
365
  │ │ └── onnx_reranker.py # jina-reranker-v2 ONNX CPU inference
366
  │ │
 
368
  │ └── sqlite_db.py # SQLite connection + in-memory load + FTS5 + search_log
369
 
370
  ├── models/
371
+ ── jina-v2-onnx/ # ONNX reranker model files (~267MB)
 
372
 
373
  ├── tests/
374
  │ ├── test_pali_utils.py # 90 assertions (autocorrect, Thai digits, etc.)
375
  │ ├── test_search_service.py # Search pipeline, merge, suggestions
376
  │ └── test_rag_service.py # Embedding cache test
377
 
378
+ ├── download_assets.py # Downloads DB + snapshots + reranker from HF dataset
379
  ├── startup.sh # HF Spaces entrypoint
380
  ├── requirements.txt
381
  └── .env / .env.example
 
416
 
417
  ```
418
  query → Phase 1: FTS5 (30 results)
419
+ → Phase 2: Embedding → Qdrant Vector (30 results, jina-embeddings-v5)
420
+ ├── Try Ollama (jina-embeddings-v5 GGUF, timeout 5s)
421
+ └── Fallback → sentence-transformers (jina-embeddings-v5 PyTorch)
422
  → Merge & deduplicate by (volume, page)
423
  → Phase 3: ONNX Reranker (jina-reranker-v2)
424
  → Sort by rerank_score
 
474
  - FTS5: `pages_fts` (virtual table on `content_text`, `unicode61` tokenizer)
475
  - In-memory: Full DB loaded to `:memory:` via `sqlite3.backup()` at startup
476
  - Search log: Disk-persisted for autocomplete (frequency-ranked)
477
+ - File size: ~238MB
478
 
479
  #### Qdrant
480
  - Two modes: **Local** (embedded, snapshot restore) or **Server** (remote connection)
481
+ - Collections:
482
+ - `tipitaka_chunks` (scripture chunks, ~41K points, 460MB snapshot)
483
+ - `tipitaka_scripture` (raw scripture, ~25K points, 496MB snapshot)
484
  - Autodetect: Checks `localhost:6333` if not configured
485
  - Snapshot restore: Extracts `.snapshot` files to storage directory on first run
486
+ - Embeding: **jina-embeddings-v5-small-retrieval** (1024 dims) dual-mode
487
  - LRU cache: 256 entries (OrderedDict) — avoids re-embedding identical queries
488
 
489
  #### ONNX Reranker
490
  - Model: **jina-reranker-v2-base-multilingual** (ONNX export, CPU)
491
+ - Location: `models/jina-v2-onnx/` (~267MB)
492
  - Fallback: Graceful if unavailable (skips reranking step)
493
 
494
  ---
495
 
496
+ ## 5. Dual-Mode Embedding System (NEW in 2.2.0)
497
+
498
+ ### 5.1 Motivation
499
+
500
+ ปัญหา: บน local ใช้ Ollama ทำ embedding ได้ แต่ **HF Space ไม่มี Ollama**
501
+ ทางแก้: เพิ่ม sentence-transformers fallback — เขียนใน `_get_embedding()` ของ RAGService
502
+
503
+ ### 5.2 Strategy
504
+
505
+ ```python
506
+ def _get_embedding(self, text: str) -> list:
507
+ # ✅ Try 1: Ollama (local dev) — timeout 5s
508
+ try:
509
+ response = httpx.post(f"{self.ollama_url}/api/embed", ...)
510
+ return embedding
511
+ except Exception:
512
+ pass # → fallback
513
+
514
+ # ✅ Try 2: sentence-transformers (HF Space / no Ollama)
515
+ if self._st_model is None:
516
+ from sentence_transformers import SentenceTransformer
517
+ self._st_model = SentenceTransformer(
518
+ "jinaai/jina-embeddings-v5-text-small-retrieval"
519
+ )
520
+ return self._st_model.encode(text, normalize_embeddings=True).tolist()
521
+ ```
522
+
523
+ ### 5.3 Model Details
524
+
525
+ | | Ollama Mode | Sentence-Transformers Mode |
526
+ |---|---|---|
527
+ | **Model** | `jina-embeddings-v5-small-retrieval` (GGUF) | `jina-embeddings-v5-text-small-retrieval` (PyTorch) |
528
+ | **Size** | 596MB (Q6_K) | ~600MB |
529
+ | **Dimensions** | 1024 | 1024 |
530
+ | **Load type** | External process (Ollama) | In-process (Python) |
531
+ | **RAM usage** | ~700MB (Ollama process) | ~1.2GB |
532
+ | **Speed** | ~50ms (network call) | ~100ms (first call slower) |
533
+ | **Cache** | LRU 256 entries — both modes | Shared |
534
 
535
+ ### 5.4 Configuration
536
+
537
+ ```python
538
+ # rag_service.py — constants
539
+ OLLAMA_URL = "http://localhost:11434"
540
+ EMBED_MODEL = "hf.co/jinaai/...-v5-small-retrieval-Q6_K.gguf"
541
+ ST_EMBED_MODEL = "jinaai/jina-embeddings-v5-text-small-retrieval"
542
+ EMBED_DIMS = 1024 # ตรวจสอบแล้วว่าทั้งสอง mode ใช้ 1024 dims
543
+ ```
544
+
545
+ - `OLLAMA_URL` สามารถ override ได้ผ่าน environment variable
546
+ - ถ้า `sentence-transformers` ก็ไม่ต้อง setup อะไรเพิ่ม — โหลด model auto จาก HF Hub
547
+
548
+ ---
549
+
550
+ ## 6. Deployment
551
+
552
+ ### 6.1 Hugging Face Spaces
553
 
554
  ```dockerfile
555
  # Dockerfile — multi-stage build
 
561
  ```
562
 
563
  **startup.sh** downloads:
564
+ - `tipitaka_mcu.db` (SQLite corpus, ~238MB)
565
+ - `qdrant/tipitaka_chunks.snapshot` (vector index, ~460MB)
566
+ - `qdrant/tipitaka_scripture.snapshot` (vector index, ~496MB)
567
+ - ONNX reranker via huggingface_hub (~267MB)
568
+
569
+ ทั้งหมดดึงมาจาก dataset: `dhammawatthumpra/tipitaka-storage`
570
 
571
  **Qdrant lock cleanup:** ก่อน start uvicorn, startup.sh ลบ Qdrant lock file ที่ค้างจาก session ก่อนหน้า:
572
  ```bash
 
574
  ```
575
  ป้องกัน error `"already accessed by another instance"` หลัง HF Space restart
576
 
577
+ ### 6.2 Environment Variables (.env)
578
 
579
  | Variable | Default | Description |
580
  |----------|---------|-------------|
 
587
  | `QDRANT_PATH` | Auto-detect | Qdrant storage path |
588
  | `QDRANT_URL` | `None` | Remote Qdrant URL (optional) |
589
  | `SNAPSHOT_DIR` | Auto-detect | Snapshot restore directory |
590
+ | `OLLAMA_URL` | `http://localhost:11434` | Ollama endpoint (override for HF/docker) |
591
  | `CORS_ORIGINS` | `"*"` | CORS allowed origins |
592
  | `SERVE_STATIC` | `false` | Serve built frontend (HF Spaces) |
593
  | `PORT` | `8000` | Server port |
594
  | `DEBUG` | `true` | Debug mode |
595
 
596
+ ### 6.3 Path Auto-Detection
597
 
598
  Config intelligently resolves paths based on environment:
599
 
 
606
 
607
  ---
608
 
609
+ ## 7. Key Design Decisions
610
 
611
+ ### 7.1 Why Qdrant over ChromaDB?
612
  - **Persistence**: Qdrant stores on disk reliably; ChromaDB had data-loss issues
613
  - **Dual mode**: Local (embedded, no Docker) + Server (production) — same API
614
  - **Snapshot restore**: `.snapshot` files enable deployment without reindexing
615
  - **Performance**: Faster vector search with tunable `score_threshold`
616
 
617
+ ### 7.2 Why jina-embeddings-v5 over jina-embeddings-v3?
618
+ - **Newer model**: v5-small-retrieval (Oct 2024) vs v3 (Mar 2024)
619
+ - **1024 dims เหมือนกัน**: ไม่ต้อง re-index Qdrant
620
+ - **Thai/Pali accuracy**: Confirmed same dimension space, better retrieval quality
621
+ - **ยังคงมี GGUF ผ่าน Ollama**: สำหรับ local dev
622
+ - **v5 ยังมี PyTorch บน HF Hub**: `jinaai/jina-embeddings-v5-text-small-retrieval` ใช้กับ sentence-transformers ได้
623
+
624
+ ### 7.3 Why sentence-transformers Fallback?
625
+ - **HF Space ไม่มี Ollama**: ต้องมี embedding source อื่น
626
+ - **No extra dependencies**: `sentence-transformers>=3.0` + `torch>=2.0` มีใน requirements.txt อยู่แล้ว
627
+ - **One model, two runtimes**: ใช้ jina-embeddings-v5 ทั้งใน Ollama (GGUF) และ PyTorch — vector space เดียวกัน
628
+ - **Seamless fallback**: ถ้า Ollama ไม่ว่าง → ใช้ ST โดยอัตโนมัติ, user ไม่รู้สึก
629
+ - **Benchmark**: Ollama OFF (15.9s) vs Ollama ON (22.3s) — fallback เร็วกว่าเพราะ in-process
630
+
631
+ ### 7.4 Why Page-by-Page over Continuous Scroll?
632
  - **Simplicity**: Avoids virtual scrolling complexity
633
  - **Performance**: One page loaded at a time (no memory buildup)
634
  - **Navigation**: Clear page markers, prev/next predictable
635
  - **Swipe**: Mobile gesture ← → works naturally with page boundaries
636
 
637
+ ### 7.5 Why No PWA/Offline?
638
  - **Content is dynamic**: Pages rendered with AI context and search results
639
  - **Token-based auth**: Not applicable (public content, but still server-dependant)
640
  - **Snapshot data too large**: 250MB+ not practical for offline storage
641
 
642
+ ### 7.6 Why Split First Page at "ขอนอบน้อม"?
643
  - **Universal**: Works for all 45 volumes — the homage line is the only consistent structural element on page 1
644
  - **Volume 34 edge case**: L0 section header appears *before* the homage (unique among all volumes) — splitting at L0 would break. Splitting at homage handles this seamlessly
645
  - **Visual hierarchy**: Homage belongs to the top "ceremonial" zone (centered, clean); actual content starts below with normal styling
646
 
647
+ ### 7.7 Why text-justify for Body Text?
648
  - **Reading experience**: Justified text mimics printed Tipitaka books, creating a familiar sacred-text feel
649
  - **CSS**: `text-justify: inter-character` ensures even spacing in Thai script (where inter-word gaps would look uneven due to lack of explicit word boundaries in Thai)
650
  - **User preference**: ผู้ใช้ request ให้จัด justify (2026-05-07)
651
 
652
  ---
653
 
654
+ ## 8. Test Coverage
655
 
656
  | Module | Tests | What it covers |
657
  |--------|-------|----------------|