UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

159 lines (158 loc) • 6.79 kB
/** * Pinecone Vector Store Adapter * * Implements the `VectorStore` contract (see `src/lib/types/rag.ts`) against * a Pinecone index, using client injection: callers construct their own * `@pinecone-database/pinecone` client/index and pass it in. No Pinecone SDK * is a runtime dependency of this package — `PineconeIndexLike` below is a * minimal structural interface satisfied by the real `Index` object (and by * hand-written mocks in tests). * * ## indexName -> namespace mapping * * The `VectorStore` contract's `indexName` parameter is mapped onto a * Pinecone **namespace**, not a Pinecone index. Pinecone ties one client * `Index` object to a single physical index (created ahead of time via * Pinecone's control-plane API); namespaces are Pinecone's mechanism for * partitioning data *within* that one index, and are the closest match to * this SDK's per-call `indexName` concept. * * - If the injected client exposes `.namespace(name)` (as the real * `@pinecone-database/pinecone` `Index.namespace()` does), every * query/upsert/delete call routes through `client.namespace(indexName)`. * - If the injected client does not expose `.namespace` (e.g. a caller * already pre-scoped their client to a fixed namespace, or a minimal * mock), calls go straight to the injected client and `indexName` has * no further effect. Document this for your callers if you construct a * `PineconeVectorStore` from a namespace-fixed client. * * ## Metadata filter translation * * `MetadataFilter` (Mongo/Sift-style) is translated to Pinecone's native * filter DSL, which already shares the same `$op` shape for comparison * operators. Supported operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, * `$lte`, `$in`, `$nin` (field-level) and `$and` / `$or` (logical, * recursively translated). Any other operator — `$not`, `$nor`, `$exists`, * `$contains`, `$regex`, `$size` — has no Pinecone equivalent and throws a * clear `Error` naming the offending operator/field rather than silently * mis-filtering (Pinecone's filter DSL has no negation, existence, string * containment, regex, or array-length operators). */ /** Comparison operators Pinecone's filter DSL can express, verbatim. */ const SUPPORTED_COMPARISON_OPS = new Set([ "$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in", "$nin", ]); /** Logical operators Pinecone's filter DSL can express, verbatim. */ const SUPPORTED_LOGICAL_OPS = new Set(["$and", "$or"]); /** * Translate a `MetadataFilter` into Pinecone's native filter object. * * Throws a descriptive `Error` for any operator Pinecone cannot express * ($not, $nor, $exists, $contains, $regex, $size) instead of silently * dropping or mis-applying it. */ export function translatePineconeFilter(filter) { const result = {}; for (const [key, value] of Object.entries(filter)) { if (key.startsWith("$")) { if (!SUPPORTED_LOGICAL_OPS.has(key)) { throw new Error(`PineconeVectorStore: MetadataFilter operator "${key}" cannot be translated to Pinecone's filter DSL ` + `(supported logical operators: ${[...SUPPORTED_LOGICAL_OPS].join(", ")}). ` + `Restructure the filter to avoid "${key}".`); } const subFilters = value; result[key] = subFilters.map((sub) => translatePineconeFilter(sub)); continue; } if (typeof value === "object" && value !== null && !Array.isArray(value)) { const ops = value; const translatedOps = {}; for (const opKey of Object.keys(ops)) { if (!SUPPORTED_COMPARISON_OPS.has(opKey)) { throw new Error(`PineconeVectorStore: MetadataFilter operator "${opKey}" on field "${key}" cannot be translated to ` + `Pinecone's filter DSL (supported: ${[...SUPPORTED_COMPARISON_OPS].join(", ")}). ` + `Restructure the filter to avoid "${opKey}" on "${key}".`); } translatedOps[opKey] = ops[opKey]; } result[key] = translatedOps; } else { // Primitive/array/null value: Pinecone treats a bare value as // implicit equality, matching InMemoryVectorStore's direct-equality // fallback for non-object filter values. result[key] = value; } } return result; } /** * Pinecone-backed implementation of the `VectorStore` contract. * * @example * ```typescript * import { Pinecone } from '@pinecone-database/pinecone'; * import { PineconeVectorStore } from '@juspay/neurolink/rag'; * * const client = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! }); * const store = new PineconeVectorStore(client.index('my-index')); * * await store.upsert('tenant-a', [{ id: '1', vector: [...], metadata: { text: 'hello' } }]); * const results = await store.query({ indexName: 'tenant-a', queryVector: [...], topK: 5 }); * ``` */ export class PineconeVectorStore { client; constructor(client) { this.client = client; } /** Resolve the Pinecone client scoped to `indexName` (see class docs re: namespace mapping). */ resolveClient(indexName) { return this.client.namespace ? this.client.namespace(indexName) : this.client; } async query(params) { const { indexName, queryVector, topK = 10, filter, includeVectors = false, } = params; const target = this.resolveClient(indexName); const response = await target.query({ vector: queryVector, topK, ...(filter ? { filter: translatePineconeFilter(filter) } : {}), includeMetadata: true, includeValues: includeVectors, }); const matches = response.matches ?? []; return matches.map((match) => { const metadata = match.metadata ?? {}; return { id: match.id, score: match.score, text: metadata.text, metadata, ...(includeVectors && match.values ? { vector: match.values } : {}), }; }); } /** Add or update vectors in the namespace mapped from `indexName`. */ async upsert(indexName, items) { const target = this.resolveClient(indexName); await target.upsert(items.map((item) => ({ id: item.id, values: item.vector, metadata: item.metadata ?? {}, }))); } /** Delete vectors by id from the namespace mapped from `indexName`. */ async delete(indexName, ids) { const target = this.resolveClient(indexName); await target.deleteMany(ids); } }