8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Confluent で非構造化データを扱う:Claim Check パターンで作る RAG パイプライン

8
Last updated at Posted at 2026-09-14

はじめに:Kafka で非構造化データを扱う難しさ

Kafka はもともと構造化されたイベントデータを高速・大量に流すために設計されたメッセージバスです。ログ、トランザクション、センサーデータなど、スキーマが定義された比較的小さなメッセージを扱うのが得意です。

一方で、PDF・画像・動画といった非構造化データをパイプラインに乗せようとすると途端に話が複雑になります。Kafka のメッセージサイズには上限があり、大きなバイナリをそのままトピックに流すのは現実的ではありません。

この記事では、Confluent(Kafka)で非構造化データを扱う方法として Claim Check パターンを紹介し、PDF をアップロードしてベクトル DB に格納するまでのパイプラインの実装例を紹介します。


2つのアプローチ:Event Chunking と Claim Check

非構造化データを Kafka パイプラインに乗せる方法は、大きく2つに分類できます。

アプローチ① Event Chunking

大きなファイルを複数の小さなチャンクに分割して、それぞれを個別の Kafka メッセージとして送る方式です。

メリット

  • Kafka だけで完結する(外部ストレージ不要)
  • ストリーミング処理がしやすい

デメリット

  • Consumer 側でチャンクを再結合する実装が必要
  • チャンクの順序保証・欠損検知が複雑
  • Kafka ブローカーへの負荷が高い
  • RAG のようにファイル単位で意味的な処理をしたい場合に不向き

アプローチ② Claim Check

ファイル本体は外部ストレージ(S3 など)に保存し、Kafka には「このファイルがここにある」という参照情報(URI)だけを流す方式です。ホテルのクロークで荷物を預けてチケットだけ持ち歩くイメージです。

メリット

  • Kafka に流れるメッセージは軽量(URI のみ)
  • ファイル本体の処理は Consumer が S3 から取得して自由に行える
  • 大きなファイルでも Kafka の設定変更不要
  • S3 側で独立したライフサイクル管理(バージョニング・バックアップ)が可能

デメリット

  • 外部ストレージへの依存が増える
  • Consumer が S3 にアクセスできる必要がある

どちらを選ぶべきか

観点 Event Chunking Claim Check
Kafka のみで完結 ✅ ❌
大きなファイル(1MB超) ❌ Kafkaに高負荷 ✅
再結合ロジック ❌ 複雑 ✅ 不要
RAG など意味的処理 ❌ 全チャンク再結合後に処理が必要 ✅ S3 から完全なファイルをそのまま取得できる
本番運用のシンプルさ ❌ ✅

PDF・画像など意味的なまとまりを持つ非構造化データの場合は Claim Check 方式が実装しやすく、実用的かと思い、試しにClaimCheck方式でパイプラインを作ってみました。


実装:Claim Check 方式で作る RAG パイプライン

Claim Check 方式を採用し、PDF をアップロードすると自動的にテキスト抽出・Embedding が行われ、ベクトル DB に格納されるパイプラインを構築しました。なお、ベクトル DB に格納されたデータを使った RAG クエリ(ベクトル検索 + LLM による回答生成)の部分はこの記事の対象外です。

使用サービス

役割 サービス
メッセージバス Confluent Cloud (Kafka)
ストリーミング処理 + Embedding Confluent Cloud Flink SQL
ファイルストレージ(Claim) AWS S3
Embedding モデル AWS Bedrock (Titan Embed v2)
PDF テキスト抽出 Docling(IBM OSS)
ベクトル DB AstraDB (DataStax)
UI Streamlit

事前準備

パイプラインを動かすには以下のサービスのアカウントと認証情報が必要です。取得した値は .env ファイルに設定してください。

Confluent Cloud

Confluent Cloud のアカウントを作成し、以下を用意します。

必要なもの 取得場所
Kafka クラスター(Bootstrap サーバー) Confluent Cloud コンソール → Cluster → Cluster Settings
API キー / シークレット Confluent Cloud コンソール → API Keys
Schema Registry URL / API キー Confluent Cloud コンソール → API Keys

セットアップ手順は以下の記事が参考になります。

📄 Confluent Cloudで始めるApache Kafka & Flink入門(Zenn)
📄 Confluent Cloud API Key の Webコンソールでの作成方法(Qiita)

AWS S3 & Bedrock

AWS アカウントで以下を用意します。

必要なもの 備考
S3 バケット PDF の保存先。パブリックアクセスはブロックしたまま使用可
IAM ユーザーの Access Key / Secret S3 への読み書きと Bedrock の呼び出し権限が必要
Bedrock モデルアクセス amazon.titan-embed-text-v2:0 を有効化(AWS コンソール → Amazon Bedrock → Model access)

AstraDB(DataStax)

Embedding(1024 次元ベクトル)を格納するコレクションを事前に作成しておきます。作成手順は以下の記事を参照してください。

📄 Astra DB × Langflow × watsonx.ai:ノーコードでRAGアプリを構築してみた

コレクション作成時は以下の設定にしてください。

設定項目 値
ベクトル次元数 1024
類似度関数 Cosine

作成後、API エンドポイント・トークン・キースペース・コレクション名を .env に設定してください。


アーキテクチャ全体図

パイプラインは5つのフェーズで構成されています。① Upload でユーザーが PDF をアップロードすると、② Claim Check でファイル本体は S3 に保存され、Kafka には軽量な URI イベントだけが流れます。③ PDF 解析 フェーズでは Extractor が S3 から PDF を取得し、Docling でテキスト抽出・チャンク化します。④ Embedding では Confluent Cloud Flink の AI_EMBEDDING() 関数が SQL だけでチャンクをリアルタイムにベクトル化します。⑤ 書き込み で AstraDB にインデックスされ、ベクトル検索に使えるデータとして格納されます。

コンポーネント構成

このパイプラインは 4つの独立したプロセス で構成されています。それぞれ別々に起動し、Kafka トピックを介してデータを受け渡します。

rag-pipeline/
├── uploader/
│   └── app.py            # Streamlit UI。PDF アップロード時に S3 + Kafka への送信を実行
├── extractor/
│   └── app.py            # file-uris を常時 consume して Docling でチャンク化し続ける
├── flink/
│   └── astradb_writer.py # embeddings-sink を常時 consume して AstraDB に書き込み続ける
│                         # ※ Flink SQL 自体は Confluent Cloud の SQL workspaces で実行
└── shared/
    └── config.py         # 全プロセス共通の環境変数設定

各プロセスは同一マシンでも別マシンでも動かせます。プロセス間の通信は Confluent(Kafka) が仲介するため、物理的な場所は問いません。

各プロセスの動作の違いは以下の通りです。

プロセス 動作
Uploader ブラウザで操作するたびに S3・Kafka に送信
Extractor 起動後はトピックを poll し続け、メッセージが来たら処理
AstraDB Writer 起動後はトピックを poll し続け、メッセージが来たら処理
Flink SQL Confluent Cloud 上で RUNNING 状態を維持し続ける

各フェーズのコンポーネント解説(① Upload 〜 ⑤ ベクトル DB 書き込み)

1. S3 アップロード & Kafka への Claim 送信

uploader/app.py が担当します。

  • INPUT:ユーザーがブラウザからアップロードした PDF ファイル
  • OUTPUT:file-uris トピックに S3 URI イベントを publish

Streamlit の UI から PDF を受け取ったら、S3 へのアップロード → Kafka への通知 の2ステップで処理します。

ステップ①:クライアントの初期化

Confluent Cloud(Kafka)への接続には API キーと SASL/SSL 認証が必要です。confluent-kafka の Producer に設定辞書を渡して初期化します。S3 クライアントも同様に環境変数から認証情報を読み込みます。

uploader/app.py
from confluent_kafka import Producer
import boto3

# confluent-kafka の Producer を初期化
# Confluent Cloud は SASL_SSL + PLAIN 認証を使う
# bootstrap.servers はクラスターのエンドポイント(Confluent Cloud コンソールで確認できる)
producer = Producer({
    "bootstrap.servers": os.environ["CONFLUENT_BOOTSTRAP_SERVERS"],
    "security.protocol": "SASL_SSL",   # 通信を TLS で暗号化
    "sasl.mechanism":    "PLAIN",       # 認証方式
    "sasl.username":     os.environ["CONFLUENT_API_KEY"],     # API キー
    "sasl.password":     os.environ["CONFLUENT_API_SECRET"],  # API シークレット
})

# boto3 で S3 クライアントを初期化
s3 = boto3.client(
    "s3",
    aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
    aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
    region_name=os.environ.get("AWS_REGION", "ap-northeast-1"),
)
BUCKET = os.environ["S3_BUCKET_NAME"]

ステップ②:S3 へアップロードして URI を取得

保存先のパスは uploads/日付/ファイルハッシュ/ファイル名 の形式にしています。ファイルハッシュをパスに含めることで、同じ内容のファイルが重複しても衝突せずに保存できます。

uploader/app.py
def upload_to_s3(file_bytes: bytes, filename: str, file_hash: str) -> str:
    """S3 に PDF をアップロードし、s3://バケット名/パス 形式の URI を返す。"""
    key = f"uploads/{datetime.now(timezone.utc).strftime('%Y/%m/%d')}/{file_hash}/{filename}"
    s3.put_object(Bucket=BUCKET, Key=key, Body=file_bytes, ContentType="application/pdf")
    # 呼び出し元はこの URI だけ受け取り、Kafka に流す
    return f"s3://{BUCKET}/{key}"

ステップ③:S3 URI だけを Kafka に publish(Claim Check の肝)

ファイル本体は S3 に置いたまま、Kafka には s3_uri だけを含む軽量な JSON を送ります。producer.produce() はバッファに積むだけなので、producer.flush() で実際に送信を確定させます。

uploader/app.py
def publish_event(s3_uri: str, filename: str, file_hash: str, file_size: int):
    """Kafka の file-uris トピックに URI イベントを publish する。"""
    message = {
        "message_id":      str(uuid.uuid4()),  # イベントの一意な識別子
        "s3_uri":          s3_uri,              # ← ここに PDF の場所だけ入っている
        "filename":        filename,
        "file_hash":       file_hash,
        "file_size_bytes": file_size,
        "uploaded_at":     datetime.now(timezone.utc).isoformat(),
    }
    producer.produce(
        topic="file-uris",
        key=file_hash,   # 同じファイルのイベントは同じパーティションに送られる
        value=json.dumps(message, ensure_ascii=False).encode("utf-8"),
    )
    producer.flush(timeout=10)  # バッファをフラッシュして送信を確定

Kafka に publish される file-uris メッセージはこれだけです。PDF の実体は一切乗りません。

{
  "message_id": "550e8400-e29b-41d4-a716-446655440000",
  "s3_uri": "s3://rag-pipeline-bucket/uploads/2026/08/15/abc123/report.pdf",
  "filename": "report.pdf",
  "file_hash": "abc123...",
  "file_size_bytes": 2048576,
  "uploaded_at": "2026-08-15T09:00:00Z"
}

2. Extractor:S3 から取得して Docling で処理

extractor/app.py が担当します。

  • INPUT:file-uris トピックの S3 URI イベント(セクション1の Uploader が publish)
  • OUTPUT:text-chunks トピックにテキストチャンクを publish

ステップ①:Consumer としてメッセージを待ち受ける

Extractor は file-uris トピックを購読する 常時起動プロセスです。Consumer の group.id は複数インスタンスを並列起動したときにメッセージの重複処理を防ぐための識別子です。auto.offset.reset: "earliest" は「まだ読んでいないメッセージがあれば先頭から読む」という設定で、プロセス再起動時に未処理分を取りこぼしません。

extractor/app.py
from confluent_kafka import Consumer, Producer

consumer = Consumer({
    "bootstrap.servers": os.environ["CONFLUENT_BOOTSTRAP_SERVERS"],
    "security.protocol": "SASL_SSL",
    "sasl.mechanism":    "PLAIN",
    "sasl.username":     os.environ["CONFLUENT_API_KEY"],
    "sasl.password":     os.environ["CONFLUENT_API_SECRET"],
    "group.id":          "text-extractor-group",  # Consumer グループ名。同じ名前のプロセスは1件を1回だけ処理する
    "auto.offset.reset": "earliest",              # 未読メッセージを先頭から処理する
})
consumer.subscribe(["file-uris"])  # 購読するトピックを指定

# poll() でトピックを 1 秒間隔でポーリングし続ける
while True:
    msg = consumer.poll(timeout=1.0)  # メッセージがなければ 1 秒待って None を返す
    if msg is None or msg.error():
        continue  # メッセージなし or エラーはスキップ

    # メッセージ本文(bytes)を JSON にデコード
    file_event = json.loads(msg.value().decode("utf-8"))

    # ① S3 から PDF をダウンロード(Claim の引き換え)
    pdf_bytes = download_pdf_from_s3(file_event["s3_uri"])

    # ② Docling でテキスト抽出・チャンク化 → text-chunks に publish
    chunks = extract_and_chunk(pdf_bytes, file_event["filename"])
    publish_chunks(producer, file_event, chunks)

ステップ②:Docling で PDF を解析・チャンク化

Docling は IBM がオープンソースで開発している PDF 解析ライブラリです。DocumentConverter が PDF のレイアウト(見出し・本文・図・表)を解析し、HierarchicalChunker がセクション構造を保ったままチャンクに分割します。

単純な固定長分割(例:500文字ごとに切る)と違い、HierarchicalChunker は「章の途中で切れる」ような不自然な分割を避けます。また各チャンクには 見出しのパス(例: 1章 > 1.2節)と ページ番号 がメタデータとして付くため、RAG の回答に出典情報を添えることができます。

extractor/app.py
from docling.document_converter import DocumentConverter
from docling.chunking import HierarchicalChunker

# 起動時に1回だけ初期化(毎回生成するとモデルロードのオーバーヘッドがかかる)
_converter = DocumentConverter()
_chunker   = HierarchicalChunker()

def extract_and_chunk(pdf_bytes: bytes, filename: str) -> list[dict]:
    # Docling は URL か Path を受け取る API なので、bytes を一時ファイルに書き出す
    with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
        tmp.write(pdf_bytes)
        tmp_path = Path(tmp.name)

    result = _converter.convert(tmp_path)  # PDF を解析して DoclingDocument を生成

    chunks = []
    for chunk in _chunker.chunk(result.document):  # セクション単位でチャンク化
        text = chunk.text.strip()
        if not text:
            continue  # 空チャンク(図のキャプションのみなど)はスキップ

        # chunk.meta.headings は ["1章タイトル", "1.2節タイトル"] のような階層リスト
        headings: list[str] = chunk.meta.headings or []

        # このチャンクが含まれるページ番号を重複なく収集
        page_numbers = sorted({
            ref.page_no
            for item in chunk.meta.doc_items
            for ref in item.prov
            if ref.page_no is not None
        })

        chunks.append({
            "text":            text,
            "section_heading": " > ".join(headings) if headings else None,  # 例: "1章 > 1.2節"
            "page_numbers":    page_numbers,
        })
    return chunks

チャンクは1件ずつ text-chunks トピックに publish します。各メッセージには元ファイルの file_hash・filename・s3_uri も含めており、後段で出典を追跡できるようにしています。

3. Flink SQL で Embedding をストリーミング生成

Confluent Cloud 上で動く Flink SQL ジョブが担当します。

  • INPUT:text-chunks トピックのチャンク(セクション2の Extractor が publish)
  • OUTPUT:embeddings-sink トピックにベクトル付きチャンクを publish

SQL workspaces で一度実行すると RUNNING 状態になり、text-chunks トピックにメッセージが届くたびに自動で Embedding を生成して embeddings-sink に書き出し続けます。

通常、テキストを Embedding(ベクトル)に変換するには Python コードで LLM API を呼び出し、結果を DB に書き込む処理を自前で実装する必要がありますが、Confluent Cloud Flink SQL の AI_EMBEDDING() 関数を使うことで、この処理を SQL だけで書けます。Kafka トピックに新しいメッセージが届くたびに Flink が自動で Embedding API を呼び出し、ベクトルを付与した結果を別のトピックに書き出してくれます。追加のアプリケーションコードは不要です。

ステップ①:Bedrock への接続とモデルを Flink に登録(初回のみ)

AI_EMBEDDING() を使うには、事前にどの AI サービス・モデルを使うかを Flink に登録しておく必要があります。Confluent Cloud のコンソールで左メニューの SQL workspaces を開き、ワークスペースを作成して以下の SQL を実行してください(初回のみ)。

-- Flink から AWS Bedrock に接続するための認証情報を登録する
CREATE CONNECTION bedrock_connection
  TYPE AWS_BEDROCK
  REGION 'us-east-1'
  AWS_ACCESS_KEY_ID '...'
  AWS_SECRET_ACCESS_KEY '...';

-- AI_EMBEDDING() で使うモデルを登録する
-- INPUT: チャンクのテキスト(STRING)を受け取り
-- OUTPUT: 1024 次元の浮動小数点数配列(= ベクトル)を返す
CREATE MODEL chunk_embedding
  INPUT(text STRING)
  OUTPUT(embedding ARRAY<FLOAT>)
  WITH (
    'TASK'               = 'EMBEDDING',
    'PROVIDER'           = 'BEDROCK',
    'BEDROCK.CONNECTION' = 'bedrock_connection',
    'BEDROCK.MODEL_ID'   = 'amazon.titan-embed-text-v2:0'  -- Bedrock の Titan Embed v2 を使用
  );

ステップ②:ストリーミング Embedding ジョブを起動

同じく SQL workspaces のエディタで以下の SQL を実行します。一度実行すると Flink ジョブが RUNNING 状態になり、text-chunks トピックに新しいメッセージが届くたびに自動で処理が走ります。

INSERT INTO `embeddings-sink`
SELECT
    CAST(tc.chunk_id AS BYTES)              AS `key`,       -- Kafka メッセージのキー
    tc.chunk_id,
    CAST(tc.chunk_index AS BIGINT)          AS chunk_index,
    CAST(CURRENT_TIMESTAMP AS STRING)       AS embedded_at, -- Embedding を付与した時刻
    CAST(emb.embedding AS ARRAY<DOUBLE>)    AS embedding,   -- ← AI_EMBEDDING() が生成した 1024 次元ベクトル
    tc.file_hash,
    tc.filename,
    CAST(tc.page_numbers AS ARRAY<BIGINT>)  AS page_numbers,
    tc.s3_uri,
    tc.section_heading,
    tc.`text`,
    CAST(tc.total_chunks AS BIGINT)         AS total_chunks
FROM `text-chunks` AS tc,
     -- AI_EMBEDDING() はテーブル関数なので LATERAL TABLE で結合する(SELECT 内に直接書くとエラー)
     LATERAL TABLE(AI_EMBEDDING('chunk_embedding', tc.`text`)) AS emb;

ハマりポイント:AI_EMBEDDING はスカラー関数(値を1つ返す関数)ではなくテーブル関数(行を返す関数)です。SELECT emb(...) のように SELECT 句に直接書くとエラーになります。必ず FROM 句で LATERAL TABLE(AI_EMBEDDING(...)) AS emb の形で結合してください。

4. AstraDB Writer:ベクトル DB への書き込み

flink/astradb_writer.py が担当します。

  • INPUT:embeddings-sink トピックのベクトル付きチャンク(セクション3の Flink SQL が publish)
  • OUTPUT:AstraDB コレクションへの insertOne

embeddings-sink トピックを常時 poll し続け、Flink が書き出したメッセージが届いたタイミングで AstraDB に書き込みます。

ステップ①:Confluent ワイヤフォーマットのデシリアライズ

Confluent Cloud Flink が Schema Registry 経由でトピックに書き込んだメッセージは、先頭 5 バイトにスキーマ ID が埋め込まれた Confluent ワイヤフォーマットになっています。そのままでは JSON として読めないため、JSONDeserializer を使って自動デコードします。

flink/astradb_writer.py
from confluent_kafka.schema_registry import SchemaRegistryClient
from confluent_kafka.schema_registry.json_schema import JSONDeserializer
from confluent_kafka.serialization import SerializationContext, MessageField

# Schema Registry への接続(Confluent Cloud コンソールで確認できる)
sr_client = SchemaRegistryClient({
    "url": os.environ["CONFLUENT_SR_URL"],
    "basic.auth.user.info": f"{os.environ['CONFLUENT_SR_API_KEY']}:{os.environ['CONFLUENT_SR_API_SECRET']}",
})
deserializer = JSONDeserializer(None, schema_registry_client=sr_client)

ステップ②:embeddings-sink を consume して AstraDB に書き込む

AstraDB Writer も Extractor と同様に Consumer として常時起動します。embeddings-sink トピックからメッセージを受信したら、JSONDeserializer でデコードして AstraDB の Data API に insertOne で1件ずつ書き込みます。

flink/astradb_writer.py
consumer = Consumer({
    **kafka_conf,
    "group.id":          "astradb-writer-group",
    "auto.offset.reset": "latest",  # Flink が生成したものだけ処理する。再起動時に全件再処理しない
})
consumer.subscribe(["embeddings-sink"])

while True:
    msg = consumer.poll(timeout=1.0)
    if msg is None or msg.error():
        continue

    # Confluent ワイヤフォーマット → dict へデシリアライズ
    document = deserializer(
        msg.value(),
        SerializationContext("embeddings-sink", MessageField.VALUE),
    )
    insert_to_astradb(document)

ステップ③:AstraDB Data API に insertOne で書き込む

AstraDB への書き込みは REST API(Data API v2)を使います。ベクトルは $vector フィールドに渡すと、AstraDB がコレクション定義のインデックスと紐付けて保存します。

flink/astradb_writer.py
def insert_to_astradb(document: dict) -> None:
    """AstraDB Data API にチャンク1件を書き込む。"""
    payload = {
        "insertOne": {
            "document": {
                "_id":          document["chunk_id"],   # ドキュメントの一意な ID
                "file_hash":    document["file_hash"],
                "filename":     document["filename"],
                "s3_uri":       document["s3_uri"],
                "chunk_index":  document["chunk_index"],
                "total_chunks": document["total_chunks"],
                "text":         document["text"],       # 元テキスト(回答生成に使う)
                "$vector":      document["embedding"],  # ← 1024 次元ベクトル。ここに入れると AstraDB がインデックス化する
                "embedded_at":  document["embedded_at"],
            }
        }
    }
    response = requests.post(
        f"{ASTRADB_API_ENDPOINT}/{KEYSPACE}/{COLLECTION}",
        headers={"Token": ASTRADB_TOKEN, "Content-Type": "application/json"},
        json=payload,
        timeout=15,
    )
    response.raise_for_status()

これで、PDF をアップロードしてから AstraDB にベクトルが保存されるまでのパイプラインが完成します。


Tips:Bob(AI コーディングアシスタント)× Confluent MCP サーバーで開発効率アップ

今回の開発で特に役立ったのが、IBM Bob(AI コーディングアシスタント)に Confluent の MCP サーバーを接続して自然言語で Confluent Cloud を操作するという手法です。

MCP(Model Context Protocol)は AI アシスタントが外部サービスのツールを直接呼び出せる仕組みです。Confluent が提供する MCP サーバーを Bob に接続することで、チャット画面から自然言語で Confluent Cloud を操作できます。

実際にこんな感じで使いました:

Confluent Cloud のインフラ操作

「text-chunks トピックのメッセージ件数を確認して」
→ Bob が get-partition-offsets ツールを呼び出して結果を返してくれる

「embedding-job-005 の Flink ステートメントが RUNNING かどうか確認して」
→ Bob が list-flink-statements を呼び出してステータスを教えてくれる

「embeddings-sink トピックの retention を 0 にしてパージして、その後 604800000ms に戻して」
→ Bob が alter-topic-config を2回呼び出してトピックをクリーンアップしてくれる

Flink SQL の生成

Flink SQL は独自の構文(LATERAL TABLE・CAST の書き方・型制約など)があり、ドキュメントを読みながら書くのは手間がかかります。Bob に「こういう処理をする Flink SQL を書いて」と伝えると、トピックのスキーマを MCP で確認しながら SQL を生成してくれます。

「text-chunks トピックのスキーマを確認して、embeddings-sink に AI_EMBEDDING で
  ベクトルを付与して書き込む Flink SQL を書いて」
→ Bob が describe-flink-table で text-chunks のカラム一覧を取得し、
  LATERAL TABLE(AI_EMBEDDING(...)) を使った INSERT INTO ... SELECT 文を生成してくれる

型のキャストや LATERAL TABLE の書き方など、ハマりやすい部分も Bob が自動で考慮してくれるため、SQL を一から書くより大幅に時間が短縮できました。

Confluent Cloud コンソールを開かずに、コーディング作業の合間にそのまま自然言語でインフラ操作・SQL 生成ができるのは非常に快適でした。特にデバッグ中の「トピックの中身を確認 → コード修正 → 再確認」というサイクルが格段に速くなります。

Bob への Confluent MCP サーバーの設定方法は以下の記事を参照してください。

📄 Bob × Confluent MCP サーバーの設定方法


実装する上での注意ポイント

# 問題 解決策
1 Docling headings の型エラー 最新版では list[str]。h.text ではなくそのまま join() する
2 Flink AI_EMBEDDING エラー テーブル関数なので LATERAL TABLE(...) で呼ぶ
3 Flink メッセージが読めない Confluent ワイヤフォーマット → JSONDeserializer を使う
4 AstraDB $similarity エラー includeSimilarity: true は options に指定。projection には含めない
5 Flink の型不一致エラー CAST(emb.embedding AS ARRAY<DOUBLE>) など明示的にキャスト

まとめ

Kafka で非構造化データを扱う際は、Claim Check パターンが実装の複雑さを抑えながら実用的なパイプラインを構築できる有効な選択肢です。

  • Kafka には軽量な参照情報(URI)だけを流す
  • ファイル本体は S3 などの外部ストレージで管理する
  • Consumer が必要なときに S3 から取得して処理する

今回のパイプラインでは、PDF アップロードから AstraDB へのベクトル格納まで以下の4コンポーネントで実現しています。

コンポーネント 役割
Uploader (Streamlit) S3 へのアップロードと Kafka への URI 通知
Extractor (Python) S3 から PDF を取得し Docling でチャンク化
Flink SQL AI_EMBEDDING() でストリーミング Embedding 生成
AstraDB Writer (Python) Embedding をベクトル DB に格納

特に強力だったのが Confluent Cloud Flink SQL の AI_EMBEDDING() 関数です。追加のアプリケーションコードなしに、ストリーミングデータに対してリアルタイムで Embedding API を適用できます。非構造化データを AI 活用パイプラインに乗せる際のシンプルで強力な選択肢です。
RAGなどにおいてリアルタイムにAIに与える情報を更新するニーズがある場合にはぜひこのようなパイプラインを実装してみてください。


参考リンク

8
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?