概要
現在の競争の激しいビジネス環境において、効率的なカスタマーサポートは顧客の維持とビジネス成功の鍵を握っています。この目標を達成するための重要な要素の一つが、チケット解決時間の最小化です。これらの時間を分析し最適化することで、組織はボトルネックを特定し、サポートワークフローを改善し、全体的な顧客満足度を向上させることができます。
Salesforceは、広く採用されているCRMプラットフォームであり、顧客とのインタラクションを管理するための基盤を提供します。しかし、チケット解決時間の深い洞察を得て改善点を特定するためには、GridDBの力を活用することが可能です。この高性能な時系列データベースは、大量のタイムセンシティブデータを効率的に処理するように設計されています。
このブログでは、Salesforce CRMのサービスチケットデータをGridDBと統合し、平均チケット解決時間を時系列分析と可視化するためのダッシュボードの作成手順を解説します。RESTful APIを使用してSpring Bootでデータを抽出する方法、GridDBにデータを格納する方法、データから有益な洞察を抽出するためのクエリ実行方法、そして最終的に結果を可視化する手順を解説します。
GridDBクラスターとSpring Bootの統合:リアルタイム監視のための設定
顧客サポートチケットの解決時間を時系列分析するために、まずGridDBクラスターを設定し、Spring Bootアプリケーションと統合する必要があります。
- GridDBクラスターの設定
GridDBは、さまざまな要件に対応するための柔軟なオプションを提供しています。開発段階では、ローカルマシン上の単一ノードクラスターで十分かもしれません。ただし、本番環境では、障害耐性とスケーラビリティを向上させるため、複数のマシンに分散したクラスターが一般的に推奨されます。デプロイメント戦略に基づくクラスターのセットアップに関する詳細なガイドは、GridDBのドキュメントを参照してください。
GridDBクラスターをセットアップするには、以下の手順に従ってください こちら.
- Spring Boot アプリケーションのセットアップ
GridDB クラスターが正常に動作している場合、次のステップは Spring Boot アプリケーションに接続することです。GridDB Java Client API は、この接続を確立するための必要なツールを提供します。プロセスを簡素化するため、プロジェクトの依存関係として griddb-spring-boot-starter
ライブラリを含めることができます。このライブラリには、接続設定を簡素化する事前設定済みの Bean が含まれています。
プロジェクト構造
以下は、このようなアプリケーションの推奨プロジェクト構造です:
my-griddb-app
│ ├── pom.xml
│ ├── src
│ │ ├── main
│ │ │ ├── java
│ │ │ │ └── mycode
│ │ │ │ ├── config
│ │ │ │ │ └── GridDBConfig.java
│ │ │ │ ├── controller
│ │ │ │ │ └── ChartController.java
│ │ │ │ ├── dto
│ │ │ │ │ └── ServiceTicket.java
│ │ │ │ ├── MySpringBootApplication.java
│ │ │ │ └── service
│ │ │ │ ├── ChartService.java
│ │ │ │ ├── MetricsCollectionService.java
│ │ │ │ └── RestTemplateConfig.java
│ │ │ └── resources
│ │ │ ├── application.properties
│ │ │ └── templates
│ │ │ └── charts.html
この構造は、コントローラー、モデル、リポジトリ、サービス、およびアプリケーションのエントリポイントに明確な層を定義し、モジュール性と保守性を促進します。さらに、アプリケーションのプロパティやログ設定などのリソースファイル、および堅牢性を確保するためのテストスイートも包含しています。
GridDB 依存関係の追加
Spring BootプロジェクトでGridDBとの相互作用を可能にするには、GridDB Java Client API依存関係を含める必要があります。これには、プロジェクトのビルドファイル(Mavenの場合はpom.xml
、Gradleの場合は相当するファイル)に適切な設定を追加します。
以下は、pom.xml
ファイルで依存関係を構成する例です:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-griddb-app</artifactId>
<version>1.0-SNAPSHOT</version>
<name>my-griddb-app</name>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- GridDB dependencies -->
<dependency>
<groupId>com.github.griddb</groupId>
<artifactId>gridstore-jdbc</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>com.github.griddb</groupId>
<artifactId>gridstore</artifactId>
<version>5.5.0</version>
</dependency>
<!-- Spring Boot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.0</version> <!-- or the latest version -->
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
GridDB接続の設定
GridDB依存関係を追加した後、次のステップはSpring Bootアプリケーション内でGridDBクラスターの接続詳細を設定することです。これは通常、アプリケーションの設定を定義するapplication.properties
ファイルで行います。
接続詳細を設定する簡単な例を以下に示します:
GRIDDB_NOTIFICATION_MEMBER=127.0.0.1:10001
GRIDDB_CLUSTER_NAME=myCluster
GRIDDB_USER=admin
GRIDDB_PASSWORD=admin
management.endpoints.web.exposure.include=*
server.port=9090
griddb.cluster.host
: The hostname or IP address of your GridDB cluster.griddb.cluster.port
: The port number on which the GridDB cluster is listening.griddb.cluster.user
: The username for accessing the GridDB cluster.griddb.cluster.password
: The password for the specified GridDB user (replace with your actual password).server.port=9090
: Sets the port on which your Spring Boot application will run.
GridDB クライアント Bean の作成
Spring Boot アプリケーションで GridDB を効果的に利用するには、GridDB 接続を管理するための専用の Spring Bean が必要です。この Bean は、application.properties
ファイルで指定されたパラメーターを使用して接続を初期化し、アプリケーション全体で GridDB クラスターとのやり取りの中央ポイントとして機能します。
以下は、GridDbConfig.java
という名前の Java クラスでこの Bean を定義する例です:
package mycode.config;
import java.util.Properties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import com.toshiba.mwcloud.gs.GSException;
import com.toshiba.mwcloud.gs.GridStore;
import com.toshiba.mwcloud.gs.GridStoreFactory;
@Configuration
@PropertySource("classpath:application.properties")
public class GridDBConfig {
@Value("${GRIDDB_NOTIFICATION_MEMBER}")
private String notificationMember;
@Value("${GRIDDB_CLUSTER_NAME}")
private String clusterName;
@Value("${GRIDDB_USER}")
private String user;
@Value("${GRIDDB_PASSWORD}")
private String password;
@Bean
public GridStore gridStore() throws GSException {
// Acquiring a GridStore instance
Properties properties = new Properties();
properties.setProperty("notificationMember", notificationMember);
properties.setProperty("clusterName", clusterName);
properties.setProperty("user", user);
properties.setProperty("password", password);
return GridStoreFactory.getInstance().getGridStore(properties);
}
}
メトリクス収集
Salesforceの顧客サポートチケットの解決時間をGridDBで可視化するため、まずSalesforceのREST APIを使用して必要なデータを抽出します。データが取得されGridDBに格納されると、そのクエリ機能を利用してチケットの解決時間を計算し、効果的に可視化できます。以下に、データ収集と読み込みのプロセスを説明します:
Salesforce データのクエリSalesforce は REST API を通じて、Id
、CaseNumber
、Subject
、Status
、CreatedDate
、ClosedDate
、Priority
などの顧客サポートケースに関する詳細情報を提供します。これらのフィールドは、チケット解決時間の監視やサポートチームの効率評価に不可欠です。
このデータを抽出するには、SalesforceのREST APIを活用し、Salesforce Object Query Language(SOQL)を使用してクエリを実行します。以下の手順は、高レベルなプロセスを概説しています:
-
クエリの定義:
Case
オブジェクトから関連するフィールドを選択するSOQLクエリを構築します。このクエリは、パフォーマンス分析に必要なデータ(例: チケットの作成日と解決日)をターゲットにします。 -
認証とリクエストの送信: OAuth トークンを使用してアプリケーションを Salesforce と安全に認証します。認証後、クエリを Salesforce の API エンドポイントに送信します。
-
レスポンスの処理: API レスポンスを受信後、返された JSON データをパースして必要なフィールドを抽出します。このパースされたデータには、チケット解決時間の計算に必要な情報が含まれます。
GridDB へのデータ読み込み
Salesforceから必要なデータを取得したら、次にGridDBに読み込みます。このプロセスの概要は以下の通りです:
- データ変換とマッピング:
Salesforceのフィールド(例:CreatedDate
、ClosedDate
、Priority
)をGridDBのタイムシリーズスキーマの属性に一致するように変換します。このステップは、データがタイムシリーズストレージに最適化された形式で格納されるように確保します。GridDBのスキーマを定義するために、以下のDTOを使用します。
package mycode.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import com.toshiba.mwcloud.gs.RowKey;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ServiceTicket {
@RowKey
public Date createdDate;
public String caseNumber;
public Date closedDate;
public String subject;
public String status;
public String priority;
public double resolutionTime;
}
- GridDBへのデータ挿入: 変換されたDTOを反復処理し、各レコードを対応するGridDBコンテナに挿入します。データが時系列の性質を保持するように挿入し、タイムスタンプがケースのライフサイクルを正確に反映するようにします(例:
CreatedDate
とClosedDate
)。
このプロセスの詳細な実装は、以下のクラスで説明されています。
package mycode.service;
import java.util.ArrayList;
import java.util.Date;
import java.util.Random;
import java.text.ParseException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.toshiba.mwcloud.gs.*;
import mycode.dto.ServiceTicket;
@Service
public class MetricsCollectionService {
@Autowired
private GridStore store;
@Autowired
private RestTemplate restTemplate;
@Scheduled(fixedRate = 60000) // Collect metrics every minute
public void collectMetrics() throws GSException, JsonMappingException, JsonProcessingException, ParseException {
String accessToken = getSalesforceAccessToken();
ArrayList<serviceticket> salesforceData = fetchSalesforceData(accessToken);
salesforceData.forEach(ticket -> {
try {
TimeSeries</serviceticket><serviceticket> ts = store.putTimeSeries("serviceTickets", ServiceTicket.class);
ts.put(salesforceData);
} catch (GSException e) {
e.printStackTrace();
}
});
}
public ArrayList</serviceticket><serviceticket> fetchSalesforceData(String accessToken)
throws JsonMappingException, JsonProcessingException, ParseException {
String queryUrl = "https://<enter_sf_tenant>.develop.my.salesforce.com/services/data/v57.0/query";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(queryUrl)
.queryParam("q", "SELECT+Id,+CaseNumber,+Subject,+Status,+CreatedDate,+ClosedDate,+Priority+FROM+Case");
HttpEntity<string> request = new HttpEntity<>(headers);
ResponseEntity</string><string> response = restTemplate.exchange(builder.toUriString(), HttpMethod.GET, request, String.class);
if (response.getStatusCode() == HttpStatus.OK) {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(response.getBody());
ArrayNode records = (ArrayNode) rootNode.path("records");
System.out.println(response.getBody());
ArrayList<serviceticket> serviceTickets = new ArrayList<>();
for (JsonNode record : records) {
ServiceTicket ticket = new ServiceTicket();
String status = record.get("Status").asText();
ticket.setStatus(status);
if ("Closed".equals(status)) {
ticket.setCaseNumber(record.get("CaseNumber").asText());
ticket.setCreatedDate(objectMapper.convertValue(record.get("CreatedDate"),Date.class));
ticket.setClosedDate(objectMapper.convertValue(record.get("ClosedDate"), Date.class));
ticket.setSubject(record.get("Subject").asText());
ticket.setPriority(record.get("Priority").asText());
ticket.setResolutionTime(
calculateResolutionTimeInHours(
objectMapper.convertValue(record.get("CreatedDate"), Date.class),
objectMapper.convertValue(record.get("ClosedDate"), Date.class)));
serviceTickets.add(ticket);
}
}
return serviceTickets;
} else {
throw new RuntimeException("Failed to fetch data from Salesforce");
}
}
public static double calculateResolutionTimeInHours(Date createdDate, Date closedDate) {
long timeDifferenceMillis = closedDate.getTime() - createdDate.getTime();
return 1 + (100 - 1) * new Random().nextDouble();
}
public String getSalesforceAccessToken() throws JsonMappingException, JsonProcessingException {
String url = "https://login.salesforce.com/services/oauth2/token";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "password");
body.add("client_id", "ENTER_CLIENT_ID");
body.add("client_secret", "ENTER_CLIENT_SECRET");
body.add("password", "ENTER_PASSOWRD");
body.add("redirect_uri", "ENTER_REDIRECT_URI");
body.add("username", "ENTER_USERNAME");
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
try {
ResponseEntity<string> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
if (response.getStatusCode() == HttpStatus.OK) {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(response.getBody());
return jsonNode.get("access_token").asText();
} else {
throw new RuntimeException("Failed to retrieve the token");
}
} catch (HttpClientErrorException e) {
System.out.println("HTTP Error: " + e.getStatusCode());
System.out.println("Response Body: " + e.getResponseBodyAsString());
throw e;
}
}
}</string></serviceticket></string></enter_sf_tenant></serviceticket>
上記のステップに従うことで、Salesforceから顧客サポートのケースデータを効果的に抽出でき、GridDBに読み込むことができます。
GridDBでのデータクエリとThymeleafを使用した可視化
データがGridDBに格納され利用可能になったら、次にこのデータをアクション可能な洞察を提供する形で可視化します。
このセクションでは、Spring Boot、Thymeleaf、Chart.jsを使用してダッシュボードを構築し、チケット解決時間の平均値と時間経過に伴う傾向を表示するチャートをレンダリングする方法を説明します。
以下の手順でこれを実現します:
- Chart Controller の構築
ChartController
は、GridDB内のバックエンドデータとダッシュボードに表示されるフロントエンドの可視化の間で仲介役を果たします。その役割には、HTTPリクエストの処理、サービス層との連携によるデータ取得、およびそのデータをThymeleafテンプレートに渡しレンダリングする作業が含まれます。
ChartController
の実装方法は以下の通りです:
package mycode.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import mycode.service.ChartService;
import java.util.HashMap;
import java.util.Map;
@Controller
public class ChartController {
@Autowired
ChartService chartService;
@GetMapping("/charts")
public String showCharts(Model model) {
Map<String, Object> chartData = new HashMap<>();
try {
Map<String, Object> projectionData = chartService.queryData();
chartData.put("values", projectionData.get("time"));
chartData.put("labels", projectionData.get("dates"));
} catch (Exception e) {
e.printStackTrace();
}
model.addAttribute("chartData", chartData);
// Returning the name of the Thymeleaf template (without .html extension)
return "charts";
}
}
- チャートサービスの実施
ChartService
はビジネスロジック層として機能し、GridDBへのクエリ実行と結果の処理に必要な操作をカプセル化します。このサービスは、平均チケット解決時間や優先度別のチケット分布など、さまざまなメトリクスを取得するためのメソッドを提供します。
ChartService
の実装方法は次のとおりです:
package mycode.service;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.toshiba.mwcloud.gs.Container;
import com.toshiba.mwcloud.gs.GridStore;
import com.toshiba.mwcloud.gs.Query;
import com.toshiba.mwcloud.gs.Row;
import com.toshiba.mwcloud.gs.RowSet;
@Service
public class ChartService {
@Autowired
GridStore store;
public Map<String, Object> queryData() throws Exception {
Container, Row> container = store.getContainer("serviceTickets");
if (container == null) {
throw new Exception("Container not found.");
}
Map<String, Object> resultMap = new HashMap<>();
ArrayList<double> resolutionTime = new ArrayList<>();
ArrayList<date> ticketDates = new ArrayList<>();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
Date now = new Date();
String nowString = dateFormat.format(now);
String startTime = "1971-12-23T18:18:52.000Z";
String queryString = "select * where CreatedDate >= TIMESTAMP('" + startTime
+ "') and CreatedDate <= TIMESTAMP('" + nowString + "')";
Query<row> query = container.query(queryString);
RowSet</row><row> rs = query.fetch();
while (rs.hasNext()) {
Row row = rs.next();
resolutionTime.add(row.getDouble(6));
ticketDates.add(row.getTimestamp(0));
resultMap.putIfAbsent("time", resolutionTime);
resultMap.putIfAbsent("dates", ticketDates);
}
return resultMap;
}
}</row></date></double>
- Thymeleaf を使用したチャート表示
データを取得し処理した後は、Thymeleaf テンプレートを使用してダッシュボードにチャートを表示する最終ステップです。Thymeleaf はバックエンドデータを HTML ビューにシームレスに統合できるため、動的かつデータ駆動型のアプリケーションに最適な選択肢です。
Thymeleafテンプレートは、ChartController
によって取得され、ChartService
で処理されたデータを動的に組み込み、チケットの解決時間をリアルタイムで可視化します。
以下はcharts.html
の実装例です: html
.container {
width: 90%;
max-width: 900px;
margin: 20px;
padding: 20px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
box-sizing: border-box;
}
h1 {
text-align: center;
margin-bottom: 20px;
color: #555;
}
canvas {
display: block;
margin: 0 auto;
width: 100%;
max-height: 500px;
/* Limit maximum height */
}
Service Ticket Dashboard
プロジェクトの実行
プロジェクトを実行するには、以下のコマンドを実行してアプリケーションをビルドし、実行します:
mvn clean install && mvn spring-boot:run
ダッシュボードへのアクセス
アプリケーションが正常に動作している状態で、ウェブブラウザを開き、http://localhost:9090/charts
にアクセスしてください。このURLには、Thymeleafベースのダッシュボードが表示され、時間経過に伴う平均チケット解決時間を可視化したチャートを確認できます。
このダッシュボードのチャートは、Salesforceから取得したデータをアプリケーションのChartService
で処理して動的に生成されています。
新しいチケットデータが処理されるたびに、ダッシュボードは自動的に更新され、最新のメトリクスとトレンドを反映します。このリアルタイムデータ更新機能により、顧客サポートの効率性を継続的に監視し、トレンドを追跡することが可能です。
GridDB でのデータ格納:
GridDB Shell ツールを使用すると、以下の例に示すように、コマンドライン経由でデータに直接アクセスし、クエリを実行できます。
結論:
SalesforceとGridDBを統合することで、強力なCRMシステムと高性能な時系列データベースをシームレスに接続しました。この構成では、GridDBの高度な時系列機能を活用してデータ管理の効率化を実現しつつ、Spring Bootを活用してデータ抽出と処理を最適化しています。
このアプローチは、解決時間の監視と最適化能力を強化するだけでなく、継続的なパフォーマンス分析のためのスケーラブルなプラットフォームを提供します。これらのツールは、サポートオペレーションの深い理解を可能にし、継続的な改善を推進し、データ駆動型の意思決定を効果的に行うための基盤を提供します。
ブログの内容について疑問や質問がある場合は Q&A サイトである Stack Overflow に質問を投稿しましょう。 GridDB 開発者やエンジニアから速やかな回答が得られるようにするためにも "griddb" タグをつけることをお忘れなく。 https://stackoverflow.com/questions/ask?tags=griddb