0 / 500
似ている過去作品 TOP10
類似度は意味ベクトルの近さであり「かぶり判定」の絶対的基準ではありません。
使えそうなデータセット TOP10
東京都オープンデータカタログ 全9,642データセットから検索
入力されたアイデアは保存されません
サーバー側では埋め込み計算と類似検索のみを行い、入力テキストをログ・保存・第三者送信しません。作者自身も参加者だからこそ、これを設計で保証します。
APIのソースコードを見る(functions/api/similar.js)
/**
* ============================================================================
* プライバシーの誓約 / PRIVACY PLEDGE
* ----------------------------------------------------------------------------
* このエンドポイントは、入力されたアイデア文(q)を
* ・console.log 等でログに出力しません
* ・KV / D1 / R2 など、いかなるストレージにも保存しません
* ・外部の第三者サービスへ転送・送信しません(Workers AI / Vectorize への
* 問い合わせのみを行い、いずれも本 Cloudflare アカウント内で完結します)
* サーバー側で行うのは「埋め込みベクトルの計算」と「類似検索」のみです。
* リクエストが終わればテキストは破棄され、どこにも残りません。
* 作者自身もこのハッカソンの参加者であり、他の参加者と同じ立場で
* アイデアの流出リスクを負いたくないからこそ、この設計を選んでいます。
* ============================================================================
*/
const MAX_QUERY_LENGTH = 500;
const TOP_K = 10;
const VALID_TARGETS = ["works", "datasets"];
function jsonResponse(body, status) {
return new Response(JSON.stringify(body), {
status: status || 200,
headers: {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store"
}
});
}
function badRequest(message) {
return jsonResponse({ error: message }, 400);
}
// AI.run() のレスポンス形状はモデル/バージョンによって揺れがあるため、
// {data: [[...]]} と {data: {...}} の両方を防御的に処理する。
function extractVector(resp) {
if (!resp || !resp.data) return null;
const d = resp.data;
if (Array.isArray(d)) {
const first = d[0];
if (Array.isArray(first)) return first;
if (first && typeof first === "object" && Array.isArray(first.values)) return first.values;
if (typeof first === "number") return d; // data 自体が単一ベクトルだった場合
return null;
}
if (typeof d === "object") {
if (Array.isArray(d.values)) return d.values;
if (Array.isArray(d.embedding)) return d.embedding;
}
return null;
}
async function queryIndex(index, vector, label, mapMatch) {
if (!index) {
return { list: [], note: `${label} インデックスが利用できません(未設定)。` };
}
try {
const result = await index.query(vector, { topK: TOP_K, returnMetadata: "all" });
const matches = (result && result.matches) || [];
if (!matches.length) {
return { list: [], note: `${label} インデックスにまだデータがありません。` };
}
const list = matches.map(mapMatch);
return { list: list };
} catch (err) {
// インデックス側の障害は 500 にせず、当該セクションのみ空扱いにする。
return { list: [], note: `${label} の検索中にエラーが発生しました。` };
}
}
// 過去作品インデックス: メタデータは works.json の配列インデックス(i)のみを持つ。
// タイトル等の表示情報はクライアント側で ./data/works.json と突き合わせる。
function mapWorkMatch(m) {
const meta = m.metadata || {};
return { i: meta.i, score: m.score };
}
// データセットインデックス: メタデータ自体に表示に必要な情報(タイトル/組織/
// ライセンス/カタログURL)を持たせてあるため、クライアント側で 3.3MB の
// datasets.json を取得する必要がない。ここで直接展開して返す。
function mapDatasetMatch(m) {
const meta = m.metadata || {};
return { score: m.score, t: meta.t, o: meta.o, lic: meta.lic, url: meta.url };
}
export async function onRequestPost(context) {
const { request, env } = context;
let payload;
try {
payload = await request.json();
} catch (err) {
return badRequest("リクエストボディは JSON である必要があります。");
}
const rawQ = payload && typeof payload.q === "string" ? payload.q : null;
if (rawQ === null) {
return badRequest("q(アイデア文)は必須です。");
}
const q = rawQ.trim();
if (!q) {
return badRequest("q(アイデア文)は必須です。");
}
if (q.length > MAX_QUERY_LENGTH) {
return badRequest(`q は ${MAX_QUERY_LENGTH} 文字以内で入力してください。`);
}
let targets = VALID_TARGETS;
if (Array.isArray(payload.targets) && payload.targets.length) {
targets = payload.targets.filter(function (t) { return VALID_TARGETS.indexOf(t) !== -1; });
if (!targets.length) {
return badRequest("targets は works / datasets のいずれかを含めてください。");
}
}
let vector;
try {
const embedResp = await env.AI.run("@cf/baai/bge-m3", { text: [q] });
vector = extractVector(embedResp);
} catch (err) {
return jsonResponse({ error: "埋め込みの計算に失敗しました。しばらくして再度お試しください。" }, 502);
}
if (!vector) {
return jsonResponse({ error: "埋め込みの計算結果を解釈できませんでした。" }, 502);
}
const response = {};
if (targets.indexOf("works") !== -1) {
const r = await queryIndex(env.VEC_WORKS, vector, "過去作品", mapWorkMatch);
response.works = r.list;
if (r.note) response.worksNote = r.note;
} else {
response.works = [];
}
if (targets.indexOf("datasets") !== -1) {
const r = await queryIndex(env.VEC_DATA, vector, "データセット", mapDatasetMatch);
response.datasets = r.list;
if (r.note) response.datasetsNote = r.note;
} else {
response.datasets = [];
}
return jsonResponse(response, 200);
}
// Pages Functions はメソッド別ハンドラ(onRequestPost)を onRequest より優先して
// 呼び出すため、ここに到達するのは POST 以外のメソッドのみ。明示的に 405 を返す。
export async function onRequest() {
return jsonResponse({ error: "このエンドポイントは POST のみ対応しています。" }, 405);
}