Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Vectorize GA binding changes #2443

Merged
merged 1 commit into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const test_vector_search_vector_query = {
returnMetadata: "indexed",
});
assert.equal(true, results.count > 0);
/** @type {VectorizeQueryMatches} */
/** @type {VectorizeMatches} */
const expected = {
matches: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ const unitTests :Workerd.Config = (
innerBindings = [(
name = "fetcher",
service = "vector-search-mock"
)],
),
(name = "indexId", text = "an-index"),
(name = "indexVersion", text = "v2")],
)
)
],
Expand Down
9 changes: 2 additions & 7 deletions src/cloudflare/internal/test/vectorize/vectorize-mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

/** @type {Array<VectorizeQueryMatch>} */
/** @type {Array<VectorizeMatch>} */
const exampleVectorMatches = [
{
id: "b0daca4a-ffd8-4865-926b-e24800af2a2d",
Expand Down Expand Up @@ -97,13 +97,8 @@ export default {
) {
return Response.json({});
} else if (request.method === "POST" && pathname.endsWith("/query")) {
/** @type {VectorizeQueryOptions & {vector: number[], compat: { queryMetadataOptional: boolean }}} */
/** @type {VectorizeQueryOptions<VectorizeMetadataRetrievalLevel> & {vector: number[]}} */
const body = await request.json();
// check that the compatibility flags are set
if (!body.compat.queryMetadataOptional)
throw Error(
"expected to get `queryMetadataOptional` compat flag with a value of true"
);
let returnSet = structuredClone(exampleVectorMatches);
if (
body?.filter?.["text"] &&
Expand Down
241 changes: 134 additions & 107 deletions src/cloudflare/internal/vectorize-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,144 +16,166 @@ enum Operation {
VECTOR_DELETE = 5,
}

class VectorizeIndexImpl implements VectorizeIndex {
type VectorizeVersion = "v1" | "v2";

/*
* The Vectorize beta VectorizeIndex shares the same methods, so to keep things simple, they share one implementation.
Copy link
Contributor Author

@ndisidore ndisidore Jul 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what we discussed @mikea and if this holds true, I think we're happy with this approach!

* The types here are specific to Vectorize GA, but the types here don't actually matter as they are stripped away
* and not visible to end users.
*/
class VectorizeIndexImpl implements Vectorize {
public constructor(
private readonly fetcher: Fetcher,
private readonly indexId: string
private readonly indexId: string,
private readonly indexVersion: VectorizeVersion
) {}

public async describe(): Promise<VectorizeIndexDetails> {
const res = await this._send(
Operation.INDEX_GET,
`indexes/${this.indexId}`,
{
method: "GET",
}
);
public async describe(): Promise<VectorizeIndexInfo> {
const endpoint =
this.indexVersion === "v2" ? `info` : `binding/indexes/${this.indexId}`;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For v2, account info and index id are included in the base url of the fetcher so these endpoints just become the final path part.

const res = await this._send(Operation.INDEX_GET, endpoint, {
method: "GET",
});

return await toJson<VectorizeIndexDetails>(res);
return await toJson<VectorizeIndexInfo>(res);
}

public async query(
vector: VectorFloatArray | number[],
options: VectorizeQueryOptions
options: VectorizeQueryOptions<VectorizeMetadataRetrievalLevel>
): Promise<VectorizeMatches> {
const compat = {
queryMetadataOptional: flags.vectorizeQueryMetadataOptional,
};
const res = await this._send(
Operation.VECTOR_QUERY,
`indexes/${this.indexId}/query`,
{
if (this.indexVersion === "v2") {
const res = await this._send(Operation.VECTOR_QUERY, `query`, {
method: "POST",
body: JSON.stringify({
...options,
vector: Array.isArray(vector) ? vector : Array.from(vector),
compat,
}),
headers: {
"content-type": "application/json",
accept: "application/json",
"cf-vector-search-query-compat": JSON.stringify(compat),
},
}
);

return await toJson<VectorizeMatches>(res);
});

return await toJson<VectorizeMatches>(res);
} else {
const compat = {
queryMetadataOptional: flags.vectorizeQueryMetadataOptional,
};
const res = await this._send(
Operation.VECTOR_QUERY,
`binding/indexes/${this.indexId}/query`,
{
method: "POST",
body: JSON.stringify({
...options,
vector: Array.isArray(vector) ? vector : Array.from(vector),
compat,
}),
headers: {
"content-type": "application/json",
accept: "application/json",
"cf-vector-search-query-compat": JSON.stringify(compat),
},
}
);

return await toJson<VectorizeMatches>(res);
}
}

public async insert(
vectors: VectorizeVector[]
): Promise<VectorizeVectorMutation> {
const res = await this._send(
Operation.VECTOR_INSERT,
`indexes/${this.indexId}/insert`,
{
method: "POST",
body: JSON.stringify({
vectors: vectors.map((vec) => ({
...vec,
values: Array.isArray(vec.values)
? vec.values
: Array.from(vec.values),
})),
}),
headers: {
"content-type": "application/json",
"cf-vector-search-dim-width": String(
vectors.length ? vectors[0]?.values?.length : 0
),
"cf-vector-search-dim-height": String(vectors.length),
accept: "application/json",
},
}
);

return await toJson<VectorizeVectorMutation>(res);
): Promise<VectorizeAsyncMutation> {
const endpoint =
this.indexVersion === "v2"
? `insert`
: `binding/indexes/${this.indexId}/insert`;
const res = await this._send(Operation.VECTOR_INSERT, endpoint, {
method: "POST",
body: JSON.stringify({
vectors: vectors.map((vec) => ({
...vec,
values: Array.isArray(vec.values)
? vec.values
: Array.from(vec.values),
})),
}),
headers: {
"content-type": "application/json",
"cf-vector-search-dim-width": String(
vectors.length ? vectors[0]?.values?.length : 0
),
"cf-vector-search-dim-height": String(vectors.length),
accept: "application/json",
},
});

return await toJson<VectorizeAsyncMutation>(res);
}

public async upsert(
vectors: VectorizeVector[]
): Promise<VectorizeVectorMutation> {
const res = await this._send(
Operation.VECTOR_UPSERT,
`indexes/${this.indexId}/upsert`,
{
method: "POST",
body: JSON.stringify({
vectors: vectors.map((vec) => ({
...vec,
values: Array.isArray(vec.values)
? vec.values
: Array.from(vec.values),
})),
}),
headers: {
"content-type": "application/json",
"cf-vector-search-dim-width": String(
vectors.length ? vectors[0]?.values?.length : 0
),
"cf-vector-search-dim-height": String(vectors.length),
accept: "application/json",
},
}
);

return await toJson<VectorizeVectorMutation>(res);
): Promise<VectorizeAsyncMutation> {
const endpoint =
this.indexVersion === "v2"
? `upsert`
: `binding/indexes/${this.indexId}/upsert`;
const res = await this._send(Operation.VECTOR_UPSERT, endpoint, {
method: "POST",
body: JSON.stringify({
vectors: vectors.map((vec) => ({
...vec,
values: Array.isArray(vec.values)
? vec.values
: Array.from(vec.values),
})),
}),
headers: {
"content-type": "application/json",
"cf-vector-search-dim-width": String(
vectors.length ? vectors[0]?.values?.length : 0
),
"cf-vector-search-dim-height": String(vectors.length),
accept: "application/json",
},
});

return await toJson<VectorizeAsyncMutation>(res);
}

public async getByIds(ids: string[]): Promise<VectorizeVector[]> {
const res = await this._send(
Operation.VECTOR_GET,
`indexes/${this.indexId}/getByIds`,
{
method: "POST",
body: JSON.stringify({ ids }),
headers: {
"content-type": "application/json",
accept: "application/json",
},
}
);
const endpoint =
this.indexVersion === "v2"
? `getByIds`
: `binding/indexes/${this.indexId}/getByIds`;
const res = await this._send(Operation.VECTOR_GET, endpoint, {
method: "POST",
body: JSON.stringify({ ids }),
headers: {
"content-type": "application/json",
accept: "application/json",
},
});

return await toJson<VectorizeVector[]>(res);
}

public async deleteByIds(ids: string[]): Promise<VectorizeVectorMutation> {
const res = await this._send(
Operation.VECTOR_DELETE,
`indexes/${this.indexId}/deleteByIds`,
{
method: "POST",
body: JSON.stringify({ ids }),
headers: {
"content-type": "application/json",
accept: "application/json",
},
}
);

return await toJson<VectorizeVectorMutation>(res);
public async deleteByIds(ids: string[]): Promise<VectorizeAsyncMutation> {
const endpoint =
this.indexVersion === "v2"
? `deleteByIds`
: `binding/indexes/${this.indexId}/deleteByIds`;
const res = await this._send(Operation.VECTOR_DELETE, endpoint, {
method: "POST",
body: JSON.stringify({ ids }),
headers: {
"content-type": "application/json",
accept: "application/json",
},
});

return await toJson<VectorizeAsyncMutation>(res);
}

private async _send(
Expand All @@ -162,7 +184,7 @@ class VectorizeIndexImpl implements VectorizeIndex {
init: RequestInit
): Promise<Response> {
const res = await this.fetcher.fetch(
`http://vector-search/binding/${endpoint}`, // `http://vector-search` is just a dummy host, the attached fetcher will receive the request
`http://vector-search/${endpoint}`, // `http://vector-search` is just a dummy host, the attached fetcher will receive the request
init
);
if (res.status !== 200) {
Expand Down Expand Up @@ -217,8 +239,13 @@ async function toJson<T = unknown>(response: Response): Promise<T> {
export function makeBinding(env: {
fetcher: Fetcher;
indexId: string;
}): VectorizeIndex {
return new VectorizeIndexImpl(env.fetcher, env.indexId);
indexVersion?: VectorizeVersion;
}): Vectorize {
return new VectorizeIndexImpl(
env.fetcher,
env.indexId,
env.indexVersion ?? "v1"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ensures backwards compatibility, since previously built pipelines won't have these inner globals.
We default to v1 if is it not present.

);
}

export default makeBinding;
19 changes: 18 additions & 1 deletion src/cloudflare/internal/vectorize.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ type VectorizeIndexConfig =

/**
* Metadata about an existing index.
*
* This type is exclusively for the Vectorize **beta** and will be deprecated once Vectorize RC is released.
* See {@link VectorizeIndexInfo} for its post-beta equivalent.
*/
interface VectorizeIndexDetails {
/** The unique ID of the index */
Expand All @@ -105,6 +108,20 @@ interface VectorizeIndexDetails {
vectorsCount: number;
}

/**
* Metadata about an existing index.
*/
interface VectorizeIndexInfo {
/** The number of records containing vectors within the index. */
vectorsCount: number;
/** Number of dimensions the index has been configured for. */
dimensions: number;
/** ISO 8601 datetime of the last processed mutation on in the index. All changes before this mutation will be reflected in the index state. */
processedUpToDatetime: number;
/** UUIDv4 of the last mutation processed by the index. All changes before this mutation will be reflected in the index state. */
processedUpToMutation: number;
}

/**
* Represents a single vector value set along with its associated metadata.
*/
Expand Down Expand Up @@ -217,7 +234,7 @@ declare abstract class Vectorize {
* Get information about the currently bound index.
* @returns A promise that resolves with information about the current index.
*/
public describe(): Promise<VectorizeIndexDetails>;
public describe(): Promise<VectorizeIndexInfo>;
/**
* Use the provided vector to perform a similarity search across the index.
* @param vector Input vector that will be used to drive the similarity search.
Expand Down
Loading
Loading