
React
Reactは、ユーザーインターフェースを構築するためのJavaScriptライブラリです。
Meta社(旧Facebook社)によって開発され、Webアプリケーションを構築するための最も人気のあるJavascriptライブラリの1つとなっています。
Reactは宣言型のアプローチに重点を置いており、コードの理解やデバッグを容易にします。Reactを使用すると、再利用可能なUIコンポーネントを構築し、アプリケーションの状態を管理し、効率的でパフォーマンスの高い方法でブラウザにコンポーネントをレンダリングすることができます。
また、Reactは仮想DOMを使用してUIを効率的に更新します。
React はまず仮想 DOM に変更を加え、その変更を実際のDOMと同期することで、WEBページの表示切り替えの速度を早めます。
サーバーサイドレンダリングもサポートしており、アプリケーションのパフォーマンスを向上させると同時に、検索エンジンがコンテンツをインデックスしやすくすることも可能です。
イベント

マガジン
技術ブログ
はじめに セーフィー株式会社 開発本部 ソリューション開発部の土井 慎也です。 皆さん、他人のプロジェクトや、あるいは自社のサービスのリリースノートって、隅々まで読んでいますか? 正直に告白すると、私は面倒くさくてしっかりとは読めていません。 「新機能が追加されました!詳細は以下の箇条書きをチェックしてください」 ……いや、文字が多くてパッと見で何が変わったのか分からないな。 そんな風にブラウザのタブを閉じてしまった経験は、誰しもあるのではないでしょうか。 せっかく開発者が心血を注いで実装した機能も、伝わらなければ存在しないのと同じです。テキストだけのリリースノートには、直感的な
はじめに みなさん、こんにちは。開発本部 アプリケーション開発部 Webフロント第2グループの佐々木大翔です。 普段は TypeScript や React などの枠組みの中で開発することが多く、DOM を直接触るような実装や Canvas での描画はほとんど未経験でした。 そこで今回は、Canvas を使って「マウス操作でグラフの見え方が変わる」アプリを個人開発してみて、学べたことをまとめます。 アプリを作ったきっかけ 所属グループで「もくもく開発勉強会」を実施しており、低レイヤー寄りの領域(DOM操作や Canvas 描画)も触って表現力を鍛えよう、という流れがありました。
こんにちは、広野です。 以下の記事の続編記事です。RAG で CSV データからの検索精度向上を目指してみました。本記事は UI 編で、主にフロントエンド (React) のコードや UI の動作について記載しています。全体的なアーキテクチャやバックエンドについては前回記事をご覧ください。 Amazon Bedrock Knowledge Bases で構造化データ(CSV)を使用した RAG をつくる -アーキテクチャ編- Amazon Bedrock Knowledge Bases と Amazon S3 Vectors で構築した RAG 環境で、構造化データをデータソースにしたときの検索精度向上を目指しました。本記事はアーキテクチャ編です。 blog.usize-tech.com 2026.03.09 Amazon Bedrock Knowledge Bases で構造化データ(CSV)を使用した RAG をつくる -実装編- Amazon Bedrock Knowledge Bases と Amazon S3 Vectors で構築した RAG 環境で、構造化データをデータソースにしたときの検索精度向上を目指しました。本記事は実装編です。 blog.usize-tech.com 2026.03.23 UI は以前書いた以下の記事の UI をカスタマイズしています。 React で Amazon Bedrock Knowledge Bases ベースの簡易 RAG チャットボットをつくる [2026年1月版] UI編 AWS re:Invent 2025 で、Amazon S3 Vectors が GA されました。それを受けて、以前作成した RAG チャットボット環境をアレンジしてみました。本記事は UI 編です。 blog.usize-tech.com 2026.01.06 この記事では雑多な非構造化データ (PDF等) の中から参考となる情報やファイル名を見つけ出すことを目的としていましたが、本記事では構造化データ (CSV) から参考となる情報やその情報のメタデータを見つけ出すことを目的としています。コードはほぼ同じです。メタデータフィルタリングの機能が追加されているぐらいだと思って下さい。 やりたいこと (再掲) 以下のような架空のヘルプデスク問い合わせ履歴データ (CSV) を用意しました。 ヘルプデスク担当者が新たな問い合わせを受けたときに、似たような過去の対応履歴を引き当てられるようにしたい、というのが目的です。 LLM に、今届いた新しい問い合わせに対する回答案を提案させたい。 回答案を生成するために、自然言語で書かれた問い合わせ内容と回答内容から、意味的に近いデータを引き当てたい。 カテゴリで検索対象をフィルタしたい。その方が精度が上がるケースがあると考えられる。 LLM が回答案を提案するときには、参考にした過去対応履歴がどの問合せ番号のものか、提示させたい。その問合せ番号をキーに、生の対応履歴データを参照できるようにしたい。 以下の前提があります。 データソースとなる CSV ファイルは 1つのみ。過去の対応履歴は 1 つの CSV ファイルに収まっているということ。 つまり、データの1行が1件の問い合わせであり、その項目間には意味的なつながりがある。 まあ、ごくごく一般的なニーズではないかと思います。 前回記事のおさらい メタデータフィルタリングについて 今回のブログ記事では、簡略化のため上記のように「販売形態」と「カテゴリ」の 2 つのメタデータのみフィルタリング可能なように設計します。特定の項目でフィルタリングすることで、検索精度を向上させます。 フィルタ対象項目 販売形態(2種類: 直販, 代理店) カテゴリ(10種類: 家庭用コタツ, 家庭用テーブル, 家庭用収納棚, 家庭用チェア, 家庭用デスク, 業務用ラック, 業務用キャビネット, 業務用会議テーブル, 業務用チェア, 業務用デスク) フィルタ条件選択時の動作 両方未選択 → フィルタリングなし(全件検索) 片方のみ選択 → equals (完全一致) で単一条件検索 両方選択 → andAll で AND 条件検索、それぞれは equals (完全一致) とする つくったもの UI 一般的なチャットボット UI ですが、ユーザーのメッセージ入力欄の下に「絞り込み」という欄を追加しています。ここにあるプルダウンメニューの項目であれば、フィルタリングできるようになっています。 全件検索した例 メタデータフィルタリングを使用せず(プルダウンを選択せず)、「海外発送ができるか」を問い合わせてみました。 過去の問い合わせ履歴データから、4件が見つかりました。 参考問合せ番号のリンクを押すと、それぞれの実データを参照できます。ここでは省略しますが、以下の販売形態、カテゴリで海外発送に言及している履歴があることがわかりました。 問合せ番号 販売形態 カテゴリ AB01234650 直販 家庭用デスク AB01234636 代理店 業務用キャビネット AB01234577 代理店 業務用チェア AB01234653 直販 家庭用テーブル 以降、メタデータフィルタリングでこの検索結果を絞り込みたいと思います。 メタデータ1件でフィルタリングした例 販売形態が「代理店」でフィルタリングして、同じく「海外発送ができるか」を問い合わせた例です。 想定通り、「販売形態が代理店」の問い合わせ履歴データが 2 件、検索されました。 メタデータ2件でフィルタリングした例 販売形態が「直販」、かつカテゴリが「家庭用テーブル」でフィルタリングして、同じく「海外発送できるか」を問い合わせた例です。 想定通り、該当する問い合わせ履歴データ 1 件だけが検索されました。 参考問合せ番号のリンクを押すと、該当問い合わせ履歴の実データが見られます。確かに海外発送についての問い合わせです。 React コード この画面を提供している React のコードですが、詳細は実コードを見てください。 絞り込みのオプションはベタ書きの固定値にしており、選択したデータを Amazon API Gateway に渡しているだけです。当たり前ですが、フォーマットはバックエンドの AWS Lambda 関数で定義したものと合わせています。 AWS AppSync Events から送られてくるレスポンスの中に、citation と呼ばれる、回答の参考になったチャンクとそのメタデータが含まれます。それを元に参考問合せ番号のリンクを作成し、モーダルウィンドウで問い合わせ番号詳細を表示しています。 import { useState, useEffect, useRef } from "react"; import { Container, Grid, Box, Paper, Typography, TextField, Button, Avatar, Dialog, DialogTitle, DialogContent, DialogActions, Link, FormControl, InputLabel, Select, MenuItem } from "@mui/material"; import SendIcon from '@mui/icons-material/Send'; import { blue, grey } from '@mui/material/colors'; import { v4 as uuidv4 } from "uuid"; import { events } from "aws-amplify/data"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { inquireRagSr } from "./Functions.jsx"; import Header from "../Header.jsx"; import Menu from "./Menu.jsx"; const RagSr = (props) => { //定数定義 const groups = props.groups; const sub = props.sub; const idtoken = props.idtoken; const imgUrl = import.meta.env.VITE_IMG_URL; const channelOptions = ["直販", "代理店"]; const categoryOptions = ["家庭用コタツ", "家庭用テーブル", "家庭用収納棚", "家庭用チェア", "家庭用デスク", "業務用ラック", "業務用キャビネット", "業務用会議テーブル", "業務用チェア", "業務用デスク"]; //変数定義 const appsyncSessionIdRef = useRef(); const bedrockSessionIdRef = useRef(null); const channelRef = useRef(); const streamingRefMap = useRef(new Map()); const citationDataMap = useRef(new Map()); //state定義 const [prompt, setPrompt] = useState(""); const [conversation, setConversation] = useState([]); const [streaming, setStreaming] = useState({ text: "", refs: [] }); const [dialogOpen, setDialogOpen] = useState(false); const [selectedCitation, setSelectedCitation] = useState(null); const [filterChannel, setFilterChannel] = useState(""); const [filterCategory, setFilterCategory] = useState(""); //RAGへの問い合わせ送信関数 const putRagSr = () => { if (streaming.text) { setConversation(prev => [ ...prev, { role:"ai", text: streaming.text, ref: streaming.refs }, { role:"user", text: prompt, ref: [] } ]); streamingRefMap.current.clear(); setStreaming({ text:"", refs:[] }); } else { setConversation(prev => [...prev, { role:"user", text: prompt, ref: [] }]); } inquireRagSr(prompt, appsyncSessionIdRef.current, bedrockSessionIdRef.current, idtoken, (() => { const f = []; if (filterChannel) f.push({"販売形態": filterChannel}); if (filterCategory) f.push({"カテゴリ": filterCategory}); return f; })()); //プロンプト欄をクリア setPrompt(""); }; //content.textから問合せ内容と回答内容を抽出 const parseContent = (text) => { const inquiryMatch = text.match(/問合せ内容:\s*([\s\S]*?)(?=\n\n回答内容:|$)/); const answerMatch = text.match(/回答内容:\s*([\s\S]*?)$/); return { inquiry: inquiryMatch ? inquiryMatch[1].trim() : "", answer: answerMatch ? answerMatch[1].trim() : "" }; }; //問合せ番号クリック時の処理 const handleCitationClick = (inquiryNumber) => { const data = citationDataMap.current.get(inquiryNumber); if (data) { setSelectedCitation(data); setDialogOpen(true); } }; //Dialog閉じる処理 const handleDialogClose = () => { setDialogOpen(false); setSelectedCitation(null); }; //サブスクリプション開始関数 const startSubscription = async () => { const appsyncSessionId = appsyncSessionIdRef.current; if (channelRef.current) await channelRef.current.close(); const channel = await events.connect(`rag-stream-response/${sub}/${appsyncSessionId}`); channel.subscribe({ next: (data) => { //Bedrock Knowledge base の session id 取得 if (data.event.type === "bedrock_session") { console.log("=== Session received ==="); console.log(data.event.bedrock_session_id); bedrockSessionIdRef.current = data.event.bedrock_session_id; return; } //問い合わせに対する回答メッセージ (chunkされている) if (data.event.type === "text") { console.log("=== Message received ==="); setStreaming(s => ({ ...s, text: s.text + data.event.message })); return; } //回答に関する関連ドキュメント (citation) if (data.event.type === "citation") { console.log("=== Citation received ==="); console.log(data.event.citation); data.event.citation.forEach(ref => { const inquiryNumber = ref.metadata?.["問合せ番号"]; if (!inquiryNumber) return; if (!streamingRefMap.current.has(inquiryNumber)) { streamingRefMap.current.set(inquiryNumber, { id: inquiryNumber, label: inquiryNumber }); citationDataMap.current.set(inquiryNumber, { metadata: ref.metadata, content: ref.content }); setStreaming(s => ({ ...s, refs: Array.from(streamingRefMap.current.values()) })); } }); } }, error: (err) => console.error("Subscription error:", err), complete: () => console.log("Subscription closed") }); channelRef.current = channel; }; //セッションIDのリセット、サブスクリプション再接続関数 const resetSession = async () => { appsyncSessionIdRef.current = uuidv4(); bedrockSessionIdRef.current = null; setPrompt(""); streamingRefMap.current.clear(); citationDataMap.current.clear(); setStreaming({ text:"", refs:[] }); setConversation([]); setFilterChannel(""); setFilterCategory(""); await startSubscription(); }; //画面表示時 useEffect(() => { //画面表示時に最上部にスクロール window.scrollTo(0, 0); //Bedrockからのレスポンスサブスクライブ関数実行 appsyncSessionIdRef.current = uuidv4(); bedrockSessionIdRef.current = null; startSubscription(); //アンマウント時にチャンネルを閉じる return () => { if (channelRef.current) channelRef.current.close(); }; }, []); //Chatbot UI 会話部分 const renderMessage = (msg, idx) => ( <Box key={idx} sx={{ display: "flex", justifyContent: msg.role === "user" ? "flex-end" : "flex-start", alignItems: "flex-start", mb: 1, width: "100%", minWidth: 0 }} > {msg.role === "ai" && ( <Avatar src={`${imgUrl}/images/ai_chat_icon.svg`} alt="AI" sx={{ mr: 2, mt: 2 }} /> )} <Paper elevation={2} sx={{ p: 2, my: 1, maxWidth: "90%", minWidth: 0, wordBreak: "break-word", overflowWrap: "break-word", bgcolor: msg.role === "user" ? blue[100] : grey[100] }} > <ReactMarkdown remarkPlugins={[remarkGfm]} components={{ p: ({node, ...props}) => <p style={{margin: 0}} {...props} />, code: ({node, inline, ...props}) => ( <code style={{whiteSpace: inline ? 'normal' : 'pre-wrap', wordBreak: 'break-word', overflowWrap: 'break-word'}} {...props} /> ) }} > {msg.text} </ReactMarkdown> {msg.ref.length > 0 && ( <> <h4>参考問合せ番号</h4> <ul style={{ paddingLeft: 20, margin: 0 }}> {msg.ref.map(s => ( <li key={s.id}> <Link component="button" variant="body2" onClick={() => handleCitationClick(s.id)} sx={{ cursor: "pointer" }} > {s.label} </Link> </li> ))} </ul> </> )} </Paper> {msg.role === "user" && ( <Avatar src={`${imgUrl}/images/human_chat_icon.svg`} alt="User" sx={{ ml: 2, mt: 2 }} /> )} </Box> ); return ( <> {/* Header */} <Header groups={groups} signOut={props.signOut} /> <Container maxWidth="lg" sx={{mt:2}}> <Grid container spacing={4}> {/* Menu Pane */} <Grid size={{xs:12,md:4}} order={{xs:2,md:1}}> {/* Sidebar */} <Menu /> </Grid> {/* Contents Pane IMPORTANT */} <Grid size={{xs:12,md:8}} order={{xs:1,md:2}} my={2}> <main> <Grid container spacing={2}> {/* Heading */} <Grid size={{xs:12}}> <Typography id="bedrocksrtop" variant="h5" component="h1" mb={3} gutterBottom>Amazon Bedrock RAG Stream Response テスト</Typography> </Grid> <Grid size={{xs:12}}> {/* Chatbot */} <Paper sx={{p:2,mb:2,width:"100%"}}> {/* あいさつ文(固定) */} {renderMessage({ role: "ai", text: "こんにちは。何かお困りですか?", ref: []}, -1)} {/* 会話履歴 */} {conversation.map((msg, idx) => renderMessage(msg, idx))} {/* 直近のレスポンス */} {streaming.text && renderMessage({ role:"ai", text: streaming.text, ref: streaming.refs }, "stream")} </Paper> {/* 入力エリア */} <Box sx={{display:"flex",gap:1}}> <TextField fullWidth multiline value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Type message here..." sx={{ flexGrow: 1 }} /> <Button variant="contained" size="small" onClick={putRagSr} disabled={!prompt} startIcon={<SendIcon />} sx={{ whiteSpace: "nowrap", flexShrink: 0 }}>送信</Button> </Box> {/* オプション */} <Box sx={{mt:1,p:2,border:"1px solid",borderColor:"divider",borderRadius:1}}> <Typography variant="subtitle2" mb={1}>絞り込み</Typography> <Box sx={{display:"flex",flexWrap:"wrap",gap:2}}> <FormControl size="small" sx={{minWidth:150}}> <InputLabel>販売形態</InputLabel> <Select value={filterChannel} label="販売形態" onChange={(e) => setFilterChannel(e.target.value)}> <MenuItem value="">すべて</MenuItem> {channelOptions.map(v => <MenuItem key={v} value={v}>{v}</MenuItem>)} </Select> </FormControl> <FormControl size="small" sx={{minWidth:150}}> <InputLabel>カテゴリ</InputLabel> <Select value={filterCategory} label="カテゴリ" onChange={(e) => setFilterCategory(e.target.value)}> <MenuItem value="">すべて</MenuItem> {categoryOptions.map(v => <MenuItem key={v} value={v}>{v}</MenuItem>)} </Select> </FormControl> </Box> </Box> {/* クリアボタン */} {(streaming.text || conversation.length > 0) && ( <Box sx={{ display: "flex", justifyContent: "flex-end", mt: 2 }}> <Button variant="contained" size="small" onClick={resetSession}>問い合わせをクリアする</Button> </Box> )} </Grid> </Grid> </main> </Grid> </Grid> </Container> {/* Citation詳細Dialog */} <Dialog open={dialogOpen} onClose={handleDialogClose} maxWidth="md" fullWidth> <DialogTitle>問合せ詳細</DialogTitle> <DialogContent dividers> {selectedCitation && (() => { const { inquiry, answer } = parseContent(selectedCitation.content.text); const m = selectedCitation.metadata; return ( <Box> <Typography variant="subtitle2" color="text.secondary">問合せ番号</Typography> <Typography variant="body1" mb={2}>{m["問合せ番号"] || "-"}</Typography> <Typography variant="subtitle2" color="text.secondary">受付日時</Typography> <Typography variant="body1" mb={2}>{m["受付日時"] || "-"}</Typography> <Typography variant="subtitle2" color="text.secondary">完了日時</Typography> <Typography variant="body1" mb={2}>{m["完了日時"] || "-"}</Typography> <Typography variant="subtitle2" color="text.secondary">販売形態</Typography> <Typography variant="body1" mb={2}>{m["販売形態"] || "-"}</Typography> <Typography variant="subtitle2" color="text.secondary">商品番号</Typography> <Typography variant="body1" mb={2}>{m["商品番号"] || "-"}</Typography> <Typography variant="subtitle2" color="text.secondary">カテゴリ</Typography> <Typography variant="body1" mb={2}>{m["カテゴリ"] || "-"}</Typography> <Typography variant="subtitle2" color="text.secondary">問合せ内容</Typography> <Typography variant="body1" mb={2} sx={{ whiteSpace: "pre-wrap" }}>{inquiry || "-"}</Typography> <Typography variant="subtitle2" color="text.secondary">回答内容</Typography> <Typography variant="body1" mb={2} sx={{ whiteSpace: "pre-wrap" }}>{answer || "-"}</Typography> <Typography variant="subtitle2" color="text.secondary">ステータス</Typography> <Typography variant="body1" mb={2}>{m["ステータス"] || "-"}</Typography> </Box> ); })()} </DialogContent> <DialogActions> <Button onClick={handleDialogClose}>閉じる</Button> </DialogActions> </Dialog> </> ); }; export default RagSr; 途中、inquireRagSr という関数がありますが、axios で Amazon API Gateway をコールするだけの関数です。 まとめ いかがでしたでしょうか。 今回のシリーズ記事の肝は Amazon Bedrock Knowledge Bases データソースのカスタムチャンキングでしたが、結果をこうしてアプリの UI で確認できるようになると、本当にできていることを実感できますよね。引き続き検証して精度向上に努めます。 本記事が皆様のお役に立てれば幸いです。




















