Files
2meet-data-optimizer/docs/adr-004-search-index-mode-for-hp-wc.md
T
wpdev fab07655a2 docs(2meet-data-optimizer): 1.2.0 release notes
- Bump to 1.2.0; CHANGELOG / readme entries for PR-066–080, with the post-upgrade ops commands
- ADR-004 Release 2.2 design (scope, drift check / fix / router / resync) and CONTEXT.md drift paragraph
- Verify: composer test, integration, phpcs 0
- PR: PR-086
2026-09-27 09:52:22 +08:00

10 KiB
Raw Blame History

ADR-004:HivePress / WooCommerce 站採「搜尋索引模式」

  • 狀態:已接受(2026-09-26)
  • 日期:2026-09-26
  • 相關:dev36 整合測試報告(notes/tmdo-integration-test-dev36-2026-09-26.md)、ADR-001、ADR-003、backlog PR-052〜PR-063(Release 2.1)

背景

2026-09-26 在 dev36(HivePress 1.7.31 + Marketplace + 10 個擴充、WooCommerce 11.1.2、Redis、10 萬筆 listing)做整合與壓力測試,確認:

  1. HivePress model 與 WC data store 用空 key 讀 meta(get_post_meta( $id )),1.0.x 的 Hook Bus 對空 key 放行、只讀原生。所以 aeav_only 或 cleanup 之後,HP 的 verified / rating、WC 的價格與顧客資料讀成空值(dev20 / dev21 盤點:WC 商品 0/20 可購買)。
  2. WC 以直接 SQL 更新 _stock、total_sales,扁平表跟不上。
  3. shadow_read 會遺失寫入:WP 的 update_metadata() 經 get_metadata_raw 讀舊值,攔截器從扁平表回答,新舊相同就不寫原生。
  4. Router 在 OR、LIKE、多 post type、跨 group、具名 orderby 上結果錯誤,HivePress 的 _sort 沒被改寫,前台搜尋幾乎沒有變快。
  5. 陣列值被存成 "Array"。

帶 key 的單筆讀取本來就沒有速度優勢:update_meta_cache() 一次載入全部 key,之後都是記憶體讀取。扁平表真正的價值在搜尋與篩選(BENCHMARK-SCALE.md 結論 1、2)。

決策(使用者 2026-09-26)

  1. HP / WC 站採「搜尋索引模式」:原生 meta 是唯一真實資料來源,扁平表是它的搜尋索引。模式最高到 shadow_read;aeav_only 與 cleanup 由 Promotion Guard 擋下。
  2. 移除 WC 扁平化(Release 2.2:core 的 post/wc_product 群組、WC AddOn 的 user hot 群組與 hot-zone 欄位)。
  3. 既有 aeav_only 站(dev20 / 21 / 22)先唯讀盤點,處置由使用者決定(notes/ops-aeav-sites-audit-2026-09-26.md)。
  4. 分兩版:2.1 = 止血 + 資料正確性 + Router 與 HP 排序;2.2 = 範圍收斂 + 漂移偵測 + CLI 與診斷。

設計(Release 2.1)

寫入:扁平表跟隨原生

  • dual_write / shadow_read 下,pre-filter(update_/add_/delete_{type}_metadata)只做路由判斷後回 null,讓 WordPress 自己寫原生。
  • 扁平表在 added_ / updated_ / deleted_{type}_meta action(priority 1)裡從原生重新同步:直接 SQL 讀原生第一筆 row(ORDER BY meta_id LIMIT 1,不經 filter),扁平值 = to_db( maybe_unserialize( v ) ),原生沒有 row 時設為 NULL(TMDO_Hook_Bus::sync_from_native())。
  • delete_metadata( …, $delete_all ):deleted_ action 拿到的 object id 是 0,所以在 delete_{type}_meta 先記下 object ids。
  • update_metadata_by_mid() 改 key:新舊兩個 key 都重新同步。
  • 原生沒有值時只把既有列的欄位設為 NULL,不新增列:刪除實體時 adapter 先刪扁平列,WordPress 才逐筆刪 meta。
  • aeav_only 保留原本的 pre-filter short-circuit(非 HP / WC 站仍可使用)。

讀取:shadow_read = 讀原生、抽樣比對扁平

intercept_get 在 shadow_read 回 $check(讓 WordPress 讀原生),並以 tmdo_shadow_compare_sample_rate(預設 0.1)抽樣比對扁平值、記錄差異。查詢加速不受影響,因為 Router 仍由 reads_from_flat()(shadow_read 以上)控制。

Promotion Guard

TMDO_Promotion_Guard,由 TMDO_Mode_Manager::set()、TMDO_Post_Migration::cleanup() 與 TMDO_Phase_Cleanup::execute() 呼叫:

  • HivePress 啟用時,post / user / term / comment 不得升到 aeav_only 或執行 cleanup;WooCommerce 啟用時,post / user 不得。
  • 回傳 WP_Error( 'promotion_guarded' );特殊情況用 filter tmdo_promotion_guard_allow 放行。
  • 已經在 aeav_only 的站讀取時不強制降級(降級會讀到空的原生),只在後台顯示 notice。處置用 wp tmdo bridge-restore-native <entity> --demote。

回寫原生(bridge-restore-native)

依模式決定策略:aeav_only 預設 overwrite(寫入只到扁平表、原生已過期),其他模式預設 only_missing(原生可能較新,例如 WC 直接 SQL 寫庫存)。相同的值不重寫;扁平值等於欄位 default 且原生沒有 row 時不寫(不捏造預設值)。

序列化值

text / textarea 遇到陣列或物件時 maybe_serialize(),讀出時 TMDO_Safe_Unserialize;text(VARCHAR(255))超長存 NULL 並記 text_overflow。既有的 "Array" 由 wp tmdo repair-array-literals 從原生修復。

Router:只在語意與原生相同時路由

TMDO_Post_Query_Router(pre_get_posts priority 1000,排在外掛於預設 priority 加的條件之後):

  • 全有或全無:任何一個已註冊 key 的 clause 不能路由,整個查詢交回 WordPress。不能路由的情況:
    • top-level OR
    • 含已註冊 key 的巢狀 group(排序形狀除外)
    • 多個 post type
    • 白名單以外的 compare
    • type 與欄位語意不同:無 type(CHAR)的數值範圍比較、decimal 欄位的 CHAR 比較、text 欄位用 NUMERIC
  • LIKE 包成 %esc_like%。
  • 每個 group 各自 LEFT JOIN;扁平表一文章一列(UNIQUE post_id),不加 GROUP BY。
  • 狀態 / 日期副本路由遇到 NOT EXISTS 時停用:INNER JOIN 會漏掉沒有扁平列的文章。
  • 排序(wpdo_post_sort):
    • HivePress _sort 形狀:OR 裡剛好是同 key、同 type 的 NOT EXISTS + EXISTS,其中一個具名,並且是唯一的 orderby。這個 group 從 meta_query 移除,改成 LEFT JOIN 扁平表並 ORDER BY col, ID DESC;ASC 時 NULL 排最前,與原生相同。
    • 具名 clause 作為唯一的 orderby。
    • meta_key + meta_value_num(字串欄位用 meta_value),加上 col IS NOT NULL(原生是 INNER JOIN)。
    • 欄位排序不等於 CAST( meta_value AS type ) 時退回原生。
  • flat-first:依扁平欄位排序、且 routed 條件已要求扁平列存在(同一 group、非 NOT EXISTS)、沒有 post__in / author 等高選擇性條件時,改寫成 SELECT STRAIGHT_JOIN … FROM flat INNER JOIN wp_posts,ORDER BY 的 wp_posts.ID 改讀等值的 post_id,排序只在一張表完成。依日期排序的搜尋不套用(由 posts 驅動較快)。filter tmdo_router_flat_first 可關閉。
  • 維持 WordPress 的 split query:posts_request 移除 SQL_CALC_FOUND_ROWS(總數改由快取的 COUNT 回答)會讓 WordPress 以為 request 被改過而撈整列。split_the_query 對 routed 查詢恢復 ID-first,posts_request_ids 再移除一次 SQL_CALC_FOUND_ROWS。

HivePress AddOn 0.2.0 在 post router 啟用時不再綁定舊的 hot-zone optimize_query / optimize_search。

設計(Release 2.2)

範圍收斂

  • core 不再註冊 post/wc_product;WooCommerce AddOn 0.2.0 不再註冊 product / order / subscription 的 zone 欄位與 user hot 群組。WooCommerce 的 meta 只存在原生。既有表以 bridge-restore-native post --group=wc_product --retire 封存。

漂移偵測與修復(TMDO_Drift_Check)

  • wp tmdo drift-check <entity> 以值比對扁平與原生(第一筆原生 row → 安全 unserialize → Type_Caster::to_db),逐 key 回報 miss / diff / extra,結果寫入 wpdo_drift_status。
  • --fix 從原生重建漂移列,刪除已刪實體與非本 group post type 的列,不寫原生。
  • aeav_only 不適用:那時扁平才是真實資料,原生本來就過期。--fix 拒絕、每日抽樣略過、Router 不因漂移退回原生。
  • Router:上次檢查有漂移的 group 不路由,整個查詢維持原生;從未檢查的 group 照常路由(決策 2026-09-27:升級後不變慢)。
  • 每日抽樣:wpdo_daily_health_check,每 group 500 筆。
  • Hook Bus 關閉期間的寫入:entity 從 disabled 進入 dual_write / shadow_read、或 Hook Bus flag 由關轉開時,60 秒後 tmdo_drift_resync 執行全量 --fix。不用 post_modified_gmt 增量,因為 WooCommerce 直接 SQL 寫 meta 不會更新它。
  • Auto-promoter 以 clean_since()(2 天內乾淨的檢查)取代 shadow diff 數量;Promotion Guard 仍擋 HP / WC 站。

查詢

  • 純排序搜尋(HivePress _sort、沒有路由篩選)在該 post type 每篇都有扁平列時改由扁平表驅動;Router 在 wp_insert_post 替新文章建空列,覆蓋率快取 15 分鐘。

後果

好處

  • 資料正確性回到 WordPress 原生語意:HP / WC 的空 key 讀取、直接 SQL 寫入都不再造成資料不一致或遺失。
  • 查詢加速保留,而且擴及 HivePress 前台的篩選與排序(實測見 BENCHMARK-SCALE.md「前台與寫入鏈實測」)。
  • 任何時候都可以降回 disabled,原生資料完整。

代價與限制

  • 寫入開銷:dual_write / shadow_read 每次 meta 寫入多一次原生讀取與一次扁平寫入。dev36 實測 HP listing 整條寫入鏈 +29%、WC product +127%(後者在 2.2 移除 WC 扁平化後消失)。
  • 沒有讀取加速:帶 key 的單筆讀取仍走原生(WordPress meta cache)。
  • 漂移:繞過 meta API 的寫入(直接 SQL、Hook Bus 關閉期間的匯入)會讓扁平表過期,Router 會回傳過期的篩選結果。2.2 起由 drift-check(每日抽樣、Router 避開漂移中的 group)與重新啟用時的全量 --fix 處理;兩次抽樣之間的漂移仍可能短暫回傳過期結果,大量匯入後可手動執行 drift-check <entity> --fix。
  • decimal 欄位搭配 NUMERIC / DECIMAL:原生先 CAST 成整數(DECIMAL 預設 (10,0)),扁平表精確比較,因此帶小數的值在區間邊界上可能不同。dev36 資料沒有這種值。
  • text 欄位超過 255 字元存 NULL,這類值的篩選結果會與原生不同(有 text_overflow 記錄)。
  • HivePress 搜尋 p50 為 321–866 ms(原生 600–1,713 ms),尚未達到 R-2.1 的 300 ms 目標(驗收:notes/tmdo-acceptance-2.1-2026-09-26.md)。價格篩選頁的 SQL 只剩約 75 ms、其餘為頁面渲染;純排序(如 rating__desc)必須 LEFT JOIN 保留沒有值的文章,無法 flat-first。

不在範圍內

  • aeav_only 的空 key 讀取合併(B02):HP / WC 站不再使用 aeav_only,是否實作依其他站需求決定。
  • Hot zone 的移除(ADR-003 的後續步驟)。