Pattern 一句話

把 filter 變成 source 的 query 參數、source 端就回符合的、client 不 post-filter。

對應 #59 Filter × Source 合成策略 的策略 A。


何時用、何時不用

  • Source 支援該 filter 條件(已索引、能在 query 表達)
  • 想避免任何 client-side post-filter
  • 想避免層錯位(見 #55)

不用

  • Source 不支援(pagefind 對 title-only 沒 native 支援)
  • 條件需要 client-side 計算(依 viewport / 隨機抽樣)
  • 推進 query 後 cardinality 仍大、還是要 paginate(這時 A + B 並用)

推進的層次

層次範例成本
Query 參數?type=post&tag=js最低、改 URL
Filter APIpagefind.search(q, { filters: { type: 'post' } })低、用 SDK
Re-query重新呼叫 search、不是同個 result 集再過濾
Index 重建Build 時加新欄位 / 新 index中-高、要 build
Schema 修改改 DB schema、加欄位、reindex

選哪一層 = source 的 capabilities 決定。


評估 Source Capabilities

寫之前讀 source docs / API spec、列出:

問題答案範例
Source 接受哪些 filter 條件?=, IN, BETWEEN, full-text, …
哪些欄位已索引?type, tag, date (not title)
哪些 filter 不支援、需要重 index?title contains(需 full-text title)
Filter 有沒有 cost cap(rate limit)?100 query / sec

不評估就寫 = 寫到一半發現 source 不支援、回頭走策略 B 或 C。


範例:Pagefind

支援的 filter

1// pagefind 已支援 filter(透過 _pagefind/filter.json)
2const r = await pagefind.search('keyword', {
3  filters: {
4    type: 'post',           // 支援
5    tag: { any: ['js', 'css'] }, // 支援多選
6  },
7});

不支援的 filter

1// pagefind 不支援「只搜 title」
2// 因為 pagefind 的 search 對 full-text、不分區
3const r = await pagefind.search('keyword', {
4  scope: 'title-only',  // 不存在(不支援)
5});

要解決:

  • 方案 1:build 時用 data-pagefind-body 把 title 標成獨立 region、用 body filter(pagefind v1.1+)
  • 方案 2:建兩個獨立 index(一個只 index title、一個只 index content) — 走策略 C
  • 方案 3:放棄推進 query、用策略 B 自動續抓 + post-filter

跟原本 query 邏輯的並用

推進 filter 通常不取代原本 query、是「補上條件」:

1// 使用者輸入 query "css"、選 type=post
2const r = await pagefind.search('css', {  // query
3  filters: { type: 'post' },              // filter
4});
5// 兩個都進 source、source 算交集

Filter 跟 query 是不同維度:query 是「找什麼」、filter 是「在哪些範圍找」。


反例

反例 1:推進不完全、留 client-side post-filter 補

1const r = await pagefind.search(q, { filters: { type: 'post' } });
2const filtered = r.results.filter(x => x.title.includes(q));
3// ↑ 這行還是 #55 層錯位

如果 source 不支援 title-filter、不要用「半推進」 — 直接走策略 C 或 B。

反例 2:忽略 cost cap

1input.addEventListener('input', async () => {
2  // 每個鍵盤事件 fire 一個 search query
3  const r = await pagefind.search(input.value, { filters: ... });
4});
5// → query rate 100+/秒、撞 rate limit

加 debounce:

1let timer;
2input.addEventListener('input', () => {
3  clearTimeout(timer);
4  timer = setTimeout(() => pagefind.search(input.value, ...), 200);
5});

反例 3:客製欄位沒進 index、寫了 query 失效

1// 期望 filter 「閱讀時間 > 5 分鐘」
2const r = await pagefind.search(q, { filters: { readingTime: { gt: 5 } } });
3// → 但 build 時沒把 readingTime 進 filter index → filter 被忽略

預期 source 不支援 → 評估「是否值得加進 index」(成本 vs 使用率)。


跟其他 Pattern 的關係

  • A 是最優 — 在 source capabilities 範圍內優先選
  • A 不可行 → 評估 C(建獨立 index)
  • C 也不可行 → 退到 B(自動續抓)
  • 都不可行 → D(誠實 UX)

選擇順序:A → C → B → D


判讀徵兆

訊號該做的事
Filter 條件能在 source 端表達用本 pattern
Source 不支援、考慮要不要重 index評估 C 的成本
用了 filter 還寫 client-side post-filter半推進是反模式、要嘛全推進、要嘛換策略
Filter 觸發 query rate 高加 debounce / throttle
Query 跟 filter 概念混淆區分:query = 「找什麼」、filter = 「範圍」

核心原則:能推進 query 就推 — 沒層錯位、沒 silent 失敗、跟使用者意圖最近。但前提是 source 支援;不支援就要退到 B / C / D、不要做半推進。