1
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?

Vertex AI Search + Flutter Webで簡単にナレッジ検索RAGアプリを作成してみた

Last updated at Posted at 2025-04-03

はじめに

Vertex AI Search と Flutterを使って、独自データを使ったナレッジ検索などができる簡易的なRAGアプリを作成してみました。

本記事では、Vertex AI Searchで簡易的なRAGを作成しアプリに組み込む手順を記載します。

対象読者

  • Flutterでアプリ開発をしている方
  • Vertex AI Searchに興味がある方

アプリの概要

このアプリは、独自のドキュメントから学習したデータを基に、簡易的なQ&Aやナレッジの検索をするアプリを簡易的に試したものになります。

  • ナレッジ検索:Vertex AI Agent BuilderのAPIを呼び出し、検索結果を取得
  • 結果表示:APIから取得した結果を画面に表示

実装

使用技術

  • Vertex AI Agent Builder
  • Flutter Web

Agent BuilderでRAGアプリ作成

GCP上の Vertex AI Agent Builder のコンソールからアプリを作成します。

「+アプリを作成する」を選択します。

スクリーンショット 2025-03-28 12.07.24.png

今回はGoogle Cloud StorageにアップしたドキュメントをRAGとして扱えるようにするので「ウェブサイト検索」を選択します。

スクリーンショット 2025-03-28 12.08.43.png

アプリの構成はそのまま「アプリ名」と「会社名または組織名」を入力します。

スクリーンショット 2025-03-28 12.09.12.png

データストアを作成します。
今回はこれがGoogle Cloud Storageに当たります。

スクリーンショット 2025-03-28 12.09.56.png

スクリーンショット 2025-03-28 12.10.12.png

今回は「非構造化ドキュメント(PDF、 HTML、TXTなど)」で試します。

スクリーンショット 2025-03-28 12.10.31.png

データストア名を入力したらアプリとデータストアの完成です。

スクリーンショット 2025-03-28 12.11.36.png

スクリーンショット 2025-03-28 12.12.09.png

API設定の確認

作成したアプリを選択し、「統合」の中のAPIタブで確認できます。
ウィジェットとして検索窓を追加する方法や、Python、Java、Node.js などの言語での接続方法も記載されていますが、今回はAPIでの接続をベースにFlutter Webからの接続を試していきます。

スクリーンショット 2025-04-02 9.24.06.png

Flutter Webの設定

今回はRAGの動作検証のため、Flutter側の実装はmain.dartのみで行ったサンプルを記載します。
Agent Builderで作成したAPIを実行して画面に検索結果を表示します。
GCPにて事前にサービスアカウントの作成が必要になります。

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:googleapis_auth/auth_io.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'RAG Search',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // 検索結果を格納する変数
  String _response = '';
  // 検索テキスト入力欄のコントローラー
  final TextEditingController _queryController = TextEditingController();
  // 検索中かどうかのローディング用フラグ
  bool isLoading = false;

  @override
  void initState() => super.initState();

  /// データ取得処理
  Future<void> _fetchData() async {
    setState(() => isLoading = true);
    try {
      String accessToken = await _getAccessToken();

      final dio = Dio();
      dio.options.headers['Content-Type'] = 'application/json';
      dio.options.headers['Authorization'] = 'Bearer $accessToken';

      // Agent Builderで作成したアプリの「統合 > APIタブ」を参照してエンドポイントを設定
      final response = await dio.post(
        'https://discoveryengine.googleapis.com/v1alpha/projects/xxxxx/default_search:search',
        // ※dataは一例です
        data: {
          'query': _queryController.text,
          'pageSize': 10,
          'queryExpansionSpec': {'condition': 'AUTO'},
          'spellCorrectionSpec': {'mode': 'AUTO'},
          'contentSearchSpec': {
            'summarySpec': {
              'summaryResultCount': 5,
              'modelSpec': {'version': 'preview'},
              'ignoreAdversarialQuery': true,
              'includeCitations': true,
            },
            'snippetSpec': {'returnSnippet': true},
            'extractiveContentSpec': {'maxExtractiveAnswerCount': 1},
          },
        },
      );

      // 結果を表示用の変数に上書きする
      setState(() {
        final responseData = response.data;
        final extractiveAnswersContent = responseData['summary']['summaryText'];
        _response = extractiveAnswersContent;
      });
    } catch (e) {
      setState(() => _response = 'Error: $e');
    } finally {
      setState(() => isLoading = false);
    }
  }

  /// アクセストークン取得処理
  Future<String> _getAccessToken() async {
    // assetsフォルダなどに配置したサービスアカウントの認証情報を取得
    String jsonString = await rootBundle.loadString('assets/xxxxx.json');
    Map<String, dynamic> jsonData = json.decode(jsonString);
    // サービスアカウントの認証情報を設定
    final credentials = ServiceAccountCredentials(
      jsonData['client_email'],
      ClientId(jsonData['client_id']),
      jsonData['private_key']
    );

    // アクセストークンの取得
    final client = await clientViaServiceAccount(credentials, [
      'https://www.googleapis.com/auth/cloud-platform',
    ]);
    final accessToken = (client.credentials).accessToken.data;
    return accessToken;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: PreferredSize(
        preferredSize: const Size.fromHeight(80.0),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Container(height: 20.0),
              AppBar(
                title: const Text('RAG Search'),
              ),
            ],
          ),
        ),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              ConstrainedBox(
                constraints: const BoxConstraints(maxWidth: 800),
                // 検索入力欄
                child: Row(
                  children: [
                    Expanded(
                      child: TextField(
                        controller: _queryController,
                        decoration: const InputDecoration(
                          labelText: '検索入力欄',
                          border: UnderlineInputBorder(),
                        ),
                      ),
                    ),
                    const SizedBox(width: 16),
                    SizedBox(
                      height: 56,
                      child: ElevatedButton(
                        onPressed: isLoading ? null : _fetchData,
                        style: ElevatedButton.styleFrom(
                          padding: const EdgeInsets.symmetric(horizontal: 32.0),
                          textStyle: const TextStyle(fontSize: 18),
                        ),
                        child: isLoading ? const CircularProgressIndicator(strokeWidth: 5) : const Text('検索'),
                      ),
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 16),
              // 検索結果表示
              if (_response.isNotEmpty)
                ConstrainedBox(
                  constraints: const BoxConstraints(maxWidth: 800),
                  child: Card(
                    elevation: 4,
                    margin: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
                    child: Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Text(
                        _response,
                        style: const TextStyle(fontSize: 16.0),
                      ),
                    ),
                  ),
                ),
            ],
          ),
        ),
      ),
    );
  }
}

検索欄と検索結果表示飲みのシンプルな構成ですがAgent Builderからの検索結果を表示することの確認ができました。

スクリーンショット 2025-04-03 10.04.14.png

※黒塗り部分は検索結果部分になります。

参考資料

Vertex AI Agent Builder ドキュメント

おわりに

Vertex AI Agent Builderを使用することで、簡単に独自のRAGを作成し、例えば社内向けデータを使ったナレッジ検索用のRAGなどを簡単に構築しアプリに統合する方法の検証をすることができました。

今回の検証ではAgent Builderの簡単な扱い方で試しましたが、もっと複雑なケースなどに合わせて柔軟に実装することができそうでした。

Cloud Storageに追加するだけでRAG精度の調整など検証できていない部分があるので試していこうと思います。

1
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
1
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?