ReadNext: AI Content Recommendations for Node JS

Recently I posted about AI Content Recommendations with TypeScript, which concluded by introducing a new NPM package I've been working on called ReadNext. This post is dedicated to ReadNext, and will go into more detail about how to use ReadNext in Node JS, React, and other JavaScript projects.

What it is

ReadNext is a Node JS package that uses AI to generate content recommendations. It's designed to be easy to use, and can be integrated into any Node JS project with just a few lines of code. It is built on top of LangChain, and delegates to an LLM of your choice for summarizing your content to generate recommendations. It runs locally, does not require you to deploy anything, and has broad support for a variety of content types and LLM providers.

Watch a 3 minute video on how to use ReadNext

ReadNext is not an AI itself, nor does it want your money, your data or your soul. It's just a library that makes it easy to find related content for developers who use JavaScript as their daily driver. It's best used at build time, and can be integrated into your CI/CD pipeline to generate recommendations for your content as part of your build process.

How to use it

Get started in the normal way:

npm install read-next
npm install read-next

Configure a ReadNext instance:

import { ReadNext } from 'read-next'

const readNext = await ReadNext.create({
cacheDir: path.join(__dirname, 'read-next'),
parallel: 10
})
import { ReadNext } from 'read-next'

const readNext = await ReadNext.create({
cacheDir: path.join(__dirname, 'read-next'),
parallel: 10
})

Index your content:

await readNext.index({
sourceDocuments: [
{
pageContent: 'This is an article about React Server Components',
id: 'rsc'
},
{
pageContent: 'This is an article about React Hooks',
id: 'hooks'
},
//... as many as you like
]
})
await readNext.index({
sourceDocuments: [
{
pageContent: 'This is an article about React Server Components',
id: 'rsc'
},
{
pageContent: 'This is an article about React Hooks',
id: 'hooks'
},
//... as many as you like
]
})

Generate recommendations:

const related = await readNext.suggest({
sourceDocument: {id: 'rsc'},
limit: 5
})
const related = await readNext.suggest({
sourceDocument: {id: 'rsc'},
limit: 5
})

That's it! Under the covers, ReadNext creates embeddings for your content - after first running it through a summarization process - then stores the embeddings in a FAISS vector store. This allows it to keep a local cache of the work it has done, and to quickly generate recommendations for your content.

Full Example usage in a React application

The RSC Examples app uses ReadNext
The RSC Examples app uses ReadNext

I use ReadNext on this blog to generate related content recommendations for each post. It's a Next JS app, so I run ReadNext as part of the build process to generate recommendations for each post. The recommendations are stored in the frontmatter of each post (I use .mdx files for the blog content), and displayed at the bottom of each post.

It's also being used inside my RSC Examples project, which is an open source collection of examples of how to use React Server Components in various contexts. Each example has some explanatory text and code snippets, along with a live example, but even though that's not a traditional "article" per se, ReadNext is flexible enough to work with it.

Here's the actual script that RSC Examples uses to generate related examples for each example:

related.tsx
import path from 'path'
import { ReadNext } from 'read-next'

import Examples, { Example } from '../lib/examples'

const summarizationPrompt = `
The following content is a markdown document about an example of how to use React Server
Components. It contains sections of prose explaining what the example is about, may contain
links to other resources, and almost certainly contains code snippets.

Your goal is to generate a summary of the content that can be used to suggest related examples.
The summary will be used to create embeddings for a vector search. When you come across code
samples, please summarize the code in natural language.

Do not reply with anything except your summary of the example.`

const cacheDir = path.join(__dirname, '..', '..', 'read-next')
async function main() {
// STEP 1 - create a ReadNext instance
const readNext = await ReadNext.create({
cacheDir,
summarizationPrompt,
})

// STEP 2 - index all the examples
const examples = new Examples()
const { publishedExamples } = examples

const sourceDocuments = publishedExamples.map((example: Example) => ({
pageContent: examples.getContent(example),
id: example.slug,
}))

await readNext.index({ sourceDocuments })

// STEP 3 - generate related examples for each example
for (const example of publishedExamples) {
const {related} = await readNext.suggest({
sourceDocument: sourceDocuments.find((s: any) => s.id === example.slug)!,
limit: 5,
})

examples.updateMatter(example, {
related: related.map(
(suggestion: any) => suggestion.sourceDocumentId,
),
})
}
}

main()
.catch(console.error)
.then(() => process.exit(0))
related.tsx
import path from 'path'
import { ReadNext } from 'read-next'

import Examples, { Example } from '../lib/examples'

const summarizationPrompt = `
The following content is a markdown document about an example of how to use React Server
Components. It contains sections of prose explaining what the example is about, may contain
links to other resources, and almost certainly contains code snippets.

Your goal is to generate a summary of the content that can be used to suggest related examples.
The summary will be used to create embeddings for a vector search. When you come across code
samples, please summarize the code in natural language.

Do not reply with anything except your summary of the example.`

const cacheDir = path.join(__dirname, '..', '..', 'read-next')
async function main() {
// STEP 1 - create a ReadNext instance
const readNext = await ReadNext.create({
cacheDir,
summarizationPrompt,
})

// STEP 2 - index all the examples
const examples = new Examples()
const { publishedExamples } = examples

const sourceDocuments = publishedExamples.map((example: Example) => ({
pageContent: examples.getContent(example),
id: example.slug,
}))

await readNext.index({ sourceDocuments })

// STEP 3 - generate related examples for each example
for (const example of publishedExamples) {
const {related} = await readNext.suggest({
sourceDocument: sourceDocuments.find((s: any) => s.id === example.slug)!,
limit: 5,
})

examples.updateMatter(example, {
related: related.map(
(suggestion: any) => suggestion.sourceDocumentId,
),
})
}
}

main()
.catch(console.error)
.then(() => process.exit(0))

The script just does 3 simple things:

  1. Creates a ReadNext instance
  2. Indexes all the examples
  3. Generates related examples for each example

The RSC Examples project stores its content as .mdx files, so the final part of the script is just calling a utility function to update the frontmatter on each example with the related examples that ReadNext generated.

The summarizationPrompt is optional but here we're taking advantage of it to better explain to the LLM that the content it is about to transform is a markdown document about an example of how to use React Server Components, not a long form article as it would usually expect. Here's the full thing:

Here's the actual commit that was everything required to get ReadNext completely integrated with RSC Examples (the next commit shows the output that ReadNext generated). There are a couple of simple UI components to display the recommendations, otherwise it's just one script that runs ReadNext to (re-)generate the recommendations.

Where to use it

I spend most of my time in React, usually within Next JS, and typically write TypeScript, so most of what I create is a union of those technologies. ReadNext is really a node project, and you don't need anything to do with React/Next/TypeScript to use it. But it does work really well with those technologies, because that's my stack and it would annoy me if it didn't.

The first time ReadNext runs, it needs to index all of the content you give it. Because this involves a summarization step, this can take a few seconds per article. Subsequent runs will be faster because ReadNext caches the summarizations and only regenerates them if it detects that an article's content has changed.

Every time a new article is added, or an existing article is updated, it's a good idea to re-run ReadNext as it's possible that your changes will alter the recommendations for one or more of your articles. Automating this as part of your build process is a good idea, and because ReadNext is a self-contained package it should be able to run pretty much anywhere.

Share Post:

What to Read Next