2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

データベースに話しかけて欲しい情報を手に入れる - OCI ADB Select AIを使用した自然言語検索

Posted at

概要

従来、データベースから情報を取得するにはSQLの知識が不可欠でした。しかし、AI技術の進化により、エンドユーザーが自然言語で“話しかける”だけで、必要な情報を取得できる時代が到来しています。
本記事では、Oracle Cloud Infrastructure(OCI)の Autonomous Database Service に搭載された Select AI 機能を活用し、より実践的に自然言語によるデータ検索を可能にするユーザーインターフェースを構築する方法をご紹介します。 技術的な実装のポイントや、ユーザー体験を高める工夫についても解説します。

構成

構成は以下のとおりとなります

  • OCI Autonomous Database(ADB)
    Oracle Databaseサービス、Select AI機能により生成AIと連携して自然言語からSELECT分を作成し結果を回答します
  • OCI Generative AI
    生成AIサービス
  • Windows
    チャットボット風のUIを提供する簡易Webサーバーを構築します
    OCI上にVMを立てるか自前のWindowsを利用可能です

①ブラウザを通じて質問を投げます(例:2024年の飲酒の支出金額が一番多い都市は?)
②SELECT AI SHOWSQLでADBにSQL作成を実行します
③ADBから生成AIにSQL作成指示します
④生成AIからSQL回答を受け取ります
⑤ADBからSQLを回答します
⑥得られたSQLからSELECT分を実行します
⑦SELECT結果を受け取ります
⑧SELECT結果とSELECT文を回答します

手順

作成手順について解説します

前提

OCIの生成AIサービスは2025年11月現在日本では大阪リージョンで提供されており東京リージョンでは提供されていません。そのためお試しのFree Tierを行う場合はホームリージョンを大阪に設定する必要があります。ホームリージョンはどこに設定してもあまり問題ではありませんが変更できないため慎重に進める必要があります。

OCI契約済の場合は大阪リージョンが利用可能になるよう設定してください

ADB作成

ADBは以下を参考に作成してください

Oracle Cloud Infrastructure チュートリアル / Oracle AI Database編 - Autonomous AI Database (ADB)を使ってみよう / 111: SELECT AIを試してみよう
111: SELECT AIを試してみよう
https://oracle-japan.github.io/ocitutorials/adb/adb111-select-ai/

データ入れ替え(オプション)

馴染みやすいデータで検証を行うため、政府統計調査から外食に関する家計調査を使用します

  • ADB作成時の手順"111: SELECT AIを試してみよう>>2-2. データセットの準備"を参考に以下のデータをADBにロードします

外食に関する家計調査で各外食の都市毎の支出金額
https://github.com/tktk2712/selectaidemo_feh/blob/main/FEH001.xlsx

[データについて]
家計調査(二人以上の世帯)品目別都道府県庁所在市及び政令指定都市のデータをe-Statより取得し データを加工してアップロードしています

出典:政府統計の総合窓口(e-Stat)(https://www.e-stat.go.jp/) 家計調査 / 家計収支編 二人以上の世帯

  • ビューを作成
CREATE OR REPLACE VIEW FEH001_VIEW AS
SELECT 
    KEY,
    YEAR,
    CODE,
    CITY_NAME,
    SOBA_UDON AS C001,
    RAMEN AS C002,
    NOODLE AS C003,
    SUSHI AS C004,
    WASHOKU AS C005,
    CHINESE AS C006,
    WESTERN AS C007,
    YAKINIKU AS C008,
    HAMBURGER AS C009,
    OTHER_EATOUT AS C010,
    CAFE AS C011,
    ALCOHOL AS C012,
    NULL AS OTHER
FROM 
    FEH001;

ヒント
意味をもつカラム名がAI推測に影響するためこの例ではカラム名を変更しています

  • コメント設定
COMMENT ON TABLE SELECT_AI_USER.FEH001_VIEW IS '外食に関する家計調査で各外食の都市毎の支出金額。使用できる単語(そば・うどん,中華そば,他麺類,寿司,和食,中華料理,洋食,焼肉,ハンバーガー,他外食,喫茶,飲酒)。それ以外の単語は不正な質問。';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.YEAR IS '西暦年';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.CODE IS '地域区分';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.CITY_NAME IS '都市名';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.C001 IS 'このカラムは質問の単語が"日本そば・うどん"に完全一致する場合のみ使用可能。それ以外は不正な質問です。単位は円。';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.C002 IS 'このカラムは質問の単語が"中華そば"に完全一致する場合のみ使用可能。それ以外は不正な質問です。単位は円。';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.C003 IS 'このカラムは質問の単語が"他麺類"に完全一致する場合のみ使用可能。それ以外は不正な質問です。単位は円。';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.C004 IS 'このカラムは質問の単語が"寿司"に完全一致する場合のみ使用可能。それ以外は不正な質問です。単位は円。';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.C005 IS 'このカラムは質問の単語が"和食"に完全一致する場合のみ使用可能。それ以外は不正な質問です。単位は円。';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.C006 IS 'このカラムは質問の単語が"中華料理"に完全一致する場合のみ使用可能。それ以外は不正な質問です。単位は円。';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.C007 IS 'このカラムは質問の単語が"洋食"に完全一致する場合のみ使用可能。それ以外は不正な質問です。単位は円。';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.C008 IS 'このカラムは質問の単語が"焼肉"に完全一致する場合のみ使用可能。それ以外は不正な質問です。単位は円。';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.C009 IS 'このカラムは質問の単語が"ハンバーガー"に完全一致する場合のみ使用可能。それ以外は不正な質問です。単位は円。';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.C010 IS 'このカラムは質問の単語が"他外食"に完全一致する場合のみ使用可能。それ以外は不正な質問です。単位は円。';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.C011 IS 'このカラムは質問の単語が"喫茶"に完全一致する場合のみ使用可能。それ以外は不正な質問です。単位は円。';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.C012 IS 'このカラムは質問の単語が"飲酒"に完全一致する場合のみ使用可能。それ以外は不正な質問です。単位は円。';
COMMENT ON COLUMN SELECT_AI_USER.FEH001_VIEW.OTHER IS 'このカラムは質問の単語が食べ物や物に関する場合に使用可能。単位は円。';

ヒント
似た言葉で判定することを防ぐために厳格なルールを設定しています(例えばハンバーグを検索するとハンバーガーが選択されることを回避)

AIモデル変更(オプション)

生成AIのAIモデルについて、別のAIモデルを利用するためAIプロファイルを追加作成します

BEGIN
    DBMS_CLOUD_AI.CREATE_PROFILE(
        'GENAI_LLAMA33_70B_COMMENTS_FEH001',
        '{
            "provider": "oci",
            "credential_name": "OCI_GENAI_CRED",
            "model":"meta.llama-3.3-70b-instruct",
            "oci_apiformat":"GENERIC",
            "region": "ap-osaka-1",
            "comments":"true", 
            "object_list": [
                    {"owner": "select_ai_user", "name": "FEH001_VIEW"}
            ]
        }'
    );
END;
/

Windows作成

OCI上にVMを立てるか自前のWindowsを準備します

Web Server作成

Oracle環境準備

  • OCIコンソールからOracle AI Database>>Autonomous AI Database>>該当のADBを選択
  • [データベース接続]を押下して[ウォレットのダウンロード]を押下
  • パスワードを入力して[ダウンロード]を押下
  • ダウンロードしたZIPファイルを任意のフォルダに展開(例 c:\oracle\wallet)
  • sqlnet.oraを編集してDIRECTORYにフォルダ名を指定(例 DIRECTORY="c:\oracle\wallet")

Python環境準備

  • Pythonのインストール
  • pipを使用して, flask, ociおよびoracledbをインストール
C:\path\to>pip install flask
C:\path\to>pip install oci
C:\path\to>pip install oracledb
または
C:\path\to>python -m pip install flask
C:\path\to>python -m pip install oci
C:\path\to>python -m pip install oracledb

ソースファイル配置

project_root/
├── selectai.py                 # アプリ本体
├── templates/
│   └── index.html              # HTMLテンプレート
└── static/
    ├── css/
    │   └── style.css           # CSSファイル
    └── images/
         └── selectai_title.jpg # バナーイメージ
  • selectai.py
    ソースコードをコピーして、以下の値を環境に合わせて設定してください
    ・BASIC認証のusernameおよびpassword
    ・connection=oracledb.connectの
     password,
     dsnはデータベース名+"_low",
     wallet_password
from flask import Flask, request, Response, jsonify, render_template
import oci
import oracledb

app = Flask(__name__)

# 認証
def check_auth(username, password):
    return username == "<<login name>>" and password == "<<login password>>"

def authenticate():
    return Response(
        "認証が必要です", 401,
        {"WWW-Authenticate": 'Basic realm="Login Required"'}
    )

@app.route('/')
def index():
    auth = request.authorization
    if not auth or not check_auth(auth.username, auth.password):
        return authenticate()
    return render_template('index.html')

@app.route('/chat', methods=['POST'])
def chat():
    user_input = request.json.get('message')

    try:
        connection=oracledb.connect(
            config_dir=r"c:\oracle\wallet",
            user="select_ai_user",
            password="<<password>>",
            dsn="<<database name>>_low",
            wallet_location=r"c:\oracle\wallet",
            wallet_password="<<password>>"
        )

    except (oracledb.DatabaseError, SystemExit) as e:
        error_message = str(e)
        return jsonify({
            'error': f"データベースへの接続に失敗しました。エラー内容: {error_message}"
        }), 500

    cur = connection.cursor()

    try:
        # プロファイル設定
        out_val = cur.var(str)
        cur.callproc('DBMS_CLOUD_AI.SET_PROFILE',["GENAI_LLAMA33_70B_COMMENTS_FEH001",out_val])

        # 1回目: AIにSQLを生成させる
        sql_input = "SELECT AI SHOWSQL "+user_input+";"
        print(sql_input)

        cur.execute(sql_input)
        rows = cur.fetchall()
        for r in rows:
            print(r)

        # 1回目の結果からSQL文を取り出す
        generated_sql = None
        if rows:
            generated_sql = rows[0][0]
            print("生成されたSQL:", generated_sql)

        # 2回目: 生成されたSQLを実行
        columns = []
        second_rows = []

        if generated_sql:
            cur.execute(generated_sql)
            # カラム名を取得
            columns = [col[0] for col in cur.description]
            second_rows = cur.fetchmany(120)
            # 件数チェック
            if len(second_rows) > 100:
                return jsonify({
                    'error': '申し訳ございません。結果が100件を超えているか、エラーが発生しました。 条件を変更して再度実行してください。',
                    # エラー時もSQLを返す
                    'first_sql': generated_sql
                }), 400

    except oracledb.DatabaseError as e:
        # Oracleエラーをキャッチして返す
        error_obj = e.args[0]
        return jsonify({
            'error': f"申し訳ございません。エラーが発生しました。条件を変更して再度実行してください 。エラー内容: {error_obj.message}",
            # エラー時もSQLを返す
            'first_sql': generated_sql
        }), 400

    finally:
        cur.close()
        connection.close()

    second_result = [list(r) for r in second_rows]

    # 結果を返す
    return jsonify({
        'first_sql': generated_sql,
        'columns': columns,
        'second_result': second_result
    })

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5001, debug=True)

ヒント
TLSサーバーではないためBASIC認証は暗号化されていません。そのためローカルアクセスのみ許可するよう127.0.0.1でListenしています。Load Balancer TLS終端等クライアント~サーバー間の暗号化が可能な場合はhost='0.0.0.0'に設定してください

  • index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Select AI Chat</title>
  <!-- 外部CSSを読み込む -->
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>

  <div class="header">
    <img src="{{ url_for('static', filename='images/selectai_title.jpg') }}" alt="Select AI Demo">
  </div>

  <div id="chat">
    <p><strong>AI:</strong> 外食に関する家計調査で各外食の年ごとの支出金額を提供します</p>
    <p><strong>AI:</strong> 2015-2024年県庁所在地および政令指定都市について以下の部類で集計しています</p>
    <p><strong>AI:</strong> そば・うどん,中華そば,他麺類,寿司,和食,中華料理,洋食,焼肉,ハンバーガー,他外食,喫茶,飲酒</p>
  </div>

  <div class="input-wrapper">
    <input type="text" id="input" placeholder="質問してください">
    <button id="sendBtn" onclick="sendMessage()">送信</button>
  </div>

  <script>
    async function sendMessage() {
      const input = document.getElementById('input');
      const message = input.value.trim();
      if (!message) return;

      input.value = '';
      document.getElementById('chat').innerHTML += `<p><strong>あなた:</strong> ${message}</p>`;

      const res = await fetch('/chat', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({ message })
      });

      const data = await res.json();

      if (data.error) {
        document.getElementById('chat').innerHTML += `
          <p style="color:red;"><strong>エラー:</strong> ${data.error}</p>
          <details>
            <summary>AIが生成したSQLを見る</summary>
            <pre>${data.first_sql}</pre>
          </details>
        `;
      } else {
        document.getElementById('chat').innerHTML += `
          <p><strong>AI:</strong></p>
          ${renderTable(data.columns, data.second_result)}
          <details>
            <summary>AIが生成したSQLを見る</summary>
            <pre>${data.first_sql}</pre>
          </details>
        `;
      }

      document.getElementById('chat').scrollTop = document.getElementById('chat').scrollHeight;
    }

      function renderTable(columns, rows) {
        let html = "<table border='1' style='border-collapse:collapse; margin-top:10px;'>";
        // ヘッダー行
        html += "<tr>";
        for (const col of columns) {
          html += `<th>${col}</th>`;
        }
        html += "</tr>";
        // データ行
        for (const row of rows) {
          html += "<tr>";
          for (const cell of row) {
            html += `<td>${cell}</td>`;
          }
          html += "</tr>";
        }
        html += "</table>";
        return html;
      }
  </script>
</body>
</html>
  • style.css
body {
  font-family: sans-serif;
  margin: 20px;
  background-color: #ffffff;
}

.header {
  margin-bottom: 20px;
  text-align: left;
}

.header img {
  height: 60px;
}

h2 {
  margin-bottom: 10px;
}

#chat {
  border: 1px solid #ccc;
  border-radius: 10px;
  padding: 10px;
  height: calc(100vh - 240px);
  overflow-y: auto;
  background-color: #fff;
}

.input-wrapper {
  position: relative;
  margin-top: 10px;
  width: 100%;
}

#input {
  width: 100%;
  height: 50px;
  padding: 10px 80px 10px 10px;
  border-radius: 10px;
  border: 1px solid #ccc;
  font-size: 16px;
  box-sizing: border-box;
  background-color: #f0f0f0;
}

#sendBtn {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  height: 35px;
  padding: 0 16px;
  border: none;
  background-color: #007bff;
  color: white;
  border-radius: 20px;
  font-size: 14px;
  cursor: pointer;
}

#sendBtn:hover {
   background-color: #0056b3;
}

実行

selectai.pyを保存したフォルダで実行します

C:\Path\to>python selectai.py

実行結果

WebServerにてWebブラウザを起動して httpで127.0.0.1:5001 にアクセスします
認証を行って入力画面が表示されることを確認します

質問を入力すると回答が返ってきます

SQLも確認可能です

上記以外にもいろいろと検索が可能です
項目に一致しない単語については0件になるよう検索項目の一致精度を上げています。

  • 2024年の飲酒の支出金額が多い順に上位10位の都市名と2019年から2024年の年ごとの金額
都市名 2019年 2020年 2021年 2022年 2023年 2024年
熊本市 33024 10172 6647 12056 18129 38205
東京都区部 33932 18797 9238 19168 34712 34911
さいたま市 30839 11539 3577 14459 30772 31442
大津市 16423 7656 4224 11231 15482 26484
富山市 24195 8584 6521 7758 27731 25578
京都市 15102 4504 1990 9074 10709 25543
福岡市 22200 13954 7474 8231 25774 25490
高知市 37691 13354 12717 16563 32185 24099
静岡市 19835 12105 8985 11144 18973 23191
千葉市 22679 11403 4133 14260 16433 23137
  • 名古屋市の2015年から2024年の喫茶の支出金額を年ごとに表示して
喫茶の支出金額
2015 12578
2016 10323
2020 8732
2021 9377
2022 13427
2017 14148
2018 11304
2019 12851
2023 14120
2024 14897
  • 2024年の中華そばの消費額が多い都市について上位5位の都市名と金額
都市名 中華そばの消費額
山形市 22389
新潟市 16292
仙台市 15534
富山市 13775
さいたま市 12553
  • 2024年の餃子の消費額が多い都市について上位5位の都市名と金額
都市名 消費額

(餃子についての統計データはないため結果正しい)

検索精度を上げるために

ユーザーインターフェースを活用することで、より多くの対話が可能となり、データ検索のテストを実施することができました。 その中で、検索精度を向上させるために行った取り組みは以下のとおりです。

  • テーブル、ビューやカラムのコメント
    コメントを追加することにより検索精度が上がります。完全一致する文字以外を使用させたくない場合は「使用」「無視」「有効」「意味を持たない」「不正」等の文字によりAIに理解させる必要があります
  • カラム名
    カラム名もAIが検索する場合の情報になります。今回は類似した単語が誤って選択されたことからC001等の意味を持たないカラム名に置き換えています。(例:ハンバーグを検索するハンバーガーに類似していると判定されカラム名HAMBURGERを選択してしまう)
  • AIモデル
    AIモデルにより検索精度が変わることもあります。OCI生成AIでもcohereとllama両方でテストを行いましたが異なる結果が出ています。ADB Select AIにて選択可能なAIモデルはOCI生成AIのみならずOpenAIやAWSのBedrockも利用可能なので表データの特性により最適なAIモデルを探してみてください
2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?