はじめに
みなさん、こんにちは。
第1回では、HerokuにPredictionIOの実行環境を構築しました。第2回では、Salesforceのデータを一括取得してインポートするJavaアプリケーションの開発と、インポートした学習データを利用してEngineのトレーニングを行うところまでを試してみたいと思います。
必要なもの
- Salesforce Developer Editionなど
- Java開発環境(JDK、Eclipseなど)
Salesforceのカスタマイズ
顧客、ゲーム、お気に入りの3つのカスタムオブジェクトを作成します。(カスタムオブジェクトをMetaデータから作成されたい方はこちらから)
顧客オブジェクト:Customer__c
No. | ラベル | API参照 | データ型 | 文字数 | 参照先 | 必須 | 数式 | 補足 |
1 | 顧客名 | Name | string | 80 | ||||
2 | 顧客No. | CustomerNumber__c | string | 30 | ○ | 自動採番"C-{000000}" |
ゲームオブジェクト:Game__c
No. | ラベル | API参照 | データ型 | 文字数 | 参照先 | 必須 | 数式 | 補足 |
1 | ジャンル | Genre__c | string | 255 | ||||
2 | メーカー | Maker__c | string | 255 | ||||
3 | 画像URL | ImageURL__c | string | 255 | ||||
4 | イメージ | Image__c | string | 1300 | IMAGE( ImageURL__c , "ゲーム画像" , 400, 400) | |||
5 | ゲームNo. | GameNumber__c | string | 30 | ○ | 自動採番"G-{000000}" |
お気に入りオブジェクト:Favorite__c
No. | ラベル | API参照 | データ型 | 文字数 | 参照先 | 必須 | 数式 | 補足 |
1 | お気に入りNo. | Name | string | 80 | ○ | 自動採番"F-{000000}" | ||
2 | 顧客 | Customer__c | reference | 18 | Customer__c | |||
3 | ゲーム | Game__c | reference | 18 | Game__c | |||
4 | ゲームNo. | GameNumber__c | string | 1300 | Game__r.GameNumber__c | |||
5 | 顧客No. | CustomerNumber__c | string | 1300 | Customer__r.CustomerNumber__c | |||
6 | イメージ | Image__c | string | 1300 | Game__r.Image__c | |||
7 | ジャンル | Genre__c | string | 1300 | Game__r.Genre__c |
作成したオブジェクトにデータを入れておきます。(サンプルデータをDataLoader等でインポートされたい方はこちらから)
Javaアプリケーション
JavaアプリケーションはSalesforceのSOAP(Partner API)を使ってSalesforceからデータを取得する機能、取得したデータをPredictionIO Java SDKを使ってEvent Serverに対してHTTPリクエストする機能(Event Serverはリクエストを受け付けるとPostgreSQLに登録します)、これらをまとめて実行するメイン機能の3つで構成します。
作成するクラス
PIODataImportCmdLineSmple.java | mainメソッドを持つアプリケーション |
SfdcService.java | SalesforceのPartner APIを操作しデータを取得する |
PIODataImportService.java | PredictionIOに対してEventデータをリクエストする |
Salesforceからデータを取得
Salesforceが提供するSOAP APIへのアクセスは、「Force.com Web Services Connector(WSC)」を使うことにより簡単に行えます。Javaのサンプルコードを含む手順は、Developerforceのサイトが参考になります。
1.SalesfoceからWSDLのダウンロード
Salesforceの 設定 > 開発 > API のページから、Partner WSDLをダウンロードし「partner.wsdl」という名前で保存します。
2.WSCのjarファイルをダウンロード
最新はMaven Repositoryから取得できます。(執筆時点はver 38.0.4)
3.jarファイル(SOAP APIのスタブ)をPartner WSDLとWSCを使って自動生成
force-wsc-38.0.4.jar、partner.wsdlを同一フォルダに置き、そのフォルダに移動して以下のコマンドを実行します。
$ java -classpath force-wsc-38.0.4.jar com.sforce.ws.tools.wsdlc partner.wsdl partner.jar
本家サイトでは上記コマンドでしたが、私の環境では依存ライブラリが不足しているエラーが出ましたので、String Template 4のjarファイルをダウンロードし、classpathに追加して実行しました。(執筆時点の最新ver4.0.8)
$ java -classpath ST-4.0.8.jar:force-wsc-38.0.4.jar com.sforce.ws.tools.wsdlc partner.wsdl partner.jar
ダウンロードしたWSCのjarファイルおよび出来上がったpartner.jarをBuild Pathに設定し、Salesforceからデータを取得するコードを記述します。
ソースコード:SfdcService.java
/** * SalesforceのデータをPartner APIを利用して取得するサービスクラスです */ public class SfdcService { private static final String USER_NAME = "<YOUR_LOGIN_ID>"; private static final String USER_PASSWORD = "<YOUR_PASSWORD>"; private static final String AUTH_END_POINT = "https://login.salesforce.com/services/Soap/u/38.0"; private static final Integer QUERY_BATCH_SIZE = 2000; private PartnerConnection connection = null; /** * コンストラクタ * @param * @return SfdcService */ private SfdcService(){ login(); } /** * SfdcServiceを使用します * @param * @return SfdcService */ public static SfdcService use(){ return new SfdcService(); } /** * Salesforceにログインします * @param * @return */ private void login(){ ConnectorConfig config = new ConnectorConfig(); config.setUsername(USER_NAME); config.setPassword(USER_PASSWORD); config.setAuthEndpoint(AUTH_END_POINT); try { connection = new PartnerConnection(config); } catch (ConnectionException e) { e.printStackTrace(); } } /** * 顧客レコードを取得します * @param * @return */ public List<customer> getCustomers(){ List<customer> customers = new ArrayList<customer>(); try{ connection.setQueryOptions(QUERY_BATCH_SIZE); String soql = "Select Id, Name, CustomerNumber__c From Customer__c Order by CustomerNumber__c"; QueryResult qr = connection.query(soql); boolean done = false; while(!done){ SObject[] records = qr.getRecords(); for(int i = 0; i < records.length; i++){ SObject sObj = records[i]; Customer customer = new Customer(); customer.setName(String.valueOf(sObj.getField("Name"))); customer.setCustomerNumber(String.valueOf(sObj.getField("CustomerNumber__c"))); customers.add(customer); } if(qr.isDone()){ done = true; }else{ qr = connection.queryMore(qr.getQueryLocator()); } } }catch(ConnectionException ce){ ce.printStackTrace(); } return customers; } /** * ゲームレコードを取得します * @param * @return */ public List<game> getGames(){ List<game> games = new ArrayList<game>(); try{ String soql = "Select Id, Name, GameNumber__c, Genre__c, Maker__c, Image__c From Game__c Order by GameNumber__c"; QueryResult qr = connection.query(soql); boolean done = false; while(!done){ SObject[] records = qr.getRecords(); for(int i = 0; i < records.length; i++){ SObject sObj = records[i]; Game game = new Game(); game.setName(String.valueOf(sObj.getField("Name"))); game.setGameNumber(String.valueOf(sObj.getField("GameNumber__c"))); game.setGenre(String.valueOf(sObj.getField("Genre__c"))); games.add(game); } if(qr.isDone()){ done = true; }else{ qr = connection.queryMore(qr.getQueryLocator()); } } }catch(ConnectionException ce){ ce.printStackTrace(); } return games; } /** * お気に入りレコードを取得します * @param * @return */ public List<favorite> getFavorites(){ List<favorite> favorites = new ArrayList<favorite>(); try{ String soql = "Select Id, Name, Customer__c, CustomerNumber__c, Game__c, GameNumber__c From Favorite__c Order by Name"; QueryResult qr = connection.query(soql); boolean done = false; while(!done){ SObject[] records = qr.getRecords(); for(int i = 0; i < records.length; i++){ SObject sObj = records[i]; Favorite favorite = new Favorite(); favorite.setFavoriteNumber(String.valueOf(sObj.getField("Name"))); favorite.setCustomerNumber(String.valueOf(sObj.getField("CustomerNumber__c"))); favorite.setGameNumber(String.valueOf(sObj.getField("GameNumber__c"))); favorites.add(favorite); } if(qr.isDone()){ done = true; }else{ qr = connection.queryMore(qr.getQueryLocator()); } } }catch(ConnectionException ce){ ce.printStackTrace(); } return favorites; } /** * 顧客インナークラス */ public class Customer{ private String name; private String customerNumber; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCustomerNumber() { return customerNumber; } public void setCustomerNumber(String customerNumber) { this.customerNumber = customerNumber; } } /** * ゲームインナークラス */ public class Game{ private String name; private String genre; private String gameNumber; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getGenre() { return genre; } public void setGenre(String genre) { this.genre = genre; } public String getGameNumber() { return gameNumber; } public void setGameNumber(String gameNumber) { this.gameNumber = gameNumber; } } /** * お気に入りインナークラス */ public class Favorite{ private String favoriteNumber; private String customerNumber; private String gameNumber; public String getFavoriteNumber() { return favoriteNumber; } public void setFavoriteNumber(String favoriteNumber) { this.favoriteNumber = favoriteNumber; } public String getCustomerNumber() { return customerNumber; } public void setCustomerNumber(String customerNumber) { this.customerNumber = customerNumber; } public String getGameNumber() { return gameNumber; } public void setGameNumber(String gameNumber) { this.gameNumber = gameNumber; } } }
Event Serverにデータをリクエスト
Event Serverにデータをリクストする機能の作成にあたり、PredictionIO Java SDKおよび依存するライブラリをダウンロードし、Build pathに登録しておきます。
執筆時点で使用したjarファイル
- client-0.9.5.jar (PredictionIO Java SDK Client)
- async-http-client-1.7.24.jar (Asynchronous Http Client)
- gson-2.7.jar (Gson)
- guava-20.0-rc1.jar (Google Core Libraries For Java)
- joda-time-2.9.4.jar (Joda Time)
- netty-3.2.10.Final.jar (The Netty Project)
- slf4j-api-1.7.21.jar (SLF4J API Module)
ソースコード:PIODataImportService.java
/** * PredictionIOのEventServerにEventを送信するサービスクラスです */ public class PIODataImportService { private static final String APP_URL = "https://<YOUR_EVENT_SERVER_NAME>.herokuapp.com"; private static final String ACCESS_KEY = "<ACCESS_KEY>"; /** * 顧客レコードを追加します * @param * @return */ public void addCustomers(){ EventClient client = new EventClient(ACCESS_KEY, APP_URL); List<FutureAPIResponse> futureAPIResponses = new ArrayList<>(); try { for(SfdcService.Customer customer : SfdcService.use().getCustomers()){ Map<String, Object> emptyUserProperties = new HashMap<String, Object>(); FutureAPIResponse future = client.setUserAsFuture(customer.getCustomerNumber(), emptyUserProperties); futureAPIResponses.add(future); Futures.addCallback(future.getAPIResponse(), getFutureCallback("user " + customer.getCustomerNumber())); } } catch (IOException e) { e.printStackTrace(); }finally{ client.close(); } } /** * ゲームレコードを追加します * @param * @return */ public void addGames(){ EventClient client = new EventClient(ACCESS_KEY, APP_URL); List<FutureAPIResponse> futureAPIResponses = new ArrayList<>(); try { for(SfdcService.Game game : SfdcService.use().getGames()){ Map<String, Object> itemProperties = new HashMap<String, Object>(); List<String> genres = new ArrayList<String>(); genres.add(game.getGenre()); itemProperties.put("genre", genres); FutureAPIResponse future = client.setItemAsFuture(game.getGameNumber(), itemProperties); futureAPIResponses.add(future); Futures.addCallback(future.getAPIResponse(), getFutureCallback("item " + game.getGameNumber())); } } catch (IOException e) { e.printStackTrace(); }finally{ client.close(); } } /** * お気に入りレコードを追加します * @param * @return */ public void addFavorites(){ EventClient client = new EventClient(ACCESS_KEY, APP_URL); List<FutureAPIResponse> futureAPIResponses = new ArrayList<>(); try { for(SfdcService.Favorite favorite : SfdcService.use().getFavorites()){ Map<String, Object> emptyActionItemProperties = new HashMap<String, Object>(); FutureAPIResponse future = client.userActionItemAsFuture("view", favorite.getCustomerNumber(), favorite.getGameNumber(), emptyActionItemProperties); futureAPIResponses.add(future); Futures.addCallback(future.getAPIResponse(), getFutureCallback("actionItem customer " + favorite.getCustomerNumber() + " game " + favorite.getGameNumber())); } } catch (IOException e) { e.printStackTrace(); }finally{ client.close(); } } /** * EventServerとのコールバック処理です * @param name * @return FutureCallback<APIResponse> */ private FutureCallback<APIResponse> getFutureCallback(final String name) { return new FutureCallback<APIResponse>(){ @Override public void onSuccess(APIResponse response) { System.out.println(name + " added: " + response.getMessage()); } @Override public void onFailure(Throwable thrown) { System.out.println("failed to add " + name + ": " + thrown.getMessage()); } }; } }
PIODataImportServiceでは addCustomers()、addGames()、addFavorites()の各メソッドでSalesforceの該当オブジェクトからレコードを取得し、EventClientを使用してリクエストするJsonを組み立て、非同期でEvent Serverに対してリクエストを送信しています。Event Serverからのcall backはgetFutureCallback()メソッドで処理しています。
Event Serverに対するリクエストは以下のURLに対してJsonをPOSTしています。
http://<YOUR_EVENT_SERVER_NAME>.herokuapp.com/events.json?accessKey=<ACCESS_KEY>
Event Serverには以下のようなJsonがリクエストされます。
顧客を登録するEventデータ { "event" : "$set", "entityType" : "user", "entityId" : "<顧客No.>" } ゲームを登録するEventデータ { "event" : "$set", "entityType" : "item", "entityId" : "<ゲームNo.>", "properties" : { "genre" : ["<ジャンル>"] } } お気に入りを登録するEventデータ { "event" : "view", "entityType" : "user", "entityId" : "<顧客No.>", "targetEntityType" : "item", "targetEntityId" : "<ゲームNo.>" }
メイン機能
Salesforceからのデータ一括取得&Event Serverへのリクエストの一連を動かすためのクラスを作成します。
ソースコード:PIOCmdLineSampleApp.java
/** * PredictionIOにSalesforceのデータをインンポートするコマンドラインアプリ */ public class PIOCmdLineSampleApp { /** * mainメソッドです * @param args * @return */ public static void main(String[] args) { PIODataImportService service = new PIODataImportService(); service.addCustomers(); System.out.println("--- User import done ---"); service.addGames(); System.out.println("--- Item import done ---"); service.addFavorites(); System.out.println("--- UserActionItem import done ---"); } }
このクラスは単純にPIODataImportServeのインスタンスを生成し、addCustomers()、addGames()、addFavorites()メソッドを呼び出すだけです。
Eclipse等からアプリを実行し、Event Serverへのリクエストに成功すると以下のようなログが確認できます。
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
user C-000001 added: {"eventId":"1ee69b1df5274e7a9a94728715ceb607"}
user C-000002 added: {"eventId":"5a60733cc9ee4bf5a61e15b001e74ac7"}
user C-000003 added: {"eventId":"f83812910c9c4ab5a322c2e8dd7b3523"}
user C-000004 added: {"eventId":"c1fb45618e094e13bfeaf9f0b4549680"}
--- User import done ---
item G-000001 added: {"eventId":"876646148a5d494795cc765d4e2bc284"}
item G-000002 added: {"eventId":"c8dd1510edd74fd8aecf3479a9abe488"}
item G-000003 added: {"eventId":"dc71d6fca0a94cc4b8ad8840ec861577"}
item G-000004 added: {"eventId":"78cb97774dae491b9aa9537a493d374e"}
item G-000005 added: {"eventId":"1cbd5f6a106143668ccb700b696e595b"}
item G-000006 added: {"eventId":"9d6e855a82d2411b871d8307c13e1ec2"}
--- Item import done ---
actionItem customer C-000001 game G-000001 added: {"eventId":"38f483d623514df7be33a79e350d832c"}
actionItem customer C-000001 game G-000002 added: {"eventId":"8fc2c51ba45f4111b9a0dcf697a212ce"}
actionItem customer C-000001 game G-000003 added: {"eventId":"14715aed4a1a46d48c8b35806e0bf991"}
actionItem customer C-000002 game G-000004 added: {"eventId":"9e7f5dc56c11469b8628f50122629e72"}
actionItem customer C-000002 game G-000005 added: {"eventId":"c86ac59a935e4be4a8b3ad58d26f925b"}
--- UserActionItem import done ---
ブラウザからEventデータの登録結果を確認
ブラウザから以下のURLを入力して確認します。
http://<YOUR_EVENT_SERVER_NAME>.herokuapp.com/events.json?accessKey=<ACCESS_KEY>&limit=10
Eventデータが正しく登録されていると以下のような結果が確認できます。
Engineのトレーニング
Eventデータが無事に登録されましたので、いよいよEngineのトレーニングです。
Herokuのフリープランの範囲でトレーニングを実行したいので、dynoのスケールダウンを設定します。
$ heroku ps:scale web=0 train=0
Engineのトレーニングを実行します。
$ heroku run train
トレーニングを行うと大量のログが出力されますが、トレーニングに成功すると以下のようなログが確認できます。
[INFO] [Engine$] ALSModel does not support data sanity check. Skipping check. [INFO] [Engine$] EngineWorkflow.train completed [INFO] [Engine] engineInstanceId=95acbab2-863a-47b3-b59d-e610c0f5ff8e [INFO] [CoreWorkflow$] Inserting persistent model [INFO] [CoreWorkflow$] Updating engine instance [INFO] [CoreWorkflow$] Training completed successfully.
dynoの設定を戻します。
$ heroku ps:scale web=1 train=0
ブラウザからEngineの状態を確認します。
http://<YOUR_ENGINE_NAME>.herokuapp.com
トレーニングの開始・終了日時やData SourceとしてEvent Serverに作成したアプリケーションと関連付いていることなどが確認できます。
第3回に続く
EngineにSalesforceのデータをインポートし、トレーニングを行いました。ここまでの設定により、リコメンドができる状態になっています。第3回では最終的な仕上げとして、Salesforceの画面からゲームのリコメンドを受け取り、お勧めゲームの表示を行うところを作成します。