# Article Recommendation System with TypeScript and Supabase

We want to write a function with the following specification:

Given a post *P*, return all posts similar to *P*.

The way we'll determine similarity between posts is by labeling posts with categories.

For this, we need to model a many-to-many relationship between posts and categories.

We can then use cosine similarity to determine similarity between posts.

```
export function getCosineSimilarity(a: boolean[], b: boolean[]): number {
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
const a = Number(a[i]);
const b = Number(b[i]);
dotProduct += a * b;
normA += a * a;
normB += b * b;
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
```

The ordered vectors `A`

and `B`

we're looking for map one-to-one
with the categories we define.

For example, here's how we might categorize posts about web development.

Category | Rendering Patterns | Basics of Web Development |
---|---|---|

HTML | `false` | `true` |

CSS | `false` | `true` |

JavaScript | `true` | `true` |

React | `true` | `false` |

So if *Rendering Patterns* is `A`

, then `A = [false, false, true, true]`

.

We'll use a `Set`

to generate the category vectors in linear time.

```
export function getCategoryVector(
postCategoryIds: Set<number>,
allCategoryIds: number[],
): boolean[] {
return allCategoryIds.map((categoryId) => postCategoryIds.has(categoryId));
}
```

We now have the components needed to build our recommendation algorithm.

Here's how the algorithm will go:

- Fetch all posts and categories
- Get the category vectors for each post
- Find and remove
*P* - Get the cosine similarity between the remaining posts and
*P* - Filter out the remaining posts with no cosine similarity
- Return the remaining posts sorted by cosine similarity

```
export async function getRelatedPosts(postId: number): Promise<Post[]> {
// 1. Fetch all posts and categories
const [{data: allPosts}, {data: allCategories}] = await Promise.all([
supabase
.from<Post>('posts')
.select('post_id, post_categories (categories (category_id))')
.throwOnError(),
supabase
.from<Category>('categories')
.select('category_id')
.order('category_id')
.throwOnError(),
]);
// No data was returned, return no related posts
if (allPosts == null || allCategories == null) {
return [];
}
// 2. Get the category vectors for each post
const allCategoryIds = allCategories.map(({category_id}) => category_id);
const posts = allPosts.map((post) => {
return Object.assign(
{
categoryVector: getCategoryVector(
new Set<number>(
post.post_categories?.map(
({categories: {category_id}}) => category_id,
) ?? [],
),
allCategoryIds,
),
},
post,
);
});
// 3. Find P
const p = posts.find((post) => post.post_id === postId);
// The post wasn't found, return no related posts
if (p == null) {
return [];
}
return (
posts
// 3.5 Remove P
.filter((post) => post.post_id !== p.post_id)
// 4. Get the cosine similarity between remaining posts and P
.map((post): DBPostCosineSimilarity => {
return Object.assign(
{
cosineSimilarity: getCosineSimilarity(
p.categoryVector,
post.categoryVector,
),
},
post,
);
})
// 5. Filter out the remaining posts with no cosine similarity
.filter((post) => post.cosineSimilarity > 0)
// 6. Return the remaining posts sorted by cosine similarity
.sort((a, b) => b.cosineSimilarity - a.cosineSimilarity)
);
}
```

And with that, we have a post recommendation system. ๐