Files
2meet-data-optimizer/docs/adr-004-search-index-mode-for-hp-wc.md
T
wpdev e8731c5119
Anti-EAV Lint + Quality Gate / anti-eav-lint (pull_request) Successful in 18s
Tests / PHP Lint (pull_request) Successful in 59s
Tests / PHPCS (pull_request) Successful in 30s
Tests / PHPStan (pull_request) Successful in 1m12s
Tests / Unit Tests (pull_request) Successful in 47s
Tests / Integration Tests (pull_request) Successful in 57s
docs(2meet-data-optimizer): add PR-057b and PR-062c to the 1.1.0 notes
- CHANGELOG / readme 1.1.0: flat-first routing for flat-sorted searches (tmdo_router_flat_first) and the all-NULL flat row left after deleting an entity
- ADR-004: NULL sync never inserts a row; flat-first rule; front-end p50 from the R-2.1 acceptance run
- BENCHMARK-SCALE: front-end table from the acceptance run (flat tables backfilled) and the join-order finding behind PR-062c
- Verify: composer phpcs (0)
- PR: PR-064b
2026-09-26 21:07:51 +08:00

8.5 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。

後果

好處

  • 資料正確性回到 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 與重新啟用時的增量 backfill(E01 / E02)處理;在那之前,大量匯入後要重新 backfill。
  • 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 的後續步驟)。