@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
281 lines (280 loc) • 11.6 kB
JavaScript
/**
* Chroma vector store adapter
*
* Implements the `VectorStore` contract (see `src/lib/types/rag.ts`) against
* a Chroma collection. This module has zero runtime dependency on the
* `chromadb` package: callers construct their own `ChromaClient` (or any
* compatible object) and inject it here. The structural interfaces below
* mirror the subset of the `chromadb` API this adapter relies on
* (`getOrCreateCollection`, `collection.upsert/query/delete`).
*
* ## Distance -> similarity scoring
*
* Chroma returns *distances*, not similarities, and the meaning of that
* distance depends entirely on the collection's configured `hnsw:space`
* (this adapter does not — and cannot — set that; it is chosen when the
* collection is created on the Chroma side). `distanceMetric` tells this
* adapter which space the collection uses so it can invert the distance
* into a `score` consistent with `InMemoryVectorStore` (higher = more
* similar):
*
* - `"cosine"` (default): Chroma returns `distance = 1 - cosineSimilarity`,
* so `score = 1 - distance` reproduces `InMemoryVectorStore`'s cosine
* score exactly.
* - `"ip"`: Chroma returns `distance = 1 - dot(a, b)`, so
* `score = 1 - distance` yields the raw dot product — only equal to
* cosine similarity if both vectors are unit-normalized.
* - `"l2"`: Chroma returns squared Euclidean distance, range
* `[0, Infinity)`, which has no exact inverse into a cosine-similarity
* scale. It is converted via `score = 1 / (1 + distance)`, a
* monotonically decreasing-in-distance map into `(0, 1]`. This preserves
* result ordering but is NOT numerically comparable to cosine scores.
*
* Passing a `distanceMetric` that does not match the collection's actual
* `hnsw:space` will silently produce meaningless scores (Chroma has no way
* to report its configured space back to the client), so callers must keep
* the two in sync.
*
* ## Metadata round-tripping
*
* Chroma metadata values must be strings, numbers, or booleans. Values that
* are `undefined` are dropped (Chroma rejects `undefined`); any other
* non-primitive value (object, array, `null`) is `JSON.stringify`-ed on
* write. This is lossy: such values come back as JSON-encoded strings on
* read, not their original type. Store only primitive metadata values if
* you need exact round-tripping.
*
* ## indexName -> collection mapping
*
* `indexName` is used verbatim as the Chroma collection name (1:1,
* lazily created via `getOrCreateCollection` and cached for the lifetime of
* this adapter instance).
*/
// Structural Chroma client/collection types (`ChromaClientLike`,
// `ChromaCollectionLike`, `ChromaUpsertParams`, etc.) live in
// `src/lib/types/vectorStoreChroma.ts` per this repo's canonical-type-
// location rule (see CLAUDE.md Critical Rules 2 & 12) and are imported from
// the types barrel above. This file only re-exports runtime values
// (`ChromaVectorStore`, `translateMetadataFilter`) — consumers who need the
// structural types should import them from `@juspay/neurolink` (root) or
// the types barrel directly, per Critical Rule 13.
// ---------------------------------------------------------------------------
// MetadataFilter -> Chroma `where` translation
// ---------------------------------------------------------------------------
const CHROMA_SUPPORTED_FIELD_OPS = new Set([
"$eq",
"$ne",
"$gt",
"$gte",
"$lt",
"$lte",
"$in",
"$nin",
]);
const CHROMA_UNSUPPORTED_FIELD_OP_REASONS = {
$exists: 'Chroma metadata filtering has no existence check on the "where" clause.',
$contains: 'Chroma metadata filtering has no substring-match operator on the "where" clause (Chroma\'s substring matching only applies to document text via "where_document", not metadata field values).',
$regex: 'Chroma metadata filtering has no regular-expression operator on the "where" clause.',
$size: 'Chroma metadata filtering has no array-length operator on the "where" clause.',
};
function unsupportedFieldOpError(opKey, field) {
const reason = CHROMA_UNSUPPORTED_FIELD_OP_REASONS[opKey] ??
`Chroma metadata filtering does not support the "${opKey}" operator on the "where" clause.`;
return new Error(`Unsupported operator "${opKey}" (field "${field}"): ${reason}`);
}
/**
* Translates a field-level filter value (either a direct-equality primitive
* or an operator bag) into a Chroma `where` clause fragment for that field.
*
* Returns `undefined` when the filter value carries no enforceable
* constraint (an array, or an operator bag with no recognized keys) — this
* mirrors `InMemoryVectorStore.matchesFilter`, which treats any non-null
* object value as an "operator bag" and silently applies none of its checks
* when no recognized `$`-key is present, i.e. the field matches
* unconditionally. Omitting the field from the Chroma `where` clause
* reproduces that same "always matches" behavior.
*/
function translateFieldFilter(field, value) {
if (Array.isArray(value)) {
return undefined;
}
if (value === null || typeof value !== "object") {
return { [field]: { $eq: value } };
}
const ops = value;
const opKeys = Object.keys(ops);
if (opKeys.length === 0) {
return undefined;
}
const chromaOps = {};
for (const opKey of opKeys) {
if (!CHROMA_SUPPORTED_FIELD_OPS.has(opKey)) {
throw unsupportedFieldOpError(opKey, field);
}
chromaOps[opKey] = ops[opKey];
}
return { [field]: chromaOps };
}
function translateLogicalOperator(key, value) {
switch (key) {
case "$and":
return {
$and: value.map(translateMetadataFilter),
};
case "$or":
return {
$or: value.map(translateMetadataFilter),
};
case "$not":
throw new Error('Chroma metadata filtering has no negation operator on the "where" clause; "$not" cannot be translated. Rewrite the filter using $ne/$nin instead.');
case "$nor":
throw new Error('"$nor" is declared on MetadataFilter but is not implemented by InMemoryVectorStore either, and Chroma has no negation operator on the "where" clause. Rewrite using $and of $ne/$nin clauses.');
default:
throw new Error(`Unknown logical operator "${key}" in MetadataFilter.`);
}
}
/**
* Translates a `MetadataFilter` into a Chroma `where` clause.
*
* Throws a clear error for any operator Chroma's metadata `where` clause
* cannot express ($not, $nor, $exists, $contains, $regex, $size) rather than
* silently dropping the constraint and mis-filtering results.
*/
export function translateMetadataFilter(filter) {
const clauses = [];
for (const [key, value] of Object.entries(filter)) {
const clause = key.startsWith("$")
? translateLogicalOperator(key, value)
: translateFieldFilter(key, value);
if (clause !== undefined) {
clauses.push(clause);
}
}
if (clauses.length === 0) {
return {};
}
if (clauses.length === 1) {
return clauses[0];
}
// Chroma requires a single top-level operator; combine multiple field/
// logical clauses with an explicit $and (mirrors matchesFilter ANDing
// every top-level entry).
return { $and: clauses };
}
// ---------------------------------------------------------------------------
// Metadata <-> Chroma metadata conversion
// ---------------------------------------------------------------------------
function toChromaMetadataValue(value) {
if (typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean") {
return value;
}
// Chroma metadata values must be string | number | boolean. Non-primitive
// values (object, array, null) are JSON-stringified — lossy on read back,
// see module doc comment.
return JSON.stringify(value);
}
function toChromaMetadata(metadata) {
const result = {};
for (const [key, value] of Object.entries(metadata)) {
if (value === undefined) {
// Chroma rejects `undefined` metadata values outright.
continue;
}
result[key] = toChromaMetadataValue(value);
}
return result;
}
function distanceToScore(distance, metric) {
switch (metric) {
case "cosine":
case "ip":
return 1 - distance;
case "l2":
return 1 / (1 + distance);
default: {
const exhaustive = metric;
throw new Error(`Unknown Chroma distance metric: ${String(exhaustive)}`);
}
}
}
// ---------------------------------------------------------------------------
// ChromaVectorStore
// ---------------------------------------------------------------------------
export class ChromaVectorStore {
client;
collections = new Map();
distanceMetric;
constructor(client, options = {}) {
this.client = client;
this.distanceMetric = options.distanceMetric ?? "cosine";
}
getCollection(indexName) {
let collection = this.collections.get(indexName);
if (!collection) {
collection = this.client.getOrCreateCollection({ name: indexName });
this.collections.set(indexName, collection);
}
return collection;
}
/**
* Add or update vectors in an index (Chroma collection).
*/
async upsert(indexName, items) {
if (items.length === 0) {
return;
}
const collection = await this.getCollection(indexName);
await collection.upsert({
ids: items.map((item) => item.id),
embeddings: items.map((item) => item.vector),
metadatas: items.map((item) => toChromaMetadata(item.metadata ?? {})),
});
}
/**
* Query vectors by similarity.
*/
async query(params) {
const { indexName, queryVector, topK = 10, filter, includeVectors = false, } = params;
const collection = await this.getCollection(indexName);
const include = ["metadatas", "distances"];
if (includeVectors) {
include.push("embeddings");
}
const response = await collection.query({
queryEmbeddings: [queryVector],
nResults: topK,
...(filter ? { where: translateMetadataFilter(filter) } : {}),
include,
});
const ids = response.ids[0] ?? [];
const metadatasRow = response.metadatas?.[0] ?? [];
const distancesRow = response.distances?.[0] ?? [];
const embeddingsRow = response.embeddings?.[0] ?? [];
return ids.map((id, i) => {
const metadata = (metadatasRow[i] ?? {});
const distance = distancesRow[i];
const score = distance === null || distance === undefined
? undefined
: distanceToScore(distance, this.distanceMetric);
return {
id,
score,
text: metadata.text,
metadata,
...(includeVectors ? { vector: embeddingsRow[i] ?? [] } : {}),
};
});
}
/**
* Delete vectors from an index (Chroma collection) by id.
*/
async delete(indexName, ids) {
if (ids.length === 0) {
return;
}
const collection = await this.getCollection(indexName);
await collection.delete({ ids });
}
}