If you own an eCommerce store, a blog or a social networking app, you would need to implement a recommendation system to help improve engagements, maximize discovery and purchases. No need to install plugins or add-ons.
In this posts, I will show you show to implement an AI powered recommendation system for your products and store profiles.
Technologies used
I built this for use with
- Next.js,
- Nodejs,
- Cloudflare Vectorize and D1 databases and
- Woocommerce.
- OpenAI
- Cohere Rerank
- Axios
To keep things loose and pluggable, I would avoid using code examples that are tied to frameworks or libraries like Nextjs, Woocommerce.
Main Goals
- Load you products data from data source.
- Prepare data for creating product data vector/embedding on Cloudflare Vectorize.
- Bulk save existing product data to vector database.
- Save recommendations to database to improve subsequent queries.
- Display your products on the front-end
- Query / Lookup or find recommended product using embedding and OpenAI or Cohere Rerank.
Asking Tohju for Guidance🙏
Coding Begins
Load you products data from data source.
This can be an API request or a file on your computer. The reason I need to load all the products is because to suggest or recommend products to user, there has to be an existing index of products records on a vector database that I can query for similarity or relationship.
Here is code that fetches from products from a GraphQl CMS. You should have something similar with the CMS you are using. Lots of CMSs support REST or GraphQL queries. By using GraphQL, I can select the fields that I need saved on the vector database.
export const fetchProducts = async (limit: any = 50) => {
const query = gql`
query fetchProducts($limit: Int!) {
products (first: $limit) {
id
name
price
createdAt
}
}
`;
const result: any = await graphqlClient.request(query, {
limit: limit,
});
return result.products;
};
A sample response from that request is this JSON array of objects below.
{
"data": {
"products": [
{
"id": "cm42vkouf0nuq07ioin8gcv7a",
"name": "Semi Really Cool Product",
"shortDescription": "Nisi aliquam dolorum",
"price": "0.05",
"createdAt": "2024-11-29T15:05:05.180381+00:00"
},
{
"id": "cm42v5u5e0iy307io7el9egs8",
"name": "Soluta labore corrup",
"shortDescription": "Possimus tenetur ut",
"price": "0.05",
"createdAt": "2024-11-29T14:53:32.208029+00:00"
},
{
"id": "cm3s0c3bu3dsf07ik51g8tt2o",
"name": "Brand New Apple AirPods ",
"shortDescription": "",
"price": "59",
"createdAt": "2024-11-22T00:32:54.189704+00:00"
},
{
"id": "cm3om730649wo07kc22k42psv",
"name": "Men’s Columbia short sleeve button shirt",
"shortDescription": "",
"price": "0",
"createdAt": "2024-11-19T15:33:47.311781+00:00"
},
{
"id": "cm3om6smq49s807kc7e9zzhcv",
"name": "Tohju Can cooler",
"shortDescription": "",
"price": "0",
"createdAt": "2024-11-19T15:33:33.853876+00:00"
}
]
}
}
Now that we have that, we can proceed to insert our products into a vector database.
Using Cloudflare’s Vectorize Database
Here’s how Cloudflare describes Vectorize.
I hope you now understand why I initially loaded in the list of products I have available using GraphQL. I would need to create a new Vectorize index. I also need to create a Cloudflare worker to access the Vectorize index.
Create a new Cloudflare Worker
Coding Cloudflare Workers is fairly easy. To save time, I use an open-source worker project that creates a REST API that can perform CRUD operations on Cloudflare Vectorize database. Clone the repo from here and follow the setup instructions. You can watch the video below for how I did it.
https://www.youtube.com/watch?v=XhG0KNtG0t0
Test out the Worker by running npx wrangler dev and visit the Swagger Docs REST Client: http://localhost:8787
Bulk Insert products into Vector DB
Here, we prepare our list of products to be inserted or saved into the vector database. The code below run the fetchProducts/getAllProducts operation and then format the response to a payload which is sent in a request to the Cloudflare worker that is connected to the vector database.
export const main = async () => {
try {
console.log('Starting...')
const getProducts = await getAllProducts()
console.log(`Fetched %s products`, getProducts.length)
// prepare to send products details to cf worker
const payload = getProducts.map(product => ({
text: `name${product.name}\n${product.shortDescription}`,
metadata: {
productId: product.id,
price: product.price
}
}))
// send request
const request = await axios.post(`${process.env.CF_WORKER_URL}/api/namespaces/products/insert`, {
vectors: payload,
model: "text-embedding-3-large"
})
console.log(request.data)
} catch (err) {
// @ts-ignore
console.log(err.response.data)
}
}
Forgive the console.log statements in the code. Remember to implement your own getAllProducts logic and import the function. You will also need to install axios. pnpm install axios.
Try to query the database.
Head on to your browser and visit the Swagger Docs REST Client for the Cloudflare worker at http://localhost:8787 .
Scroll to the Query tab or section and click “try it out”.
Enter the name of one of the products you have from the getAllProducts operation eg. “Brand New Apple AirPods”. Click on Execute. Here’s an example of the response you should get.
{
"success": true,
"matches": [
{
"id": "c000356d-adc2-463b-afa6-29766d9aa02b",
"score": 0.83289236,
"source": "nameBestFrens Snapback Hat\nnull",
"metadata": {
"price": "0",
"productId": "clzt269py7ca608k4ryupcysf"
}
},
{
"id": "334fcf28-4a96-49c4-a570-b3df8f4d0fea",
"score": 0.7077526,
"source": "nameBest Frens Dad hat\nnull",
"metadata": {
"price": "0",
"productId": "clzt27pxz7dap08k4148i5z7r"
}
}
]
}
Yass!!! 🥳 It is Working! Right ?
You can definitely say that at this point. Depending on your needs, this can be extended. I need to show this to customers on a browser and also avoid running this query every time the product details page is rendered.
Saving the recommendations to your database.
The idea is when a user views a product page, it also queries a recommendation table for products to suggest. The recommendations the app responds with are populated when the product being viewed is created and updated. I thought of a simple table structure where each row or entry contains two fields; a productId field, and a recommended product field. That way, to look up the recommended products list for a specific product, I could query something like product.find({where: { productId: product.id }}) .
Asking Tohju for Guidance🙏
I hoping to extend and use the table for 3 types of recommendations. I want to be flexible, I will create a table structure with 3 fields. entryId, recommendationId and entryType. Entrytype will be used for values like “product” or “store” or “profile”.
I want to query and save a products recommendation when a product is created and when it is updated. The create operation is fairly straight forward since there are no prior records existing. The update record will require checking any existing vector records and creating a new ones. It also makes sense to delete any saved embedding when a product is removed to avoid missing records. If you are using WooCommerce, you can find hooks to plug into that would make a HTTP request to the deployed Cloudflare Worker to create or delete a vector record.
To handle the create product flow, I need to create the embedding / save the vectors, then, query for product recommendation and save the response to the database so it can be shown on the product page as recommended products.
Now, I need a method to create a single product embedding.
export const createProductVector = async (
product
: {
id: string,
name: string,
shortDescription: string,
price: string,
}) => {
const request = await axios.post(`${process.env.CF_WORKER_URL}/api/namespaces/products/insert`, {
vectors: [
{
text: `name: ${product.name}\ndescription: ${product.shortDescription}`,
metadata: {
productId: product.id,
price: product.price
}
}
],
model: "text-embedding-3-large"
})
return request.data
}
Now call createProductVector() from a function that saves or creates a new product. Here is some javascript pseudo-code to guide you.
saveProduct: async (product) => {
const response: any = await addProduct({
...product,
})
await createProductVector({
id: response.id,
name: product.name,
price: product.price,
shortDescription: product.shortDescription
})
if (!response) {
return;
}
const new_id = response.publishProduct.id;
return response
},
Next, I can now query for recommended products from the Vectorize database. I will pass in the product name as a query value.
export const fetchProductRecommendation = async (productName, numberOfResults = 4) => {
const request = await axios.post(`${process.env.CF_WORKER_URL}/api/namespaces/products/query?topK=${numberOfResults}&returnValues=false&returnMetadata=true`, {
inputs: productName
})
return request.data
}
Add this right after createProductVector statement. The response from the fetchProductRecommendation will contain the product ids.
{
"success": true,
"matches": [
{
"id": "c000356d-adc2-463b-afa6-29766d9aa02b",
"score": 0.83289236,
"source": "nameBestFrens Snapback Hat\nnull",
"metadata": {
"price": "0",
"productId": "clzt269py7ca608k4ryupcysf"
}
},
{
"id": "334fcf28-4a96-49c4-a570-b3df8f4d0fea",
"score": 0.7077526,
"source": "nameBest Frens Dad hat\nnull",
"metadata": {
"price": "0",
"productId": "clzt27pxz7dap08k4148i5z7r"
}
}
]
}
My GraphQl API does not support bulk create methods. I use an async for loop to create the recommendation entries. So the saveProduct function will look like
saveProduct: async (product) => {
const response: any = await addProduct({
...product,
})
await createProductVector({
id: response.id,
name: product.name,
price: product.price,
shortDescription: product.shortDescription
})
const fetchRecommendation = await fetchProductRecommendation(product.name)
for (const {metadata} of fetchRecommendation ) {
await createRecommendedProduct({
entryId: response.id,
productId: metadata.productId,
})
}
if (!response) {
return;
}
const new_id = response.publishProduct.id;
return response
},
Assuming createRecommendedProduct has an entry argument with a default value of “product” for entry type
const createRecommendedProduct: ({ entryId, productId, entryType, }: {
entryId: any;
productId: any;
entryType?: string | undefined; // entryType = "product"
}) => Promise<any>
Before I go on to write code to query the just saved recommendations, I want to write code for the update product recommendation flow. When a product data is updated, the app should request for recommendation using fetchProductRecommendation, then check for an existing database record before creating new recommendation records. If your database supports composite primary keys, you could ensure that all entryType_productId_recommendationId values are unique. That way, you can save an API or database call when checking for existing recommendations.
So here’s a bit of code to help you understand how I implemented the update product recommendation logic.
export const updateProductRecommendation = async (entryId, productId) => {
const response = await graphQLClient.request(CHECK_EXISTING_PRODUCT_RECOMMENDATION, {
entryId,
productId
});
const isExisting = !!response.data.recommendationsConnection.edges.length
if (!isExisting) {
await graphQLClient.request(CREATE_RECOMMENDED_PRODUCT, {
entryId,
recommendation: {
connect: {
id: productId
}
},
entryType: "Product"
});
return true
} else {
return false
}
}
I want to add some logic to delete saved recommendations and vectors when a product is deleted later on. I am eager to push this feature so its time to show recommendations on the product page.
Query Recommendations
I am using Next.js and Tailwind for the front end of the store. I have a simple ProductsGrid component to display the list of recommendations.
function ProductsGrid({ products, title }) {
return (
<>
{
products.length === 0 ? <></> : (
<div className="flex w-full flex-col gap-8 mt-6">
<div className="flex justify-between">
<h3 className="text-3xl font-bold dark:text-white">
{title}
</h3>
</div>
<div className="space-y-8 md:grid md:grid-cols-3 lg:grid-cols-4 md:gap-12 md:space-y-0">
{
products.map((product, index) => (
<ProductCard key={index} product={product} />
))
}
</div>
</div>
)
}
</>
)
}
Now to fetch the recommended products. I am using Next.js so I will add a fetchRecommendationsByEntryId function to the top page getServerSideProps.
export default function SingleProductPage({recommendedProducts}) {
return (
<div className="flex flex-col">
<ProductsGrid products={recommendedProducts} title="Related Products" />
</div>
)
}
export async function getServerSideProps({ params }: any) {
const recommendedProducts = findRecommendationsByEntryId(data.product.id)
return {
props: {recommendedProducts},
};
}
This will return all products that can be recommended for the currently viewed product.
Here’s what it looks like
Success 🎇
I hope you can use this as a guide to implement recommendations and related products, profile or articles feature in your own app. I will also extend this