@langchain/community
Version:
Third-party integrations for LangChain.js
321 lines (319 loc) • 13 kB
JavaScript
import { __export } from "../_virtual/rolldown_runtime.js";
import { Document } from "@langchain/core/documents";
import { VectorStore } from "@langchain/core/vectorstores";
//#region src/vectorstores/prisma.ts
var prisma_exports = {};
__export(prisma_exports, { PrismaVectorStore: () => PrismaVectorStore });
const IdColumnSymbol = Symbol("id");
const ContentColumnSymbol = Symbol("content");
const OpMap = {
equals: "=",
in: "IN",
notIn: "NOT IN",
isNull: "IS NULL",
isNotNull: "IS NOT NULL",
like: "LIKE",
lt: "<",
lte: "<=",
gt: ">",
gte: ">=",
not: "<>"
};
/**
* A specific implementation of the VectorStore class that is designed to
* work with Prisma. It provides methods for adding models, documents, and
* vectors, as well as for performing similarity searches.
*/
var PrismaVectorStore = class PrismaVectorStore extends VectorStore {
tableName;
vectorColumnName;
selectColumns;
filter;
idColumn;
contentColumn;
columnTypes;
/**
* When true, addDocuments uses INSERT statements to create new records.
* When false (default), addDocuments uses UPDATE statements to update existing records by ID.
* Set to true when using with ParentDocumentRetriever or when documents don't pre-exist in the database.
*/
useInsert;
static IdColumn = IdColumnSymbol;
static ContentColumn = ContentColumnSymbol;
db;
Prisma;
_vectorstoreType() {
return "prisma";
}
constructor(embeddings, config) {
super(embeddings, {});
this.Prisma = config.prisma;
this.db = config.db;
const entries = Object.entries(config.columns);
const idColumn = entries.find((i) => i[1] === IdColumnSymbol)?.[0];
const contentColumn = entries.find((i) => i[1] === ContentColumnSymbol)?.[0];
if (idColumn == null) throw new Error("Missing ID column");
if (contentColumn == null) throw new Error("Missing content column");
this.idColumn = idColumn;
this.contentColumn = contentColumn;
this.tableName = config.tableName;
this.vectorColumnName = config.vectorColumnName;
this.columnTypes = config.columnTypes;
this.useInsert = config.useInsert ?? false;
this.selectColumns = entries.map(([key, alias]) => alias && key || null).filter((x) => !!x);
if (config.filter) this.filter = config.filter;
}
/**
* Creates a new PrismaVectorStore with the specified model.
* @param db The PrismaClient instance.
* @returns An object with create, fromTexts, and fromDocuments methods.
*/
static withModel(db) {
function create(embeddings, config) {
return new PrismaVectorStore(embeddings, {
...config,
db
});
}
async function fromTexts(texts, metadatas, embeddings, dbConfig) {
const docs = [];
for (let i = 0; i < texts.length; i += 1) {
const metadata = Array.isArray(metadatas) ? metadatas[i] : metadatas;
const newDoc = new Document({
pageContent: texts[i],
metadata
});
docs.push(newDoc);
}
return PrismaVectorStore.fromDocuments(docs, embeddings, {
...dbConfig,
db
});
}
async function fromDocuments(docs, embeddings, dbConfig) {
const instance = new PrismaVectorStore(embeddings, {
...dbConfig,
db
});
await instance.addDocuments(docs);
return instance;
}
return {
create,
fromTexts,
fromDocuments
};
}
/**
* Adds the specified models to the store.
* @param models The models to add.
* @returns A promise that resolves when the models have been added.
*/
async addModels(models) {
return this.addDocuments(models.map((metadata) => {
const pageContent = metadata[this.contentColumn];
if (typeof pageContent !== "string") throw new Error("Content column must be a string");
return new Document({
pageContent,
metadata
});
}));
}
/**
* Adds the specified documents to the store.
* @param documents The documents to add.
* @returns A promise that resolves when the documents have been added.
*/
async addDocuments(documents) {
const texts = documents.map(({ pageContent }) => pageContent);
const vectors = await this.embeddings.embedDocuments(texts);
if (this.useInsert) return this.addDocumentsWithVectors(vectors, documents);
return this.addVectors(vectors, documents);
}
/**
* Adds the specified vectors to the store.
* @param vectors The vectors to add.
* @param documents The documents associated with the vectors.
* @returns A promise that resolves when the vectors have been added.
*/
async addVectors(vectors, documents) {
const idColumnRaw = this.Prisma.raw(`"${this.idColumn}"`);
const tableNameRaw = this.Prisma.raw(`"${this.tableName}"`);
const vectorColumnRaw = this.Prisma.raw(`"${this.vectorColumnName}"`);
await this.db.$transaction(vectors.map((vector, idx) => {
const idValue = documents[idx].metadata[this.idColumn];
const columnType = this.columnTypes?.[this.idColumn];
let whereClause;
if (columnType === "uuid") whereClause = this.Prisma.sql`${idColumnRaw} = ${idValue}::uuid`;
else if (columnType === "integer") whereClause = this.Prisma.sql`${idColumnRaw} = ${idValue}::integer`;
else if (columnType === "bigint") whereClause = this.Prisma.sql`${idColumnRaw} = ${idValue}::bigint`;
else whereClause = this.Prisma.sql`${idColumnRaw} = ${idValue}`;
return this.db.$executeRaw(this.Prisma.sql`UPDATE ${tableNameRaw}
SET ${vectorColumnRaw} = ${`[${vector.join(",")}]`}::vector
WHERE ${whereClause}
`);
}));
}
/**
* Adds documents with their corresponding vectors to the store using INSERT statements.
* This method ensures documents are created if they don't exist, making it compatible
* with ParentDocumentRetriever which creates new child documents.
* @param vectors The vectors to add.
* @param documents The documents associated with the vectors.
* @returns A promise that resolves when the documents have been added.
*/
async addDocumentsWithVectors(vectors, documents) {
const tableNameRaw = this.Prisma.raw(`"${this.tableName}"`);
const vectorColumnRaw = this.Prisma.raw(`"${this.vectorColumnName}"`);
const columnNames = this.selectColumns.map((col) => this.Prisma.raw(`"${col}"`));
const allColumns = [...columnNames, vectorColumnRaw];
await this.db.$transaction(vectors.map((vector, idx) => {
const document = documents[idx];
const vectorString = `[${vector.join(",")}]`;
const columnValues = this.selectColumns.map((col) => {
if (col === this.contentColumn) return document.pageContent;
return document.metadata[col];
});
const allValues = [...columnValues, this.Prisma.sql`${vectorString}::vector`];
return this.db.$executeRaw(this.Prisma.sql`
INSERT INTO ${tableNameRaw} (${this.Prisma.join(allColumns, ", ")})
VALUES (${this.Prisma.join(allValues, ", ")})
`);
}));
}
/**
* Performs a similarity search with the specified query.
* @param query The query to use for the similarity search.
* @param k The number of results to return.
* @param _filter The filter to apply to the results.
* @param _callbacks The callbacks to use during the search.
* @returns A promise that resolves with the search results.
*/
async similaritySearch(query, k = 4, filter = void 0) {
const results = await this.similaritySearchVectorWithScore(await this.embeddings.embedQuery(query), k, filter);
return results.map((result) => result[0]);
}
/**
* Performs a similarity search with the specified query and returns the
* results along with their scores.
* @param query The query to use for the similarity search.
* @param k The number of results to return.
* @param filter The filter to apply to the results.
* @param _callbacks The callbacks to use during the search.
* @returns A promise that resolves with the search results and their scores.
*/
async similaritySearchWithScore(query, k, filter) {
return super.similaritySearchWithScore(query, k, filter);
}
/**
* Performs a similarity search with the specified vector and returns the
* results along with their scores.
* @param query The vector to use for the similarity search.
* @param k The number of results to return.
* @param filter The filter to apply to the results.
* @returns A promise that resolves with the search results and their scores.
*/
async similaritySearchVectorWithScore(query, k, filter) {
const vectorColumnRaw = this.Prisma.raw(`"${this.vectorColumnName}"`);
const tableNameRaw = this.Prisma.raw(`"${this.tableName}"`);
const selectRaw = this.Prisma.raw(this.selectColumns.map((x) => `"${x}"`).join(", "));
const vector = `[${query.join(",")}]`;
const articles = await this.db.$queryRaw(this.Prisma.join([
this.Prisma.sql`
SELECT ${selectRaw}, ${vectorColumnRaw} <=> ${vector}::vector as "_distance"
FROM ${tableNameRaw}
`,
this.buildSqlFilterStr(filter ?? this.filter),
this.Prisma.sql`
ORDER BY "_distance" ASC
LIMIT ${k};
`
].filter((x) => x != null), ""));
const results = [];
for (const article of articles) if (article._distance != null && article[this.contentColumn] != null) results.push([new Document({
pageContent: article[this.contentColumn],
metadata: article
}), article._distance]);
return results;
}
buildSqlFilterStr(filter) {
if (filter == null) return null;
return this.Prisma.join(Object.entries(filter).flatMap(([key, ops]) => Object.entries(ops).map(([opName, value]) => {
const opNameKey = opName;
const colRaw = this.Prisma.raw(`"${key}"`);
const opRaw = this.Prisma.raw(OpMap[opNameKey]);
const columnType = this.columnTypes?.[key];
switch (OpMap[opNameKey]) {
case OpMap.notIn:
case OpMap.in:
if (!Array.isArray(value)) throw new Error(`Invalid filter: IN operator requires an array. Received: ${JSON.stringify(value, null, 2)}`);
if (value.length === 0) {
const isInOperator = OpMap[opNameKey] === OpMap.in;
return this.Prisma.sql`${!isInOperator}`;
}
if (columnType === "uuid") {
const castedValues = value.map((v) => this.Prisma.sql`${v}::uuid`);
return this.Prisma.sql`${colRaw} ${opRaw} (${this.Prisma.join(castedValues)})`;
} else if (columnType === "integer") {
const castedValues = value.map((v) => this.Prisma.sql`${v}::integer`);
return this.Prisma.sql`${colRaw} ${opRaw} (${this.Prisma.join(castedValues)})`;
} else if (columnType === "bigint") {
const castedValues = value.map((v) => this.Prisma.sql`${v}::bigint`);
return this.Prisma.sql`${colRaw} ${opRaw} (${this.Prisma.join(castedValues)})`;
} else if (columnType === "jsonb") {
const castedValues = value.map((v) => {
const jsonValue = typeof v === "object" ? JSON.stringify(v) : v;
return this.Prisma.sql`${jsonValue}::jsonb`;
});
return this.Prisma.sql`${colRaw} ${opRaw} (${this.Prisma.join(castedValues)})`;
}
return this.Prisma.sql`${colRaw} ${opRaw} (${this.Prisma.join(value)})`;
case OpMap.isNull:
case OpMap.isNotNull: return this.Prisma.sql`${colRaw} ${opRaw}`;
default: if (columnType === "uuid") return this.Prisma.sql`${colRaw} ${opRaw} ${value}::uuid`;
else if (columnType === "integer") return this.Prisma.sql`${colRaw} ${opRaw} ${value}::integer`;
else if (columnType === "bigint") return this.Prisma.sql`${colRaw} ${opRaw} ${value}::bigint`;
else if (columnType === "jsonb") {
const jsonValue = typeof value === "object" ? JSON.stringify(value) : value;
return this.Prisma.sql`${colRaw} ${opRaw} ${jsonValue}::jsonb`;
} else if (columnType) return this.Prisma.sql`${colRaw} ${opRaw} ${value}::${this.Prisma.raw(columnType)}`;
else return this.Prisma.sql`${colRaw}::text ${opRaw} ${value}`;
}
})), " AND ", " WHERE ");
}
/**
* Creates a new PrismaVectorStore from the specified texts.
* @param texts The texts to use to create the store.
* @param metadatas The metadata for the texts.
* @param embeddings The embeddings to use.
* @param dbConfig The database configuration.
* @returns A promise that resolves with the new PrismaVectorStore.
*/
static async fromTexts(texts, metadatas, embeddings, dbConfig) {
const docs = [];
for (let i = 0; i < texts.length; i += 1) {
const metadata = Array.isArray(metadatas) ? metadatas[i] : metadatas;
const newDoc = new Document({
pageContent: texts[i],
metadata
});
docs.push(newDoc);
}
return PrismaVectorStore.fromDocuments(docs, embeddings, dbConfig);
}
/**
* Creates a new PrismaVectorStore from the specified documents.
* @param docs The documents to use to create the store.
* @param embeddings The embeddings to use.
* @param dbConfig The database configuration.
* @returns A promise that resolves with the new PrismaVectorStore.
*/
static async fromDocuments(docs, embeddings, dbConfig) {
const instance = new PrismaVectorStore(embeddings, dbConfig);
await instance.addDocuments(docs);
return instance;
}
};
//#endregion
export { PrismaVectorStore, prisma_exports };
//# sourceMappingURL=prisma.js.map