การค้นหาสำหรับไซต์ของคุณ
public read API ถูกออกแบบโดยตั้งใจให้ไม่ใช่ query engine (SPEC.md §7) มันให้บริการ
snapshot ที่เผยแพร่แล้วตามโมเดล, slug, locale, relation และ tag — ไม่มี
?q= นี่คือการตัดสินใจเชิงออกแบบ ไม่ใช่ช่องโหว่: การสแกนแบบ LIKE บน snapshot ใน D1
จะช้า ไม่มีการจัดอันดับ และเป็นรูปแบบที่ผิดสำหรับโมเดลแบบหนึ่งแถวต่อหนึ่งเอกสาร
การค้นหาเป็นหน้าที่ของฝั่งผู้ใช้ข้อมูล (consumer) มีสามรูปแบบที่ใช้ได้ในปัจจุบัน เรียงตาม ปริมาณงานที่ต้องใช้
1. Pagefind — ไซต์แบบ static ไม่ต้องมีโครงสร้างพื้นฐาน
หัวข้อที่มีชื่อว่า “1. Pagefind — ไซต์แบบ static ไม่ต้องมีโครงสร้างพื้นฐาน”หากไซต์ของคุณ build แบบ static (หรือ build ของ API ที่สร้างขึ้นส่งออกเป็น HTML) ให้ทำ index ผลลัพธ์ที่ build แล้ว:
npm install -D pagefindnpx pagefind --site distPagefind ทำ index หน้าที่ render แล้ว ให้บริการ UI แบบ WASM ขนาดเล็กจาก bucket เดียวกัน และไม่ต้องมีเซิร์ฟเวอร์ ให้รันซ้ำใน CI job เดียวกับที่ build ไซต์ นี่คือค่าเริ่มต้นที่เหมาะสมสำหรับไซต์แนวโบรชัวร์
2. Typesense / Meilisearch — การค้นหาแบบแอปพลิเคชัน
หัวข้อที่มีชื่อว่า “2. Typesense / Meilisearch — การค้นหาแบบแอปพลิเคชัน”crawl public API เข้าไปในบริการค้นหา แล้ว query บริการนั้นจาก frontend ของคุณ
// One page per model per locale, following the cursor. An item is already// projected into the requested locale, so index one locale per pass.const base = 'https://content.example.com/v1';let cursor: string | undefined;do { const url = new URL(`${base}/article`); url.searchParams.set('locale', 'en'); url.searchParams.set('limit', '100'); if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url); const { data, page } = await res.json(); for (const item of data) { await index.upsert({ id: `${item.id}:${item.locale}`, title: item.fields.title ?? '', body: item.fields.body ?? '', slug: item.slug, locale: item.locale, published_at: item.published_at, }); } // Pagination lives in `page`, beside `data`. The cursor is opaque: send back // exactly what you were given. cursor = page.has_more ? page.next_cursor : undefined;} while (cursor);ทำ index ใหม่ตามกำหนดเวลา หรือเมื่อได้รับ deploy webhook ที่ CI ของคุณได้รับอยู่แล้ว ใช้
id ของรายการ (รวมกับ locale ของมัน) เป็น key ของ index — slug เปลี่ยนได้ แต่ id ไม่เปลี่ยน
3. Worker สำหรับค้นหาขนาดเล็ก — ไม่ต้องใช้บริการภายนอก
หัวข้อที่มีชื่อว่า “3. Worker สำหรับค้นหาขนาดเล็ก — ไม่ต้องใช้บริการภายนอก”สำหรับไซต์ขนาดเล็ก Worker หนึ่งตัวที่เก็บ index ไว้ใน memory/KV และรีเฟรชด้วย cron ก็เพียงพอ:
// search-worker: GET /search?q=recipe// Refresh: cron pulls every public model once per hour.export default { async fetch(req, env): Promise<Response> { const q = new URL(req.url).searchParams.get('q')?.toLowerCase() ?? ''; if (!q) return Response.json({ results: [] }); const index = JSON.parse(await env.SEARCH_INDEX.get('all') ?? '[]'); const results = index .filter((d) => (d.title + ' ' + d.body).toLowerCase().includes(q)) .slice(0, 20); return Response.json({ results }); },
async scheduled(_event, env, _ctx) { const res = await fetch(`${env.PUBLIC_API}/article?locale=en&limit=100`); const { data } = await res.json(); await env.SEARCH_INDEX.put('all', JSON.stringify( data.map((d) => ({ title: d.fields.title, body: strip(d.fields.body), slug: d.slug })) )); },};นี่คือการจับคู่ substring ไม่ใช่การจัดอันดับ — ใช้ได้ดีเมื่อมีเอกสารไม่เกินสองสามพันรายการ แต่ไม่เหมาะเมื่อเกินกว่านั้น ให้ย้ายไปใช้ Typesense เมื่อเริ่มเป็นปัญหา
กฎที่ใช้กับทั้งสามรูปแบบ
หัวข้อที่มีชื่อว่า “กฎที่ใช้กับทั้งสามรูปแบบ”- ทำ index เฉพาะ snapshot ที่เผยแพร่แล้วเท่านั้น ผ่าน public API admin API ไม่ส่งข้อมูลเข้า search index เด็ดขาด draft และเนื้อหาที่ยังไม่เผยแพร่ต้องไม่รั่วไหลออกไป
- ใช้
idของรายการเป็น key ของเอกสาร และแสดงผลด้วยslug - ทำ index ใหม่หลัง deploy ไม่ใช่หลังการเผยแพร่ทุกครั้ง — public API มี cache tag ดังนั้น index ที่ล้าสมัยไปไม่กี่นาทีถือเป็นเรื่องปกติและไม่เป็นอันตราย
- เคารพ locale:
localeเป็นพารามิเตอร์ของ request และรายการจะถูกส่งกลับมา โดยถูก project เป็น locale เดียวแล้ว ดังนั้นให้ crawl หนึ่งครั้งต่อหนึ่ง locale และเก็บเอกสาร index หนึ่งชิ้นต่อหนึ่ง locale หรือใช้ locale facet — อย่ารวมเป็นก้อนเดียวเด็ดขาด
สิ่งที่จะทำให้การค้นหากลายเป็นส่วนหนึ่งของผลิตภัณฑ์
หัวข้อที่มีชื่อว่า “สิ่งที่จะทำให้การค้นหากลายเป็นส่วนหนึ่งของผลิตภัณฑ์”build ของ graduation ดึงข้อมูลไซต์อยู่แล้วเพื่อสร้าง API ที่มี type การส่งออก search index แบบ static เป็น build artifact อีกชิ้นหนึ่งสอดคล้องกับแนวคิดหลัก: index จะกลายเป็นไฟล์ที่ลูกค้าเป็นเจ้าของ และรีเฟรชโดย CI ตัวเดียวกับที่ build API ของพวกเขาใหม่ นั่นคือจุดที่ “การค้นหา” จะกลายเป็นฟีเจอร์ของ Dee Wan แทนที่จะเป็นสูตรสำหรับฝั่ง consumer