iT邦幫忙

2025 iThome 鐵人賽

DAY 2
0
佛心分享-SideProject30

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

Day 2:為什麼需要 Query Engine:S3 JSON → 匹配引擎的誕生

  • 分享至 

  • xImage
  •  

問題背景

一開始的任務只有一句話:在 S3 放一份可隨時修改的 file,應用程式讀取後「照條件做事」
筆者我很自然就想:那我用 functional programming 一個個解析條件式並進行匹配不就好了?

真的動手寫後,問題一個個浮現:

  • 條件散落多處,重複邏輯難以維護
  • 每筆資料都要重新解析條件,浪費效能,不符合「客家精神」
  • 面對 Array、巢狀欄位、正規表示式等複雜情境時,條件篩選式漸漸變得難以維護

筆者開始意識到:我們其實需要的是「描述條件的語言」以及「穩定的執行引擎」
這就是 query engine 存在的理由

技術細節、程式碼範例、解析

  1. 直寫 if/else 的代價(維護性問題)
def eligible?(user)
  return false unless user

  over_18 = user[:age] && user[:age] >= 18
  from_tw = user[:country] == 'TW' || user[:region] == 'ASIA-TW'
  has_ruby = Array(user[:tags]).any? { |t| t[:name] == 'ruby' }

  (over_18 && from_tw) || has_ruby
end

當需求變成「18 歲以上、且標籤含 ruby、且狀態 active 或 pending」,你會再加一層 OR/AND 嗎?很快地,這段 code 就會難以閱讀與測試

  1. 用 JSON 描述條件,但需要「可解析的引擎」
{
  "age": { "$gte": 18 },
  "$and": [
    { "status": { "$in": ["active", "pending"] } },
    { "tags": { "$elemMatch": { "name": "ruby" } } }
  ]
}

JSON 很適合「攜帶資訊」,但它不是「程式語言」
如果只是 parse 成 Ruby Hash,再用 if/else 解讀一次,本質問題依舊:你只是在另一個地方重寫一套解讀器

  1. Query Engine 的關鍵設計目標
  • 一致的語意:同一份條件,在任何資料結構(Hash、自定義物件)上,結果一致

  • 可觀測性:能 explain(樹狀結構)、能 trace(逐步匹配),方便排錯

  • 可擴充:新增運算子(如 $every)不影響既有邏輯

  • 效能:條件只解析一次,後續以可執行的 matcher tree 直接跑

  1. 從 JSON 到 Query Engine:最小可行範例
require 'mongory'
Mongory.enable_symbol_snippets!
Mongory.register(Array)

records = [
  { 'name' => 'Jack', 'age' => 18, 'status' => 'active', 'tags' => [{ 'name' => 'ruby' }] },
  { 'name' => 'Jill', 'age' => 15, 'status' => 'pending', 'tags' => [{ 'name' => 'rails' }] },
  { 'name' => 'Bob',  'age' => 21, 'status' => 'inactive', 'tags' => [{ 'name' => 'ruby' }] }
]

condition = {
  "age": { "$gte": 18 },
  "$and": [
    { "status": { "$in": ["active", "pending"] },
    { "tags": { "$elem_match": { "name": "ruby" } }
  ]
}

records.mongory.where(condition).each { |r| p r }

差異在於:這不是把 JSON 轉成 if/else,而是把條件「編譯成 matcher tree」,比對時直接執行每個 matcher 的邏輯

  1. 為什麼不直接用 MongoDB?
  • Mongory 的目的不是替代 DB,而是把「查詢語意」帶到記憶體世界

  • 典型情境 1:

    • 先用 DB 取出一批資料(走索引)

    • 再用 Mongory 在應用層處理「沒有索引的欄位」或「更複雜的陣列條件」

  • 典型情境 2:

    • API 拉回一批資料
    • 透過 Mongory 用同一條 condition 表達式進行篩選
  • 典型情境 3:

    • 條件為前端動態傳入,程式需要依靠動態傳入的條件進行篩選邏輯
    • 使用 Mongory Matcher 解析條件表達式並進行匹配
  1. 設計抉擇(預告後續篇章會展開)
  • 單子條件解包(unwrap):避免多餘的 AND 包裝,減少 callstack

  • $in/$nin 對 Array 的「交集」語意:與單值 include? 不同

  • Array 欄位的包裝:{ tags: 'a' } 會自動轉為 $elemMatch

  • Explain 與 Trace:診斷複雜條件的必備工具

結語

今天我們了解了「為什麼需要 Query Engine」:

  • 它讓條件成為「可閱讀、可測試、可擴充」的語言,而不是散落的 if/else

  • 它把 JSON 的資料攜帶能力,補上「可執行語意」

  • 它提供 explain/trace,補足了篩選過程的「可觀測性」

明天(Day 3)我們會正式走進 matcher tree 的世界,從 eq/field/and 開始,把各個元件拆解清楚;後面會談進階運算子($gt/$gte/$in/$nin/$elemMatch)與為什麼需要單子條件解包

專案首頁(Ruby 版)

如果讀者您在專案中也有「應用層過濾」的需求,歡迎在 Issue 分享你的情境~


上一篇
Day 1:快速上手與緣起:5 分鐘跑起 Mongory(Quick Start + 起源)
下一篇
Day 3:AST 與 matcher tree 基本結構
系列文
Mongory:打造跨語言、高效能的萬用查詢引擎10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言