場所情報のやり取りには、以下のLocationDataクラスを使用しております。
@Data
@AllArgsConstructor
@RequiredArgsConstructor
//WeatherAPIライブラリの方に、すでにLocationというクラスが存在するので衝突防止にこの名称にしました。
public class LocationData {
//	ユーザーの入力
	@JsonProperty(required = true, value = "input")
	private String input;
//	「"都市名-行政区画名"」の形で格納 (例:新宿区-東京都)
	@JsonProperty(required = true, value = "city_region")
	private String cityRegion;
//	緯度経度 小数第四位までを「"latitude,longitude"」の形で格納
	@JsonProperty(required = true, value = "latlon")
	private String latlon;
}
初めて検索される地名に関しては、全てOpenAIAPIを通じて「input(ユーザー入力値)、cityRegion(都市名-行政区画名)、latlon(緯度経度)」といったフィールドを持つJSONに変換し、それをさらにLocationDataオブジェクトとして返すようにしておりました。
(視認性のために一部簡略化しております)
ただ、例えば「草津」や「池田」など、同一名称の市区町村が複数存在する地名に関しては、OpenAIAPIの処理に時間がかかり、リクエストタイムアウトしてしまうことが多くありました。
Herokuではタイムアウトの設定値が30秒であり、これは変更することができないとのことだったので、以下の2点の対応をいたしました。それぞれ解説いたします。
- 辞書登録
- 非同期処理
辞書登録
ユーザーの入力が都道府県名、または同一名称の市区町村が複数存在する地名だったとき用に、「input(入力値)、city-region(市区町村名-都道府県名)、latlon(緯度経度)」のデータをあらかじめデータベースに登録しております。(正確にいうと、アプリ起動時に挿入されます。H2のメモリモードを使用しているので、データは毎回リセットされます。)
同名の市区町村に関しては、こちらの記事にあるデータを使用いたしました。
inputが都道府県名のときのデータも用意したのは、「福岡」と入力した時、OpenAIAPIからは中央区と博多区のデータが出力されることがあったからです。県庁のある行政区域のデータを出力するようにプロンプトを与えたのですが、改善しなかったため、辞書登録するようにしました。
非同期処理
OpenAIAPIのレスポンスに時間がかかかっていたのは、日本国内に同一地名が複数存在する時でした。よって、辞書登録だけでもリクエストタイムアウトの可能性を低減できます。ただ、それでも可能性は0ではないということ、そして世界の同一地名に関してはカバーできていないことから、OpenAIAPIの呼び出しは非同期で処理するようにしました。
クライアントからのリクエストに対してすぐに待ちページ(waiting.html)を返し、その裏でOpenAIAPIの呼び出しを行なっております。結果が戻ってきたら、Server-Sent Events(以下SSE)によってクライアントにイベントを送信し、続きの処理をします。
	//	OpenAiApiを呼び出し、ユーザーの入力値からLocationDataオブジェクト(地名、都市名-行政区画名、緯度経度)を生成する
	@Override
	@Async("Thread")
	public void fetchLocationDataFromOpenAi(String input, String jobId, SseEmitter emitter) throws IOException {
        // (中略)
		try {
			//LocationDataオブジェクト(地名、都市名-行政区画名、緯度経度)を生成する
			List<LocationData> locations = openAiApi.generateLocationData(input).getLocations();
			if (locations.isEmpty()) {
				emitter.send(SseEmitter.event().name("not_found").data("error"));
			} else {
				weatherForecastSearchMapper.insertLocations(locations);
				//jobId、locationsのセットでデータを保持
				results.put(jobId, locations);
				emitter.send(SseEmitter.event().name("done").data("ok"));
			}
		} catch (IOException e) {
			emitter.completeWithError(e);
		}
	}
また、ユーザーの入力値やOpenAIAPIから取得したLocationDataリストはセッションに保存していたのですが、同一のクライアントが複数のタブで同時に天気予報を検索した際は、一番後に検索した地名の結果しか戻ってこない(またはエラーになる)といった問題がありました。
例えば①ひとつのタブで「ビクトリア」と検索し、②別のタブで「ワシントン」と検索した場合、「ビクトリア」を属性名「input」としてセッションに保存しても、後から検索した「ワシントン」にすぐに上書きされてしまいます。よって、クライアントから見ると「ビクトリア」と検索したはずなのに「ワシントン」の天気予報が返ってくるという結果になります。
また、クライアントからの送信に多少のラグがあり、下記画像②④⑤の段階では「input」が「ビクトリア」であったとしても、それらの処理の途中でセッションに保存されている「input」が「ワシントン」に置き換わった(③)場合、⑥のHttpSession#getAttribute()では「ワシントン」と返され、⑦の戻り値はnullになります。なぜなら、⑤でMapにキーとして登録されているのは「ビクトリア」であるからです。
このパターンの時は、後にlocations.size()というメソッド呼び出しがあるので、このタイミングでNullPointerExceptionになります。
よってセッションは使用せず、マルチスレッドで処理することにしました。具体的にいうと、処理ごとにjobIdを発行し、jobIdと処理の結果を結びつけるようにしました。
参考
herokuのタイムアウトについて
Server-Sent Eventsについて
- 小森祐介. [改訂新版]プロになるためのWeb技術入門. 技術評論社, 第9章「サーバプッシュ技術」
- 渋川よしき. Real World HTTP 第3版.オライリージャパン, p254
クラスSseEmitterについて
非同期処理、マルチスレッドについて
- 山田祥寛. 独習Java第6版 .翔泳社,「11.1.8 スレッド処理の後処理を定義する━CompletableFutureクラス」







