1import type * as runtime from "@cloudflare/workers-types";
2import * as vectorize from "@distilled.cloud/cloudflare/vectorize";
3import * as Effect from "effect/Effect";
4import type * as HttpClient from "effect/http/HttpClient";
5import type { Credentials } from "../Credentials.ts";
6import type { SearchIndexClient } from "./SearchIndex.ts";
7
8// Shared HTTP scaffolding for the Vectorize `SearchIndex` binding. NOT
9// re-exported from `index.ts` — only the contract and the impl layers are
10// public. A future token-scoped `SearchIndexHttp` layer reuses this builder
11// with an auth minted from an `AccountApiToken`; `SearchIndexLocal` builds the
12// auth from the ambient current-credentials context.
13
14/**
15 * Injectable auth shared by the Local (current-credentials) impl and a future
16 * Http (scoped-token) impl. `authorize` discharges the
17 * `Credentials | HttpClient` requirement of a distilled op; `accountId` is the
18 * Cloudflare account the ops run against.
19 */
20export interface SearchIndexAuth {
21 authorize: <A, E>(
22 eff: Effect.Effect<A, E, Credentials | HttpClient.HttpClient>,
23 ) => Effect.Effect<A, E>;
24 accountId: string;
25}
26
27/**
28 * Build a {@link SearchIndexClient} over the Vectorize HTTP API.
29 *
30 * `indexName` is an Effect so the resolution stays deferred to each call —
31 * inside an Action it resolves through the apply-time RuntimeContext. The
32 * credentials are provided ONLY around the distilled op (via `auth.authorize`),
33 * never around the name accessor, matching the D1 / KV Local variants. `orDie`
34 * mirrors the native binding, whose client methods surface transport failures
35 * as defects.
36 *
37 * Two methods have no Cloudflare HTTP equivalent and `Effect.die`:
38 * - `raw` — there is no HTTP-backed `runtime.Vectorize` object to hand back.
39 * - `queryById` — the HTTP query endpoint only accepts a raw vector, not an id.
40 */
41export const makeHttpSearchIndexClient = (
42 auth: SearchIndexAuth,
43 indexName: Effect.Effect<string>,
44): SearchIndexClient => {
45 const { accountId } = auth;
46 const local = <A, E>(
47 fn: (
48 name: string,
49 ) => Effect.Effect<A, E, Credentials | HttpClient.HttpClient>,
50 ): Effect.Effect<A> =>
51 Effect.flatMap(indexName, (name) => auth.authorize(fn(name))).pipe(
52 Effect.orDie,
53 );
54
55 return {
56 raw: Effect.die(
57 new Error(
58 "SearchIndex over HTTP: `raw` is not available — use a native Worker binding (SearchIndexBinding) for direct access.",
59 ),
60 ),
61 describe: () =>
62 local((name) =>
63 vectorize
64 .getIndexInfo({ accountId, indexName: name })
65 .pipe(Effect.map(toIndexInfo)),
66 ),
67 query: (vector, options) =>
68 local((name) =>
69 vectorize
70 .queryIndex({
71 accountId,
72 indexName: name,
73 vector: Array.from(vector),
74 topK: options?.topK,
75 returnValues: options?.returnValues,
76 returnMetadata: toReturnMetadata(options?.returnMetadata),
77 filter: options?.filter,
78 })
79 .pipe(Effect.map(toMatches)),
80 ),
81 queryById: () =>
82 Effect.die(
83 new Error(
84 "SearchIndex over HTTP: `queryById` is not supported — the HTTP query endpoint only accepts a raw query vector. Fetch the vector with `getByIds` and pass its values to `query`.",
85 ),
86 ),
87 insert: (vectors) =>
88 local((name) =>
89 vectorize
90 .insertIndex({
91 accountId,
92 indexName: name,
93 body: toNdjsonBlob(vectors),
94 })
95 .pipe(Effect.map(toMutation)),
96 ),
97 upsert: (vectors) =>
98 local((name) =>
99 vectorize
100 .upsertIndex({
101 accountId,
102 indexName: name,
103 body: toNdjsonBlob(vectors),
104 })
105 .pipe(Effect.map(toMutation)),
106 ),
107 deleteByIds: (ids) =>
108 local((name) =>
109 vectorize
110 .deleteByIdsIndex({ accountId, indexName: name, ids })
111 .pipe(Effect.map(toMutation)),
112 ),
113 getByIds: (ids) =>
114 local((name) =>
115 vectorize
116 .getByIdsIndex({ accountId, indexName: name, ids })
117 .pipe(
118 Effect.map((result) => (result ?? []) as runtime.VectorizeVector[]),
119 ),
120 ),
121 } satisfies SearchIndexClient;
122};
123
124// ── shape adapters (runtime types <-> distilled HTTP types) ─────────────────
125
126/**
127 * Serialize vectors to an ndjson Blob — the raw `application/x-ndjson` request
128 * body of the Vectorize v2 insert/upsert endpoints.
129 */
130const toNdjsonBlob = (vectors: runtime.VectorizeVector[]): Blob =>
131 new Blob([toNdjson(vectors)]);
132
133/** Serialize vectors to ndjson — one JSON vector per line. */
134const toNdjson = (vectors: runtime.VectorizeVector[]): string =>
135 vectors
136 .map((v) =>
137 JSON.stringify({
138 id: v.id,
139 // A `VectorFloatArray` (Float32Array) would `JSON.stringify` to an
140 // object, not an array — normalize to a plain number array.
141 values: Array.from(v.values),
142 ...(v.namespace !== undefined ? { namespace: v.namespace } : {}),
143 ...(v.metadata !== undefined ? { metadata: v.metadata } : {}),
144 }),
145 )
146 .join("\n");
147
148const toReturnMetadata = (
149 value: runtime.VectorizeQueryOptions["returnMetadata"],
150): "none" | "indexed" | "all" | undefined =>
151 value === undefined
152 ? undefined
153 : typeof value === "boolean"
154 ? value
155 ? "all"
156 : "none"
157 : value;
158
159const toMutation = (r: {
160 mutationId?: string | null;
161}): runtime.VectorizeAsyncMutation => ({ mutationId: r.mutationId ?? "" });
162
163const toIndexInfo = (
164 r: vectorize.GetIndexInfoResponse,
165): runtime.VectorizeIndexInfo =>
166 ({
167 vectorCount: r.vectorCount ?? 0,
168 dimensions: r.dimensions ?? 0,
169 processedUpToDatetime: r.processedUpToDatetime ?? undefined,
170 processedUpToMutation: r.processedUpToMutation ?? undefined,
171 }) as unknown as runtime.VectorizeIndexInfo;
172
173const toMatches = (r: vectorize.QueryIndexResponse): runtime.VectorizeMatches =>
174 ({
175 count: r.count ?? r.matches?.length ?? 0,
176 matches: (r.matches ?? []).map((m) => ({
177 id: m.id ?? "",
178 score: m.score ?? 0,
179 ...(m.values != null ? { values: m.values } : {}),
180 ...(m.namespace != null ? { namespace: m.namespace } : {}),
181 ...(m.metadata != null ? { metadata: m.metadata } : {}),
182 })),
183 }) as unknown as runtime.VectorizeMatches;