Enabling Upserts For VectorStore (Using Langchain Code Node)

This is a quick tutorial on how you can use the rarely mentioned Langchain Code Node to support upserts for your favourite vectorstore.
Disclaimer: Iā€™m still trying to wrap my head around this node so this might not be the best/recommend way for achieve this. Feedback is very welcome. Use at your own peril!

Background

At time of writing, n8nā€™s Vectorstore nodes do not support upserts because you canā€™t define IDs to go with your embeddings. This means youā€™ll get duplicate vector documents if you try to run the same content through a second time. Not an issue if you are able to clear the vectorstore when you insertā€¦ but what if you just canā€™t and only want to update a few specific documents at a time? If this is you, then using the Langchain Code Node is one way to achieve this.

Prerequisites

  • Self-hosted n8n. The Langchain Code Node is only available on the self-hosted version.
  • Ability to set NODE_FUNCTION_ALLOW_EXTERNAL environmental variable. For this tutorial, you kinda need this to access the Pinecone client library. I suspect the same to be true for other vectorstore services.
    • For this tutorial, youā€™ll need to set the following: NODE_FUNCTION_ALLOW_EXTERNAL=@pinecone-database/pinecone
  • A Vectorstore that supports upserts. I think all the major ones supported by Langchain do but no harm in mentioning it here.
  • Youā€™re not afraid of a little code. Iā€™ve attached the template below so you can copy/paste the code as is but if thatā€™s not enough, Iā€™m happy to answer any questions in this thread or can offer paid support for more custom requirements.

Step 1. Add the Langchain Code Node

The Langchain Code Node is an advanced node designed to fill in for functionality n8n isnā€™t currently supporting right now. As such, itā€™s pretty raw, light on documentation and intended for the technically inclined - especially those who have used Langchain outside of n8n.

  • In your workflow, open the nodes sidepanel.
  • Select Advanced AI ā†’ Other AI Nodes ā†’ Miscellaneous ā†’ Langchain Code
  • The Langchain Code Node should open in edit mode but if not, you can double click the node to bring up its editor.
  • Under Inputs,
    • Add an input with type ā€œMainā€, max connections set to ā€œ1ā€ and required set to ā€œtrueā€
    • Add an input with type ā€œEmbeddingā€, max connections set to ā€œ1ā€ and required set to ā€œtrueā€
  • Under Outputs, add an output with type ā€œmainā€.
  • Go back to the canvas.
  • On the Langchain code node you just created, add an Embedding Subnode. Iā€™ve gone with OpenAI Embeddings but you can just any you like. We do this to save on writing extra code for this later.

Step 2. Writing the Langchain Code

Now the fun part! For this tutorial, weā€™ve set up a scenario where we want to vectorise a webpage to power our website search. The previous node supplies the webpage URL and our Langchain Code node will load and vectorise the webpageā€™s contents into our Pinecone Vectorstore. Itā€™s a good use-case for using upserts because some webpages change often whilst others do not. We will be able to make frequent updates to this webpageā€™s vectors without duplicates or rebuilding the entire index. Sweet!

  • Open the Langchain Code Node in edit mode again.
  • Under Code ā†’ Add Code, select the Execute option.
    • Tip: ā€œExecuteā€ for main node, ā€œSupply Dataā€ for subnodes.
  • In the Javascript - Execute textarea, weā€™ll enter the following code.
    • Be sure to change <MY_API_KEY>, <MY_PINECONE_INDEX> and <MY_PINECONE_NAMESPACE> before running the code!
// 1. Get node inputs
const inputData = this.getInputData();
const embeddingsInput = await this.getInputConnectionData('ai_embedding', 0);

// 2. Setup Pinecone
const { PineconeStore } = require('@langchain/pinecone');
const { Pinecone } = require('@pinecone-database/pinecone');
const pinecone = new Pinecone({ apiKey: '<MY_API_KEY>' });
const pineconeIndex = pinecone.Index('<MY_PINECONE_INDEX>');
const pineconeNamespace = '<MY_PINECONE_NAMESPACE>';

const vectorStore = new PineconeStore(embeddingsInput, {
  namespace: pineconeNamespace || undefined,
  pineconeIndex,
});

// 3. load webpage url
const url = $json.url; // "https://docs.n8n.io/learning-path/"
const { CheerioWebBaseLoader } = require("langchain/document_loaders/web/cheerio");
const loader = new CheerioWebBaseLoader(url, { selector: '.md-content' });
const webpageContents = await loader.load();

// 4. initialise a text splitter (optional)
const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter");
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 0,
});

// 5. Create smaller docs to vectorise (optional)
// - Depends on your use-case: smaller docs are usually preferred for RAG applications.
const docs = [];
for (contents of webpageContents) {
  const { pageContent, metadata } = contents;
  const cleanContent = pageContent.replaceAll('  ', '').replaceAll('\n', ' ');
  const fragments = await splitter.createDocuments([cleanContent]);
  docs.push(...fragments.map((fragment, idx) => {
    fragment.metadata = { ...fragment.metadata, ...metadata };
    
    // 5.1 Our IDs look like this "https://docs.n8n.io/learning-path/|0", "https://docs.n8n.io/learning-path/|1"
    // but is only specific to this tutorial, use whatever suits you but make sure IDs are unique!
    fragment.id = `${metadata.source}|${idx}`;
    return fragment;
  }));
};

// 6. Define IDs to enable upserts.
// - You can now run this as many times without worrying about duplicates!
const ids = docs.map(doc => `${doc.id}`);
await vectorStore.addDocuments(docs, ids);

// 7. Return for further processing
return docs.map(doc => ({ json: doc }));

Step 3. Weā€™re Done!

Weā€™ve now successfully built our own custom Vectorstore node which supports upserts :raised_hands:! Pretty rad if you ask me. I think Iā€™ll experiment a bit more with Langchain Code node and see what other fun things itā€™ll allow me to doā€¦ until next time!

  • This code can be modified to work with other popular vectorstore such as PgVector, Redis, Qdrant, Chroma etc. Change the client library (and remember to add it to NODE_FUNCTION_ALLOW_EXTERNAL)
  • Unfortunately, it doesnā€™t seem like you can access crendentials from inside the node so youā€™ll either have to hardcode your API keys/tokens as weā€™ve done here or pass them through via variables maybe?
  • The document to vectorise doesnā€™t neccessarily need to be loaded in the Langchain Code. You can bring it in through previous nodes and use this.getInputData() to access it.

Cheers,
Jim
Follow me on LinkedIn or Twitter.


Demo Template

thumbnail

upserts

8 Likes

Awesome @Jim_Le - Thanks so much for sharing, I havenā€™t thought about that way. It is a nice workaround to upsert. I just created this workflow on my localhost n8n and will start playing around with it.

Did you make any updates or came across any new thoughts since you posted this in June?

@ridingthedragon Thanks!
You may not need to use this hack as there is a planned update to the Pinecone and Supabase vector store nodes to allow for updating/upserting built-in. So definitely watch out for this in the upcoming releases.

If youā€™re using something like Qdrant however, this is probably still a good technique to learn. Learning more about how n8n works under the hood since June, I now recommend the following way to achieve the same but with the following advantages:

  1. A lot less code by using existing nodes
  2. Safer than hardcoding/exposing credentials by piggy-backing off the existing vector store node.

@Jim_Le Ah, thatā€™s good to know about pinecone and subabase. Are you involved in developing these nodes?

Re: qadrant. I actually use it in my localhost setting as you can self-host the qdrant vectorstore. So for the ā€œprivacy-firstā€ setting, your hack still stays relevant.

And the updated design is elegant. Thanks for sharing.
Iā€™ve been playing around with the HTML node (instead of cheerio) to give users a simpler way to define which parts of a website to upsert.
But I guess a more robust setting would be done with puppeteer or similar web scraping libraries.

Iā€™ll play around with this and share once I have something useful.

Hi, Jim. According to Qdrant docs to make upserts you just need to set the same ID. But I donā€™t see how can I set ID in n8n. Is it even possible without custom code?

yes, that is possible by setting metadata with expression values

but what I am missing here @Jim_Le, is the use case that most of us have:

we want to check the pageā€™s lastmod datetime to check if we need to upsert at all, so all we need is some logic sandbox in the vector store node that allows us to do so

@Alex5 If the way the document means to do it, no itā€™s not possible outside of custom code. But @Morriz solution is definitely the next best thing. I did post a short bit on why I think you should avoid upserting if possible How to set ID in Qdrant points in n8n - #3 by Jim_Le

@Morriz Hmm I wonder if doing this logic check outside the vector store would be a better idea? For example and assuming youā€™re working with websites, periodically downloading the sitemap.xml and tracking diffs between each fetch, isolating the pages which have changed and then running the upserts.

Yes, that is what I am doing now. (Actually I delete and insert as n8n has no notion of upset as it has no access to ids.)

I have an idempotent workflow that I will share here soon after cleaning it up :wink:

1 Like

yes, that is possible by setting metadata with expression values

So what I said earlier is actually NOT possible as id is not in metadata

@Jim_Le here is a full example to get WooCommerce products and Wordpress pages into Qdrant with an idempotent workflow. This allows for a cronjob to trigger the workflow:

As you can see it requires a lot of plumbing, so a UI solution in the n8n vectorstore node would be preferable :wink:

@Morriz thanks for sharing.

I donā€™t have the full specifications or the usual number of products so forgive if I assume too much but for this type of workflow, personally I wouldnā€™t bother with the upsert. Just clear the vector store and reinsert everything. Reason being if pages or products are removed/deleted, itā€™s likely your vector store is going to get out of sync.

But hereā€™s an alternative implementation which uses redis instead to keep track of modified items. It does add another component to the stack but you can also use the excellent KV storage community node instead.

@Jim_Le maybe you can help me.
I need to solve a similar problem about inserting into a database.
I loaded a RAG model with supabase.

Iā€™m trying to switch the supabase node to postgres node, but I donā€™t know how to execute a function on the postgres node.
Maybe you can show me a way.

Supabase
Delete
metadata->>file_id=like.{{ $json.file_id }}

Insert
match_documents

Read
match_documents

Postgres
Delete
DELETE FROM documents
WHERE metadata->>ā€˜file_idā€™ ILIKE ā€˜%ā€™ || ā€˜{{ $json.file_id }}ā€™ || ā€˜%ā€™;

Insert
I left the ā€œmetadataā€ function blank

Read
I donā€™t know how to indicate the ā€œmatchā€ query

Hey @brauliodiasribeiro

Would it be okay if you post a new topic for your question? That way Iā€™m sure youā€™ll get more of the community able to help with your issue.

Thanks!

Yes, sureā€¦tks for your attention.
For those who want to follow

This helped me a lot! Pretty elegant and simple.

Using this method, I created and automation where I crawl my websiteā€™s URLs, check lastmod info on sitemap, and if lastmod is newer than the last one, I execute the Upsert workflow.

Iā€™ll publish the final version of my worklows as a template and share them here.

2 Likes