注: 以下の翻訳の正確性は検証されていません。AIPを利用して英語版の原文から機械的に翻訳されたものです。

データを Phonograph にライトバックする [レガシー]

レガシー

Object Storage v1 (Phonograph) のライトバックは、開発のレガシー段階にあり、追加の開発は予定されていません。新しいワークフローでは、Phonograph のライトバックは利用できません。

オントロジーの改善点と進化したアーキテクチャを活用するために、Object Storage v2 への移行を推奨します。また、オントロジーに更新や変更を適用するには、アクションウィジェットでアクションを使用することを推奨します。詳細については、アクションタイプのドキュメントを参照してください。

Slate でユーザー入力やデータ変更を取得し、Foundry に保存するには、3つのコンポーネントが必要です。

  1. ソースデータセット: 編集できるようにしたい、Foundry に存在するデータセットです。このデータセットには主キーが必要です。
  2. Phonograph 同期: Phonograph は、Slate がソースデータセットに対するユーザー入力による変更を取得できるようにする手動編集キャッシュです。データセットを Postgres ではなく Phonograph に同期すると、そのデータセットが Slate で利用可能になり、ユーザーが編集できるようになります。
  3. ライトバックデータセット: ユーザーが変更したソースデータセットのバージョンを保存するために作成する Foundry データセットです。ソースデータセットは常に変更されないまま維持され、Phonograph キャッシュに格納されたすべてのユーザー編集が、Foundry のライトバックデータセットに保存されます。

ソースデータセットを追加する

まず、データセットタブで Slate アプリケーションにソースデータセットを追加します。+ 追加ボタンを使用して、asteroid_notes という名前のシンプルな派生データセットを追加します。この架空のデータセットには、小惑星の名前と、ユーザーが入力した小惑星に関する情報を格納するための research_notes という名前の空の列が含まれています。

データセットを追加したら、そのデータセットの同期設定を表示します。Phonograph という手動編集キャッシュへの同期を設定します。

ライトバックインデックスは struct 列のデータ型に対応していないため、DateTime 列のデータ型を含むデータセットはインデックス化に失敗します。インデックス化する前に、Contour、または SQL や Python のトランスフォームを使用して、DateTime 列を Timestamp にキャストできます。

プライマリーキーを選択します。選択する主キーは、単一の行を一意に識別する必要があります。一般に、インデックス化する前に、トランスフォームで複数の列を組み合わせて適切な主キーを生成する必要があります。列のグループを複合主キーとして定義することもできますが、データ編集がより複雑になります。

主キーを設定したら、Foundry にユーザー編集を格納するライトバックデータセットを作成します。編集内容を保存できるデータセットを Foundry に作成するには、「参照」を選択します。ポップアップウィンドウで、データセットの名前と、Foundry 内でデータセットを作成する場所を指定します。

完了すると、同期設定は次のようになります。

asteroid_notes

同期を削除するとテーブルのすべてのデータが削除されます

現在、UI から同期の登録を解除(削除)することはできません。ただし、Slate で使用する場合は、「Table Registry - Unregister」エンドポイントへのクエリを追加できます。パラメーターはテーブル RID のみで、実行するとテーブルが削除されます。入力データセットや出力データセットには影響しません。関連するデータセットの同期 UI では、登録済みのライトバック同期がなくなったことを確認できます。この操作はテーブルのすべてのデータを完全に削除するため、慎重に使用し、必ずクエリを削除するか、手動で実行するように設定してください。

クエリの例

データを Phonograph にインデックス化したので、データの読み取りと書き込みができるようになりました。最も単純なパターンは次のとおりです。

  1. クエリを使用してすべての行または一部の行を取得し、ユーザーに表示します。
  2. ユーザーがアプリケーションのウィジェットを操作して、行の追加、行の削除、または行内の値の変更を行います。
  3. ユーザーが送信するたびに(たとえば、送信ボタンをクリックしたときに)、各変更が個別に書き戻されます。
  4. すべての行を再度取得して、変更を表示します...

後ほど、bulk エンドポイントを使ってより複雑な処理を追加するオプションと、フロントエンドの表示状態をバックエンドのデータ状態から分離してアプリケーションの応答性を高める方法について説明しますが、どちらも高度なトピックです。

行を選択する

テーブルからすべての行を取得するには、新しい Slate クエリを作成する必要があります。

  1. まず、Slate アプリケーションで新しいクエリを作成します。
  2. このクエリのソースとして「Phonograph2」を選択します
  3. 「利用可能なサービス」項目で、テーブル検索サービスを選択します。
  4. 以下のコードを使って、このクエリの検索リクエスト項目を入力します。このリクエストでは、すべての行を返す matchAll ブロックを使用しています。テーブルの RID(同期設定で確認できます)と、並べ替えに使用する列(任意)を入力する必要があります。

q_getAllRows の例

searchRequest
Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "tableRids": [ "<your_table_rid>" ], "filter": { "type": "matchAll", "matchAll": {} }, "sort": { "<your_column_to_sort_by>": { "order": "desc" } } }

このクエリパターンでは、ブラウザーのメモリー負荷を軽減してパフォーマンスを向上させるためのページングと、(sort ブロックを追加することによる)並べ替えの両方が可能です。サーバー側ページング機能の使用方法について詳しくは、テーブルウィジェットを参照してください。

pagingToken
"{{w_tableWidget.gridOptions.pagingOptions.currentOffset}}"

pageSize
"{{w_phonographResults.gridOptions.pagingOptions.pageSize}}"
GetAllRows エンドポイントは非推奨

以前は行の取得に推奨されていた Get All Rows エンドポイントは非推奨です。上記の説明に従って Search エンドポイントに移行し、f_getAllRowsFormat 関数を次のように更新してください。

var rawData= {{getAllRows.result.[0].results.rows}}

を次のように変更します。

var rawData= _.map({{getAllRows.result.[0].hits}}, h => h.row)

q_getRows の例

すべての行を一度に取得するだけでなく、Get Rows エンドポイントに主キーオブジェクトのリストを指定して、ID を使って行を取得することもできます。これは、ユーザーが新しい値を入力するときに行がすでに存在するかどうかを確認する簡単な方法です。この確認は、変更を edit イベントと add イベントのどちらとして適用するかを判断するために必要です(詳しくは後述します)。

選択された行

クエリ結果をウィジェットに表示する

これらの取得エンドポイントの結果はオブジェクトの配列です。各オブジェクトが1行を表し、ネストされた primaryKey オブジェクトと columns オブジェクトの値が、その行の列と値のペアを表します。チャートやテーブルなどの Slate ウィジェットは、並列配列のデータを想定しています。各配列が1列を表し、配列のインデックスが特定の行の値に対応します。次のヘルパー関数は Get または Search の結果を受け取り、テーブルやその他のウィジェットに渡すための並列配列を返します。

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var rawData= _.map({{getAllRows.result.[0].hits}}, h => h.row) var primaryKeys=Object.keys(rawData[0].primaryKey); var columns=Object.keys(rawData[0].columns); var parsedData={} // 初期化します for (var pkey of primaryKeys) { parsedData[pkey]=[] } for (var column of columns) { parsedData[column]=[] } for (var row of rawData) { for (var pkey of primaryKeys) { parsedData[pkey].push(row.primaryKey[pkey]) } for (var column of columns) { parsedData[column].push(row.columns[column]) } } return parsedData;

null 値を考慮する

データ内の null 値は Phonograph にインデックス化されません。その結果、null 値を持つ列を含む行は Phonograph に存在しなくなります。Phonograph の結果を行と列に変換するときは、これらの欠落した値を考慮する必要がある場合があります。以下は、すべての行にわたってすべての列を検出する例です。この例を上記の例と組み合わせることで、最初の行の null 値に関する問題を回避できます。

Copied!
1 2 3 4 5 const queryResult = {{getAllRows.result.[0].hits}}.map(e => (Object.assign({}, ...[e.row.primaryKey, e.row.columns]))); const columns = [...new Set(...queryResult.map(e => Object.keys(e)))1; const queryResultFilled = Object.assign({}, ...columns.map(e => ({[e]: queryResult.map(_e => _el[e])}))); return queryResultFilled;

更新の読み取りにおける Search サービスと Storage サービスの比較

Search サービスのエンドポイントは、関連する Phonograph テーブルの検索インデックスを使用します。この検索インデックスは、Storage サービスの Post Event エンドポイントと同期して更新されません。そのため、変更が書き込まれてから、その変更が Search エンドポイントへのクエリに現れるまでには、わずかな遅延があります。簡単なパターンとして、トーストウィジェットを使って次のようなタイマーを作成できます。

  1. q_postEvent.success -> w_successToast.open
  2. w_successToast のタイムアウトを 5000(5秒)に設定
  3. w_successToast.close -> q_getAllRows

変更された行を確実に取得するために、トーストの表示時間を長くしたり短くしたりする調整が必要になる場合があります。また、getAllRows クエリには必ず sort ブロックを追加してください。デフォルトでは行ドキュメントの最終更新タイムスタンプで並べ替えられるため、編集した行がすべての結果の末尾に「移動」します。

別の方法として、データの表示と Phonograph へのデータの保存を分離するアプリケーションのパターンを定義できます。状態変数を使って現在の表示を保持し、ユーザーの操作をきっかけとして、そのデータを定期的に Phonograph に保存することで実現できます。ただし、すべての行を取得するクエリは自動的に再実行しません。このパターンは、更新が即座に反映されるように見えるため、より優れたユーザーエクスペリエンスを実現しますが、設計はより複雑になります。

primaryKey 値のリストによる行の取得を含め、テーブルストレージサービスへのリクエストには、すべての編集が含まれることが保証されます。そのため、次のようにイベントを連鎖させても安全です。q_postEvent.success -> q_getRowByPrimaryKey.run

この場合、主キーによる参照で取得した特定の行に書き込まれた変更は、必ず結果に含まれます。

データを編集する

テーブルストレージサービスの Post Event エンドポイントは、rowAdded、rowDeleted、rowModified の3種類のイベントを処理します。

3種類のイベントすべてのクエリは、次のように primaryKey オブジェクトと payload オブジェクトを含む tableEditedEventPostRequest を想定しています。

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "primaryKey": { "<primary_key_col>": "<primary_key_value>", "<second_primary_key_col>": "<second_value>" // 手動編集インデックスの作成時に複数の列を指定した場合のみ }, "payload": { "type": "rowModified", // "rowAdded" または "rowDeleted" "rowModified": { // "rowAdded" または "rowDeleted" "columns": { "<col_a>": "<new_value>", "<col_b>": "<new_value2>" } } } }

rowDeleted イベントには空の rowDeleted オブジェクトが含まれます。行を削除するために列を渡す必要はありません。

一般的なベストプラクティスとして、関数を使って tableEditedEventPostRequest を構築することが推奨されます。こうした関数は、目的のワークフローや実装に応じて、さまざまな形を取ることができます。

関数 f_createTableEditEvent の例:

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var selectedRow={{q_getAllRows.result.rows.[w_rowsTable.selectedRowsKeys.[0]]}}; var updatedCols=selectedRow.columns; updatedCols.research_notes={{w_notesInput.text}} var tableEditedEventPostRequest = { "primaryKey":{ "name": selectedRow.primaryKey.name, }, "payload": { "type": "rowModified", "rowModified": { "columns": updatedCols, } } } return tableEditedEventPostRequest;

上記の単純な関数の例では、q_getAllRows のすべての行が w_rowsTable ウィジェットに表示されます。テーブルには、選択キーとして行番号を表す列が追加されています。そのため、w_rowsTable で行を選択すると、選択キーを使って q_getAllRows の生の結果からその行を見つけられます。行を選択した状態で、research_notes 列の値を、ユーザーが w_notesInput ウィジェットに入力した値に更新できます。この処理は次の行で行われます。updatedCols.research_notes={{w_notesInput.text}}

関数 f_createTableEditEvent の最後のステップは、updatedCols オブジェクトを tableEditedEventPostRequest 変数に挿入し、関数の結果として返すことです。必要な処理がどれほど複雑であっても、この形式を関数の出力にすれば、そのままクエリに渡すことができます。

関数が完成したら、その出力である tableEditedEventPostRequest オブジェクトを、updateRow クエリの対応するパラメーター項目に設定できます(下の画像を参照)。

updateRow

クエリを手動で実行するように設定する

クエリに適切なパラメーターを設定したので、次にクエリを実行する条件を定義し、必要なときにのみテーブルが編集されるようにします。

クエリエディターでクエリを表示した状態で、実行の横にあるドロップダウンメニューを使って手動で実行にチェックを入れます。これにより、ページの再読み込み時や依存関係の変更時にクエリが実行されなくなります。

次に、ユーザーの何らかの操作に応じてクエリが実行されるように、クエリのトリガーを設定します。最も簡単な方法は、ボタンウィジェットを追加し、button.clickevent でクエリをトリガーすることですが、ほかにも多くの方法があります。Slate アプリケーションにボタンウィジェットを追加し、クリック時にクエリが実行されるように設定します。このクエリを実行しても、Phonograph キャッシュのみが更新されます(Foundry のライトバックデータセットには、編集内容がすぐには反映されません)。

新規クエリの手動実行

また、フォーム入力を検証し、フォームの横に表示するフィードバックテキストを生成する関数を別途作成することが、多くの場合ベストプラクティスです。これにより、ユーザーの入力データに起因するデータ品質の問題を防ぐことができます。このための関数は、たとえば次のようになります。

f_validateForm

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var form { disableSubmit: false, globalMessage: "", fields : { i_widget1: { value: {{i_widget1.text}}, message: "" }, i_widget2: } value: {{i_widget2.text}}, "message" } etc... } // 値の検証、ウィジェットごとのメッセージ生成、および全体の有効性の状態を判定するロジック // 値が無効と判定された場合は、disableSubmit を true に切り替えます return form;

その後、ウィジェットの disable プロパティを "{{f_validateForm.disableSubmit}}" にリンクすることで、ボタンウィジェットを無効にできます。

新しい行を追加する

新しい行の追加は、行の編集と同じように動作します。上記の例で rowModified を rowAdded に置き換えるだけです。

ただし、既存の行と同じ主キーを持つ新しい行を追加しようとすると、追加に失敗します。簡単な方法は、トーストウィジェットをトリガーして、その行がすでに存在することをユーザーに通知することです。

もう少しユーザーにとって使いやすい方法は、ユーザーが入力した主キーを使って行がすでに存在するかを確認する、Get Rows エンドポイントに対する q_validatePrimaryKey クエリを用意することです。行が存在する場合は、f_createTableEditEvent で rowModified イベントを生成し、存在しない場合は rowAdded を使います。

行を削除する

行を削除するには、Post Event エンドポイントを使って、削除する行の主キーを含む rowDeleted イベントを送信します。

Copied!
1 2 3 4 5 6 7 8 9 { "primaryKey": { "<primary_key_col>": "<primary_key_value>" }, "payload": { "type": "rowDeleted", "rowDeleted": {} } }

行を検索する

Table Search Service には、primaryKey ではなく値で行を検索する必要があるときに使える search エンドポイントがあります。構文は ElasticSearch の検索構文に似ています。利用可能な検索タイプの完全な仕様は、API ドキュメント ↗で確認できます。

すべての検索は、ネストされた hits オブジェクトを含むオブジェクトを返します。行を取得するセクションのパース関数で、rawData 変数への代入にある .rows の代わりにこのオブジェクトを渡すと、同じロジックを使って結果を並列配列に分割し、テーブルウィジェットに表示できます。

テキスト型のすべての列は、デフォルトの ElasticSearch インデクサーによって処理されます。このインデクサーは、単語構成文字以外の文字を区切りとして文字列をトークン化します。そのため、完全一致を検索する際に、予期しない検索動作が発生することがあります。完全一致の検索を行うには、queryString 検索タイプを使い、検索語を文字列リテラルの文字でエスケープします:\"8156-Apron 2 P.2\"

Terms フィルター

terms フィルタータイプは、値の配列を受け取り、すべての列、またはオプションで field プロパティに指定した単一の列に対して一致を検索します。語句のインデックス化方法については、上記の注記を参照してください。

searchRequest
Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "tableRids": [ "{{f_getTableId}}" ], "filter": { "type": "terms", "terms": { "field": "first_name", "terms": [ "agnese", "ailee", "woodman" ] } } }

queryString フィルター

queryString フィルタータイプは、自然言語検索と同等の検索を提供することを目指しています。AND および OR 演算子を含めることができ、複数の単語を引用符で囲んで完全一致を検索することもできます。例:\"multi word exact match\"

searchRequest
Copied!
1 2 3 4 5 6 7 8 9 10 11 { "tableRids": [ "{{f_getTableId}}" ], "filter": { "type": "queryString", "queryString": { "queryString": "jang" } } }

Range フィルター

範囲フィルターは、数値型および日付型の列に対して比較による絞り込みを行います。gt、gte、lt、lte をサポートしています。

searchRequest
Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 { "tableRids": [ "{{f_getTableId}}" ], "filter": { "type": "range", "range": { "field": "start_date", "lt": "2018-01-01", "format": "yyyy-MM-dd" } } }

GeoBoundingBox および GeoDistanceSearch フィルター

パイプラインで geohash ↗ を生成している場合は、これらのフィルターを使って境界ボックス検索および半径検索を実行できます。

これらのクエリの例については、Palantir チームにお問い合わせください。

複合フィルター

すべてのフィルタータイプは、AND および OR フィルタータイプでネストして、より複雑な絞り込みロジックを構築できます。一般的なパターンは、関数を使ってこれらの複合クエリを生成し、関数の出力をクエリの searchRequest パラメーターに含めることです。

searchRequest
Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "tableRids": {{f_getTableId}}, "filter": { "type": "and", "and": [ { "type": "queryString", "queryString": { "queryString": "pixoboo" } }, { "type": "terms", "terms": { "field": "first_name", "terms": [ "harcourt" ] } } ] } }

Foundry に書き戻す

先ほど作成した updateRow クエリは、Phonograph に保存されている asteroid_notes のコピーのみを更新します。Foundry にデータを表示するには、Foundry 内のデータセットのプレビューページで「ビルド」をクリックする必要があります。

データセットの詳細

これが完了すると、research_notes の編集内容が asteroid_notes_edited に反映されていることを確認できます。

asteroid_notes_edited

高度な機能

一括エンドポイント

前述の Post Event エンドポイントに加えて、Table Service には Post Events for Table エンドポイントがあり、前述の方法で生成したものと同じ tableEditedEventPostRequest の配列を受け取ります。イベントには異なる種類(rowModified、rowAdded、rowDeleted)を混在させることができますが、同じ primaryKey を配列に含められるのは 1回のみ です。つまり、たとえば1つのリクエストに、同じ primaryKey に対する rowModified イベントと rowDeleted イベントを含めることはできません。

Post Events for Table のリクエスト

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 [ // イベントリクエスト1 { "primaryKey": { "primary_key_col": "primary_key_value", }, "payload": { "type": "rowModified", // "rowAdded" または "rowDeleted" "rowModified": { // "rowAdded" または "rowDeleted" "columns": { "col_a": "value", } } } }, { "primaryKey": { "primary_key_col": "different_primary_key_value", }, "payload": { "type": "rowDeleted", // "rowAdded" または "rowDeleted" "rowDeleted": {} } }, etc...

次のセクションでは、一括エンドポイントとイベントフレームワークを使って、変更時の遅延に対処するための、より強力なソリューションを構築する方法を説明します。

遅延を管理する

手動編集キャッシュの基盤となるアーキテクチャにより、Post Event の呼び出しが成功した直後に、編集内容が主要インデックスに反映されているとは限りません。現在、Get All Rows と Get Rows の呼び出しを同期的にするための作業が進められています。これにより、q_postEditEvent クエリの成功時に q_getAllRows がトリガーされるように設定すると、すべての編集内容が確実に表示されるようになります。ただし、search エンドポイントを使用する場合は、検索インデックスが更新されるまでの数秒間の遅延に対処する必要があります。

最も簡単な方法は、編集クエリとすべての行を取得するクエリの間にトーストを挿入することです。q_postEditEvent.success イベントでトーストをトリガーし、表示時間を3~4秒(ミリ秒単位の値として3000~4000)に設定します。その後、w_editSuccessToast.closed イベントで q_getAllRows クエリをトリガーすると、この待機時間によってインデックスの更新に十分な時間を確保できるはずです。

より複雑なワークフローでは、Slate アプリの表示に使用する状態と編集インデックスの状態を分離するために、追加の処理を実装することも検討できます。この場合、ページの読み込み時に q_getAllRows クエリの結果を Slate 変数に格納し、それを「フロントエンドの状態」とします。すべての編集、削除、更新イベントは、バックエンドへのクエリを直接トリガーするのではなく、このフロントエンドの状態変数を変更します。定期的にフロントエンド変数の状態とクエリのデータの差分を取り、すべての変更を適用する一括クエリのペイロードを作成できます。

また、トーストや「未保存の変更」のカウンターなどでユーザーにフィードバックを提示し、クリックしてすべての変更を保存するための手動ボタンを用意することも望ましいでしょう。この構成は、複雑で長期間使用するアプリケーション以外には大がかりです。ほとんどの場合はトーストを使う方法で十分であり、Get Rows と Get All Rows エンドポイントの改善により、まもなくほぼすべての場合でこの構成は不要になるはずです。そのため、この方法を検討している場合は、Palantir チームに連絡して詳しく相談するのが最善でしょう。

集計

search エンドポイントは、インデックス化済みデータ全体の統計情報を生成したり、値をバケットに分類したりするための集計もサポートしています。

ElasticSearch 2.0 Aggregation ↗ 構文の一部をサポートしています。value_count、max、geohash_grid、terms、top_hits、sum、cardinality、avg、nested、filter、histogram、min が使用できます。

Elasticsearch の制限により、一部の集計結果は近似値になる場合があります。どの種類の集計を使用する場合も、まず Elasticsearch のドキュメントを参照し、正確な値か近似値かを理解してください。近似値の場合は、先に進む前に、ユースケースで許容できるかどうかを判断する必要があります。

以下は、構文の簡単な例です。visits フィールドの生のバージョンを使用する必要があります。また、文字列フィールドで集計するため、「完全一致」を有効にする必要があります。

phonograph-sync-exact-match

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "tableRids": [ "{{f_getTableId}}" ], "filter": { "type": "matchAll", "matchAll": {} }, "aggregations": { "visits": { "terms": { "field": "visits.raw" } } } }

上記のクエリは、クエリの filter セクションに含まれるすべての行を visits フィールドの値でバケットに分類し、件数を返します。集計値だけが必要な場合は、pageSize を 0 に設定して、個々のレコードのヒットを結果から除外し、アプリケーションの読み込み時間とメモリーへの負荷を減らします。

戻り値の形式は次のようになります。

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 "aggregations": { "genders": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 30, "buckets": [ { "key": "daily", "doc_count": 148 }, { "key": "often", "doc_count": 141 }, { "key": "weekly", "doc_count": 129 }, ... ] } }

集計の種類ごとに戻り値の構文は少し異なりますが、チャートやテーブルなどの Slate ウィジェットでこの種類のデータを扱う際には、行を列に変換関数が役立つ場合があります。