MarkLogicのリアルタイムアラート機能を使ってみる

この記事は[MarkLogic Advent Calendar 2017](https://qiita.com/advent-calendar/2017/marklogic)の22日目です。

# はじめに
MarkLogicにはリアルタイムアラート機能が搭載されています。
特定のドキュメントに対して、登録・更新・削除が行われた時に決められた処理を実行する機能です。

事前にルールと実行したいアクションを登録しておき、そのルールにマッチするドキュメントに対して処理が行われると、登録しておいたアクションが実行されます。
例えば「自分のメールアドレスが記載されたドキュメントが登録された場合、自分宛にメールで連絡する」や、「機密情報を含むドキュメントが登録されたときに、セキュリティ担当者に通知する」などが実現できます。

# 環境
以下の環境を使用します。

| 環境 | バージョン |
| --- | --- |
| CentOS7 | 7.4.1708 |
| MarkLogic9 | 9.0-3 |

なお、アラート機能を商用で利用するには、本体とは別にライセンスが必要だそうです。
お試しで使う際は無料で使える開発者ライセンスで大丈夫ですが、商用利用の場合はお気を付け下さい。

# アラート機能を使用するための作業
アラート機能を使用するためには、以下の作業を行う必要があります。

* "fast reverse searches"インデックスを有効にする
* アラートAPIを使って、ルールやアクション等を定義する
* アラートを実行するための仕組みを用意する

以下、それぞれ概要を記します。

# fast reverse searchesインデックスの設定
通常、SQLやXQueryなどデータベースのクエリは、そのルールにマッチしたドキュメントを見つけるために使用します。

一方、MarkLogicのアラートは、あるドキュメントがマッチするルール（クエリ）を検索し、ルールが見つかった場合、紐付いてるアクションを実行します。ドキュメントからクエリを検索するこの仕組みを「reverse query」と呼んでいます。アラートはこの「reverse query」を使って実現しています。

![pic01_alerting.png](https://qiita-image-store.s3.amazonaws.com/0/187299/b2229641-627f-091c-f33d-83a273fc70a3.png)

このリバースクエリを高速に実行するために、専用のインデックス「fast reverse searches」が用意されています。
「fast reverse searches」を設定するためにはWebブラウザでMarkLogicの管理画面（ポート8001番）にアクセスします。
左ペインの「Configure」→「Databases」を辿り、アラートを設定するデータベースを選択します。右ペインの設定項目の中から「fast reverse searches」を探し、「true」に設定します。

# アラートAPI
MarkLogicのアラート機能を使用するためにはアラートAPIを使用して、アラート対象のドキュメントやルール、実行する処理の内容などを定義します。
具体的には以下の4つの作業を行います。1～3までがアラートに関する設定です。4つ目は、設定したアラートの実行確認です。

1. アラートの定義
2. アラート発生時に実行するアクション
3. アラート発生の条件（ルール）
4. アラートの実行

1～3の設定内容はXMLファイルに記述しMarkLogicに登録します。このXMLファイルの作成と登録は、それぞれ専用の関数が用意されています。
4番目の動作確認は、アラートを実行するための専用の関数が用意されており、これを使用します。

各項目について概要を記します。

## 1．アラートの定義(Alert Config)
アラートの定義を表すXMLを作成します。
[alert:make-config](https://docs.marklogic.com/alert:make-config)関数を使用して作成します。
作成したXMLは、[alert:config-insert](https://docs.marklogic.com/alert:config-insert)関数でデータベースに登録します。

アラート定義には、アラートを一意に特定するためのURIや、アラートの名前等を設定します。
以下の例では、"/my/alert/config/uri"というURIで一意に決まるアラート定義を設定しています。
このアラート定義に対して、アラート条件や実行する処理を紐付けていきます。

alert:make-config関数の引数の内容は以下の通りです。

| 引数 | 概要 |
| --- | --- |
| $uri | アラート定義を表すURI。アラートのルールやアクションの関連はこのURIで紐付ける。 |
| $name	| このアラート定義の名前 |
| $description | 説明、概要 |
| $options | オプション |

以下、アラート定義を作成する例です。
ここでは、"/my/alert/action/uri"というURIでアラートを定義しています。

```
xquery version "1.0-ml";
import module namespace alert = "http://marklogic.com/xdmp/alert" at "/MarkLogic/alert.xqy";
let $config := alert:make-config(
    "/my/alert/action/uri",
    "My Alerting App",
    "Alerting config for my app",
    <alert:options/> )
return
alert:config-insert($config)
```

## 2．アラート発生時に実行するアクション
アラート発生時に実行するアクションは、XQueryやJavascriptで実装してモジュールDBに格納しておきます。アクションの内容は、DBのドキュンメントの更新やメール送信などアプリケーションの要件に従った処理を実装します。

アクション定義のXMLは[alert:make-action](https://docs.marklogic.com/alert:make-action)関数で作成します。
作成したXMLは[alert:action-insert](https://docs.marklogic.com/alert:action-insert)関数でデータベースに登録します。この時、alert:action-insertの第1引数で、アラート定義のURIを指定することで、アラート定義とアクションを紐付けます。

alert:make-action関数の引数の内容は以下の通りです。

| 引数 | 概要 |
| --- | --- |
| $name | このアクションの名前 |
| $description | 概要、説明 |
| $module-db | XQueryモジュールを格納したデータベースIDを記述します。[xdmp:module-database()]()関数を使えばIDを取得できます。 |
| $module-root | モジュールを格納したルートディレクトリを記述します。[xdmp:module-root()](https://docs.marklogic.com/xdmp:modules-root)関数を使えば取得できます。 |
| $module | アクションを実装したXQueryやJavascriptを記述します。 |
| $options | オプションを記述します。 |

以下、アラートアクションの設定例です。
ここでのアラート処理の実装は/alert-action.xqyというXQueryになります。
先ほど作成したアラート定義のURIである"/my/alert/action/uri"に紐付けて登録しています。

```
xquery version "1.0-ml";
import module namespace alert = "http://marklogic.com/xdmp/alert" at "/MarkLogic/alert.xqy";
let $action := alert:make-action(
    "hello-world-action",
    "alerting for hello world documents",
    xdmp:modules-database(),
    xdmp:modules-root(),
    "/alert-action.xqy",
    <alert:options></alert:options> )
return
alert:action-insert("/my/alert/action/uri", $action)
```

### アラートアクションの実装について
アラートのアクションはXQueryで実装します。アクションの実行時には以下の外部変数が渡されます。

```
declare variable $alert:config-uri as xs:string external;
declare variable $alert:doc as node() external;
declare variable $alert:rule as element(alert:rule) external;
declare variable $alert:action as element(alert:action) external;
```

ここでは以下のようなアクションalert-action.xqyを作成しました。
ドキュメントがルールにマッチした場合、/alert-result/ディレクトリ配下に、現在日時をファイル名とするXMLファイルが作成されます。
このファイルの内容は、マッチしたアラート定義のURI、ルールのID、該当したドキュメントのURIと内容、現在日時になります。

```
xquery version "1.0-ml";

declare namespace alert = "http://marklogic.com/xdmp/alert";

import module "http://marklogic.com/xdmp/alert" at "/MarkLogic/alert.xqy";

declare variable $alert:config-uri as xs:string external;
declare variable $alert:doc as node() external;
declare variable $alert:rule as element(alert:rule) external;
declare variable $alert:action as element(alert:action) external;

let $current := fn:current-dateTime()

return
  xdmp:document-insert(
    fn:concat("/alert-result/",$current,".xml"),
    <alert>
      <config-uri>{$alert:config-uri}</config-uri>
      <rule-id>{alert:rule-get-id($alert:rule)}</rule-id>
      <document-uri>{fn:base-uri($alert:doc)}</document-uri>
      <document>{$alert:doc}</document>
      <alert-time>{$current}</alert-time>
    </alert>)
```

## 3．アラート発生の条件（ルール）
アラートを発生させる条件であるルールを定義します。
ルール定義のXMLは[alert:make-rule](https://docs.marklogic.com/alert:make-rule)関数で作成します。
作成したXMLは[alert:rule-insert](https://docs.marklogic.com/alert:rule-insert)関数でデータベースに登録します。この時、alert:rule-insertの第1引数で、アラート定義のURIを指定することで、アラート定義とルールを紐付けます。

alert:make-rule関数の引数は以下の通りです。

| 引数 | 概要 |
| --- | --- |
| $name | ルールの名称 |
| $description | ルールの説明 |
| $user-id | このルールにマッチした場合の通知先のユーザID。0を指定すると、このクエリの実行者となる。 |
| $query | ルール定義したクエリ |
| $action | ルールにマッチしたときに実行するアクションの名前 |
| $options | オプション |

以下、アラートルールの設定例です。
"Hello,World"という文字列を記述したドキュメントに対してこのルールが適用されてアクション"hello-world-action"が実行されます。

```
xquery version "1.0-ml";
import module namespace alert = "http://marklogic.com/xdmp/alert" 
		  at "/MarkLogic/alert.xqy";

let $rule := alert:make-rule(
    "hello-world-rule", 
    "hello world rule",
    0, 
    cts:word-query("Hello,World"),
    "hello-world-action",
    <alert:options/> )
return
alert:rule-insert("/my/alert/action/uri", $rule)
```

## 4．アラートの実行
1～3の手順でアラートの設定を行いましたが、設定しただけではドキュメントに対してアラートを実行することはできません。ドキュメントに対する処理（登録、更新、削除）を検知して、そのドキュメントに対してアラートを実行する仕組みが必要になります。

この仕組みには以下の方法があります。

* アラートAPIの関数を使用して直接実行する
* MarkLogicのTriggers機能を使う
* MarkLogicのContent Processing Framework(CPF)を使う

アラートAPIの関数を使用して直接実行する
---
アラートAPIにはアラートを実行するための関数[alert:invoke-matching-actions](https://docs.marklogic.com/alert:invoke-matching-actions)、及び、[alert:spawn-matching-actions](https://docs.marklogic.com/alert:spawn-matching-actions)が用意されています。これらは、対象のドキュメントとアラートURIを指定して、直接アラートを実行する関数です。
アラートを組み込んだ独自アプリケーションを作成する場合や、アラート自体の単体テストに使用します。

MarkLogicのTriggers機能を使う
---
MarkLogicにはトリガー機能が搭載されています。ドキュメントに対する登録、更新、削除などを検知して、事前に定めておいたXQueryモジュールを実行する機能です。
このトリガーを使ってアラートを実行するように定義すれば、ドキュメントに対する処理を検知してリアルタイムアラートを実現できるようになります。
トリガーの公式ドキュメントは[こちら](https://docs.marklogic.com/guide/app-dev/triggers)をご覧下さい。

MarkLogicのContent Processing Framework(CPF)を使う
---
MarkLogicにはContent Processing Framework(以下、CPF)というフレームワークが搭載されています。
これは、上記のトリガーを応用したフレームワークで、ドキュメントに対するワークフローを実現できます。
例えば、ドキュメントが登録されたらヘッダ部に作成日、フッタ部にコピーライトを付与してPDFに変換する、などドキュメントに対するオートメーションを実現できます。
実行する処理はパイプラインと呼ばれます。パイプラインは事前に用意されているものがありますが、自分で実装することも可能です。用意されているパイプラインには、HTML変換やPDF変換などがあり、アラートを実行するパイプラインもあります。
アラート用のパイプラインを使えば、簡単にリアルタイムアラートを設定できます。

CPFの公式ドキュメントは[こちら](https://docs.marklogic.com/guide/cpf)をご覧下さい。

アラートを実行してみる
---
作成したアラートの動作確認のため、ここではアラートAPIの関数を使用した実行方法を記します。アラート定義、アクションとXQuery、ルールが登録されていることを前提とします。

アラートを実行する関数には、既述のように[alert:invoke-matching-actions](https://docs.marklogic.com/alert:invoke-matching-actions)と
[alert:spawn-matching-actions](https://docs.marklogic.com/alert:spawn-matching-actions)の2つの関数があります。1つのドキュメントが複数のアラートルールに該当した場合、各ルールに紐付くアクションを実行することになります。複数のアクションを逐次的に実行するか、並列で実行するかの違いで関数が分かれています。前者のalert:invoke-matching-actionsは逐次処理、alert:spawn-matching-actionsは並列処理になります。

ここではalert:invoke-matching-actionsを使用した例を記します。

* ドキュメントがルールにマッチする場合

まずはアラートのルールにマッチするドキュメントの場合の例です。
alert:invoke-matching-actionsの第1引数は実行するアラート定義のURI、第二引数は対象のドキュメントです。ここでは、<message>Hello,World</message>というドキュメントを指定しています。

```
xquery version "1.0-ml";
import module namespace alert = "http://marklogic.com/xdmp/alert" 
		  at "/MarkLogic/alert.xqy";
      
alert:invoke-matching-actions("/my/alert/action/uri",<message>Hello,World</message>, <options/>)
```

この場合、ドキュメントとルールがマッチするため、アクションであるalert-action.xqyクエリが実行されます。このクエリは以下のようなドキュメントを1つ登録します。

```
<?xml  version="1.0" encoding="UTF-8"?>
<alert>
  <config-uri>/my/alert/action/uri</config-uri>
  <rule-id>11645059556693399313</rule-id>
  <document-uri></document-uri>
  <document>
    <message>Hello,World</message>
  </document>
  <alert-time>2017-12-15T22:57:26.731797+09:00</alert-time>
</alert>
```

document-uriが空になっていますが、これはDBに登録していないドキュメントを使用したためです。

* ドキュメントがルールにマッチしない場合

次に指定したドキュメントがルールにマッチしない場合の例です。
以下は、<message>Hello World</message>というドキュメントがルールにマッチするか確認しています。ルールでは”Hello World"を含んでいることを条件としており、本ドキュメントは"Hello,World"であるためアンマッチになります。よって、アクションのクエリは実行されず、結果は何も残りません。

```
xquery version "1.0-ml";
import module namespace alert = "http://marklogic.com/xdmp/alert" 
		  at "/MarkLogic/alert.xqy";
      
alert:invoke-matching-actions("/my/alert/action/uri",<message>Hello World</message>, <options/>)
```

以上がアラートAPIを使用してアラートを実行する方法になりますが、この方法の場合、ドキュメントに対する処理を切っ掛けとしてアラートを実行する、いわゆるリアルタイムアラートの実現には至っていません。

# Content Processing Framework(CPF)を使ってみる
ここでは前段で紹介したCPFを使用して、リアルタイムアラートを実現してみます。

CPFを使うには、MarkLogicの設定変更が必要になります。

* トリガーデータベースの設定
* CPFの設定
* アラート定義とCPFの紐付け

## トリガーデータベースの設定
CPFはトリガー機能を使用しています。トリガーの設定はトリガー専用のデータベースに行い、これをアラートを設定するデータベースに紐付けます。

WebブラウザからMarkLogicの管理画面(8001番ポート)にアクセスし、左ペインの「Configure」-「Databases」を辿り、データベースを選択します。右ペインの設定項目「triggers database」にて「Triggers」を選択し、「ok」を押下します。
これでデータベースにトリガー用のデータベースを紐付けました。

![pic02_cpf01.png](https://qiita-image-store.s3.amazonaws.com/0/187299/25461142-f2be-f4c9-a157-beb433d79c48.png)

## CPFの設定
続いて、CPFを使えるようにします。これも管理画面で設定します。

### CPFの有効化
管理画面の左ペインの当該データベースの配下に「Content Processing」という項目があるため選択します。
右ペインの「Install」タブをクリックし、「enable conversion」で「true」を選択して「install」を押下します。続いて、CPFを設定して良いか確認されるため「ok」を押下します。

![pic03_cpf02.png](https://qiita-image-store.s3.amazonaws.com/0/187299/916afd3a-522d-d4f4-1e3d-9a4e3384799c.png)

![pic04_cpf03.png](https://qiita-image-store.s3.amazonaws.com/0/187299/ced3f6ae-e8bc-623b-8a65-8db607930bd5.png)

### アラート用のCPF設定
続いて、CPFでアラートを使えるように設定します。
左ペインでデータベース配下の「Content Processing」-「Domains」を選択し、右ペインに表示される「Create」タブを選択します。

![pic05_cpf04.png](https://qiita-image-store.s3.amazonaws.com/0/187299/8c2efa72-2990-7c58-475f-4579c3cc12bd.png)

右ペインの設定項目を入力し、「ok」を押下します。入力項目は以下の通りです。

| 項目名 | 概要 | 今回の設定値 |
| --- | --- | --- |
| domain name | CPFはドメインという単位で設定します。その名前を入力します。 | AlertingDatabaseDomain |
| domain description | このドメインの説明を入力します。 | 空欄 |
| document scope | CPFを実行させるドキュメント範囲を選択します。directoryは指定したディレクトリ配下のドキュメントに対して処理が行われたときにCPFを実行します。collectionならば指定したコレクションが割り当てられたドキュメントが対象になります。 | directory |
| uri | document scopeで選択した内容に対する具体的なディレクトリやコレクション等を設定します。 | /AlertData/ |
| depth | ディレクトリにサブディレクトリが存在する場合、その範囲も対象とするかを設定します。"infinity"ならばサブディレクトリ配下のドキュメントも対象になります。"1"ならばディレクトリ直下のドキュメントのみが対象となります。| infinity |
| modules | CPFで使用するXQuery等のモジュールの格納先を選択します。 | AlertingDatabase |
| root | モジュールを格納したディレクトリのルートを入力します。 | / |

続いて、このCPFで使用するパイプラインを選択します。CPFでは、ドキュメントに対して自動的に実行する処理をパイプラインとして定義しています。パイプラインは予め用意されているものがあり、アラートのパイプラインも用意されています。今回はそれを使用します。

管理画面の左ペインのデータベース配下から「Content Processing」-「Domains」-対象のドメイン名-「Pipelines」を辿ります。
右ペインにMarkLogicに搭載されているパイプラインの一覧が表示されます。この中から、以下の2つを選択して「ok」ボタンを押下します。

* Alerting
* Status Change Handling

![pic07_cpf06.png](https://qiita-image-store.s3.amazonaws.com/0/187299/8e213f71-45c4-d157-3ce4-7216a874c70f.png)

## アラート定義とCPFの紐付け
これまでの手順で、当該データベースにおいてCPFが使えるようになりアラートパイプラインを設定しました。

次に、にアラートAPIを使用して作成したアラート定義とCPFのドメインを紐付けます。これはXQueryで行います。紐付ける関数は[alert:config-set-cpf-domain-names](https://docs.marklogic.com/alert:config-set-cpf-domain-names)です。
この関数の第1引数はアラート定義になります。第2引数はCPFのドメイン名になります。今回の設定例では「AlertingDatabaseDomain」です。

WebブラウザからQueryConsoleにアクセスします(8000番ポート)。以下のクエリを実行します。

```
import module namespace alert = "http://marklogic.com/xdmp/alert" 
		  at "/MarkLogic/alert.xqy";

(: アラート定義のURIから、アラート定義を取得する。 :)
let $alert-config :=  alert:config-get("/my/alert/action/uri")

(: アラート定義とCPFのドメイン名を紐付けて登録する。 :)
let $config := alert:config-set-cpf-domain-names($alert-config, "AlertingDatabaseDomain")
return
  alert:config-insert($config)
```

これでCPFを使ったアラートの設定が終わりました。

## 動作確認してみる
以上の設定でCPFによるリアルタイムアラートが実現します。この例では、"/AlertData/"ディレクトリ配下のドキュメントに対して登録、更新などの処理が行われたときにCPFが実行され、そのドキュメント内に"Hello,World"という文字列が含まれている場合にアラート処理が実行されます。
"/AlertData/"ディレクトリ以外の場合はCPFが実行されません。
"/AlertData/"ディレクトリ配下であったとしても、そのドキュメントに"Hello,World"という文字列が含まれていなければ、アラートは実行されません。

以下の例ではアラートが実行されます。

```
xdmp:document-insert("/AlertData/this-will-be-alerting.xml", <message>Hello,World</message>)
```

アラート定義に従って、以下のようなXMLが保存されました。これはアラートAPIで実行したときと同じ結果です。

```
<alert>
  <config-uri>/my/alert/action/uri</config-uri>
  <rule-id>16282476121901845864</rule-id>
  <document-uri>/AlertData/this-will-be-alerting.xml</document-uri>
  <document>
    <message>Hello,World</message>
  </document>
  <alert-time>2017-12-16T23:31:11.807679+09:00</alert-time>
</alert>
```

# MarkLogicの活用の幅が広がります
リアルタイムアラートを使用すれば、ドキュメントを監視して多くの処理を実行できるようになります。
今回は紹介できませんでしたが、アラートのアクションにはJavascriptも使用可能です。HTTPのPOSTやGETなども使えるため、ドキュメントの処理を契機として、外部のWebサービス/アプリケーションとの連携も可能です。