mike-neckのブログ

Java or Groovy or Swift or Golang

datomicチュートリアル6日目 #datomic

今日はルールについて書きます

ルールとは

クエリーが複雑になるにつれて:where句に内容の重複が発生してきます。その無駄を省くために再利用可能な条件式を定義できます。

ルールはいくつかの条件式を名前をつけてまとめたもので、:where句の中で利用することができます。

例::communityのうち:community/type:community.type/twitterのものを選び出すルール

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

ルールはリストのリストで記述されます。

  1. 最初のリストはヘッドと呼び、ルールの名前と引数を定義します
  2. 残りのリストではルールを構成する条件式を記述します

上記のルールの例は、twitterという名前を持ち、?communityという入力の引数を取ります。そして本体部分の条件式ではエンティティ?communityの要素:community/type:community.type/twitterであるかテストをします。

このルールは出力となる引数はありません。その代わりに?communityが条件をみたすかどうかの真偽値を返します。

こうした複数のルールをリストにまとめてルールセットを構成することで実際のクエリーで使うことができます。

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

ルールセットをクエリーに適用する方法は以下のとおりです。

  • ルールセットをクエリーに入力ソースとして渡して、:in句の中でそれを%で受け取ります
  • :where句の中でルールを呼び出します

ルールを呼び出す式は次のとおりです

(rule-name rule-arguments*)

ルールの呼び出しはルール名と引数のリストです。(なお、丸括弧で記述していますが、角括弧でも呼び出すことは可能らしいです。)

@Test
void ルールの書き方使い方() {
    // ルール名の定義
    def ruleName = 'twitter'
    def argument = '?arg'
    def head = "[${ruleName} ${argument}]"
    // ルールの定義
    def condition = '[?arg :community/type :community.type/twitter]'
    def conditions = [condition]
    def rule = "[${head} ${conditions.join('')}]"
    // ルールはルールのリストで定義される
    def rules = [rule]
    def rulesDefinition = "[${rules.join('')}]" as String
    // ルール呼び出し
    def ruleArgument = '?community'
    def ruleInvocation = "(${ruleName} ${ruleArgument})"
    // 最終的なクエリー
    def query = $/[
:find
    [?name ...]
:in
    $
    %
:where
    [${ruleArgument} :community/name ?name]
    ${ruleInvocation}]/$ as String
    def results = Peer.query(query, db, rulesDefinition)
    assert results.size() == 6
    LOG.debug "[${test.methodName}] results: [${results.join(', ')}]"
}

このサンプルコードで実行するクエリは次のようになります。

[
:find
    [?name ...]
:in
    $
    %
:where
    [?community :community/name ?name]
    (twitter ?community)]

適用するルールは次のとおりです。

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

実行結果はこのようになります。

results: [Magnolia Voice, Columbia Citizens, Discover SLU, Fremont Universe, Maple Leaf Life, MyWallingford]

複雑なルール

:community:neighborhood:district:regionとたどって、:regionで絞込をかけたい場合も、結合条件の式をルール化することでクエリーが単純になります。

@Test
void 少し複雑なルールの例() {
    // :community -> :neighborhood -> :district -> :region でマッチング
    def rule = $/[
[region ?community ?region]
    [?community :community/neighborhood ?nbr]
    [?nbr :neighborhood/district ?dist]
    [?dist :district/region ?dir]
    [?dir :db/ident ?region]]/$
        def rules = "[${[rule].join('')}]" as String
    def rules = "[${[rule].join('')}]" as String
    // 指定した:district/regionにマッチする:community/nameを取得するクエリー
    def query = $/[
:find
    [?name ...]
:in
    $
    ?reg
    %
:where
    [?community :community/name ?name]
    (region ?community ?reg)]/$ as String
    [':region/ne', ':region/sw'].each {region ->
        def results = Peer.query(query, db, region, rules)
        LOG.debug "[${test.methodName}] results: [size: [${results.size()}], values: [${results.join(', ')}]]"
    }
}

実行結果

results: [region:[:region/ne], size: [9], values: [Maple Leaf Community Council, Hawthorne Hills Community Website, KOMO Communities - View Ridge, KOMO Communities - U-District, Magnuson Community Garden, Laurelhurst Community Club, Aurora Seattle, Maple Leaf Life, Magnuson Environmental Stewardship Alliance]]
results: [region:[:region/sw], size: [34], values: [Beach Drive Blog, KOMO Communities - Green Lake, Delridge Produce Cooperative, Alki News, Longfellow Creek Community Website, Licton Springs Neighborhood , Friends of Green Lake, KOMO Communities - Wallingford, MyWallingford, Greenwood Community Council Announcements, Alki News/Alki Community Council, Nature Consortium, Broadview Community Council, KOMO Communities - West Seattle, ArtsWest, Community Harvest of Southwest Seattle, Highland Park Improvement Club, Morgan Junction Community Association, Admiral Neighborhood Association, Greenwood Community Council, Greenwood Community Council Discussion, Genesee-Schmitz Neighborhood Council, Fauntleroy Community Association, KOMO Communities - Greenwood-Phinney, Delridge Grassroots Leadership, Greenlake Community Wiki, Highland Park Action Committee, Greenwood Aurora Involved Neighbors, Greenwood Phinney Chamber of Commerce, Junction Neighborhood Organization, Delridge Neighborhoods Development Association, Greenwood Blog, My Greenlake Blog, Greenlake Community Council]]

同じルール名

同じルール名を持つルールをルールセットに入れることで「OR」検索もできます。

@Test
void 同じ名前のルールも設定できる() {
    // 同じ名前で条件が異なるルールを定義
    def ruleName = 'social-media'
    def argument = '?community'
    def rules = [$/[
[${ruleName} ${argument}]
    [?community :community/type :community.type/twitter]]/$, $/[
[${ruleName} ${argument}]
    [?community :community/type :community.type/facebook-page]]/$]
    def ruleDefinition = "[${rules.join('\n')}]" as String
    // クエリー
    def query = $/[
:find
    [?name ...]
:in
    $
    %
:where
    [?community :community/name ?name]
    (social-media ?community)]/$ as String
    def results = Peer.query(query, db, ruleDefinition)
    assert results.size() == 9
    LOG.debug "[${test.methodName}] results: [size: [${results.size()}], value: [${results.join(', ')}]]"
}

上記のルールセットはsocial-mediaという同じ名前を持つルールが二つ定義されています。一つは:community/type:community.type/twitterであるかテストするルールであり、もう一つは:community/type:community.type/facebook-pageのものであるかをテストするルールです。

これと同様で個別に検索を行った結果は過去のエントリーにあるので、結果の数は9であることが想定されます。

これを実行するとassertはとおり、次のように表示されます。

results: [size: [9], value: [Magnolia Voice, Columbia Citizens, Discover SLU, Fauntleroy Community Association, Eastlake Community Council, Fremont Universe, Maple Leaf Life, MyWallingford, Blogging Georgetown]]

ルールは他のルールを適用できる

ルールはルールセット中の他のルールを適用することができます。

@Test
void ルールの中で他のルールを適用する() {
    // regionのルール
    def region = $/[
        [region ?community ?region]
            [?community :community/neighborhood ?nbr]
            [?nbr :neighborhood/district ?dist]
            [?dist :district/region ?dir]
            [?dir :db/ident ?region]]/$
    // ソーシャルメディアのルール
    def socialMedia = ['twitter', 'facebook-page'].collect{"/${it}"}.collect {
        $/[[social-media ?community]
            [?community :community/type :community.type${it}]]/$
    }.join('\n    ')
    // ルールregionを適用するルール
    def applyingRegion = [
            [ruleName: 'northern', region: ':region/ne'],
            [ruleName: 'northern', region: ':region/n'],
            [ruleName: 'northern', region: ':region/nw'],
            [ruleName: 'southern', region: ':region/sw'],
            [ruleName: 'southern', region: ':region/s'],
            [ruleName: 'southern', region: ':region/se']
    ].collect {
        "[[${it.ruleName} ?arg] (region ?arg ${it.region})]"
    }.join('\n    ')
    // 最終的なルール
    def rulesDefinition = $/[
    ${region}
    ${socialMedia}
    ${applyingRegion}]/$ as String
    LOG.debug "[${test.methodName}] : rule\n${rulesDefinition}"
    // クエリ実行
    def results = Peer.query($/[
:find
    [?name ...]
:in
    $
    %
:where
    [?community :community/name ?name]
    (southern ?community)
    (social-media ?community)]/$ as String, db, rulesDefinition)
    assert results.size() == 4
    LOG.debug "[${test.methodName}] results: [size: [${results.size()}], value: [${results.join(', ')}]]"
}

「ルールregionを適用するルール」というコメント以降で定義されているルールでは、その内部で「regionのルール」で定義されているregionを適用しています。

    [[northern ?arg] [region ?arg :region/ne]]

このようなルールセットで一緒に定義されている他のルールを適用することが可能です。

なお実行結果は次のようになります。

ルールセットの定義

[
    [
        [region ?community ?region]
            [?community :community/neighborhood ?nbr]
            [?nbr :neighborhood/district ?dist]
            [?dist :district/region ?dir]
            [?dir :db/ident ?region]]
    [[social-media ?community]
            [?community :community/type :community.type/twitter]]
    [[social-media ?community]
            [?community :community/type :community.type/facebook-page]]
    [[northern ?arg] (region ?arg :region/ne)]
    [[northern ?arg] (region ?arg :region/n)]
    [[northern ?arg] (region ?arg :region/nw)]
    [[southern ?arg] (region ?arg :region/sw)]
    [[southern ?arg] (region ?arg :region/s)]
    [[southern ?arg] (region ?arg :region/se)]]

実行結果の出力

results: [size: [4], value: [Columbia Citizens, Fauntleroy Community Association, MyWallingford, Blogging Georgetown]]

昨日、今日と関数、ルールという、うまく活用すればかなりパワフルな表現を学習しました。

明日はdatomicの売りである時間軸とうまくやっていく方法を学んでいきます。


おわり