iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
佛心分享-SideProject30

Mongory:打造跨語言、高效能的萬用查詢引擎系列 第 15

Day 14:Matchers 架構總覽與 Adapter 邊界

  • 分享至 

  • xImage
  •  

今天把 Mongory 的 Matchers 架構一次講透:骨架(base/composite/explainable/traversable)、建構 → 匹配 → 遍歷的資料流、priority/unwrap/trace 的掛點,另外用最小範例示意 Regex 與 Custom 的 adapter 設計,最後說清楚為什麼要把這些邏輯外包給宿主語言。

名詞表與主要類別

  • 基本 matcher 類型(部分):
    • compare$eq/$ne/$gt/$gte/$lt/$lte 等比較;面向單值。
    • inclusion$in/$nin;面向集合查找。
    • field:選擇欄位並把值(或子結構)交給子 matcher。
    • array$elemMatch/$every;針對陣列逐元素套用子 matcher。
    • composite$and/$or/Condition;多子節點合併布林結果。
    • external$regex 與自訂 $custom;由 adapter 接到宿主語言。
  • 關鍵內部結構:
    • mongory_matcher(base):定義共用欄位與行為指標。
    • mongory_composite_matcher:帶 children 的容器。
    • mongory_matcher_traverse_context:explain/遍歷的上下文。
    • extern_ctx:跨語言或外部狀態的掛載點,主要供使用者自定義 custom matcher 調用

骨架與職責地圖(base/composite/explainable/traversable)

  • base:mongory_matcher 承載必要欄位:
    • nameconditionmatchpoolextern_ctx
    • explain/trace:explaintraversetrace_stacktrace_level
    • 排序與控制:priority
  • composite:children(以 mongory_array 承載),例如 $and/$or/$elemMatch/$every
  • explainable/traversable:統一的遍歷上下文 mongory_matcher_traverse_context,explain 與 trace 皆以 traversal 為基礎輸出。
  • external:regexcustom 走 adapter,核心只保證生命週期與錯誤註記,邏輯交給宿主語言提供的回呼。

補充:matcher_explainable.hmatcher_traversable.h 把「如何走樹、如何輸出」從「如何比對」切開,降低耦合

為什麼是這樣的架構

  • 筆者一開始在 Clang 實作 matcher tree 時,使用的是 binary tree 的結構(因為比較炫),但 explain/trace 一上來,維護成本爆炸,根本不知道要怎麼擴充下去。把 matcher tree 架構改用「扁平化/可遍歷」的 n-ary tree 後,explain 與 trace 都變得直接。
  • C core 專注在「資料結構+可預測行為」,把語言特定或常變的邏輯(regex、特定業務規則)用 adapter 外包,讓 Mongory 保持可攜性與可觀測性。

資料流:build → match → traverse/explain/trace

  1. build(構建樹)
    • table_cond 讀取條件文件(hash/table),遇到 $ 開頭當作運算子,其他視為欄位。
    • 建立對應 matcher,串在 composite 的 children 裡。
    • 同步套用 priority 與 unwrap 規則(單子解包),將不必要的中介層拿掉。
  2. match(執行匹配)
    • 自根往下呼叫 match(matcher, value),composite 依語意合併子結果(AND/OR/elemMatch/every)。
    • trace(若啟用):進入/離開時推入/彈出 trace_stack,並標記結果。
  3. traverse/explain/trace(觀察)
    • 使用 traverse 走訪整棵樹,explain 基於 traversal 直譯節點語意產生可讀輸出,trace 也基於 traversal 替換整棵樹的 match func 以達到追蹤比對過程的效果。

更細部的 build 步驟(以 table condition 為例):

// 偽碼示意:將一個條件 table 轉為 matcher 樹
mongory_matcher *build_table_cond(mongory_memory_pool *pool, mongory_value *table) {
  mongory_matcher *root = mongory_matcher_composite_new(pool, table, NULL);
  // 1) 走訪 table 的每個鍵值
  // 2) "$" 開頭 → 運算子 matcher;其他 → field matcher
  // 3) children.push(child)
  // 4) children 依 priority 排序
  // 5) 單子節點 unwrap(若允許)
  return root;
}

match 階段的早停(early exit):

  • $and:遇到第一個 false 即可返回 false
  • $or:遇到第一個 true 即可返回 true
  • $elemMatch:有任一元素為 true 即可返回 true
  • $every:有任一元素為 false 即可返回 false

Priority、unwrap、trace 的掛點

  • priority:在 composite 增加 children 時就決定排序(例如先做便宜且可早停的條件),降低平均成本。
  • unwrap:在 build 期若遇單一子節點的中介層(且不影響語意者),直接移除層級,讓 explain 更乾淨、match 更少呼叫。
  • trace:以 trace_stack 追蹤呼叫深度與節點結果,mongory_matcher_trace_result_colorful_set(true) 可開彩色輸出(閱讀友善)。

Priority 設計心法與實作切點:

  • 心法:
    • 先便宜、再昂貴:常數時間或小集合查找先做;正則或跨橋接最後。
    • 先高鑑別、再低鑑別:能快速判否的條件先做,放大早停機率。
  • 實作:
    • 在 children push 完畢後,依 matcher 類型賦予權重(例如 $eq < $in < $regex)。
    • 維持穩定排序,避免 explain 排序抖動。

Unwrap 的邊界與反例:

  • 可解包:
    • $and 僅一個 child 時可解包。
    • 只有單一 field → 單一子 matcher,且不影響 trace 與 explain 的語意時。
  • 必須保留:
    • $every$elemMatch 此類「語意承載」節點,不應因單子而移除。
    • 任何攜帶特定屬性(如 trace 掛點)的節點,若解包會喪失觀測訊息。

小範例(explain 前後對照):

Before (unwrap 前)
And
  Field("age")
    Gt(18)

After (unwrap 後)
Field("age")
  Gt(18)

Trace 設計要點:

  • trace_stack 記錄節點進出與結果(Matched/Dismatch)。
  • level 用於縮排;count/total 可用於進度條式輸出。
  • 彩色模式開關:由 config 設定,便於在 CI 或純文字環境切換。

Regex/Custom:最小示例(示意)

以下示例展示如何在初始化時註冊 adapter 回呼,讓 C core 在執行 $regex 或自定義運算子時,委派至宿主語言。

#include <mongory-core/foundations/config.h>
#include <mongory-core/foundations/value.h>

// Regex adapter:由宿主語言實作具體比對與 stringify
static bool my_regex_match(mongory_memory_pool *pool, mongory_value *pattern, mongory_value *value) {
  (void)pool;
  // 由宿主語言綁過來的 regex engine 處理;此處示意直接回 false
  return false;
}

static char *my_regex_stringify(mongory_memory_pool *pool, mongory_value *pattern) {
  (void)pool; (void)pattern;
  return NULL; // 示意:可將 pattern 轉成 "~/.../i" 形式
}

// Custom matcher adapter:lookup/build/match 三段式
static bool my_custom_lookup(char *key) {
  return (key && key[0] == '$'); // 示意:所有 $ 開頭皆視為可用
}

// 正確型別:建議回傳 mongory_matcher_custom_context*
static mongory_matcher_custom_context *my_custom_build(char *key, mongory_value *cond, void *extern_ctx) {
  (void)cond; (void)extern_ctx;
  // 這裡通常會建立一個外部(宿主語言)matcher 並包成 ctx 回傳
  return NULL; // 示意用
}

static bool my_custom_match(void *external_matcher, mongory_value *value) {
  (void)external_matcher; (void)value;
  return false; // 示意:實務上委派宿主語言的邏輯
}

static void register_adapters() {
  mongory_regex_func_set(my_regex_match);
  mongory_regex_stringify_func_set(my_regex_stringify);

  mongory_custom_matcher_lookup_func_set(my_custom_lookup);
  mongory_custom_matcher_build_func_set(my_custom_build);
  mongory_custom_matcher_match_func_set(my_custom_match);
}

注意:上例僅示意註冊點與責任邊界;實務上會在宿主語言側維護 registry、錯誤轉譯與生命週期(詳見 Day16/Day17/Bonus 3)。

Adapter 實務策略:

  • Regex:
    • 在 Ruby 使用 Regexp/Onigmo,在 Go 走 regexp 標準庫;透過 stringify 讓 explain 顯示人類可讀的樣式(如 /^foo.*/i)。
    • 時間成本高且語義易變化,應置於較低優先級;必要時新增快取層。
  • Custom:
    • lookup:決定哪些 $key 交由宿主語言接管,避免 C core 誤判。
    • build:把條件(可能是 table/array/scalar)打包為外部 matcher 的初始化資訊,並綁到 mongory_matcher_custom_context
    • match:在匹配階段回呼宿主語言;注意錯誤與例外要折返成 pool->error 或 false。

跨語言錯誤處理:

  • 宿主語言發生例外時,應捕捉並以錯誤碼或訊息回填到 pool->error,避免讓 C core 處於不一致狀態。
  • 為避免隱性拋例外穿越 cgo/Ruby C 邊界,建議全改為顯性返回值與錯誤欄位。

為什麼要透過 adapter 外包給宿主語言

  • 語義穩定與擴充性:Regex 引擎或業務規則在不同生態差異巨大,外包可換引擎、可演進。
  • 可移植性:C core 不綁平台特性,減少跨平台維護成本。
  • 效能/迭代:熱點可以由宿主語言以成熟庫優化,C core 專注在資料流與調度。
  • 可觀測性:邊界清楚,trace/explain 在 C core 仍能完整輸出決策路徑。

實務案例:從條件文件到 explain/trace 的完整旅程

條件(概念示意):

{
  "age": { "$gte": 18 },
  "$or": [
    { "status": { "$in": ["active", "pending"] } },
    { "name": { "$regex": "^J" } }
  ]
}

旅程步驟:

  1. table_cond 識別 age 為欄位、$or 為運算子。
  2. 生成 Field("age") -> Gte(18)Or(children=[...])
  3. Or 的第一個 child 是 Field("status") -> In(["active","pending"]);第二個是 Field("name") -> Regex("^J")
  4. 若啟用 priority:GteIn 會優先於 Regex
  5. explain 生成一棵可讀樹;trace 在每個節點記錄結果與耗時(如有)。

explain(簡化示意):

And: {"age"=>{"$gte"=>18}, "$or"=>[{"status"=>{"$in"=>...}
├─ Field: "age" to match: {"$gte"=>18}
│  └─ Gte: 18
└─ Or: [{"status"=>{"$in"=>...
   ├─ Field: "status" to match: {"$in"=>["active","pending"]}
   │  └─ In: ["active","pending"]
   └─ Field: "name" to match: {"$regex"=>/^J/}
      └─ Regex: /^J/

trace(簡化示意):

QueryMatcher Matched, condition: {"age"=>{"$gte"=>18}, "$or"=>[{"status"=>...
  AndMatcher Matched, condition: {"age"=>{"$gte"=>18}, "$or"=>[{"status"=>...
    FieldMatcher Matched, condition: {"$gte"=>18}, field: "age", record: {"age"=>25}
      GteMatcher Matched, condition: 18, record: 25
    OrMatcher Matched, condition: [{"status"=>{..}}, {"name"=>{..}], record: {..}
      FieldMatcher Matched, condition: {"$in"=>["active","pending"]}, field: "status", record: {"status"=>"active"}
        InMatcher Matched, condition: ["active","pending"], record: "active"

常見陷阱與對策

  • $every 誤解包,導致語意改變。
    • 對策:建立 must-preserve 清單,unwrap 僅允許白名單類型。
  • $in/$nin 的集合型別不一致或含 NULL 導致邊界行為不明。
    • 對策:在 build 期規範化集合(剔除 NULL 或轉型),或於 compare 層嚴格型別檢查。
  • Regex 的引擎差異(大小寫、Unicode、錨點)導致跨語言不一致。
    • 對策:把語意定義留在宿主語言,C core 僅做委派;explain 使用 stringify 確認最終樣式。
  • trace 輸出過大影響效能。
    • 對策:等級化的 trace level;在 CI 僅開關鍵案例。

錯誤處理與生命週期

  • 錯誤源:
    • 記憶體配置失敗(例如擴容/建樹)→ 設定 pool->error = &MONGORY_ALLOC_ERROR
    • 外部 adapter 回呼失敗 → 設定錯誤並讓 match 返回 false 或上拋至建樹失敗。
  • 生命週期:
    • 所有 matcher 與其子結構從同一 pool 配置,pool_free 時批次回收。
    • extern_ctx 僅保存指標,不主導釋放;釋放由宿主語言生命週期負責(Day 17 展開)。

擴充點與進階主題(預告關聯)

  • Day 16:VALUE ↔ C value 轉換策略,界定 converter 與 adapter 的責任邊界,鋪陳 shallow/deep/recover。
  • Day 17:GC × memory pool 生命週期,解決 extern_ctx 與外部物件壽命協同。
  • Bonus 3:Custom matcher 全攻略(registry、build/match/lookup、跨語言錯誤、效能)。

小結與預告

  • Mongory 的 Matchers 架構以「可遍歷+可觀測」為核心,透過 priority 與 unwrap 降低平均成本,再用 adapter 把多變的邏輯外包,維持核心簡潔。
  • 明天 Day 15 將回到 Ruby 的世界,我們來探討 Ruby C 擴充與 submodule:extconf.rb/build 流程、macOS/Clang 要點、子模組同步與本地發佈。

專案(C Core)


上一篇
Day 13:Hash Table(Table):bucket/雜湊/衝突策略取捨
下一篇
Day 15:Ruby C 擴充與 submodule
系列文
Mongory:打造跨語言、高效能的萬用查詢引擎25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言