summaryrefslogtreecommitdiff
path: root/src/blog_search.ts
diff options
context:
space:
mode:
authorRikki <i@rikki.moe>2025-05-26 14:25:17 +0800
committerRikki <i@rikki.moe>2025-05-26 14:25:17 +0800
commit818f6fdcfbfe296dab032af4023d98a1ac566fcf (patch)
treeb09da2a901c1c0e5c97ced1bddccf375b232fa08 /src/blog_search.ts
Diffstat (limited to 'src/blog_search.ts')
-rw-r--r--src/blog_search.ts77
1 files changed, 77 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