ユーザー定義テーブル関数 (UDTF) は、0 個以上のスカラー値を入力として受け取り、複数の行を出力として返します。返される各行は、1 つ以上の列を持つことができます。これは、入力を 1 つの出力値にマッピングするスカラー関数とは異なります。
Flink は、いくつかの種類の関数を区別します。
| 関数タイプ | 動作 |
|---|---|
| スカラー関数 | スカラー値を新しいスカラー値にマッピングします |
| ユーザー定義テーブル関数 (UDTF) | スカラー値を新しい行にマッピングします |
| 集計関数 | 複数の行のスカラー値を新しいスカラー値にマッピングします |
| テーブル集約関数 | 複数の行のスカラー値を新しい行にマッピングします |
1 つの入力行から複数の出力行を生成するロジックが必要な場合(例:区切り文字で区切られた文字列を個別の行に分割する場合)、UDTF を使用します。
Flink のユーザー定義関数の背景については、「ユーザー定義関数」をご参照ください。
前提条件
開始する前に、以下の要件を満たしていることを確認してください。
-
ローカルマシンに Apache Maven がインストールされていること
-
ローカルマシンに IntelliJ IDEA がインストールされていること
-
Realtime Compute for Apache Flink で Flink SQL ジョブ環境がセットアップされていること
UDTF の開発
Realtime Compute for Apache Flink は、ユーザー定義スカラー関数、ユーザー定義集計関数 (UDAF)、UDTF の事前設定された開発環境と実装例を含むサンプルプロジェクト (ASI_UDX_Demo) を提供しています。このサードパーティの GitHub リポジトリへのアクセスは、時折遅くなったり、利用できなくなったりする場合があります。
-
ASI_UDX_Demo サンプルプロジェクトをダウンロードし、ローカルマシンに解凍します。解凍後、以下のファイルを含む
ASI_UDX-mainフォルダーが作成されます。-
pom.xml: Maven プロジェクト設定ファイル。プロジェクトの座標、依存関係、ビルド設定を記述します。 -
\ASI_UDX-main\src\main\java\ASI_UDTF\ASI_UDTF.java: UDTF のサンプル Java コード。
-
-
IntelliJ IDEA で、[File] > [Open] をクリックし、解凍した
ASI_UDX-mainフォルダーを開きます。 -
\ASI_UDX-main\src\main\java\ASI_UDTF\ASI_UDTF.javaを開き、UDTF ロジックを実装します。UDTF はTableFunction<T>を拡張します。ここで、Tは出力タイプです。eval()メソッドはvoidを返す必要があります。returnステートメントの代わりに、collect()を呼び出して出力行を発行します。これは、returnを使用するスカラー関数とは異なります。次の例は、縦棒 (|) を区切り文字として使用して、文字列を 2 つの列に分割します。package ASI_UDTF; import org.apache.flink.api.java.tuple.Tuple2; import org.apache.flink.table.functions.TableFunction; public class ASI_UDTF extends TableFunction<Tuple2<String, String>> { public void eval(String str) { String[] split = str.split("\\|"); String name = split[0]; String place = split[1]; // 2 つの列を持つ 1 つの行を発行 collect(Tuple2.of(name, place)); } }出力タイプの宣言 Flink は、UDTF の出力スキーマを宣言するための 2 つのアプローチをサポートしています。
-
@FunctionHintと@DataTypeHintを使用した Row タイプ (推奨): フィールド名とタイプを明示的に宣言し、出力スキーマを Flink と読者にとって明確にします。@FunctionHint(output = @DataTypeHint("ROW<word STRING, length INT>")) public static class SplitFunction extends TableFunction<Row> { public void eval(String str) { for (String s : str.split(" ")) { // collect(...) を使用して行を発行 collect(Row.of(s, s.length())); } } } -
Tuple タイプ (ジェネリクスから型推論): 記述が簡単ですが、列名は明示的ではありません。
TableFunction<Tuple2<String, Integer>>
説明TableFunctionでサポートされるデータ型と型推論メカニズムは、Flink のメジャーバージョンによって異なります。上記の例は、Flink 1.15 のドキュメント (データ型 および 型推論) に従っています。お使いの Ververica Runtime (VVR) バージョンに対応する Flink ドキュメントを確認してください。Flink バージョンを確認するには、「ストレージの管理と運用」をご参照ください。 -
-
ASI_UDX-mainフォルダー内のpom.xmlを開き、依存関係を設定します。サンプルプロジェクトには、Flink 1.11 用の以下の Maven 依存関係が含まれています。<dependencies> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-streaming-java_2.12</artifactId> <version>1.11.0</version> <!--<scope>provided</scope>--> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-table</artifactId> <version>1.11.0</version> <type>pom</type> <!--<scope>provided</scope>--> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-core</artifactId> <version>1.11.0</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-table-common</artifactId> <version>1.11.0</version> </dependency> </dependencies>UDTF に上記以外の追加の依存関係がない場合は、次の手順に進んでください。それ以外の場合は、必要な依存関係エントリを
pom.xmlに追加します。 -
pom.xmlが含まれるディレクトリで、次のコマンドを実行してプロジェクトをパッケージ化します。mvn package -Dcheckstyle.skipビルドが成功すると、
\ASI_UDX-main\target\ディレクトリにASI_UDX-1.0-SNAPSHOT.jarが生成されます。UDTF の開発が完了しました。
非同期テーブル関数 (AsyncTableFunction)
非同期テーブル関数 (AsyncTableFunction) は、Flink SQL または Table API で外部サービスに非同期にアクセスし、各呼び出しに対して 0 行、1 行、または複数行を返します。これは、ユーザー定義テーブル関数 (UDTF) の一種です。典型的なシナリオには、製品 ID による複数のタグのクエリ、または検索条件に一致する複数のレコードの取得が含まれます。非同期テーブル関数は、SQL の LATERAL TABLE または Table API の joinLateral によって呼び出されるユーザー定義関数として、非同期相関操作を構成できます。また、非同期ルックアップ結合のクエリ実装として、ディメンションテーブルコネクタによって提供することもできます。
設定パラメーター
次のパラメーターは、非同期テーブル関数のランタイム動作を制御します。
| パラメーター | シナリオ | 目的 |
|---|---|---|
table.exec.async-table.max-concurrent-operations |
Async Correlate | 非同期テーブル関数の同時リクエスト数を制御します。 |
table.exec.async-table.timeout |
Async Correlate | 有効なリトライを含む、呼び出しの合計待機時間を設定します。 |
table.exec.async-table.retry-strategy |
Async Correlate | 関数呼び出しのリトライ戦略を設定します。 |
table.exec.async-lookup.buffer-capacity |
Async Lookup Join | 非同期ルックアップのバッファ容量を制御します。 |
table.exec.async-lookup.timeout |
Async Lookup Join | 非同期ディメンションテーブルルックアップのタイムアウトを設定します。適用可能な LOOKUP ヒントを使用して、単一の結合のタイムアウトを指定することもできます。 |
開発例
AsyncTableFunction を実装する場合、public で非 static の eval メソッドを宣言します。最初のパラメーターは、結果を送信するために使用される CompletableFuture<Collection<T>> であり、残りのパラメーターは SQL 呼び出しによって渡されるビジネスパラメーターです。
関数は、非同期リクエストを送信した後、すぐに戻り、完了コールバックで result.complete(rows) を呼び出して結果を送信するか、result.completeExceptionally(error) を呼び出して例外を送信します。1 回の呼び出しで得られる複数の行は、1 つのコレクションとして送信されます。結果がない場合は、空のコレクションを送信します。
package com.example.udf;
import org.apache.flink.table.functions.AsyncTableFunction;
import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.annotation.FunctionHint;
import org.apache.flink.types.Row;
import org.apache.flink.table.functions.FunctionContext;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
/**
* 非同期 HTTP インターフェイスを介して製品タグをクエリします。
*
* <p>インターフェイスの仕様: 200 は 1 行に 1 つのタグを含む UTF-8 テキストを返し、204 と 404 はタグがないことを示します。
* この例は、短いテキストを返す読み取り専用クエリに適しています。他のステータスコードは呼び出し失敗として扱われ、エンジンに渡されます。
*/
@FunctionHint(output = @DataTypeHint("ROW<tag STRING>"))
public class AsyncProductTags extends AsyncTableFunction<Row> {
private transient HttpClient client;
private transient URI serviceBaseUri;
@Override
public void open(FunctionContext context) {
String baseUrl = context.getJobParameter("product.service.base-url", null);
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalArgumentException("product.service.base-url を設定してください。");
}
serviceBaseUri = URI.create(baseUrl);
if (!("https".equalsIgnoreCase(serviceBaseUri.getScheme())
|| "http".equalsIgnoreCase(serviceBaseUri.getScheme()))
|| serviceBaseUri.getHost() == null
|| serviceBaseUri.getRawQuery() != null
|| serviceBaseUri.getRawFragment() != null
|| !baseUrl.endsWith("/")) {
throw new IllegalArgumentException("サービス URL は、スラッシュ (/) で終わる HTTP(S) ベース URL である必要があります。");
}
client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build();
}
@Override
public boolean isDeterministic() {
// 製品情報は変更される可能性があります。同じ ID が異なる時点で異なる結果を返す可能性があります。
return false;
}
public void eval(CompletableFuture<Collection<Row>> result, Long productId) {
if (productId == null) {
result.complete(Collections.emptyList());
return;
}
try {
HttpRequest request =
HttpRequest.newBuilder(
serviceBaseUri.resolve("products/" + productId + "/tags"))
.timeout(Duration.ofSeconds(5))
.header("Accept", "text/plain")
.GET()
.build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))
.thenApply(
response -> {
if (response.statusCode() == 200) {
return response.body().lines()
.map(String::strip)
.filter(tag -> !tag.isEmpty())
.map(tag -> Row.of(tag))
.collect(Collectors.toList());
}
if (response.statusCode() == 204 || response.statusCode() == 404) {
return Collections.<Row>emptyList();
}
throw new CompletionException(
new IOException(
"製品のクエリに失敗しました。HTTP ステータスコード: " + response.statusCode()));
})
.whenComplete(
(rows, error) -> {
if (error != null) {
result.completeExceptionally(error);
} else {
result.complete(rows);
}
});
} catch (RuntimeException e) {
result.completeExceptionally(e);
}
}
}
HttpClient.sendAsync はネットワークリクエストを非同期で実行するため、関数は独自のスレッドプールを作成する必要はありません。コールバックは短いテキストのみを解析し、結果を送信します。別のクライアントを使用する場合は、そのクライアントが提供する非同期インターフェイスを優先的に使用してください。JDK クライアントの動作については、「HttpClient API」をご参照ください。
製品タグは時間の経過とともに変更される可能性があるため、この例では isDeterministic() を false に設定しています。同じ入力に対して常に同じ結果を返す関数の場合は、デフォルトの動作を維持してください。
カスタムタイムアウト処理
デフォルトでは、eval 呼び出しがタイムアウトする前に CompletableFuture を完了せず、設定されたリトライが使い果たされた場合、フレームワークは java.util.concurrent.TimeoutException をスローし、ジョブを失敗させます。AsyncTableFunction サブクラスで eval メソッドに対応する timeout(...) メソッドを宣言して、フォールバック行を返すか、カスタムタイムアウト処理を実装できます。
リモート LLM クラスターを呼び出す場合、タイムアウトは比較的に発生しやすく、通常、タイムアウトによってジョブを直接失敗させるべきではありません。timeout メソッドを宣言すると、タイムアウト時にフォールバック結果が返され、ジョブは続行されます。
timeout メソッドは、以下のすべての条件を満たす必要があります。
-
メソッド宣言: メソッドは
publicインスタンスメソッドである必要があり、staticにすることはできず、timeoutという名前である必要があります。 -
evalと一致するシグネチャ: パラメーターリストは、対応するevalメソッドのものと厳密に同一である必要があります。最初のパラメーターはCompletableFutureで、そのジェネリック型はevalのものと厳密に同じである必要があります。また、残りのパラメーターの型と順序も厳密に同じである必要があります。timeoutはオーバーロードをサポートしています。フォールバックロジックを必要とする各evalのオーバーロードに対して、個別のtimeoutメソッドを宣言できます。 -
同期完了 (必須): このコールバックはオペレーターのメールボックススレッドで実行され、メソッドが戻る前に future を完了する必要があります。メソッドが戻った直後、フレームワークは
future.isDone()をチェックします。future が完了していない場合、フレームワークはIllegalStateExceptionで強制的に完了します。したがって:-
メソッド本体で別のリトライや 2 回目のクエリなど、別の非同期呼び出しを開始し、そのコールバックが future を完了することを期待しないでください。コールバックがトリガーされるまでに、フレームワークはすでにレコードを放棄しています。
-
同じ理由で、future を非同期で完了するために新しいスレッドを開始しないでください。
-
メソッド本体で行うことは 1 つだけです。定数行、NULL 行、空のコレクションなどの軽量なフォールバック結果を提供するか、カスタムビジネス例外で
completeExceptionally(...)を呼び出します。
-
-
安全な例外スロー: メソッド本体で同期的に例外をスローすることは可能です。フレームワークは例外をダウンストリームの
ResultFutureに渡します。これは、明示的にfuture.completeExceptionally(thrown)を呼び出すのと同等です。
これらの制約に加えて、フレームワークは次のシナリオでも安定した動作を保証します。
-
timeoutメソッドが宣言されていない: フレームワークのデフォルトのTimeoutExceptionが使用され、コード生成中に失敗は発生しません。 -
互換性のないシグネチャ:
timeoutメソッドが有効な可視性を持つものの、そのパラメーターリストが呼び出しサイトの実際の引数型と一致しない場合、フレームワークは計画時 (コード生成) にValidationExceptionで失敗します。エラーメッセージには、関数の完全修飾クラス名が含まれ、期待されるシグネチャと実際のシグネチャの両方が表示されます。 -
フォールバックとしての空のコレクション:
future.complete(Collections.emptyList())が呼び出されると、INNER ルックアップ結合では行が破棄され、LEFT OUTER ルックアップ結合では右テーブルのフィールドが NULL で埋められます。
次の例は、非同期テーブル関数に timeout コールバックを追加し、future を同期的に完了してフォールバック行を返す方法を示しています。
import org.apache.flink.table.functions.AsyncTableFunction;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
public static class BackgroundFunctionWithTimeout extends AsyncTableFunction<Long> {
public void eval(CompletableFuture<Collection<Long>> future, Integer waitMax) {
// ...前の開発例と同じロジック:非同期タスクをディスパッチし、
// コールバックスレッドで `future` を完了します。
}
// パラメーターリストは eval() と一致させる必要があります。同じ CompletableFuture ジェネリック型、
// および残りのパラメーター (Integer waitMax) の同じ型と順序。
// メソッド本体は `future` を同期的に完了する必要があります。別の非同期呼び出しを開始したり、
// 新しいスレッドを開始して非同期で完了したりしないでください。このコールバックがトリガーされると、
// オペレーターはすでにこのレコードを放棄しています。
public void timeout(CompletableFuture<Collection<Long>> future, Integer waitMax) {
future.complete(Collections.singletonList(-1L));
// デフォルトの TimeoutException の代わりにカスタムビジネス例外を使用することもできます。
// future.completeExceptionally(new RuntimeException(waitMax));
}
}
UDTF の登録
UDTF を登録する方法については、「ユーザー定義関数 (UDF) の管理」をご参照ください。
UDTF の使用
UDTF を登録した後、Flink SQL ジョブで使用します。
-
Flink SQL ジョブを開発します。詳細については、「ジョブ開発マップ」をご参照ください。次の SQL は、
|区切り文字を使用してASI_UDTF_Sourceのmessageフィールドを 2 つの列に分割し、結果をASI_UDTF_Sinkに挿入します。LATERAL TABLE構文は、各入力行を UDTF によって発行される行とクロス結合します。CREATE TEMPORARY TABLE ASI_UDTF_Source ( `message` VARCHAR ) WITH ( 'connector' = 'datagen' ); CREATE TEMPORARY TABLE ASI_UDTF_Sink ( name VARCHAR, place VARCHAR ) WITH ( 'connector' = 'blackhole' ); INSERT INTO ASI_UDTF_Sink SELECT name, place FROM ASI_UDTF_Source, LATERAL TABLE(ASI_UDTF(`message`)) AS T(name, place); -
[Operation Center] > [ジョブ運用保守] ページで、対象のジョブを見つけ、[Actions] 列の [Start] をクリックします。ジョブが開始されると、
ASI_UDTF_Sourceのmessageフィールドの文字列が|区切り文字で分割され、結果の列がASI_UDTF_Sinkに挿入されます。