@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
JavaScript
/**
* 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);
}
}