diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/blog_search.ts | 77 | ||||
| -rw-r--r-- | src/index.ts | 20 | ||||
| -rw-r--r-- | src/types.d.ts | 1 |
3 files changed, 98 insertions, 0 deletions
diff --git a/src/blog_search.ts b/src/blog_search.ts new file mode 100644 index 0000000..9855cd5 --- /dev/null +++ b/src/blog_search.ts @@ -0,0 +1,77 @@ +import { z } from 'zod' + +export const ROUTE_PATH = '/api/blog_search' + +const EMBEDDING_DIMENSION = 1024 as const +const MAX_TEXT_LENGTH = 1_000 +const MAX_RETRIES = 5 +const WORKERS_AI_MAX_RETRIES = 3 + +const RequestBodySchema = z.object({ + text: z.string(), +}) + +const AIResponseSchema = z.object({ + data: z.array(z.array(z.number()).length(EMBEDDING_DIMENSION)).length(1), +}) + +const textResponse = (msg: string, status = 400) => new Response(msg, { status }) + + +export async function handle(c: HonoContext) { + let json: unknown + try { + json = await c.req.json() + } catch { + return textResponse('Bad Request: Invalid JSON', 400) + } + + const body = RequestBodySchema.safeParse(json) + if (!body.success) { + return textResponse(`Bad Request: ${body.error.message}`, 400) + } + + const text = body.data.text.trim() + if (text.length > MAX_TEXT_LENGTH) { + return textResponse('Bad Request: Text is too long', 400) + } + + let embedding: number[] | null = null + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + try { + const raw = await c.env.AI.run( + '@cf/baai/bge-m3', + { text }, + { + gateway: { + id: 'workers-embedding', + retries: { maxAttempts: WORKERS_AI_MAX_RETRIES }, + }, + }, + ) + + const parsed = AIResponseSchema.safeParse(raw) + if (!parsed.success) { + return textResponse(`Internal Server Error: ${parsed.error.message}`, 500) + } + + embedding = parsed.data.data[0] + break + } catch (err) { + console.error(`Embedding attempt ${attempt + 1} failed`, err) + } + } + + if (!embedding) { + return textResponse('Internal Server Error: Embedding failed', 500) + } + + // ─── Respond with binary vector ──────────────────────────────────────────── + const buffer = new Float32Array(embedding).buffer + + const headers: Record<string, string> = { + 'Content-Type': 'application/octet-stream', + } + + return new Response(buffer, { status: 200, headers }) +}
\ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..faa50bb --- /dev/null +++ b/src/index.ts @@ -0,0 +1,20 @@ +import { Hono } from 'hono' +import { cors } from 'hono/cors' + +const app = new Hono<{ Bindings: Env }>() + +// blog-search +import * as blog_search from './blog_search' +app.use(blog_search.ROUTE_PATH, (c, next) => { + return cors({ + origin: c.env.ALLOWED_ORIGINS, + allowMethods: ['POST'], + allowHeaders: ['Content-Type'], + maxAge: 86400, + })(c, next); +}) +app.post(blog_search.ROUTE_PATH, blog_search.handle) + +export default { + fetch: app.fetch, +} as ExportedHandler<Env> diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..2574155 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1 @@ +type HonoContext = import('hono').Context<{ Bindings: Env }>;
\ No newline at end of file |
