@turbopuffer/turbopuffer
Advanced tools
Comparing version 0.4.2 to 0.5.0
@@ -7,3 +7,4 @@ /** | ||
*/ | ||
import "isomorphic-fetch"; | ||
import type { HTTPClient } from "./httpClient"; | ||
export { TurbopufferError } from "./httpClient"; | ||
/** | ||
@@ -16,10 +17,8 @@ * Utility Types | ||
export type AttributeType = null | string | number | string[] | number[]; | ||
export type Attributes = { | ||
[key: string]: AttributeType; | ||
}; | ||
export type Vector = { | ||
export type Attributes = Record<string, AttributeType>; | ||
export interface Vector { | ||
id: Id; | ||
vector?: number[]; | ||
attributes?: Attributes; | ||
}; | ||
} | ||
export type DistanceMetric = "cosine_distance" | "euclidean_squared"; | ||
@@ -37,3 +36,10 @@ export type FilterOperator = "Eq" | "NotEq" | "In" | "NotIn" | "Lt" | "Lte" | "Gt" | "Gte" | "Glob" | "NotGlob" | "IGlob" | "NotIGlob" | "And" | "Or"; | ||
}[]; | ||
export type NamespaceDesc = { | ||
export type QueryMetrics = { | ||
approx_namespace_size: number; | ||
cache_hit_ratio: number; | ||
cache_temperature: string; | ||
processing_time: number; | ||
exhaustive_search_count: number; | ||
}; | ||
export interface NamespaceDesc { | ||
id: string; | ||
@@ -43,41 +49,23 @@ approx_count: number; | ||
created_at: string; | ||
}; | ||
export type NamespacesListResult = { | ||
} | ||
export interface NamespacesListResult { | ||
namespaces: NamespaceDesc[]; | ||
next_cursor?: string; | ||
}; | ||
export type RecallMeasurement = { | ||
} | ||
export interface RecallMeasurement { | ||
avg_recall: number; | ||
avg_exhaustive_count: number; | ||
avg_ann_count: number; | ||
}; | ||
export declare class TurbopufferError extends Error { | ||
error: string; | ||
status?: number; | ||
constructor(error: string, { status }: { | ||
status?: number; | ||
}); | ||
} | ||
export declare class Turbopuffer { | ||
private baseUrl; | ||
apiKey: string; | ||
constructor({ apiKey, baseUrl, }: { | ||
http: HTTPClient; | ||
constructor({ apiKey, baseUrl, connectTimeout, // timeout to establish a connection | ||
connectionIdleTimeout, // socket idle timeout in ms, default 1 minute | ||
warmConnections, }: { | ||
apiKey: string; | ||
baseUrl?: string; | ||
connectTimeout?: number; | ||
connectionIdleTimeout?: number; | ||
warmConnections?: number; | ||
}); | ||
statusCodeShouldRetry(statusCode: number): boolean; | ||
delay(ms: number): Promise<unknown>; | ||
doRequest<T>({ method, path, query, body, compress, retryable, }: { | ||
method: string; | ||
path: string; | ||
query?: { | ||
[key: string]: string | undefined; | ||
}; | ||
body?: any; | ||
compress?: boolean; | ||
retryable?: boolean; | ||
}): Promise<{ | ||
body?: T; | ||
headers: Headers; | ||
}>; | ||
/** | ||
@@ -131,2 +119,17 @@ * List all your namespaces. | ||
/** | ||
* Queries vectors and returns performance metrics along with the results. | ||
* See: https://turbopuffer.com/docs/reference/query | ||
*/ | ||
queryWithMetrics({ ...params }: { | ||
vector?: number[]; | ||
distance_metric?: DistanceMetric; | ||
top_k?: number; | ||
include_vectors?: boolean; | ||
include_attributes?: boolean | string[]; | ||
filters?: Filters; | ||
}): Promise<{ | ||
results: QueryResults; | ||
metrics: QueryMetrics; | ||
}>; | ||
/** | ||
* Export all vectors at full precision. | ||
@@ -133,0 +136,0 @@ * See: https://turbopuffer.com/docs/reference/list |
@@ -19,121 +19,36 @@ "use strict"; | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Namespace = exports.Turbopuffer = exports.TurbopufferError = void 0; | ||
const pako_1 = __importDefault(require("pako")); | ||
require("isomorphic-fetch"); | ||
const package_json_1 = require("../package.json"); | ||
/* Error type */ | ||
class TurbopufferError extends Error { | ||
constructor(error, { status }) { | ||
super(error); | ||
this.error = error; | ||
this.status = status; | ||
const httpClient_1 = require("./httpClient"); | ||
var httpClient_2 = require("./httpClient"); | ||
Object.defineProperty(exports, "TurbopufferError", { enumerable: true, get: function () { return httpClient_2.TurbopufferError; } }); | ||
function parseServerTiming(value) { | ||
let output = {}; | ||
const sections = value.split(", "); | ||
for (let section of sections) { | ||
let tokens = section.split(";"); | ||
let base_key = tokens.shift(); | ||
for (let token of tokens) { | ||
let components = token.split("="); | ||
let key = base_key + "." + components[0]; | ||
let value = components[1]; | ||
output[key] = value; | ||
} | ||
} | ||
return output; | ||
} | ||
exports.TurbopufferError = TurbopufferError; | ||
function parseIntMetric(value) { | ||
return value ? parseInt(value) : 0; | ||
} | ||
function parseFloatMetric(value) { | ||
return value ? parseFloat(value) : 0; | ||
} | ||
/* Base Client */ | ||
class Turbopuffer { | ||
constructor({ apiKey, baseUrl = "https://api.turbopuffer.com", }) { | ||
this.baseUrl = baseUrl; | ||
this.apiKey = apiKey; | ||
constructor({ apiKey, baseUrl = "https://api.turbopuffer.com", connectTimeout = 10 * 1000, // timeout to establish a connection | ||
connectionIdleTimeout = 60 * 1000, // socket idle timeout in ms, default 1 minute | ||
warmConnections = 0, // number of connections to open initially when creating a new client | ||
}) { | ||
this.http = (0, httpClient_1.createHTTPClient)(baseUrl, apiKey, connectTimeout, connectionIdleTimeout, warmConnections); | ||
} | ||
statusCodeShouldRetry(statusCode) { | ||
return statusCode >= 500; | ||
} | ||
delay(ms) { | ||
return new Promise((resolve) => setTimeout(resolve, ms)); | ||
} | ||
async doRequest({ method, path, query, body, compress, retryable, }) { | ||
const url = new URL(`${this.baseUrl}${path}`); | ||
if (query) { | ||
Object.keys(query).forEach((key) => { | ||
let value = query[key]; | ||
if (value) { | ||
url.searchParams.append(key, value); | ||
} | ||
}); | ||
} | ||
let headers = { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
"Accept-Encoding": "gzip", | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
Authorization: `Bearer ${this.apiKey}`, | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
"User-Agent": `tpuf-typescript/${package_json_1.version}`, | ||
}; | ||
if (body) { | ||
headers["Content-Type"] = "application/json"; | ||
} | ||
let requestBody = null; | ||
if (body && compress) { | ||
headers["Content-Encoding"] = "gzip"; | ||
requestBody = pako_1.default.gzip(JSON.stringify(body)); | ||
} | ||
else if (body) { | ||
requestBody = JSON.stringify(body); | ||
} | ||
const maxAttempts = retryable ? 3 : 1; | ||
var response; | ||
var error = null; | ||
for (let attempt = 0; attempt < maxAttempts; attempt++) { | ||
response = await fetch(url.toString(), { | ||
method, | ||
headers, | ||
body: requestBody, | ||
}); | ||
if (response.status >= 400) { | ||
let message = undefined; | ||
if (response.headers.get("Content-Type") === "application/json") { | ||
try { | ||
let body = await response.json(); | ||
if (body && body.status === "error") { | ||
message = body.error; | ||
} | ||
else { | ||
message = JSON.stringify(body); | ||
} | ||
} | ||
catch (_) { } | ||
} | ||
else { | ||
try { | ||
let body = await response.text(); | ||
if (body) { | ||
message = body; | ||
} | ||
} | ||
catch (_) { } | ||
} | ||
error = new TurbopufferError(message || response.statusText, { status: response.status }); | ||
} | ||
if (error && | ||
this.statusCodeShouldRetry(response.status) && | ||
attempt + 1 != maxAttempts) { | ||
await this.delay(150 * (attempt + 1)); // 150ms, 300ms, 450ms | ||
continue; | ||
} | ||
break; | ||
} | ||
if (error) { | ||
throw error; | ||
} | ||
if (!response.body) { | ||
return { | ||
headers: response.headers, | ||
}; | ||
} | ||
const json = await response.json(); | ||
if (json.status && json.status === "error") { | ||
throw new TurbopufferError(json.error || json, { | ||
status: response.status, | ||
}); | ||
} | ||
return { | ||
body: json, | ||
headers: response.headers, | ||
}; | ||
} | ||
/** | ||
@@ -144,3 +59,3 @@ * List all your namespaces. | ||
async namespaces({ cursor, page_size, }) { | ||
return (await this.doRequest({ | ||
return (await this.http.doRequest({ | ||
method: "GET", | ||
@@ -178,3 +93,3 @@ path: "/v1/vectors", | ||
const batch = vectors.slice(i, i + batchSize); | ||
await this.client.doRequest({ | ||
await this.client.http.doRequest({ | ||
method: "POST", | ||
@@ -195,3 +110,3 @@ path: `/v1/vectors/${this.id}`, | ||
async delete({ ids }) { | ||
await this.client.doRequest({ | ||
await this.client.http.doRequest({ | ||
method: "POST", | ||
@@ -213,3 +128,12 @@ path: `/v1/vectors/${this.id}`, | ||
var params = __rest(_a, []); | ||
return (await this.client.doRequest({ | ||
let resultsWithMetrics = await this.queryWithMetrics(params); | ||
return resultsWithMetrics.results; | ||
} | ||
/** | ||
* Queries vectors and returns performance metrics along with the results. | ||
* See: https://turbopuffer.com/docs/reference/query | ||
*/ | ||
async queryWithMetrics(_a) { | ||
var params = __rest(_a, []); | ||
let response = await this.client.http.doRequest({ | ||
method: "POST", | ||
@@ -219,3 +143,17 @@ path: `/v1/vectors/${this.id}/query`, | ||
retryable: true, | ||
})).body; | ||
}); | ||
const serverTimingStr = response.headers.get("Server-Timing"); | ||
const serverTiming = serverTimingStr | ||
? parseServerTiming(serverTimingStr) | ||
: {}; | ||
return { | ||
results: response.body, | ||
metrics: { | ||
approx_namespace_size: parseIntMetric(response.headers.get("X-turbopuffer-Approx-Namespace-Size")), | ||
cache_hit_ratio: parseFloatMetric(serverTiming["cache.hit_ratio"]), | ||
cache_temperature: serverTiming["cache.temperature"], | ||
processing_time: parseIntMetric(serverTiming["processing_time.dur"]), | ||
exhaustive_search_count: parseIntMetric(serverTiming["exhaustive_search.count"]), | ||
}, | ||
}; | ||
} | ||
@@ -227,3 +165,3 @@ /** | ||
async export(params) { | ||
let response = await this.client.doRequest({ | ||
const response = await this.client.http.doRequest({ | ||
method: "GET", | ||
@@ -244,3 +182,3 @@ path: `/v1/vectors/${this.id}`, | ||
async approxNumVectors() { | ||
let response = await this.client.doRequest({ | ||
const response = await this.client.http.doRequest({ | ||
method: "HEAD", | ||
@@ -250,3 +188,3 @@ path: `/v1/vectors/${this.id}`, | ||
}); | ||
let num = response.headers.get("X-turbopuffer-Approx-Num-Vectors"); | ||
const num = response.headers.get("X-turbopuffer-Approx-Num-Vectors"); | ||
return num ? parseInt(num) : 0; | ||
@@ -259,3 +197,3 @@ } | ||
async deleteAll() { | ||
await this.client.doRequest({ | ||
await this.client.http.doRequest({ | ||
method: "DELETE", | ||
@@ -271,3 +209,3 @@ path: `/v1/vectors/${this.id}`, | ||
async recall({ num, top_k, filters, queries, }) { | ||
return (await this.client.doRequest({ | ||
return (await this.client.http.doRequest({ | ||
method: "POST", | ||
@@ -298,5 +236,6 @@ path: `/v1/vectors/${this.id}/_debug/recall`, | ||
} | ||
let attributes = {}; | ||
const attributes = {}; | ||
vectors.forEach((vec, i) => { | ||
for (let [key, val] of Object.entries(vec.attributes || {})) { | ||
var _a; | ||
for (const [key, val] of Object.entries((_a = vec.attributes) !== null && _a !== void 0 ? _a : {})) { | ||
if (!attributes[key]) { | ||
@@ -315,4 +254,5 @@ attributes[key] = new Array(vectors.length).fill(null); | ||
function fromColumnar(cv) { | ||
let res = new Array(cv.ids.length); | ||
const attributeEntries = Object.entries(cv.attributes || {}); | ||
var _a; | ||
const res = new Array(cv.ids.length); | ||
const attributeEntries = Object.entries((_a = cv.attributes) !== null && _a !== void 0 ? _a : {}); | ||
for (let i = 0; i < cv.ids.length; i++) { | ||
@@ -319,0 +259,0 @@ res[i] = { |
{ | ||
"name": "@turbopuffer/turbopuffer", | ||
"version": "0.4.2", | ||
"version": "0.5.0", | ||
"description": "Official Typescript API client library for turbopuffer.com", | ||
@@ -9,3 +9,3 @@ "scripts": { | ||
"postinstall:workspaces": "npm run build", | ||
"test": "jest --config jest_node.config.js && jest --config jest_jsdom.config.js", | ||
"test": "jest --config jest_node.config.js", | ||
"format": "prettier --check . --ignore-path ./.gitignore", | ||
@@ -36,3 +36,2 @@ "format:fix": "prettier --check . --ignore-path ./.gitignore --write", | ||
"devDependencies": { | ||
"@types/isomorphic-fetch": "^0.0.39", | ||
"@types/jest": "^29.5.12", | ||
@@ -45,3 +44,2 @@ "@types/pako": "^2.0.3", | ||
"jest": "^29.7.0", | ||
"jest-environment-jsdom": "^29.7.0", | ||
"prettier": "^3.2.5", | ||
@@ -52,5 +50,5 @@ "ts-jest": "^29.1.2", | ||
"dependencies": { | ||
"isomorphic-fetch": "^3.0.0", | ||
"pako": "^2.1.0" | ||
"pako": "^2.1.0", | ||
"undici": "^6.13.0" | ||
} | ||
} |
@@ -13,2 +13,4 @@ The **official TypeScript SDK** for Turbopuffer. | ||
// Make a new client | ||
// Connections are pooled for the lifetime of the client | ||
// We recommend creating a single instance and reusing it for all calls | ||
const tpuf = new Turbopuffer({ | ||
@@ -15,0 +17,0 @@ apiKey: process.env.TURBOPUFFER_API_KEY as string, |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
42225
10
11
636
59
0
+ Addedundici@^6.13.0
+ Addedundici@6.21.0(transitive)
- Removedisomorphic-fetch@^3.0.0
- Removedisomorphic-fetch@3.0.0(transitive)
- Removednode-fetch@2.7.0(transitive)
- Removedtr46@0.0.3(transitive)
- Removedwebidl-conversions@3.0.1(transitive)
- Removedwhatwg-fetch@3.6.20(transitive)
- Removedwhatwg-url@5.0.0(transitive)