dhammawatthumpra commited on
Commit
9b44273
·
1 Parent(s): 2f3b92c

Expand POPULAR_TERMS: 40 -> 293 via 84000.org dictionary

Browse files
tipatika-wiki/wiki/concepts/popular-terms-seed.md ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ type: concept
3
+ title: POPULAR_TERMS — คำศัพท์ตั้งต้นสำหรับ Autocomplete
4
+ tags: [search, seed-data, buddhism, dictionary]
5
+ related: [search-autocomplete, search-pipeline]
6
+ created: 2026-04-30
7
+ updated: 2026-04-30
8
+ ---
9
+
10
+ # POPULAR_TERMS — คำศัพท์ตั้งต้น
11
+
12
+ ## Description
13
+
14
+ List **293** คำศัพท์พระไตรปิฎกที่ curate ไว้ใน `search_service.py` เป็น fallback เมื่อ `search_log` ยังว่างหรือ match ไม่ครบ
15
+
16
+ ## Location
17
+
18
+ `webapp/tipitaka-api/app/services/search_service.py` → `POPULAR_TERMS: List[str]`
19
+
20
+ ## Source
21
+
22
+ | Source | Count |
23
+ |--------|-------|
24
+ | Core curated terms (original v1) | 40 |
25
+ | 84000.org dictionary topic headings (v2 expansion) | 253 |
26
+ | **Total** | **293** |
27
+
28
+ คำศัพท์ 253 คำใหม่มาจากหัวข้อ (topic headings) ของ [พจนานุกรมพุทธศาสตร์ ฉบับประมวลธรรม](https://84000.org/tipitaka/dic/) โดยตัดเลขหมวด (varga numbers) ออก ทำ unique และ filter กับรายการเดิม
29
+
30
+ ## รายการปัจจุบัน (2026-04-30)
31
+
32
+ ### หมวด 1 — หลักธรรมพื้นฐาน (40 คำ — original)
33
+
34
+ อริยสัจ, นิพพาน, ไตรลักษณ์, อนิจจัง, ทุกข์, อนัตตา, สติปัฏฐาน, อัฏฐมรรค, โพชฌงค์, ปฏิจจสมุปบาท, สมาธิ, วิปัสสนา, สมถะ, ศีล, ปัญญา, กรรม, วิบาก, กิเลส, ตัณหา, อวิชชา, โพธิสัตว์, ปรินิพพาน, สังสารวัฏ, มรนสติ, เมตตา, กรุณา, มุทิตา, อุเบกขา, อภิธรรม, วินัยปิฎก, สุตตันตปิฎก, ขันธ์, อายตนะ, ธาตุ, อินทรีย์, โพธิปักขิยธรรม, สัมมาสมาธิ, สัมมาสติ, บารมี, เจตสิก, จิต, รูป, นาม
35
+
36
+ ### หมวด 2 — จาก 84000.org dictionary (253 คำ — v2 expansion)
37
+
38
+ กถาวัตถุ, กรรมกิเลส, กรรมฐาน, กสิณ, กัลยาณมิตตตา, กัลยาณมิตรธรรม, กาม, กามคุณ, กามโภคี, กาลามสูตรกังขานิยฐาน, กิจ, กิจในอริยสัจจ์, กุลจิรัฏฐิติธรรม, กุศลกรรมบถ, กุศลมูล, กุศลวิตก, คารวะ, ฆราวาสธรรม, จรณะ, จริต, จักขุ, จักร, จักรวรรดิวัตร, ฌาน, ฌาน 2 ประเภท, ญาณ, ถูปารหบุคคล, ทวาร, ทศพลญาณ, ทักขิณาวิสุทธิ, ทาน, ทิฏฐธัมมิกัตถสังวัตตนิกธรรม, ทิฏฐิ, ทิศ, ที่สุด, ทุกขตา, ทุจริต, ธรรม, ธรรมขันธ์, ธรรมคุณ, ธรรมคุ้มครองโลก, ธรรมทำให้งาม, ธรรมนิยาม, ธรรมมีอุปการะมาก, ธรรมสมาทาน, ธรรมสมาธิ, ธรรมสวนานิสงส์, ธรรมเทสกธรรม, ธาตุกัมมัฏฐาน, ธุดงค์, ธุระ, นวกภิกขุธรรม, นวังคสัตถุสาสน์, นาถกรณธรรม, นิมิต, นิยาม, นิวรณ์, นิโรธ, บริษัท, บุคคล, บุคคลหาได้ยาก, บุญกิริยาวัตถุ, บุตร, บุพนิมิตแห่งมรรค, บูชา, ปฏิปทา, ปฏิสันถาร, ปฏิสัมภิทา, ปธาน, ปปัญจะ, ปปัญจธรรม, ปรมัตถธรรม, ประมาณ, ปริญญา, ปริเยสนา, ปัจจัย, ปัจจัยให้เกิดสัมมาทิฏฐิ, ปัพพชิตอภิณหปัจจเวกขณ์, ปาฏิหาริย์, ปาปณิกธรรม, ปาพจน์, ปาริสุทธิศีล, ปิยรูป สาตรูป, ปีติ, ผล, พร, พรหมวิหาร, พละ, พละ 5 ของพระมหากษัตริย์, พหูสูตมีองค์, พุทธคุณ, พุทธจริยา, พุทธโอวาท, ภพ, ภรรยา, ภัพพ���าธรรม, ภาวนา, ภูมิ, มงคล, มรรค, มรรคมีองค์, มละ, มหาปเทส, มหาปเทส 4 เฉพาะในทางพระวินัย, มหาภูต, มัจฉริยะ, มานะ, มาร, มิจฉัตตะ, มิตรปฏิรูปก์, รัตนตรัย, ราชธรรม, ฤทธิ์, ลักษณะตัดสินธรรมวินัย, ลัทธินอกพระพุทธศาสนา, ลีลาการสอน, วณิชชา, วรรณะ, วัฏฏะ, วัฒนมุข, วัตถุประสงค์ในการบัญญัติวินัย, วิชชา, วิญญาณ, วิญญาณฐิติ, วิธีปฏิบัติต่อทุกข์-สุข, วิบัติ, วิปัลลาส, วิปัสสนาญาณ, วิปัสสนูปกิเลส, วิมุตติ, วิรัติ, วิสุทธิ, วิเวก, วิโมกข์, วุฒิ, ศรัทธา, ศีล 8 ทั้งอาชีวะ, สกทาคามี, สมชีวิธรรม, สมบัติ, สมาธิภาวนา, สมาบัติ, สรณะ, สวรรค์, สังขตลักษณะ, สังขาร, สังคหวัตถุ, สังคหวัตถุของผู้ครองแผ่นดิน, สังคหะ, สังฆคุณ, สังวร, สังเวชนียสถาน, สังโยชน์, สัจจะ, สัญญา, สัตตาวาส, สัทธรรม, สันโดษ, สัปปายะ, สัปปุริสทาน, สัปปุริสธรรม, สัปปุริสบัญญัติ, สัมปชัญญะ, สัมปทา, สัมปรายิกัตถสังวัตตนิกธรรม, สัมผัส, สัมมัตตะ, สารณียธรรม, สาสน์, สิกขา, สุข, สุขของคฤหัสถ์, สุจริต, สุทธาวาส, สุทธิ, สุหทมิตร, อกุศลกรรมบถ, อกุศลมูล, อกุศลวิตก, อคติ, อธิปไตย, อธิษฐาน, อนันตริยกรรม, อนาคามี, อนุตตริยะ, อนุบุพพวิหาร, อนุปุพพิกถา, อนุสติ, อนุสัย, อบาย, อบายมุข, อปริหานิยธรรม, อปัณณกปฏิปทา, อปัสเสนะ, อภิญญา, อภิฐาน, อภิณหปัจจเวกขณ์, อภิสังขาร, อรหันต์, อริยทรัพย์, อริยบุคคล, อริยวงค์, อริยวัฑฒิ, อริยสัจจ์, อรูป, อสังขตลักษณะ, อสุภะ, อัคคิ, อัตถะ, อันตคาหิกทิฏฐิ, อันตรายของภิกษุสามเณรผู้บวชใหม่, อัปปมาทะ, อาการที่พระพุทธเจ้าทรงสั่งสอน, อาจารย์, อานาปานสติ 16 ฐาน, อายตนะภายนอก, อายตนะภายใน, อายุสสธรรม, อาวาสิกธรรม, อาสวะ, อาหาร, อิทธิบาท, อุบาสกธรรม, อุปกิเลส, อุปัญญาตธรรม, อุปาทาน, อุปาทารูป, เจดีย์, เจตนา, เถรธรรม, เทพ, เทวทูต, เทศนา, เบญจธรรม, เมถุนสังโยค, เวทนา, เวปุลลธรรม, เวปุลละ, เวสารัชชกรณธรรม, เวสารัชชะ, โกศล, โภควิภาค, โภคอาทิยะ, โยคะ, โยนิ, โยนิโสมนสิการ, โลก, โลกธรรม, โลกุตตรธรรม, โสดาบัน, โสตาปัตติยังคะ, โอฆะ, ไตรปิฎก
39
+
40
+ ## หลักเกณฑ์การเพิ่ม
41
+
42
+ 1. **ที่มาเด่น**: คำศัพท์จาก [84000.org/tipitaka/dic/](https://84000.org/tipitaka/dic/) — หัวข้อหมวดธรรม (topic headings) โดยตัดเลขหมวดออก
43
+ 2. **ไม่มีเลขหมวด**: ตัดตัวเลขท้าย เช่น "กรรม 2" → "กรรม", "กาม 2" → "กาม"
44
+ 3. **ไม่มี cross-reference**: ตัดตัวเลขวงเล็บ เช่น "ธรรมมีอุปการะมาก 7 [292]" → "ธรรมมีอ���ปการะมาก"
45
+ 4. **deduplicate**: คำที่มีอยู่แล้วใน POPULAR_TERMS ไม่เพิ่มซ้ำ
46
+ 5. **ควรมีผลลัพธ์ใน DB**: คำที่ seed ควรตรงกับข้อมูลใน tipitaka_mcu.db จริง
tipatika-wiki/wiki/log.md ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Research Log
2
+
3
+ ## 2026-04-30
4
+
5
+ - Project created
6
+ - Initialized `purpose.md` — defined project scope: RAG-based Q&A system for Pali Tipitaka, covering architecture, themes, milestones, and vision
7
+ - Corrected `purpose.md` — repo URL changed from GitHub to Hugging Face Spaces (https://huggingface.co/spaces/dhammawatthumpra/tipitaka)
8
+
9
+ ## 2026-04-30 (ingest | Search Autocomplete Feature)
10
+
11
+ Pages written: [[search-autocomplete]], [[popular-terms-seed]], [[search-pipeline]]
12
+ Pages updated: index.md, overview.md
13
+
14
+ Files changed in codebase:
15
+ - `sqlite_db.py` — get_disk_connection(), ensure_search_log_table()
16
+ - `search_service.py` — POPULAR_TERMS, get_suggestions(), log_search(), search() restructured
17
+ - `routers/search.py` — GET /api/search/suggestions endpoint
18
+ - `main.py` — ensure_search_log_table() on startup
19
+ - `searchStore.ts` — suggestions state + fetchSuggestions()
20
+ - `NavDrawer.tsx` — debounce, dropdown UI, keyboard nav
21
+
22
+ ## 2026-04-30 (expand | 84000 Dictionary → POPULAR_TERMS)
23
+
24
+ Pages updated: [[popular-terms-seed]]
25
+
26
+ Summary: Scraped 359 topic headings from [พจนานุกรมพุทธศาสตร์ ฉบับประมวลธรรม](https://84000.org/tipitaka/dic/), stripped varga numbers, deduplicated, and merged with existing POPULAR_TERMS (40 terms → 293 terms total).
27
+
28
+ Files changed in codebase:
29
+ - `search_service.py` — POPULAR_TERMS expanded from 40 → 293 terms (253 new from dictionary)
webapp/tipitaka-api/app/database/sqlite_db.py CHANGED
@@ -56,6 +56,32 @@ class SQLiteDB:
56
 
57
  self._mem_conn = mem
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  @property
60
  def is_in_memory(self) -> bool:
61
  return self._mem_conn is not None
 
56
 
57
  self._mem_conn = mem
58
 
59
+ @contextmanager
60
+ def get_disk_connection(self) -> Generator[sqlite3.Connection, None, None]:
61
+ """Always connects to disk — used for persistent writes (e.g. search_log)."""
62
+ conn = sqlite3.connect(self.db_path)
63
+ conn.row_factory = sqlite3.Row
64
+ try:
65
+ yield conn
66
+ conn.commit()
67
+ finally:
68
+ conn.close()
69
+
70
+ def ensure_search_log_table(self) -> None:
71
+ """Create search_log table on disk if not exists."""
72
+ with sqlite3.connect(self.db_path) as conn:
73
+ conn.execute("""
74
+ CREATE TABLE IF NOT EXISTS search_log (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ query TEXT NOT NULL COLLATE NOCASE,
77
+ count INTEGER NOT NULL DEFAULT 1,
78
+ last_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
79
+ UNIQUE(query)
80
+ )
81
+ """)
82
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_search_log_query ON search_log(query)")
83
+ conn.commit()
84
+
85
  @property
86
  def is_in_memory(self) -> bool:
87
  return self._mem_conn is not None
webapp/tipitaka-api/app/main.py CHANGED
@@ -21,6 +21,8 @@ async def lifespan(app: FastAPI):
21
  logger.info("Initializing database...")
22
  db.load_to_memory()
23
  logger.info("Database loaded to memory — all queries now run in-memory.")
 
 
24
 
25
  yield
26
  # Shutdown logic (if any)
 
21
  logger.info("Initializing database...")
22
  db.load_to_memory()
23
  logger.info("Database loaded to memory — all queries now run in-memory.")
24
+ db.ensure_search_log_table()
25
+ logger.info("Search log table ready.")
26
 
27
  yield
28
  # Shutdown logic (if any)
webapp/tipitaka-api/app/routers/search.py CHANGED
@@ -1,4 +1,5 @@
1
  from fastapi import APIRouter, Depends, Query
 
2
  from app.schemas import SearchResponse
3
  from app.services.search_service import SearchService
4
  from app.database.sqlite_db import get_db, SQLiteDB
@@ -8,6 +9,15 @@ router = APIRouter(prefix="/search", tags=["Search"])
8
  def get_search_service(db: SQLiteDB = Depends(get_db)) -> SearchService:
9
  return SearchService(db)
10
 
 
 
 
 
 
 
 
 
 
11
  @router.get("", response_model=SearchResponse)
12
  def search(
13
  q: str = Query(..., min_length=1),
 
1
  from fastapi import APIRouter, Depends, Query
2
+ from typing import List
3
  from app.schemas import SearchResponse
4
  from app.services.search_service import SearchService
5
  from app.database.sqlite_db import get_db, SQLiteDB
 
9
  def get_search_service(db: SQLiteDB = Depends(get_db)) -> SearchService:
10
  return SearchService(db)
11
 
12
+ @router.get("/suggestions", response_model=List[str])
13
+ def suggestions(
14
+ q: str = Query(..., min_length=1),
15
+ limit: int = Query(8, ge=1, le=20),
16
+ service: SearchService = Depends(get_search_service)
17
+ ):
18
+ """Autocomplete suggestions based on search history + curated terms."""
19
+ return service.get_suggestions(q, limit)
20
+
21
  @router.get("", response_model=SearchResponse)
22
  def search(
23
  q: str = Query(..., min_length=1),
webapp/tipitaka-api/app/services/search_service.py CHANGED
@@ -1,5 +1,6 @@
1
  import time
2
  import re
 
3
  from app.database.sqlite_db import SQLiteDB
4
  from app.schemas import SearchResponse, SearchResultItem
5
 
@@ -55,10 +56,153 @@ def to_thai_digits(s: str) -> str:
55
  return s.translate(str.maketrans("0123456789", "๐๑๒๓๔๕๖๗๘๙"))
56
 
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  class SearchService:
59
  def __init__(self, db: SQLiteDB):
60
  self.db = db
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  def clean_text(self, text: str) -> str:
63
  """Port of v2.1 _clean(): strips line numbers, footnotes, separators."""
64
  if not text:
@@ -210,10 +354,14 @@ class SearchService:
210
 
211
  time_ms = (time.time() - start_time) * 1000
212
 
213
- return SearchResponse(
214
  query=corrected_q,
215
  total_results=total_count,
216
  results=paginated,
217
  time_taken_ms=time_ms,
218
  breakdown_by_volume=breakdown_data,
219
  )
 
 
 
 
 
1
  import time
2
  import re
3
+ from typing import List
4
  from app.database.sqlite_db import SQLiteDB
5
  from app.schemas import SearchResponse, SearchResultItem
6
 
 
56
  return s.translate(str.maketrans("0123456789", "๐๑๒๓๔๕๖๗๘๙"))
57
 
58
 
59
+ # ── Curated popular terms (fallback when search_log is empty) ──────
60
+ # Source: https://84000.org/tipitaka/dic/ (topic headings, stripped of varga numbers)
61
+ POPULAR_TERMS: List[str] = [
62
+ # ── Core doctrines ──────────────────────────────────────────
63
+ "อริยสัจ", "นิพพาน", "ไตรลักษณ์", "อนิจจัง", "ทุกข์", "อนัตตา",
64
+ "สติปัฏฐาน", "อัฏฐมรรค", "โพชฌงค์", "ปฏิจจสมุปบาท",
65
+ "สมาธิ", "วิปัสสนา", "สมถะ", "ศีล", "ปัญญา",
66
+ "กรรม", "วิบาก", "กิเลส", "ตัณหา", "อวิชชา",
67
+ "โพธิสัตว์", "ปรินิพพาน", "สังสารวัฏ", "มรนสติ",
68
+ "เมตตา", "กรุณา", "มุทิตา", "อุเบกขา",
69
+ "อภิธรรม", "วินัยปิฎก", "สุตตันตปิฎก",
70
+ "ขันธ์", "อายตนะ", "ธาตุ", "อินทรีย์",
71
+ "โพธิปักขิยธรรม", "สัมมาสมาธิ", "สัมมาสติ",
72
+ "บารมี", "เจตสิก", "จิต", "รูป", "นาม",
73
+ # ── From 84000 dictionary ───────────────────────────────────
74
+ "กถาวัตถุ", "กรรมกิเลส", "กรรมฐาน", "กสิณ",
75
+ "กัลยาณมิตตตา", "กัลยาณมิตรธรรม", "กาม", "กามคุณ",
76
+ "กามโภคี", "กาลามสูตรกังขานิยฐาน", "กิจ", "กิจในอริยสัจจ์",
77
+ "กุลจิรัฏฐิติธรรม", "กุศลกรรมบถ", "กุศลมูล", "กุศลวิตก",
78
+ "คารวะ", "ฆราวาสธรรม", "จรณะ", "จริต", "จักขุ",
79
+ "จักร", "จักรวรรดิวัตร", "ฌาน", "ฌาน 2 ประเภท", "ญาณ",
80
+ "ถูปารหบุคคล", "ทวาร", "ทศพลญาณ", "ทักขิณาวิสุทธิ",
81
+ "ทาน", "ทิฏฐธัมมิกัตถสังวัตตนิกธรรม", "ทิฏฐิ", "ทิศ",
82
+ "ที่สุด", "ทุกขตา", "ทุจริต", "ธรรม", "ธรรมขันธ์",
83
+ "ธรรมคุณ", "ธรรมคุ้มครองโลก", "ธรรมทำให้งาม", "ธรรมนิยาม",
84
+ "ธรรมมีอุปการะมาก", "ธรรมสมาทาน", "ธรรมสมาธิ",
85
+ "ธรรมสวนานิสงส์", "ธรรมเทสกธรรม", "ธาตุกัมมัฏฐาน",
86
+ "ธุดงค์", "ธุระ", "นวกภิกขุธรรม", "นวังคสัตถุสาสน์",
87
+ "นาถกรณธรรม", "นิมิต", "นิยาม", "นิวรณ์", "นิโรธ",
88
+ "บริษัท", "บัญญัติ 2", "บุคคล", "บุคคลหาได้ยาก",
89
+ "บุญกิริยาวัตถุ", "บุตร", "บุพนิมิตแห่งมรรค", "บูชา",
90
+ "ปฏิปทา", "ปฏิสันถาร", "ปฏิสัมภิทา", "ปธาน",
91
+ "ปปัญจะ, ปปัญจธรรม", "ปรมัตถธรรม", "ประมาณ", "ปริญญา",
92
+ "ปริเยสนา", "ปัจจัย", "ปัจจัยให้เกิดสัมมาทิฏฐิ",
93
+ "ปัพพชิตอภิณหปัจจเวกขณ์", "ปาฏิหาริย์", "ปาปณิกธรรม",
94
+ "ปาพจน์", "ปาริสุทธิศีล", "ปิยรูป สาตรูป", "ปีติ",
95
+ "ผล", "พร", "พรหมวิหาร", "พละ",
96
+ "พละ 5 ของพระมหากษัตริย์", "พหูสูตมีองค์", "พุทธคุณ",
97
+ "พุทธจริยา", "พุทธโอวาท", "ภพ", "ภรรยา",
98
+ "ภัพพตาธรรม", "ภาวนา", "ภูมิ", "มงคล", "มรรค",
99
+ "มรรคมีองค์", "มละ", "มหาปเทส",
100
+ "มหาปเทส 4 เฉพาะในท���งพระวินัย", "มหาภูต", "มัจฉริยะ",
101
+ "มานะ", "มาร", "มิจฉัตตะ", "มิตรปฏิรูปก์",
102
+ "รัตนตรัย", "ราชธรรม", "ฤทธิ์", "ลักษณะตัดสินธรรมวินัย",
103
+ "ลัทธินอกพระพุทธศาสนา", "ลีลาการสอน", "วณิชชา", "วรรณะ",
104
+ "วัฏฏะ", "วัฒนมุข", "วัตถุประสงค์ในการบัญญัติวินัย",
105
+ "วิชชา", "วิญญาณ", "วิญญาณฐิติ",
106
+ "วิธีปฏิบัติต่อทุกข์-สุข", "วิบัติ", "วิปัลลาส",
107
+ "วิปัสสนาญาณ", "วิปัสสนูปกิเลส", "วิมุตติ", "วิรัติ",
108
+ "วิสุทธิ", "วิเวก", "วิโมกข์", "วุฒิ", "ศรัทธา",
109
+ "ศีล 8 ทั้งอาชีวะ", "สกทาคามี", "สมชีวิธรรม", "สมบัติ",
110
+ "สมาธิภาวนา", "สมาบัติ", "สรณะ", "สวรรค์",
111
+ "สังขตลักษณะ", "สังขาร", "สังคหวัตถุ",
112
+ "สังคหวัตถุของผู้ครองแผ่นดิน", "สังคหะ", "สังฆคุณ",
113
+ "สังวร", "สังเวชนียสถาน", "สังโยชน์", "สัจจะ", "สัญญา",
114
+ "สัตตาวาส", "สัทธรรม", "สันโดษ", "สัปปายะ",
115
+ "สัปปุริสทาน", "สัปปุริสธรรม", "สัปปุริสบัญญัติ",
116
+ "สัมปชัญญะ", "สัมปทา", "สัมปรายิกัตถสังวัตตนิกธรรม",
117
+ "สัมผัส", "สัมมัตตะ", "สารณียธรรม", "สาสน์", "สิกขา",
118
+ "สุข", "สุขของคฤหัสถ์", "สุจริต", "สุทธาวาส", "สุทธิ",
119
+ "สุหทมิตร", "อกุศลกรรมบถ", "อกุศลมูล", "อกุศลวิตก",
120
+ "อคติ", "อธิปไตย", "อธิษฐาน", "อนันตริยกรรม",
121
+ "อนาคามี", "อนุตตริยะ", "อนุบุพพวิหาร", "อนุปุพพิกถา",
122
+ "อนุสติ", "อนุสัย", "อบาย", "อบายมุข",
123
+ "อปริหานิยธรรม", "อปัณณกปฏิปทา", "อปัสเสนะ",
124
+ "อภิญญา", "อภิฐาน", "อภิณหปัจจเวกขณ์", "อภิสังขาร",
125
+ "อรหันต์", "อริยทรัพย์", "อริยบุคคล", "อริยวงศ์",
126
+ "อริยวัฑฒิ", "อริยสัจจ์", "อรูป", "อสังขตลักษณะ",
127
+ "อสุภะ", "อัคคิ", "อัตถะ", "อันตคาหิกทิฏฐิ",
128
+ "อันตรายของภิกษุสามเณรผู้บวชใหม่", "อัปปมาทะ",
129
+ "อาการที่พระพุทธเจ้าทรงสั่งสอน", "อาจารย์",
130
+ "อานาปานสติ 16 ฐาน", "อายตนะภายนอก", "อายตนะภายใน",
131
+ "อายุสสธรรม", "อาวาสิกธรรม", "อาสวะ", "อาหาร",
132
+ "อิทธิบาท", "อุบาสกธรรม", "อุปกิเลส", "อุปัญญาตธรรม",
133
+ "อุปาทาน", "อุปาทารูป", "เจดีย์", "เจตนา", "เถรธรรม",
134
+ "เทพ", "เทวทูต", "เทศนา", "เบญจธรรม", "เมถุนสังโยค",
135
+ "เวทนา", "เวปุลลธรรม", "เวปุลละ", "เวสารัชชกรณธรรม",
136
+ "เวสารัชชะ", "โกศล", "โภควิภาค", "โภคอาทิยะ",
137
+ "โยคะ", "โยนิ", "โยนิโสมนสิการ", "โลก", "โลกธรรม",
138
+ "โลกุตตรธรรม", "โสดาบัน", "โสตาปัตติยังคะ", "โอฆะ",
139
+ "ไตรปิฎก",
140
+ ]
141
+
142
+
143
  class SearchService:
144
  def __init__(self, db: SQLiteDB):
145
  self.db = db
146
 
147
+ # ── Autocomplete suggestions ────────────────────────────────────
148
+
149
+ def get_suggestions(self, prefix: str, limit: int = 8) -> List[str]:
150
+ """
151
+ Return autocomplete suggestions for `prefix`.
152
+ Priority: search_log (by frequency) → POPULAR_TERMS (curated).
153
+ """
154
+ if not prefix or len(prefix.strip()) < 1:
155
+ return []
156
+
157
+ prefix = prefix.strip()
158
+ results: List[str] = []
159
+ seen: set[str] = set()
160
+
161
+ # 1. From search history (disk) — most searched first
162
+ try:
163
+ with self.db.get_disk_connection() as conn:
164
+ rows = conn.execute("""
165
+ SELECT query FROM search_log
166
+ WHERE query LIKE ? COLLATE NOCASE
167
+ ORDER BY count DESC, last_at DESC
168
+ LIMIT ?
169
+ """, (f"{prefix}%", limit)).fetchall()
170
+ for row in rows:
171
+ q = row["query"]
172
+ if q not in seen:
173
+ results.append(q)
174
+ seen.add(q)
175
+ except Exception:
176
+ pass
177
+
178
+ # 2. Fill remaining from POPULAR_TERMS
179
+ if len(results) < limit:
180
+ for term in POPULAR_TERMS:
181
+ if term.startswith(prefix) and term not in seen:
182
+ results.append(term)
183
+ seen.add(term)
184
+ if len(results) >= limit:
185
+ break
186
+
187
+ return results[:limit]
188
+
189
+ def log_search(self, query: str) -> None:
190
+ """Upsert query into search_log (increment count if exists)."""
191
+ q = query.strip()
192
+ if not q:
193
+ return
194
+ try:
195
+ with self.db.get_disk_connection() as conn:
196
+ conn.execute("""
197
+ INSERT INTO search_log(query, count, last_at)
198
+ VALUES(?, 1, datetime('now','localtime'))
199
+ ON CONFLICT(query) DO UPDATE SET
200
+ count = count + 1,
201
+ last_at = datetime('now','localtime')
202
+ """, (q,))
203
+ except Exception:
204
+ pass # Never fail a search because of logging
205
+
206
  def clean_text(self, text: str) -> str:
207
  """Port of v2.1 _clean(): strips line numbers, footnotes, separators."""
208
  if not text:
 
354
 
355
  time_ms = (time.time() - start_time) * 1000
356
 
357
+ response = SearchResponse(
358
  query=corrected_q,
359
  total_results=total_count,
360
  results=paginated,
361
  time_taken_ms=time_ms,
362
  breakdown_by_volume=breakdown_data,
363
  )
364
+
365
+ # Log after `with` block closes (non-blocking, won't affect response)
366
+ self.log_search(corrected_q)
367
+ return response
webapp/tipitaka-web/src/components/layout/NavDrawer.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
2
  import { useUIStore, useReaderStore, useThemeStore } from '../../stores/appStore';
3
  import { useSearchStore } from '../../stores/searchStore';
4
  import api from '../../lib/api';
@@ -22,11 +22,15 @@ const NavDrawer: React.FC = () => {
22
  const {
23
  query, setQuery, results, totalResults,
24
  isLoading, performSearch, breakdown, timeTaken,
 
25
  } = useSearchStore();
26
 
27
  const [toc, setToc] = useState<any[]>([]);
28
  const [loadingToc, setLoadingToc] = useState(false);
29
  const [volumes, setVolumes] = useState<any[]>([]);
 
 
 
30
 
31
  const s = DRAWER_STYLES[theme] ?? DRAWER_STYLES.dark;
32
 
@@ -49,8 +53,68 @@ const NavDrawer: React.FC = () => {
49
  if (window.innerWidth < 1024) setNavOpen(false);
50
  };
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  const handleSearch = (e: React.FormEvent) => {
53
  e.preventDefault();
 
54
  setActiveTab('search');
55
  performSearch();
56
  };
@@ -161,16 +225,49 @@ const NavDrawer: React.FC = () => {
161
  {/* ── Search ── */}
162
  {activeTab === 'search' && (
163
  <div className="p-3 space-y-3">
164
- <form onSubmit={handleSearch} className="relative">
165
  <input
166
  type="text"
167
  value={query}
168
- onChange={e => setQuery(e.target.value)}
 
 
169
  placeholder="ค้นหาข้อความ เช่น อริยสัจ…"
 
170
  className={`w-full pl-9 pr-3 py-2.5 rounded-xl text-sm outline-none border ${s.border} ${s.input} ${s.text} focus:border-[#c8860a] transition-colors`}
171
  />
172
  <Search size={16} className="absolute left-3 top-3 text-[#888]" />
173
  {isLoading && <Loader2 size={14} className="absolute right-3 top-3.5 animate-spin text-[#c8860a]" />}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  </form>
175
 
176
  {results.length > 0 && (
 
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
2
  import { useUIStore, useReaderStore, useThemeStore } from '../../stores/appStore';
3
  import { useSearchStore } from '../../stores/searchStore';
4
  import api from '../../lib/api';
 
22
  const {
23
  query, setQuery, results, totalResults,
24
  isLoading, performSearch, breakdown, timeTaken,
25
+ suggestions, showSuggestions, fetchSuggestions, setShowSuggestions,
26
  } = useSearchStore();
27
 
28
  const [toc, setToc] = useState<any[]>([]);
29
  const [loadingToc, setLoadingToc] = useState(false);
30
  const [volumes, setVolumes] = useState<any[]>([]);
31
+ const [activeSuggestion, setActiveSuggestion] = useState(-1);
32
+ const searchRef = useRef<HTMLDivElement>(null);
33
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
34
 
35
  const s = DRAWER_STYLES[theme] ?? DRAWER_STYLES.dark;
36
 
 
53
  if (window.innerWidth < 1024) setNavOpen(false);
54
  };
55
 
56
+ // Debounced suggestion fetch
57
+ const handleQueryChange = useCallback((value: string) => {
58
+ setQuery(value);
59
+ setActiveSuggestion(-1);
60
+ if (debounceRef.current) clearTimeout(debounceRef.current);
61
+ debounceRef.current = setTimeout(() => {
62
+ fetchSuggestions(value);
63
+ }, 280);
64
+ }, [setQuery, fetchSuggestions]);
65
+
66
+ // Close suggestions on outside click
67
+ useEffect(() => {
68
+ const handler = (e: MouseEvent) => {
69
+ if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
70
+ setShowSuggestions(false);
71
+ }
72
+ };
73
+ document.addEventListener('mousedown', handler);
74
+ return () => document.removeEventListener('mousedown', handler);
75
+ }, [setShowSuggestions]);
76
+
77
+ const selectSuggestion = (term: string) => {
78
+ setQuery(term);
79
+ setShowSuggestions(false);
80
+ setActiveSuggestion(-1);
81
+ setActiveTab('search');
82
+ setTimeout(() => performSearch(), 0);
83
+ };
84
+
85
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
86
+ if (!showSuggestions || suggestions.length === 0) {
87
+ if (e.key === 'Enter') {
88
+ e.preventDefault();
89
+ setActiveTab('search');
90
+ performSearch();
91
+ }
92
+ return;
93
+ }
94
+ if (e.key === 'ArrowDown') {
95
+ e.preventDefault();
96
+ setActiveSuggestion(i => Math.min(i + 1, suggestions.length - 1));
97
+ } else if (e.key === 'ArrowUp') {
98
+ e.preventDefault();
99
+ setActiveSuggestion(i => Math.max(i - 1, -1));
100
+ } else if (e.key === 'Enter') {
101
+ e.preventDefault();
102
+ if (activeSuggestion >= 0) {
103
+ selectSuggestion(suggestions[activeSuggestion]);
104
+ } else {
105
+ setShowSuggestions(false);
106
+ setActiveTab('search');
107
+ performSearch();
108
+ }
109
+ } else if (e.key === 'Escape') {
110
+ setShowSuggestions(false);
111
+ setActiveSuggestion(-1);
112
+ }
113
+ };
114
+
115
  const handleSearch = (e: React.FormEvent) => {
116
  e.preventDefault();
117
+ setShowSuggestions(false);
118
  setActiveTab('search');
119
  performSearch();
120
  };
 
225
  {/* ── Search ── */}
226
  {activeTab === 'search' && (
227
  <div className="p-3 space-y-3">
228
+ <form onSubmit={handleSearch} className="relative" ref={searchRef}>
229
  <input
230
  type="text"
231
  value={query}
232
+ onChange={e => handleQueryChange(e.target.value)}
233
+ onKeyDown={handleKeyDown}
234
+ onFocus={() => query && fetchSuggestions(query)}
235
  placeholder="ค้นหาข้อความ เช่น อริยสัจ…"
236
+ autoComplete="off"
237
  className={`w-full pl-9 pr-3 py-2.5 rounded-xl text-sm outline-none border ${s.border} ${s.input} ${s.text} focus:border-[#c8860a] transition-colors`}
238
  />
239
  <Search size={16} className="absolute left-3 top-3 text-[#888]" />
240
  {isLoading && <Loader2 size={14} className="absolute right-3 top-3.5 animate-spin text-[#c8860a]" />}
241
+
242
+ {/* Suggestions dropdown */}
243
+ <AnimatePresence>
244
+ {showSuggestions && suggestions.length > 0 && (
245
+ <motion.ul
246
+ initial={{ opacity: 0, y: -4 }}
247
+ animate={{ opacity: 1, y: 0 }}
248
+ exit={{ opacity: 0, y: -4 }}
249
+ transition={{ duration: 0.12 }}
250
+ className={`absolute left-0 right-0 top-full mt-1 z-50 rounded-xl border shadow-lg overflow-hidden ${s.bg} ${s.border}`}
251
+ >
252
+ {suggestions.map((term, i) => (
253
+ <li key={term}>
254
+ <button
255
+ type="button"
256
+ onMouseDown={() => selectSuggestion(term)}
257
+ className={`w-full text-left px-4 py-2.5 text-sm flex items-center gap-2 transition-colors ${
258
+ i === activeSuggestion
259
+ ? 'bg-[#c8860a]/15 text-[#c8860a]'
260
+ : `${s.text} hover:bg-white/10`
261
+ }`}
262
+ >
263
+ <Search size={12} className="opacity-40 flex-shrink-0" />
264
+ {term}
265
+ </button>
266
+ </li>
267
+ ))}
268
+ </motion.ul>
269
+ )}
270
+ </AnimatePresence>
271
  </form>
272
 
273
  {results.length > 0 && (
webapp/tipitaka-web/src/stores/searchStore.ts CHANGED
@@ -20,11 +20,15 @@ interface SearchState {
20
  breakdown: Record<number, VolumeStats>;
21
  isLoading: boolean;
22
  timeTaken: number;
23
-
 
 
24
  // Actions
25
  setQuery: (q: string) => void;
26
  performSearch: () => Promise<void>;
27
  clearResults: () => void;
 
 
28
  }
29
 
30
  export const useSearchStore = create<SearchState>((set, get) => ({
@@ -34,14 +38,31 @@ export const useSearchStore = create<SearchState>((set, get) => ({
34
  breakdown: {},
35
  isLoading: false,
36
  timeTaken: 0,
 
 
37
 
38
  setQuery: (q) => set({ query: q }),
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  performSearch: async () => {
41
  const { query } = get();
42
  if (!query.trim()) return;
43
 
44
- set({ isLoading: true });
45
  try {
46
  const response = await api.get('/api/search', { params: { q: query } });
47
  set({
@@ -57,5 +78,5 @@ export const useSearchStore = create<SearchState>((set, get) => ({
57
  }
58
  },
59
 
60
- clearResults: () => set({ results: [], totalResults: 0, breakdown: {}, query: '' })
61
  }));
 
20
  breakdown: Record<number, VolumeStats>;
21
  isLoading: boolean;
22
  timeTaken: number;
23
+ suggestions: string[];
24
+ showSuggestions: boolean;
25
+
26
  // Actions
27
  setQuery: (q: string) => void;
28
  performSearch: () => Promise<void>;
29
  clearResults: () => void;
30
+ fetchSuggestions: (prefix: string) => Promise<void>;
31
+ setShowSuggestions: (v: boolean) => void;
32
  }
33
 
34
  export const useSearchStore = create<SearchState>((set, get) => ({
 
38
  breakdown: {},
39
  isLoading: false,
40
  timeTaken: 0,
41
+ suggestions: [],
42
+ showSuggestions: false,
43
 
44
  setQuery: (q) => set({ query: q }),
45
 
46
+ fetchSuggestions: async (prefix: string) => {
47
+ if (!prefix.trim()) {
48
+ set({ suggestions: [], showSuggestions: false });
49
+ return;
50
+ }
51
+ try {
52
+ const res = await api.get('/api/search/suggestions', { params: { q: prefix, limit: 8 } });
53
+ set({ suggestions: res.data, showSuggestions: res.data.length > 0 });
54
+ } catch {
55
+ set({ suggestions: [], showSuggestions: false });
56
+ }
57
+ },
58
+
59
+ setShowSuggestions: (v) => set({ showSuggestions: v }),
60
+
61
  performSearch: async () => {
62
  const { query } = get();
63
  if (!query.trim()) return;
64
 
65
+ set({ isLoading: true, showSuggestions: false });
66
  try {
67
  const response = await api.get('/api/search', { params: { q: query } });
68
  set({
 
78
  }
79
  },
80
 
81
+ clearResults: () => set({ results: [], totalResults: 0, breakdown: {}, query: '', suggestions: [], showSuggestions: false })
82
  }));