feat(kbdb): POST /embed/query 語義查詢端點(薄殼暴露既有 semanticSearch)——Arcrun#7 A2

- routes/embed.ts 加 /embed/query:q 必填、owner_id/source/entry_type/topK 通用 filter
- 模組未開回 409+capability_hint(不假綠)、缺q回400;能力不重寫(rule07 薄殼)
- tests/embed-query.test.ts 4例、tasks 12.6 標x;tsc 0 error、vitest 9/9
This commit is contained in:
Claude
2026-07-05 08:59:05 +00:00
parent ed2e42e007
commit c9e44bb9a3
3 changed files with 108 additions and 1 deletions
+32 -1
View File
@@ -8,7 +8,7 @@
// base 對內容語意無知:只認通用 metadata.embed===true 旗標,不知 triplet/wiki(解耦)。 // base 對內容語意無知:只認通用 metadata.embed===true 旗標,不知 triplet/wiki(解耦)。
import { Hono } from 'hono'; import { Hono } from 'hono';
import type { Bindings } from '../types'; import type { Bindings } from '../types';
import { embedEnabled, backfillEmbeddings, backfillStatus } from '../embed'; import { embedEnabled, backfillEmbeddings, backfillStatus, semanticSearch } from '../embed';
export const embedRoutes = new Hono<{ Bindings: Bindings }>(); export const embedRoutes = new Hono<{ Bindings: Bindings }>();
@@ -49,4 +49,35 @@ embedRoutes.get('/backfill/status', async (c) => {
return c.json({ success: true, ...status }); return c.json({ success: true, ...status });
}); });
// POST /embed/query — 語義查詢(薄殼暴露既有 semanticSearch 能力,不重寫任何 embedding/query 邏輯,rule 07)。
// body{ q:string(必填), owner_id?, source?, entry_type?, topK?(預設20,上限100,由 semanticSearch 收斂) }。
// base 對內容語意無知:owner_id/source/entry_type 皆通用 metadata filter(不寫死 triplet/wiki 語意)。
// 模組未開 → 409 + capability_hint(比照 backfill route,不假綠,mindset §7)。q 缺/空 → 400。
// (與 GET /entries/search?mode=semantic 同一底層能力;此為 issue「POST /search 或 /embed/query」的獨立端點暴露。)
embedRoutes.post('/query', async (c) => {
if (!embedEnabled(c.env)) {
return c.json(
{ success: false, error: 'embed module not enabled (need VECTORIZE + AI bindings)', capability_hint: OFF_HINT },
409,
);
}
const body = (await c.req.json().catch(() => ({}))) as {
q?: string;
owner_id?: string;
source?: string;
entry_type?: string;
topK?: number | string;
};
const q = typeof body.q === 'string' ? body.q.trim() : '';
if (!q) return c.json({ success: false, error: 'q required' }, 400);
const hits = await semanticSearch(c.env, q, {
owner_id: body.owner_id || undefined,
source: body.source || undefined,
entry_type: body.entry_type || undefined,
topK: body.topK !== undefined ? Number(body.topK) : undefined,
});
return c.json({ success: true, query: q, hits: hits ?? [] });
});
export default embedRoutes; export default embedRoutes;
+71
View File
@@ -0,0 +1,71 @@
import { describe, it, expect } from 'vitest';
import { embedRoutes } from '../src/routes/embed';
import type { Bindings } from '../src/types';
// ── Minimal in-memory fakes (no Workers runtime) ─────────────────────────────
// POST /embed/query is a thin shell over semanticSearch (rule 07). We drive the real
// route through the real semanticSearch by faking only AI + VECTORIZE bindings.
// - module off = no AI/VECTORIZE bindings → semanticSearch returns null → route 409.
// - module on = fake AI (query→vector) + fake VECTORIZE.query (returns matches).
function makeEnv(withBindings: boolean, matches: unknown[] = []): Bindings {
const env = {
ENVIRONMENT: 'test',
...(withBindings
? {
AI: { async run(_m: string, i: { text: string[] }) { return { data: i.text.map(() => [0.1, 0.2, 0.3]) }; } },
VECTORIZE: { async query(_v: number[], _o: unknown) { return { matches }; } },
}
: {}),
} as unknown as Bindings;
return env;
}
async function post(env: Bindings, body: unknown): Promise<{ status: number; json: any }> {
// embedRoutes is the standalone sub-app; parent mounts it at '/embed', so here the path is '/query'.
const req = new Request('http://kbdb/query', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
});
const res = await embedRoutes.fetch(req, env);
return { status: res.status, json: await res.json() };
}
describe('POST /embed/query', () => {
it('module on → returns semantic hits', async () => {
const matches = [
{ id: 'e1', score: 0.9, metadata: { owner_id: 'leo', entry_type: 'workflow', source: 'wiki' } },
{ id: 'e2', score: 0.7, metadata: { owner_id: 'leo', entry_type: 'workflow', source: 'wiki' } },
];
const env = makeEnv(true, matches);
const { status, json } = await post(env, { q: 'doorbell', owner_id: 'leo', topK: 5 });
expect(status).toBe(200);
expect(json.success).toBe(true);
expect(json.query).toBe('doorbell');
expect(json.hits.map((h: { id: string }) => h.id)).toEqual(['e1', 'e2']);
expect(json.hits[0]).toMatchObject({ id: 'e1', score: 0.9, owner_id: 'leo', entry_type: 'workflow', source: 'wiki' });
});
it('module off → 409 + capability_hint (誠實不假綠)', async () => {
const env = makeEnv(false);
const { status, json } = await post(env, { q: 'doorbell' });
expect(status).toBe(409);
expect(json.success).toBe(false);
expect(typeof json.capability_hint).toBe('string');
});
it('missing q → 400', async () => {
const env = makeEnv(true);
const { status, json } = await post(env, { owner_id: 'leo' });
expect(status).toBe(400);
expect(json.success).toBe(false);
expect(json.error).toContain('q');
});
it('empty/blank q → 400', async () => {
const env = makeEnv(true);
const { status, json } = await post(env, { q: ' ' });
expect(status).toBe(400);
expect(json.success).toBe(false);
});
});
@@ -200,6 +200,11 @@
當 next-step 回給 AI(語義/關鍵字同一 KBDB MCP,D17 邊界)。薄殼模式不變(kbdbFetch)。mcp tsc exit 0。 當 next-step 回給 AI(語義/關鍵字同一 KBDB MCP,D17 邊界)。薄殼模式不變(kbdbFetch)。mcp tsc exit 0。
- [x] 12.5 **CC 幫開 vectorizeT2.4d,第一版)**:路徑=CC 寫 config `kbdb_embed:true` + `acr update`(已接 kbdbEmbed - [x] 12.5 **CC 幫開 vectorizeT2.4d,第一版)**:路徑=CC 寫 config `kbdb_embed:true` + `acr update`(已接 kbdbEmbed
→ 建 index + 注入 binding redeploy)。base 查詢回應的 `capability_hint` 是發現入口。Pages 設定頁不做(leo 排未來)。 → 建 index + 注入 binding redeploy)。base 查詢回應的 `capability_hint` 是發現入口。Pages 設定頁不做(leo 排未來)。
- [x] 12.6 **語義查詢 HTTP 端點(薄殼暴露,issue#7「POST /search 或 /embed/query」)**`kbdb/src/routes/embed.ts`
`POST /embed/query`body `{q必填, owner_id?, source?, entry_type?, topK?(預設20上限100}`)→ 呼叫既有
`semanticSearch`rule 07 薄殼:route 不重寫任何 embedding/query 邏輯);模組未開→409+OFF_HINT(不假綠);
q 缺/空→400;回 `{success, query, hits}`。與 GET /entries/search?mode=semantic 同底層能力、另一薄殼暴露。
測試 `kbdb/tests/embed-query.test.ts`(模組開回 hits/未開 409/缺 q 400/空白 q 400)。kbdb tsc exit 0、vitest 9 綠。
- [ ] 12.V **端到端驗收 ⏳ 待 leo21c 部署驗**(需官方/leo21c 帳號開 Vectorize index):開 kbdb_embed → acr update → - [ ] 12.V **端到端驗收 ⏳ 待 leo21c 部署驗**(需官方/leo21c 帳號開 Vectorize index):開 kbdb_embed → acr update →
寫一筆帶 `metadata.embed:true` 的 entry → `?mode=semantic` 搜回;未開時 `?mode=semantic` 回 keyword+capability_hint。 寫一筆帶 `metadata.embed:true` 的 entry → `?mode=semantic` 搜回;未開時 `?mode=semantic` 回 keyword+capability_hint。
本次只到 **tsc exit 0kbdb/cypher/cli/mcp 全綠)+ toml 注入 dry-run 驗證**,不假裝端到端綠(mindset §7)。 本次只到 **tsc exit 0kbdb/cypher/cli/mcp 全綠)+ toml 注入 dry-run 驗證**,不假裝端到端綠(mindset §7)。