summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/blog_search.ts77
-rw-r--r--src/index.ts20
-rw-r--r--src/types.d.ts1
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