日曜プログラミング

休日趣味でやってるプログラミング関連記事をダラダラと

datomic チュートリアルを試す(4) - アドバンスド・クエリー

前回、SQL で言うと SELECT, WHERE, JOIN の基本的な使い方についてやったような感じかな。記事はこちら。
datomic チュートリアルを試す(3) - 実体の属性値をクエリで取る - 日曜プログラミング

今回の記事で公式チュートリアルで該当する原文項目は以下。

  • Advanced queries
    • Parameterizing queries
    • Invoking functions in queries
    • Querying with fulltext search
    • Querying with rules

今回は Advanced queries らしいです。
Advanced とかの響きがカッコいいので今回は直訳しました。

パラメータクエリ(Parameterizing queries)

前回書いたクエリのクエリ部分のみ再掲。

; コミュニティのタイプが twitter なコミュニティ名を取得する
[:find ?n
 :where
 [?c :community/name ?n]
 [?c :community/type :community.type/twitter]]

このクエリをパラメータクエリにするとこんな感じ。

(d/q '[:find ?n
       :in $ ?t
       :where
       [?c :community/name ?n]
       [?c :community/type ?t]]
     (d/db cn)
     :community.type/twitter)

違いは :in の部分。$ が外から与えられるパラメータを表してて、この例だと与えられたものがクエリ内の ?t にバインドされるイメージ。
$ が外から与えられるもののプレフィックスみたいなもんで $ の後に任意で名前をつけることも可能。

これでクエリをこんな感じで関数化する事も可能に。

; t で指定されたタイプのコミュニティ名を返す
(defn find-by-type [t]
  (d/q '[:find ?n
         :in $ ?t
         :where
         [?c :community/name ?n]
         [?c :community/type ?t]]
       (d/db cn) t))

使ってみる。最初に :community.type/twitter まんま指定してクエリ実行してた時と同じ結果になる。

> (find-by-type :community.type/twitter)
#{["Discover SLU"]
  ["Fremont Universe"]
  ["Columbia Citizens"]
  ["Magnolia Voice"]
  ["Maple Leaf Life"]
  ["MyWallingford"]}

twitterfacebook いずれかのコミュニティ名を取得したい場合、:in が少し変わる。

; t で指定されたタイプのコミュニティ名を返す(複数版)
(defn find-by-types [t]
  (d/q '[:find ?n ?t
         :in $ [?t ...]
         :where
         [?c :community/name ?n]
         [?c :community/type ?t]]
       (d/db cn) t))

実行。渡すのは属性のベクタなのに注意。

> (find-by-types [:community.type/twitter :community.type/facebook-page])
#{["Discover SLU" :community.type/twitter]
  ["Magnolia Voice" :community.type/facebook-page]
  ["Blogging Georgetown" :community.type/facebook-page]
  ["Maple Leaf Life" :community.type/twitter]
  ["Fremont Universe" :community.type/twitter]
  ["Magnolia Voice" :community.type/twitter]
  ["Columbia Citizens" :community.type/facebook-page]
  ["Fauntleroy Community Association" :community.type/facebook-page]
  ["Columbia Citizens" :community.type/twitter]
  ["Eastlake Community Council" :community.type/facebook-page]
  ["Maple Leaf Life" :community.type/facebook-page]
  ["Fremont Universe" :community.type/facebook-page]
  ["Discover SLU" :community.type/facebook-page]
  ["MyWallingford" :community.type/twitter]
  ["MyWallingford" :community.type/facebook-page]}

引数で指定したタイプも :find の ?t で出すようにすることで、指定したタイプのコミュニティが抽出されている事が分かる。

更にもう一歩進んで、find-by-types でやった複数の条件を複数個渡す場合。

; t で指定したタイプのコミュニティ名を返す
(defn find-by-multi-types [t]
  (d/q '[:find ?n ?t ?ot
         :in $ [[?t ?ot]]
         :where
         [?c :community/name ?n]
         [?c :community/type ?t]]
       (d/db cn) t))

実行。渡すのはベクタベクタになる。

> (find-by-multi-types [[:community.type/email-list :community.orgtype/community]
			[:community.type/website :community.orgtype/commercial]])
#{["Fauntleroy Community Association" :community.type/website :community.orgtype/commercial]
  ["Ballard Neighbor Connection" :community.type/email-list :community.orgtype/community]
  ...}

関数呼び出し側から見ると全部 t の 1 個だからドキュメント付けとかないと 100% ワケわからなくなるな。

クエリ内関数(Invoking functions in queries)

以下の様な感じでクエリ内で関数を使用できる。最初見た時良く分からなかったのでコメントを後ろに付けた。

> (d/q '[:find ?n                ; 4. ?n を探せ
         :where
         [?c :community/name ?n] ; 1. コミュニティ名を ?n とした時、
         [(compare ?n "C") ?res] ; 2. ?n を "C" と比較した結果を ?res へ入れ、
         [(< ?res 0)]]           ; 3. ?res が 0 未満(= "C" より小さいアルファベット) な
     (d/db cn))

Clojure の as-> マクロの変形みたいなもんと考えれば覚えやすいかもしれない。as-> まんまとはいかずとも関数チェーンを許してるどうかは未確認。

上記クエリの結果はこちら。アルファベットで言う B 以前のコミュニティ名だけ抽出されているのが分かる。

#{["All About South Park"]
  ["Ballard Neighbor Connection"]
  ["Ballard Blog"]
  ["At Large in Ballard"]
  ["Ballard Chamber of Commerce"]
  ["Beacon Hill Burglaries"]
  ["Alki News"]
  ["Beacon Hill Alliance of Neighbors"]
  ["Beach Drive Blog"]
  ["Ballard Avenue"]
  ["Aurora Seattle"]
  ["Ballard Moms"]
  ["15th Ave Community"]
  ["All About Belltown"]
  ["Admiral Neighborhood Association"]
  ["Ballard District Council"]
  ["Beacon Hill Community Site"]
  ["ArtsWest"]
  ["Alki News/Alki Community Council"]
  ["Bike Works!"]
  ["Beacon Hill Blog"]
  ["Ballard Gossip Girl"]
  ["Blogging Georgetown"]
  ["Broadview Community Council"]
  ["Ballard Historical Society"]}

ちなみにクエリエンジンが名前空間なしに認識できる関数は java.lang と clojure.core のものだけらしい。「クエリエンジンが」とわざわざ書いてあると言う事は多分ソース内で use とかしても上記名前空間以外の関数は名前空間を前に付けないといけないんだろうがこれも未確認。

全文検索クエリ(Querying with fulltext search)

  • 前提条件
    • 属性の型が String タイプ
    • 属性定義の際に全文検索するような定義も併せて行う必要あり

こういう前提条件があるようだが、datomic では全文検索をサポートしているらしい。

:community/name は設定済み属性らしいので、その属性を元にしたサンプルはこちら。

; "Wallingford" が含まれているコミュニティ名を取得
> (d/q '[:find ?n
         :where
         [(fulltext $ :community/name "Wallingford") [[?e ?n]]]]
     (d/db cn))
#{["KOMO Communities - Wallingford"]}

fulltext はクエリエンジン組み込みの関数らしい。
3 つ引数を取り、上記サンプルだと

  • データベースの入力ソース→ $(これがイマイチ良く分からん)
  • 検索対象の属性名→ :community/name
  • 検索文字列→ "Wallingford"

結果を受け取るのが ?e ?n の部分。fulltext 関数の結果はエンティティと値で構成されたタプルを要素とするコレクションで、サンプルではあらかじめ destructuring して値だけ返すようにクエリを組んでる。

サンプルを少し書き換えたら分かる。

エンティティも見えるようにしたもの。(実際見えるのは数値で管理されたエンティティ ID)

> (d/q '[:find ?e ?n
         :where
         [(fulltext $ :community/name "Wallingford") [[?e ?n]]]]
     (d/db cn))
#{[17592186045605 "KOMO Communities - Wallingford"]}

結果をそのまま受け取るもの。

> (d/q '[:find ?c
         :where
         [(fulltext $ :community/name "Wallingford") ?c]]
     (d/db cn))
#{[#{[17592186045605 "KOMO Communities - Wallingford" 1021 1.0]}]}

"Wallingford" の部分をパラメータ化する事も可能。

; 指定したコミュニティのタイプ(?type)と文字列(?search)がカテゴリに含まれるものを探す
(defn find-by [type search]
  (d/q '[:find ?name ?cat
         :in $ ?type ?search
         :where
         [?c :community/name ?name]
         [?c :community/type ?type]
         [(fulltext $ :community/category ?search) [[?c ?cat]]]]
       (d/db cn)
       type search))

このクエリでは :community/category の定義を見ると分かるが Cardinality が Many となっている為、ここでも暗黙の JOIN がされている事になるのかな?。

実行例。

; タイプが website でカテゴリに "food" が含まれるコミュニティを返す
> (find-by :community.type/website "food")
#{["InBallard" "food"] ["Community Harvest of Southwest Seattle" "sustainable food"]}

ルールを適用したクエリ(Querying with rules)

ルールとは。
ごく簡単に言えば、よく使う :where 句に名前を付けて再利用・合成する事が可能な仕組み。

いきなりサンプル。クエリ部分だけ。

[[twitter ?c]
 [?c :community/type :community.type/twitter]]

最初の [] の中身は、twitter が名前。?c がルールでの引数。
次の [
] が本体。

このルールの意味合いはコミュニティのタイプが twitter な ?c が条件と言った所。
また、ルールには複数の別ルールを含める事が可能。

実際の定義ではそれが前提になっているようで、上記サンプルのような一個のルールでもこんな感じで [] で囲んでやる必要があるみたい。

[[[twitter ?c]
  [?c :community/type :community.type/twitter]]]

では定義・呼び出し・実行結果までのサンプル。

> (def rules '[[[twitter ?c]
                [?c :community/type :community.type/twitter]]])
rules

> (d/q '[:find ?n :in $ % :where [?c :community/name ?n] (twitter ?c)]
       (d/db cn)
       rules)
#{["Discover SLU"]
  ["Fremont Universe"]
  ["Columbia Citizens"]
  ["MyWallingford"]
  ["Maple Leaf Life"]
  ["Magnolia Voice"]}
  • ルールを :find で使う為に覚えるべきこと
    • ルールを外部から受け取るには :in 句内で '%' を使う
    • 定義されてる名前で :where 句内で呼び出す
    • 視認性の為ルールは()で囲むのを推奨との事だが、必須ではない

条件一つのルールじゃ正直有り難みがないので、複数条件を指定してみる。前回記事で「北西(:region/ne)にあるコミュニティ名を取得する」クエリの例があったが、この条件部分をルール化する。

> (def rule-region
    '[[[region ?c ?r]
       [?c :community/neighborhood ?n]
       [?n :neighborhood/district ?d]
       [?d :district/region ?re]
       [?re :db/ident ?r]]])
#'datomic-tut.core/rule-region

> (d/q '[:find ?n
         :in $ %
         :where
         [?c :community/name ?n]
         (region ?c :region/ne)]
       (d/db cn) rule-region)
#{["KOMO Communities - U-District"]
  ["Hawthorne Hills Community Website"]
  ...}

> (d/q '[:find ?n
         :in $ %
         :where
         [?c :community/name ?n]
         (region ?c :region/sw)]
       (d/db cn) rule-region)
#{["Greenwood Community Council Announcements"]
  ["Genesee-Schmitz Neighborhood Council"]
  ...}

ついでにパラメータ化で region がどこでも対応できるようになってる。

ルールで OR 条件を作りたい場合はこう。クエリ部分のみ。

[[[social-media ?c]
  [?c :community/type :community.type/twitter]]
 [[social-media ?c]
  [?c :community/type :community.type/facebook-page]]]

これを呼び出せば twitter or facebook なコミュニティが抽出される。

最初に書かれていたルール定義時は複数のルールに名前を付けて渡す事も可能でこんな感じで定義できる。

(def rule-set
  '[[[region ?c ?r]
     [?c :community/neighborhood ?n]
     [?n :neighborhood/district ?d]
     [?d :district/region ?re]
     [?re :db/ident ?r]]
    [[social-media ?c]
     [?c :community/type :community.type/twitter]]
    [[social-media ?c]
     [?c :community/type :community.type/facebook-page]]
    [[northern ?c] (region ?c :region/ne)]
    [[northern ?c] (region ?c :region/n)]
    [[northern ?c] (region ?c :region/nw)]
    [[southern ?c] (region ?c :region/sw)]
    [[southern ?c] (region ?c :region/s)]
    [[southern ?c] (region ?c :region/se)]])

ルール名 northern と southern を見ると分かるが、ルール本体で別のルールを呼び出す事も可能。

上記ルールを使ったサンプル。

; 南方にあるソーシャルメディア(twitter/facebook)を使ったコミュニティを取得
> (d/q '[:find ?n
         :in $ %
         :where
         [?c :community/name ?n]
         (southern ?c)
         (social-media ?c)]
       (d/db cn) rule-set)
#{["Blogging Georgetown"]
  ["Columbia Citizens"]
  ["MyWallingford"]
  ["Fauntleroy Community Association"]}

今日はここまで。