212 lines
8.2 KiB
JavaScript
212 lines
8.2 KiB
JavaScript
|
"use strict";
|
||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
exports.TurbopufferVectorStore = void 0;
|
||
|
const uuid_1 = require("uuid");
|
||
|
const documents_1 = require("@langchain/core/documents");
|
||
|
const async_caller_1 = require("@langchain/core/utils/async_caller");
|
||
|
const chunk_array_1 = require("@langchain/core/utils/chunk_array");
|
||
|
const env_1 = require("@langchain/core/utils/env");
|
||
|
const vectorstores_1 = require("@langchain/core/vectorstores");
|
||
|
class TurbopufferVectorStore extends vectorstores_1.VectorStore {
|
||
|
get lc_secrets() {
|
||
|
return {
|
||
|
apiKey: "TURBOPUFFER_API_KEY",
|
||
|
};
|
||
|
}
|
||
|
get lc_aliases() {
|
||
|
return {
|
||
|
apiKey: "TURBOPUFFER_API_KEY",
|
||
|
};
|
||
|
}
|
||
|
// Handle minification for tracing
|
||
|
static lc_name() {
|
||
|
return "TurbopufferVectorStore";
|
||
|
}
|
||
|
_vectorstoreType() {
|
||
|
return "turbopuffer";
|
||
|
}
|
||
|
constructor(embeddings, args) {
|
||
|
super(embeddings, args);
|
||
|
Object.defineProperty(this, "distanceMetric", {
|
||
|
enumerable: true,
|
||
|
configurable: true,
|
||
|
writable: true,
|
||
|
value: "cosine_distance"
|
||
|
});
|
||
|
Object.defineProperty(this, "apiKey", {
|
||
|
enumerable: true,
|
||
|
configurable: true,
|
||
|
writable: true,
|
||
|
value: void 0
|
||
|
});
|
||
|
Object.defineProperty(this, "namespace", {
|
||
|
enumerable: true,
|
||
|
configurable: true,
|
||
|
writable: true,
|
||
|
value: "default"
|
||
|
});
|
||
|
Object.defineProperty(this, "apiUrl", {
|
||
|
enumerable: true,
|
||
|
configurable: true,
|
||
|
writable: true,
|
||
|
value: "https://api.turbopuffer.com/v1"
|
||
|
});
|
||
|
Object.defineProperty(this, "caller", {
|
||
|
enumerable: true,
|
||
|
configurable: true,
|
||
|
writable: true,
|
||
|
value: void 0
|
||
|
});
|
||
|
Object.defineProperty(this, "batchSize", {
|
||
|
enumerable: true,
|
||
|
configurable: true,
|
||
|
writable: true,
|
||
|
value: 3000
|
||
|
});
|
||
|
const { apiKey: argsApiKey, namespace, distanceMetric, apiUrl, batchSize, ...asyncCallerArgs } = args;
|
||
|
const apiKey = argsApiKey ?? (0, env_1.getEnvironmentVariable)("TURBOPUFFER_API_KEY");
|
||
|
if (!apiKey) {
|
||
|
throw new Error(`Turbopuffer API key not found.\nPlease pass it in as "apiKey" or set it as an environment variable called "TURBOPUFFER_API_KEY"`);
|
||
|
}
|
||
|
this.apiKey = apiKey;
|
||
|
this.namespace = namespace ?? this.namespace;
|
||
|
this.distanceMetric = distanceMetric ?? this.distanceMetric;
|
||
|
this.apiUrl = apiUrl ?? this.apiUrl;
|
||
|
this.batchSize = batchSize ?? this.batchSize;
|
||
|
this.caller = new async_caller_1.AsyncCaller({
|
||
|
maxConcurrency: 6,
|
||
|
maxRetries: 0,
|
||
|
...asyncCallerArgs,
|
||
|
});
|
||
|
}
|
||
|
defaultHeaders() {
|
||
|
return {
|
||
|
Authorization: `Bearer ${this.apiKey}`,
|
||
|
"Content-Type": "application/json",
|
||
|
};
|
||
|
}
|
||
|
async callWithRetry(fetchUrl, stringifiedBody, method = "POST") {
|
||
|
const json = await this.caller.call(async () => {
|
||
|
const headers = {
|
||
|
Authorization: `Bearer ${this.apiKey}`,
|
||
|
};
|
||
|
if (stringifiedBody !== undefined) {
|
||
|
headers["Content-Type"] = "application/json";
|
||
|
}
|
||
|
const response = await fetch(fetchUrl, {
|
||
|
method,
|
||
|
headers,
|
||
|
body: stringifiedBody,
|
||
|
});
|
||
|
if (response.status !== 200) {
|
||
|
const error = new Error(`Failed to call turbopuffer. Response status ${response.status}\nFull response: ${await response.text()}`);
|
||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
error.response = response;
|
||
|
throw error;
|
||
|
}
|
||
|
return response.json();
|
||
|
});
|
||
|
return json;
|
||
|
}
|
||
|
async addVectors(vectors, documents, options) {
|
||
|
if (options?.ids && options.ids.length !== vectors.length) {
|
||
|
throw new Error("Number of ids provided does not match number of vectors");
|
||
|
}
|
||
|
if (documents.length !== vectors.length) {
|
||
|
throw new Error("Number of documents provided does not match number of vectors");
|
||
|
}
|
||
|
if (documents.length === 0) {
|
||
|
throw new Error("No documents provided");
|
||
|
}
|
||
|
const batchedVectors = (0, chunk_array_1.chunkArray)(vectors, this.batchSize);
|
||
|
const batchedDocuments = (0, chunk_array_1.chunkArray)(documents, this.batchSize);
|
||
|
const batchedIds = options?.ids
|
||
|
? (0, chunk_array_1.chunkArray)(options.ids, this.batchSize)
|
||
|
: batchedDocuments.map((docs) => docs.map((_) => (0, uuid_1.v4)()));
|
||
|
const batchRequests = batchedVectors.map(async (batchVectors, index) => {
|
||
|
const batchDocs = batchedDocuments[index];
|
||
|
const batchIds = batchedIds[index];
|
||
|
if (batchIds.length !== batchVectors.length) {
|
||
|
throw new Error("Number of ids provided does not match number of vectors");
|
||
|
}
|
||
|
const attributes = {
|
||
|
__lc_page_content: batchDocs.map((doc) => doc.pageContent),
|
||
|
};
|
||
|
const usedMetadataFields = new Set(batchDocs.map((doc) => Object.keys(doc.metadata)).flat());
|
||
|
for (const key of usedMetadataFields) {
|
||
|
attributes[key] = batchDocs.map((doc) => {
|
||
|
if (doc.metadata[key] !== undefined) {
|
||
|
if (typeof doc.metadata[key] === "string") {
|
||
|
return doc.metadata[key];
|
||
|
}
|
||
|
else {
|
||
|
console.warn([
|
||
|
`[WARNING]: Dropping non-string metadata key "${key}" with value "${JSON.stringify(doc.metadata[key])}".`,
|
||
|
`turbopuffer currently supports only string metadata values.`,
|
||
|
].join("\n"));
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
return null;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
const data = {
|
||
|
ids: batchIds,
|
||
|
vectors: batchVectors,
|
||
|
attributes,
|
||
|
};
|
||
|
return this.callWithRetry(`${this.apiUrl}/vectors/${this.namespace}`, JSON.stringify(data));
|
||
|
});
|
||
|
// Execute all batch requests in parallel
|
||
|
await Promise.all(batchRequests);
|
||
|
return batchedIds.flat();
|
||
|
}
|
||
|
async delete(params) {
|
||
|
if (params.deleteIndex) {
|
||
|
await this.callWithRetry(`${this.apiUrl}/vectors/${this.namespace}`, undefined, "DELETE");
|
||
|
}
|
||
|
else {
|
||
|
throw new Error(`You must provide a "deleteIndex" flag.`);
|
||
|
}
|
||
|
}
|
||
|
async addDocuments(documents, options) {
|
||
|
const vectors = await this.embeddings.embedDocuments(documents.map((doc) => doc.pageContent));
|
||
|
return this.addVectors(vectors, documents, options);
|
||
|
}
|
||
|
async queryVectors(query, k, includeVector,
|
||
|
// See https://Turbopuffer.com/docs/reference/query for more info
|
||
|
filter) {
|
||
|
const data = {
|
||
|
vector: query,
|
||
|
top_k: k,
|
||
|
distance_metric: this.distanceMetric,
|
||
|
filters: filter,
|
||
|
include_attributes: true,
|
||
|
include_vectors: includeVector,
|
||
|
};
|
||
|
return this.callWithRetry(`${this.apiUrl}/vectors/${this.namespace}/query`, JSON.stringify(data));
|
||
|
}
|
||
|
async similaritySearchVectorWithScore(query, k, filter) {
|
||
|
const search = await this.queryVectors(query, k, false, filter);
|
||
|
const result = search.map((res) => {
|
||
|
const { __lc_page_content, ...metadata } = res.attributes;
|
||
|
return [
|
||
|
new documents_1.Document({
|
||
|
pageContent: __lc_page_content,
|
||
|
metadata,
|
||
|
}),
|
||
|
res.dist,
|
||
|
];
|
||
|
});
|
||
|
return result;
|
||
|
}
|
||
|
static async fromDocuments(docs, embeddings, dbConfig) {
|
||
|
const instance = new this(embeddings, dbConfig);
|
||
|
await instance.addDocuments(docs);
|
||
|
return instance;
|
||
|
}
|
||
|
}
|
||
|
exports.TurbopufferVectorStore = TurbopufferVectorStore;
|