Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

langchain[patch]: Issue #2756 Add Qdrant custom payload on documents to query them by filter #3431

Merged
merged 12 commits into from
Dec 13, 2023
3 changes: 3 additions & 0 deletions langchain/src/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface DocumentInput<
pageContent: string;

metadata?: Metadata;
customPayload?: object;
}

/**
Expand All @@ -19,6 +20,8 @@ export class Document<

metadata: Metadata;

customPayload?: object | undefined;

constructor(fields: DocumentInput<Metadata>) {
this.pageContent = fields.pageContent
? fields.pageContent.toString()
Expand Down
29 changes: 23 additions & 6 deletions langchain/src/vectorstores/qdrant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,18 @@ export class QdrantVectorStore extends VectorStore {
* from the documents using the `Embeddings` instance and then adds the
* vectors to the database.
* @param documents Array of `Document` instances to be added to the Qdrant database.
* @param customPayload Optional 'object' in JSON format used to query database on more criteria
* @returns Promise that resolves when the documents have been added to the database.
*/
async addDocuments(documents: Document[]): Promise<void> {
async addDocuments(
documents: Document[],
customPayload?: object
): Promise<void> {
const texts = documents.map(({ pageContent }) => pageContent);
await this.addVectors(
await this.embeddings.embedDocuments(texts),
documents
documents,
customPayload
);
}

Expand All @@ -100,9 +105,14 @@ export class QdrantVectorStore extends VectorStore {
* database.
* @param vectors Array of vectors to be added to the Qdrant database.
* @param documents Array of `Document` instances associated with the vectors.
* @param customPayload Optional 'object' in JSON format used to query database on more criteria
* @returns Promise that resolves when the vectors have been added to the database.
*/
async addVectors(vectors: number[][], documents: Document[]): Promise<void> {
async addVectors(
vectors: number[][],
documents: Document[],
customPayload?: object
): Promise<void> {
if (vectors.length === 0) {
return;
}
Expand All @@ -115,6 +125,8 @@ export class QdrantVectorStore extends VectorStore {
payload: {
content: documents[idx].pageContent,
metadata: documents[idx].metadata,
...(documents[idx].customPayload ? documents[idx].customPayload != customPayload ?
documents[idx].customPayload : customPayload : customPayload),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you just do something like this?

Suggested change
documents[idx].customPayload : customPayload : customPayload),
documents[idx].customPayload ?? customPayload),

Not sure if this is valid though

},
}));

Expand Down Expand Up @@ -204,20 +216,23 @@ export class QdrantVectorStore extends VectorStore {
* @param metadatas Array or single object of metadata to be associated with the texts.
* @param embeddings `Embeddings` instance used to generate vectors from the texts.
* @param dbConfig `QdrantLibArgs` instance specifying the configuration for the Qdrant database.
* @param customPayload Optional 'object' in JSON format used to query database on more criteria
* @returns Promise that resolves with a new `QdrantVectorStore` instance.
*/
static async fromTexts(
texts: string[],
metadatas: object[] | object,
embeddings: Embeddings,
dbConfig: QdrantLibArgs
dbConfig: QdrantLibArgs,
customPayload? : object
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
customPayload? : object
customPayload?: object

nit

): Promise<QdrantVectorStore> {
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,
customPayload: customPayload,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
customPayload: customPayload,
customPayload,

nit

});
docs.push(newDoc);
}
Expand All @@ -230,15 +245,17 @@ export class QdrantVectorStore extends VectorStore {
* @param docs Array of `Document` instances to be added to the Qdrant database.
* @param embeddings `Embeddings` instance used to generate vectors from the documents.
* @param dbConfig `QdrantLibArgs` instance specifying the configuration for the Qdrant database.
* @param customPayload Optional 'object' in JSON format used to query database on more criteria
* @returns Promise that resolves with a new `QdrantVectorStore` instance.
*/
static async fromDocuments(
docs: Document[],
embeddings: Embeddings,
dbConfig: QdrantLibArgs
dbConfig: QdrantLibArgs,
customPayload?: object
): Promise<QdrantVectorStore> {
const instance = new this(embeddings, dbConfig);
await instance.addDocuments(docs);
await instance.addDocuments(docs, customPayload);
return instance;
}

Expand Down
100 changes: 100 additions & 0 deletions langchain/src/vectorstores/tests/qdrant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,103 @@ test("QdrantVectorStore works", async () => {

expect(results).toHaveLength(0);
});

test("QdrantVectorStore adds vectors with custom payload", async () => {
// Mock Qdrant client
const client = {
upsert: jest.fn(),
search: jest.fn<any>().mockResolvedValue([]),
getCollections: jest.fn<any>().mockResolvedValue({ collections: [] }),
createCollection: jest.fn(),
};

// Mock embeddings
const embeddings = new FakeEmbeddings();

// Create QdrantVectorStore instance with the mock client
const qdrantVectorStore = new QdrantVectorStore(embeddings, {
client: client as any,
});

// Define a custom payload
const customPayload = {
customField1: "value1",
customField2: "value2",
};

// Add documents with custom payload
await qdrantVectorStore.addDocuments(
[
{
pageContent: "hello",
metadata: {},
},
],
customPayload
);

// Verify that the Qdrant client's upsert method was called with the correct arguments
expect(client.upsert).toHaveBeenCalledTimes(1);
expect(client.upsert).toHaveBeenCalledWith("documents", {
wait: true,
points: [
expect.objectContaining({
payload: expect.objectContaining({
content: "hello",
metadata: {},
...customPayload,
}),
}),
],
});
});

test("QdrantVectorStore adds vectors with custom payload in Document", async () => {
// Mock Qdrant client
const client = {
upsert: jest.fn(),
search: jest.fn<any>().mockResolvedValue([]),
getCollections: jest.fn<any>().mockResolvedValue({ collections: [] }),
createCollection: jest.fn(),
};

// Mock embeddings
const embeddings = new FakeEmbeddings();

// Create QdrantVectorStore instance with the mock client
const qdrantVectorStore = new QdrantVectorStore(embeddings, {
client: client as any,
});

// Define a custom payload
const customPayload = {
customField1: "value1",
customField2: "value2",
};

// Add documents with custom payload
await qdrantVectorStore.addDocuments(
[
{
pageContent: "hello",
metadata: {},
customPayload: customPayload
},
],
);

// Verify that the Qdrant client's upsert method was called with the correct arguments
expect(client.upsert).toHaveBeenCalledTimes(1);
expect(client.upsert).toHaveBeenCalledWith("documents", {
wait: true,
points: [
expect.objectContaining({
payload: expect.objectContaining({
content: "hello",
metadata: {},
...customPayload,
}),
}),
],
});
});