概要
従来、データベースから情報を取得するには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_title.jpg
お気に入りのバナーを作成してください
こちらにサンプルをUploadしました
https://github.com/tktk2712/selectaidemo_feh/blob/main/selectai_title.jpg
実行
selectai.pyを保存したフォルダで実行します
C:\Path\to>python selectai.py
実行結果
WebServerにてWebブラウザを起動して httpで127.0.0.1:5001 にアクセスします
認証を行って入力画面が表示されることを確認します
上記以外にもいろいろと検索が可能です
項目に一致しない単語については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モデルを探してみてください

