エンベロープ デザインパターンとは?

7月 24, 2019 データと AI, MarkLogic

MarkLogicデザインパターンは、MarkLogicアプリケーションの設計における一般的な問題に対処するための、再利用可能なソリューションです。これらのパターンは、MarkLogicアプリケーションに固有のもの、あるいはMarkLogicを念頭に置いた各業界ごとのパターンになっています。MarkLogicデザインパターンは、料理のレシピのように個別的・具体的なものではなく、一般に抽象度が高く、複数のシナリオに適用できます。

エンベロープ デザインパターン

目的

外部プロセスが利用するデータと、MarkLogicデータベースシステムを強力かつ柔軟にするためのデータを分離します。包括的なエンベロープの親要素すなわち「headers」セクションを含むオブジェクトと、「instance」データセクションを作成します。これらのセクションはドキュメント内で分かれています。これは、MarkLogicデータハブフレームワークおよびMarkLogicエンティティサービスによるエンベロープの作成方法に準拠しています。

背景

よりリッチなインデックスの追加

MarkLogicでは、格納されているJSONまたはXMLドキュメントが「インデックスへのインターフェイス」になります。つまり、XMLドキュメントに要素を追加したり、ネストされたオブジェクトをJSON構造に追加したりすることで、データ、親子関係、値にインデックスが付けられます。このため、インデックスが付いたデータを追加したい場合、通常は要素やネストされたオブジェクトを追加するだけでOKです。

MarkLogic内部用に使用されるデータと、データサービスAPIが必要とするデータを分けておくと便利です。

データサービスは、次のようなデータを利用します。

  • 「Jan 20, 2005」のような、人間が読んで理解できる形式の日付
  • 決まっている標準スキーマに準拠している
  • ネストや曖昧なJSONまたはXML構造により、効率的なクエリが困難
  • コンプライアンスやポリシー上の理由で、元の入力データを一切変更せずに保持している

一方、次のようなデータによって、MarkLogicの処理やインデックス付けが向上することがあります。

  • 日付、数値、タイムスタンプなどに関して、XML標準のデータ型を使用している
  • 重要な派生情報(内部アイテム数など)が準備されている
  • ある要素がなくてもクエリを高速に実行できるよう欠損データが準備されている
  • ビジネスルールに基づいて、複数の選択肢が一意な名前の1つの要素に統合されている(自宅住所、配送先住所、会社住所の中から「代表的」な住所を1つ選択するなど)

これと同じことを実現するために、データの読み込み時およびデータサービスからのアクセス時に大量の変換を行うこともできますが、外部からアクセスする「中核的な」データをそのまま格納しておいた方が効率的でわかりやすいです。

説明をシンプルにするため、ここではこのパターンをXMLの用語(ドキュメント、要素、ノード)で説明していきますが、JSONの場合でも基本的な考え方はすべて同じです。

簡単に言うと、エンベロープパターンは2つの相反する目標を達成します。

  1. 出力したい形式と同じ形式でデータを格納する
  2. MarkLogicデータベースへの入出力形式とは違った形式で、MarkLogic内で使用するためのインデックス付け、正規化、便利な構造を定義する

システムによっては、さらに別の目的が追加されます。たとえば、複数のAPIで形式が極端に異なるデータ(CSVとJSON、XMLとRDFなど)を利用したいことがあります。この場合、headersセクションとinstanceセクションに加えて他のセクション(「triples」、「html-preformatted」など)を追加することもあります。

多様なデータの統合(「データサイロの解消」)

このパターンは、複数の大規模データセットをすばやくMarkLogicで統合する場合によく使用されます。このアプローチでは、生データや「十分使えそう」なデータをMarkLogicに直接読み込みます。最初に「headers」セクションに少数の要素を含めて、多数のデータセットに対する統一的なインデックス付け、抽出、分析を実現します。

「instance」セクションのデータはすべて、アクセス、デフォルトレンダリングを使用したレンダリング、エクスポート、管理が可能です。まず最も価値が高いデータだけを使用することで、データにアクセスするシステムを非常に迅速に開発できます。

データハブフレームワークでは、生データをまずステージングデータベースに読みます。これにより、統一すなわちハーモナイズされたデータを「instance」セクションに追加できます。

システムの柔軟性の維持

MarkLogicプロセスしか使用しないデータとデータサービスからアクセスするデータを分けておくことにより、外部レイヤーやサブシステムに手を入れずに、データを「headers」セクションに適宜追加できます。これにより大規模プロジェクトの分析、再コーディング、テスト、調整の時間を短縮できます。

適用

このエンベロープデザインパターンは、基本的にすべてのデザインにおいて使用してください。よほどの理由がない限りこれを使用しないことはおすすめしません。

このパターンは、次のような場合にお勧めします。

  • 値に基づいてファセットやグラフを作成する必要があるが、モデリング済みあるいは他から継承済みのドキュメントにはこの値が含まれていない。または値の形式が必要な形式と異なっている。
    • より一般的には、任意の目的(グラフやファセット作成に限らず)でファクトや値にインデックスを付けたいが、これらがインデックス付けに適した形になっていない場合。
  • レンジインデックスを使用して特定のSQLビューを表示したいが、生データの形式はSQL呼び出し元が要求している形式ではない。
  • インデックスを付けたい値の形式が適切でない。
  • 変更不可能なスキーマを扱っているが、格納されているレコードに後からファクトやその他のデータを追加して、インデックス付けをしたり利便性を高めたりしたい。
  • ドキュメントデータ(XMLやJSON)とRDFの両方を格納しているが、RDFを別のセクションとしてエンベロープに格納したい。
  • 複数の呼び出し元があり、同一データの大きく異なるバージョンが必要とされている。実行時に他の形式に変換するにはパフォーマンスの負荷が大き過ぎる(これは、ドキュメントを分けることで対応できる場合もあります)。
  • 組織やプロジェクト内において、「リアル」なデータモデルと判断されたものを変更するのに時間がかかるため、外部グループとの折衝なしに開発者が必要に応じて変更できるインデックスのみのセクションを持つことが重要である。

構成要素

  • 取得用コード:データを取得するコードは、データに2つ以上のセクションがあることを認識します。「headers」セクションのデータに基づいて検索またはフィルタリングを行い、データサービスが必要とする「instance」データセクションを返す必要があります。
  • 更新用コード:ドキュメントを更新または読み込むコードは、「headers」セクションを最新に保つ方法を知っている必要があります。その際、インスタンスや入力データから「headers」セクションを構築する変換を行います。トリガーも使用できますが、構造的な「すべてのアクセスはサービス経由」パターン(対象があらかじめ限定されていない)に比べると効率が落ちます。
  • インデックス:「headers」セクションには、特定のインデックス(レンジインデックスやRDFベースのトリプルインデックスなど)用の特別な形式の要素が含まれることがよくあります。

コラボレーション

「すべてのアクセスはサービス経由」は、すべての更新で「headers」セクションが追加され、すべてのクエリでこれを削除するというパターンです。ここでは「headers」セクションは呼び出し元から見えないため、MarkLogicデータレイヤー内(MarkLogic自体の中の.jsおよび.xqyコード内)の柔軟性を維持できます。

結果

「インデックス付け可能な」データを追加する作業と、データ形式を返す作業が分離されます。「headers」に変更を加えても、「instance」データに依存する外部のクライアントからは見えません。

  • MarkLogic格納時とは異なる形式でデータを取得した方が、パフォーマンスが向上します。通常、取得用コードは「return $result/core/element()」で終わり、envelopeやheaders要素をすべて除外します。
  • 挿入時にheadersデータを作成し、クエリに対して必要な部分のみを返す際のパフォーマンス負荷は低~中程度です。
  • RESTfulなデータアクセスでは、XQuery/JavaScriptコードで挿入および抽出時にデータをインターセプトして変更できるようにリソースを拡張する必要があります。標準のRESTアクセスでは、読み込み/抽出時にデータを変換しません。
  • MarkLogic mlcp は、抽出時にデータを変換しないため、headersデータを含まないデータの一括移動にはXQSyncを使用する必要があります。

実装

エンベロープ パターンを実装する際は、次の点を考慮してください。

  • パターンの透明性を確保するため、すべてのアクセスを.xqyファイルまたは.sjsファイルのセットにより実行するか、トリガーを追加することが必要です。トリガーの場合、更新のオーバーヘッドが加わります。
  • headersセクションとinstanceセクションでは別の要素を使用する必要があります。通常は、headersセクションに別のXML名前空間を使います。テキストクエリやワードクエリの場合、element-query()で囲んで検索対象をinstanceセクションかheadersセクションのいずれかに限定しない限り、両方のデータが対象になる点に注意してください。この種の要素クエリが重要な場合は、位置インデックスをオンにして、フィルタリングなしのインデックスよりも効率を高めた方がよいかもしれません。

サンプルコード

記事レポジトリ

次のような一連のXML形式の記事があり、格納、検索、アクセスできるようにする必要があるとします。

<article>
  <abstract>
    <para>You can build a fence by deciding the areas to separate, and then making a barrier from wood or metal that sits between them.</para>
  </abstract>
  <para>It is often said that good fences make good neighbors.</para>
  <para>Choosing areas to divide with your fence is the first step. Jim Smith has built a lot of fences, and says that in Paris, France, people divide garden areas from other areas, but in Cleaveland, OH, people divide chidren's play areas from the street most often</para>
  <articleInfo>
    <title>How to build a fence</title>
    <revision>
      <date>1/15/2002</date>
      <revnumber>1.0</revnumber>
    </revision>
    <author><firstname>Nihal</firstname><surname>Jain</surname></author>
  </articleInfo>
</article>
図1:記事XMLドキュメントのサンプル

図1で示されているのは、単純化されたdocBookスキーマのようなものです。ここでは、呼び出し元は、このデータが厳密にこの形式であることを必要としており、形式が異なれば無効と見なすとします。ここでレンジインデックスを使用して改訂日(revision date)で検索またはファセット作成したい場合、考慮すべき問題が2つあります。1つめは、対象データが汎用的な<date>要素に含まれているということです。このため、「date」にレンジインデックスを追加すると、別の用途で使われている<date>が他にあった場合、こちらも対象となってしまいます。2つめは、ここでのdateの形式がXML仕様のxs:dateTimeと互換性がないことです。これら2つの問題を解決するため、読み込み時に次の変換を実行します。

declare variable $article external;
declare namespace meta = "http://marklogic.com/patternExample/meta";
let $textDate := $article/articleInfo/revision/date/text()
let $xsDate := xdmp:parse-dateTime("mm/dd/yyyy", $textDate)
let $internalDate := <meta:revisionDate>{$xsDate}</meta:revisionDate>
return
  <envelope
    xmlns="http://marklogic.com/entity-services">
    <headers>
      {$internalDate}
    </headers>
    <instance>
      {$article}
    </instance>
  </envelope>
図2:dateを抽出し、検索用の新しいdate要素を作成する変換コード

図2のコードは、dateの変換済み/フォーマット済みバージョンを抽出し、別の名前空間内に具体的な名前の要素を作成します(<meta:revisionDate>)。これにより明確なインデックス付けができるので、目的のxs:date値へアクセスできます。

ここで、2002年1月のすべての記事を検索するため、日付のレンジインデックスを<meta:revisionDate>に追加し、次のようなクエリを実行してみます。

declare namespace es = "http://marklogic.com/entity-services";
declare namespace meta = "http://marklogic.com/patternExample/meta";
(: generic function to query documents, including headers, but return only the instance data :)
declare function es:queryData($q) {
 for $envelope in cts:search(/es:envelope, $q)
 return $envelope/es:instance/element()
};
let $fromQ := cts:element-range-query(xs:QName("meta:revisionDate"),
  ">=", xs:date("2002-01-01"))
let $toQ := cts:element-range-query(xs:QName("meta:revisionDate"),
  "<=", xs:date("2002-01-31"))
let $jan2002Q := cts:and-query(($fromQ, $toQ))
return es:queryData($jan2002Q)

注意が必要なのは、関数es:queryData($q)は<es:instance>要素のあらゆる子要素を返すという点です(記事だけではありません)。

ソーシャルネットワークの関係性

LinkedInやFacebookなどのソーシャルネットワークのプロファイルを表すデータの場合、人物のプロフィールをXMLとして表現し、その内部に関係性をRDFとして格納できます。この場合、RDFを「triples」セクションに含めることができます。

次に示すのは、架空の人物に関するソーシャルネットワークアプリケーションのプロフィールです。

declare namespace sn = "http://marklogic.com/patterns/example/social-network";
<sn:person>
  <sn:name>Alfred</sn:name>
  <sn:uniqueUserName>Alfred_Jones_1974</sn:uniqueUserName>
  <sn:interests>
    <sn:interest levelofinterest="7">Semantics</sn:interest>
    <sn:interest levelofinterest="10">MarkLogic</sn:interest>
    <sn:interest levelofinterest="3">Polyglot Persistence</sn:interest>
  </sn:interests>
  <sn:friends>
    <sn:friend>Sally2227</sn:friend>
    <sn:friend>MargaretTheProgrammer</sn:friend>
    <sn:friend>Neeraj</sn:friend>
  </sn:friends>
</sn:person>

図3:プロフィールデータのサンプル

各ユーザーは、ドキュメントとしてモデル化するのが理想的です。というのもドキュメントは自己記述型で階層があるからです。ただし、ソーシャルネットワーク自体はグラフなので、関係性データはRDFトリプルでモデル化するのが理想的です。

Alfred <foaf:knows> Sally
Alfred <foaf:knows> Margaret
Alfred <foaf:knows> Neeraj

図3のプロフィールを、「Alfred」が属するソーシャルネットワークのセマンティックトリプルで強化します。各ドキュメントの挿入・更新時に次のコードを実行します。

let $thisPersonName := $newPerson/sn:uniqueUserName/text()
let $knowsGraph :=
  for $friendName in $newPerson/sn:friends/sn:friend/text()
  return sem:triple(
    sem:iri($thisPersonName),
    sem:iri("http://xmlns.com/foaf/0.1/knows"), 
    sem:iri($friendName) )
let $envelope :=
  <envelope xmlns="http://marklogic.com/entity-services">
    <es:triples>
      {$knowsGraph}
    </es:triples>
    <es:instance>
    {$newPerson}
    </es:instance>
  </es:envelope>
return $envelope
図4:エンベロープにトリプルを追加する変換コード

図4のコードを実行すると、ここで求められている構造になりますす。「person」レコードはそのまま残り、このプロフィールから生成されたソーシャルネットワークを記述するセマンティックトリプルと併せてエンベロープにまとめられます。

<es:envelope xmlns:es="http://marklogic.com/entity-services">
  <es:triples>
    <sem:triple xmlns:sem="http://marklogic.com/semantics">
      <sem:subject>Alfred_Jones_1974</sem:subject>
      <sem:predicate>http://xmlns.com/foaf/0.1/knows</sem:predicate>
      <sem:object>Sally2227</sem:object>
    </sem:triple>
    <sem:triple xmlns:sem="http://marklogic.com/semantics">
      <sem:subject>Alfred_Jones_1974</sem:subject>
      <sem:predicate>http://xmlns.com/foaf/0.1/knows</sem:predicate>
      <sem:object>MargaretTheProgrammer</sem:object>
    </sem:triple>
    <sem:triple xmlns:sem="http://marklogic.com/semantics">
      <sem:subject>Alfred_Jones_1974</sem:subject>
      <sem:predicate>http://xmlns.com/foaf/0.1/knows</sem:predicate>
      <sem:object>Neeraj</sem:object>
    </sem:triple>
  </es:triples>
  <es:instance>
    <sn:person xmlns:sn="http://marklogic.com/patterns/example/social-network">
      <sn:name>Alfred</sn:name>
      <sn:uniqueUserName>Alfred_Jones_1974</sn:uniqueUserName>
      <sn:interests>
        <sn:interest levelofinterest="7">Semantics</sn:interest>
        <sn:interest levelofinterest="10">MarkLogic</sn:interest>
        <sn:interest levelofinterest="3">Polyglot Persistence</sn:interest>
      </sn:interests>
      <sn:friends>
        <sn:friend>Sally2227</sn:friend>
        <sn:friend>MargaretTheProgrammer</sn:friend>
        <sn:friend>Neeraj</sn:friend>
      </sn:friends>
    </sn:person>
  </es:instance>
</es:envelope>

この例は、triples セクションを紹介しその用途を説明するという観点から、前述の記事レポジトリの例とは少し異なっています。ここではinstanceセクションは、単なる元の「person」レコードです。

関連パターン

関連パターンとして、挿入されたり返される実際のドキュメントに対して外部からデータを追加するあらゆるパターンがあります。たとえば、URIスキーム、コレクション、プロパティフラグメント、RDFトリプルなどとして追加情報を格納するパターンがあります。

使用

エンベロープパターンは、MarkLogic導入時に常に利用されるようになっています。このパターンは、MarkLogicのデータハブフレームワークにおいて中心的に活用されています。また、データ統合が含まれるMarkLogic導入のほぼすべてで利用されています。

デーモン・フェルドマン

Damon is a passionate “Mark-Logician,” having been with the company for over 7 years as it has evolved into the company it is today. He has worked on or led some of the largest MarkLogic projects for customers ranging from the US Intelligence Community to HealthCare.gov to private insurance companies.

Prior to joining MarkLogic, Damon held positions spanning product development for multiple startups, founding of one startup, consulting for a semantic technology company, and leading the architecture for the IMSMA humanitarian landmine remediation and tracking system.

He holds a BA in Mathematics from the University of Chicago and a Ph.D. in Computer Science from Tulane University.