MarkLogicでRaspberryPi3のセンサー情報を取り込んでみよう（4）位置情報を検索してみる

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

# はじめに
[前回](https://qiita.com/t2hk/items/7bf80ae1c6f0a126b661)は東京メトロさんが公開している地物情報をMarkLogicに取り込み、XQueryで位置情報を検索するところまで実施しました。
今回はその続きで、Node.jsのアプリケーションからMarkLogicのクライアントAPIを使って位置情報を検索してみます。

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

| 環境 | バージョン |
| --- | --- |
| CentOS7 | 7.4.1708 |
| Node.js | v8.9.1 |
| MarkLogic9 | 9.0-3 |
| MarkLogic Node.js Client API | 2.0.3 |

# MarkLogicの検索APIについて
まずMarkLogicでの検索APIの仕組みを簡単に記します。
MarkLogicの検索方法には、XQuery、REST-API、Java Client API、Node.js Client API等が存在します。

今回使用するNode.js Client APIや、そのJava版であるJava Client APIは内部でREST-APIを使用して検索を行います。
そのREST-APIはMarkLogicの[Search API](https://docs.marklogic.com/guide/search-dev/search-api)を使用しています。
このSearch APIはMarkLogicの最もローレベルな検索APIであるcts:searchやcts:queryのラッパーのようなAPIで、カスタマイズ性の高い検索機能を提供します。

昨日Query Console上で実施した位置情報検索は、XQueryでcts:searchを使用しました。今回はNode.js Client APIを使用するため、Search APIに従った使い方になります。
このSearch APIは、検索条件の設定や検索結果の取得に関するカスタマイズ性が高いのが特徴です。そのため、検索方法によってはインデックスが必要になる場合があります。今回の位置情報検索もインデックスの設定が必要になります。

なお、MarkLogicがサポートする様々な検索方法については、[Support for Multiple Query Styles](https://docs.marklogic.com/guide/search-dev/searchdev#id_41512)をご参照下さい。

# MarkLogic Node.js Client APIの検索処理について
Node.js Client APIを使用したドキュメントの検索方法について記します。なお、検索に関する公式ドキュメントは[こちら](https://docs.marklogic.com/guide/node-dev/search)をご参照下さい。

なお本記事ではNode.js Client APIの例を記しますが、JavaからMarkLogicにアクセスする場合に使用する[Java Client API](https://docs.marklogic.com/guide/java)でも同様の仕組みとなります。

## 検索処理の概要
Node.jsアプリケーションで検索処理を行う場合、marklogic.queryBuilderメソッドで検索条件を作成し、DatabaseClient.documents.queryメソッドでqueryBuilderに基づいた検索処理を行います。

検索処理の実装は主に以下の流れになります。

1. 検索条件を作成する
2. 検索結果を調整する（取得する件数の指定、並び順の指定など）
3. 検索を実行する

### 1. 検索条件を作成する
検索条件は検索クエリで作成します。MarkLogicは様々な検索クエリに対応しています。以下に概要を記します。

* 文字列クエリ

指定した文字列を含むドキュメントを検索する場合に用いるクエリです。

```js
const marklogic = require('marklogic');
const my = require('./my-connection.js');

const db = marklogic.createDatabaseClient(my.connInfo);
const qb = marklogic.queryBuilder;

db.documents.query(
    qb.where(qb.parsedFrom('Tokyo'))
).result( function(results) {
  console.log(JSON.stringify(results, null, 2));
});
```

* QBE(Query By Example)

「例示による問い合わせ」という方法によるクエリです。（参考：[Wikipedia](https://ja.wikipedia.org/wiki/%E4%BE%8B%E7%A4%BA%E3%81%AB%E3%82%88%E3%82%8B%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B)）
   「都道府県が東京都のデータを探せ」のような問い合わせ方になります。

```js
const marklogic = require('marklogic');
const my = require('./my-connection.js');

const db = marklogic.createDatabaseClient(my.connInfo);
const qb = marklogic.queryBuilder;

db.documents.query(
    qb.where(qb.byExample( {Prefecture: 'Tokyo'} ))
).result( function(results) {
  console.log(JSON.stringify(results, null, 2));
});
```

* 構造化クエリ

検索条件を抽象構文木で表したものです。文字列クエリやQBEでは表現が難しい条件を指定する場合に使用します。

```js
const marklogic = require('marklogic');
const my = require('./my-connection.js');

const db = marklogic.createDatabaseClient(my.connInfo);
const qb = marklogic.queryBuilder;

db.documents.query(
  qb.where(
    qb.and(
      qb.directory('/metro/station/'), 
      qb.term('Tokyo')
    )
  )
).result( function(results) {
  console.log(JSON.stringify(results, null, 2));
});
```

### 2．検索結果を調整する
検索結果には注意が必要で、ページ分割された結果が返却されます。
デフォルトで実行するとヒットしたうちの10件のみが返却されます。他のページを取得したい場合はページ範囲を指定して再度実行する必要があります。
また、オプションであるスニペットを指定すると、ヒットした全件数や処理時間などが返却されます。

検索結果については[Refining Query Results](https://docs.marklogic.com/guide/node-dev/search#id_241609)をご確認下さい。

# 位置情報を検索するアプリケーションを作成してみる
位置情報を検索するNode.jsアプリケーションを作ってみます。
検索クエリには構造化クエリを使用します。

## インデックスの設定
前回ロードした地物情報データの緯度経度に対してインデックスを設定します。
WebブラウザでMarkLogicの管理画面（8001番ポート）にアクセスしてログインします。

左ペインの「Configure」-「Databases」-「対象データベース（今回はIoTDatabase）」-「Geospatial Point Indexes」-「Geospatial Element Pair Indexes」をクリックします。続いて、右ペインの「Add」タブをクリックします。

![pic01_index_01.png](https://qiita-image-store.s3.amazonaws.com/0/187299/f6392f06-33de-3ef5-4d9b-731b6bc459af.png)

今回対象とするデータの座標データ（緯度、経度）は、rootプロパティ配下のgeo_latプロパティとgeo_longプロパティになります。
緯度と経度がそれぞれ独立したプロパティであるため、これらのプロパティに対するインデックスとして「Geospatial Element Pair Indexes」を設定します。

設定項目は以下になります。設定値の入力後、「ok」を押下します。

| 項目名 | 設定値 |
| --- | --- |
| parent localname | root |
| latitude localname | geo_lat |
| longitude localname | geo_long|
| range value positions | true |

これで、当該データの座標情報にインデックスが付与されました。

## ソースコード
以下にNode.jsのアプリケーションを記します。
この例では、指定した緯度、経度、半径で描かれる円に含まれる座標の地物情報を検索しています。

まずは返却された結果をデフォルトでコンソール出力してみます。

```js
const request = require('request');
const marklogic = require('marklogic');
const my = require('./my-connection.js');
const db = marklogic.createDatabaseClient(my.connInfo);
const qb = marklogic.queryBuilder;

// 検索する地物情報の範囲（緯度、経度、半径（マイル））
const lat = 緯度;
const lon = 経度;
const radius = 半径（マイル）;

// 検索対象のディレクトリURI
const docUri = "/metro/station/";

// 指定した緯度・経度を中心とした半径内の座標を持つドキュメントを検索する
db.documents.query(
  qb.where(
    qb.directory(docUri),
    qb.geospatial(
      qb.geoPropertyPair(
        'root',
        'geo_lat',
        'geo_long'),
      qb.fragmentScope('documents'),
      qb.circle(radius, qb.point(lat, lon))
   ))
).result( (results) => {
   console.log(results);
});
```

結果は以下のようになります。途中は省略しましたが、デフォルトでは10件のみが返却されます。

```json
[ { uri: '/metro/station/13288342718477999877.json',
    category: 'content',
    format: 'json',
    contentType: 'application/json',
    contentLength: '391',
    content: { root: [Object] } },
  { uri: '/metro/station/12662046343536761668.json',
    category: 'content',
    format: 'json',
    contentType: 'application/json',
    contentLength: '406',
    content: { root: [Object] } },
・・・省略・・・
  { uri: '/metro/station/7463766614342629896.json',
    category: 'content',
    format: 'json',
    contentType: 'application/json',
    contentLength: '399',
    content: { root: [Object] } } ]
```

続いて、オプションのスニペットを指定してみます。
クエリ部分のみ抜粋します。
今回は、`slice(qb.snippet())`を追加しています。これは、検索結果にスニペットを追加する指定になります。

```js
db.documents.query(
  qb.where(
    qb.directory(docUri),
    qb.geospatial(
      qb.geoPropertyPair(
        'root',
        'geo_lat',
        'geo_long'),
      qb.fragmentScope('documents'),
      qb.circle(radius, qb.point(lat, lon))
   )).slice(qb.snippet()) //スニペットを指定する。
).result( (results) => {
   console.log(results);
});
```

前回と同じように10件分の結果とスニペットが表示されます。
以下にスニペットの部分のみ抜粋します。
トータルで188件がヒットしており、そのうちstartが1件目からの10件を返却したことが分かります。

```json
{ 'snippet-format': 'snippet',
    total: 188,
    start: 1,
    'page-length': 10,
    results:
     [ [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object] ] }
```

次に11件目から10件を返して貰います。

```js
db.documents.query(
  qb.where(
    qb.directory(docUri),
    qb.geospatial(
      qb.geoPropertyPair(
        'root',
        'geo_lat',
        'geo_long'),
      qb.fragmentScope('documents'),
      qb.circle(radius, qb.point(lat, lon))
   )).slice(10, 20, qb.snippet()) //11件目から20件目までを返却して貰う。
).result( (results) => {
   console.log(results);
});
```

結果は以下になります。startが11件目から10件分が返ってきました。

```json
[ { 'snippet-format': 'snippet',
    total: 188,
    start: 11,
    'page-length': 10,
    results:
     [ [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object],
       [Object] ] },
  { uri: '/metro/station/13758789981092930142.json',
    category: 'content',
    format: 'json',
    contentType: 'application/json',
    contentLength: '390',
    content: { root: [Object] } },
・・・省略・・・
```

以上のように、ページネーションに対応した検索となっているため、画面でのページネーションにも簡単に対応できます。

# おしまい
今回は位置情報検索をテーマに、Node.js Client APIの使い方についてご紹介しました。
MarkLogichaXQueryだけでなく多くの検索方法が提供されているため、開発内容に応じて適切な方式をご検討下さい。

# 次回予告
今回でNode.jsアプリケーションからMarkLogicへのアクセスができるようになりました。
次こそはMQTTを導入し、IoTデバイスからの接続に備えてみようと思います。