一開始,來自筆者工作上的需求:把條件寫在 S3 上的 file,應用程式動態讀取,決定要不要執行某些動作
但問題在於「如何規劃條件式 DSL」,以及「如何解析」
這時我把目光瞄向了 MongoDB 的 Query 語法
MongoDB Query 語法的特性是使用類 JSON 結構表達條件,結構清晰直觀,條件表達式語意明確,非常易於編寫與擴充
但市面上現有的 lib 套件找不到這種可以動態編譯 JSON 格式條件,並作為條件篩選的 engine,而 MongoDB Query 本身又深度綁定 DB,無法用於純資料
MongoDB 的 query 語法很好用,我希望把它「抽離資料庫」也能用在記憶體中的任意資料結構上
雖然後來工作上那個需求改方案了決定寫死條件,但筆者做出的最初版保留了下來,並且決定開源
於是有了 Mongory : 一個在記憶體陣列上執行 Mongo 風格篩選的 DSL filter engine!
命名就是 Mongo + query 的複合字,取其只實作 Mongo query 邏輯,但不綁定 DB 的概念
Mongory, let you query everywhere !
本篇不長篇大論,直接在 5 分鐘內跑起來,先拿到成就感,再回頭看故事與原理
gem install mongory
# 或 Gemfile
# gem 'mongory'
require 'mongory'
# 開啟 Symbol 語法糖,並註冊可在 Array 上呼叫 .mongory
Mongory.enable_symbol_snippets!
Mongory.register(Array)
# 模擬資料
records = [
{ 'name' => 'Jack', 'age' => 18, 'tags' => [{ 'name' => 'ruby', 'priority' => 6 }] },
{ 'name' => 'Jill', 'age' => 15, 'tags' => [{ 'name' => 'rails', 'priority' => 3 }] },
{ 'name' => 'Bob', 'age' => 21, 'tags' => [{ 'name' => 'ruby', 'priority' => 8 }] }
]
# 建構你的 Query
query = records.mongory
.where(:age.gte => 18) # 基本比較子
.any_of( # 巢狀 OR(語義 alias)
{ :name.regex => /^J/ },
{ :tags.elem_match => { :name => 'ruby' } }
)
# each 直接出來的就會是篩選過的資料
query.each { |r| p r }
你應該會看到 Jack
與 Bob
,因為他們年齡 >= 18,且名字以 J 開頭或 tags
內含 ruby
query.explain
實際輸出:
└─ Query: {"$and"=>[{"age"=>{"$gte"=>18}}, {"$or"=>[{"name"=>...
└─ And: [{"age"=>{"$gte"=>18}}, {"$or"=>[{"name"=> ...
├─ Field: "age" to match: {"$gte"=>18}
│ └─ Gte: 18
└─ Or: [{"name"=>{"$regex"=>/^J/}}, {"tags"=>{"$elemMatch"...
├─ Field: "tags" to match: {"$elemMatch"=>{"name"=>"ruby"}}
│ └─ ElemMatch: {"name"=>"ruby"}
│ └─ Field: "name" to match: "ruby"
│ └─ Eq: "ruby"
└─ Field: "name" to match: {"$regex"=>/^J/}
└─ Regex: /^J/
records.mongory.where(age: 18).trace { |x| ... }
輸出會列出每個 matcher 的命中與否,幫你快速定位條件設計問題
輸出 be like:
這非常好用,尤其是在你面對一堆手寫篩選條件,程式運作不如預期卻不知道從何 debug 起時
if defined?(Mongory::CMatcher)
records.mongory.c.where(:age.gte => 18).each { |x| ... }
end
一樣可以 trace
records.mongory.c.where(:age.gte => 18).trace { |x| ... }
在少量資料或是簡單條件的場景,效能通常跟一般純 Ruby 的手寫篩選持平
當資料筆數越多,或是條件越複雜時,效能甚至有機會超越純 Ruby
更不用說 Mongory 還提供比對過程的追蹤
limit(n)
會「立即」生效,且影響後續條件的資料子集(效能最佳化用途)
$in/$nin
對 Array 欄位是「交集」語意,不是單純 include?
:tags => 'a'
對 Array 欄位會自動包成 $elemMatch
走陣列元素比對
好咧,今天的文章就到這邊,相信讀者您已經:
explain
看見 matcher treetrace
追蹤逐筆匹配接下來(Day 2 ~ Day 4)會帶你走過 matcher tree 的設計與運算子細節;Week 2 會進入 C core;Week 3 介紹 Ruby Bridge 與效能戰;Week 4 探索 Go Bridge 與未來規劃
也歡迎在 Issue 留下你想要的運算子或真實案例,讓我們在接下來的篇章中一起打磨