Graphs, Jaccard, and Caching Algorithms Explained:Boosting Art Discovery in NestJS
Introduction
Discovering relevant artworks on a platform can be overwhelming — thousands of artworks, multiple artists, and endless categories. How can we intelligently recommend related artworks to users in real-time?
In this article, I’ll walk you through building a smart artwork recommendation system using algorithms: graphs, Jaccard similarity, and caching — all in NestJS and Ionic/Angular.
Why Algorithms and Caching Matter in Art Discovery
A simple search or list of artworks isn’t enough. We want to answer:
- Which artworks are by the same artist?
- Which artworks are in the same category?
- Which artworks have similar titles or descriptions?
By combining graph relationships, text similarity, and caching, we can deliver fast, relevant, and scalable recommendations.
Caching ensures that frequent queries return instantly without recalculating similarity every time.
Step 1 — Building a Graph of Artworks
We model relationships using a graph, where:
- Each node = an artwork
- Each edge = a relationship (same artist or same category)
This allows us to quickly find neighbors without scanning the whole database every time.
class ArtworkGraph {
private adjacency: Record<string, Set<string>> = {};
addEdge(a: string, b: string) {
if (!this.adjacency[a]) this.adjacency[a] = new Set();
if (!this.adjacency[b]) this.adjacency[b] = new Set();
this.adjacency[a].add(b);
this.adjacency[b].add(a);
}
getNeighbors(node: string): string[] {
return Array.from(this.adjacency[node] || []);
}
}Graphs let us instantly retrieve related artworks by artist or category.
Step 2 — Measuring Title Similarity with Jaccard
Graph connections are great, but we also want similar titles to influence recommendations.
Get Seremwe Ngoni’s stories in your inbox
Join Medium for free to get updates from this writer.
We use Jaccard similarity, which measures the overlap between word sets:
private jaccardSimilarity(a: string[], b: string[]): number {
const setA = new Set(a);
const setB = new Set(b);
const intersection = new Set([...setA].filter(x => setB.has(x)));
const union = new Set([...setA, ...setB]);
return union.size === 0 ? 0 : intersection.size / union.size;
}Example: “Sunset Painting” vs “Beautiful Sunset Painting” → high similarity.
Step 3 — Combining Graph + Jaccard into a Score
We combine graph neighbors and title similarity into a weighted score:
- Same artist = +2
- Same category = +1
- Title similarity = +1 * Jaccard score
const totalScore = graphScore + titleScore;We then sort by score and return the top N related artworks.
Step 4 — Adding Caching for Performance
Calculating related artworks repeatedly is expensive. To optimize, we cache the results:
- Cache results of
findRelated(artworkId)in memory or Redis. - Automatically invalidate the cache when a new artwork is created.
private relatedCache: Map<string, Artwork[]> = new Map();
async findRelated(artworkId: string, limit = 5): Promise<Artwork[]> {
const cacheKey = `${artworkId}:${limit}`;
// Return cached results if available
if (this.relatedCache.has(cacheKey)) return this.relatedCache.get(cacheKey);
const artwork = await this.artworkRepo.findOne({ where: { id: artworkId }, relations: ['category', 'crafter'] });
const neighbors = this.graph.getNeighbors(artworkId);
const allArtworks = await this.artworkRepo.find({ relations: ['category', 'crafter'] });
const scores = allArtworks
.filter(a => a.id !== artworkId)
.map(a => {
let graphScore = 0;
if (a.crafter?.id === artwork.crafter?.id) graphScore += 2;
if (a.category?.id === artwork.category?.id) graphScore += 1;
const titleScore = this.jaccardSimilarity(artwork.title.split(' '), a.title.split(' '));
return { artwork: a, score: graphScore + titleScore };
})
.filter(s => s.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map(s => s.artwork);
// Store results in cache
this.relatedCache.set(cacheKey, scores);
return scores;
}With caching, frequent queries return instantly, making the system fast and scalable.
Step 5 — Frontend Integration (Ionic/Angular)
this.artworkService.findRelated(artwork.id).subscribe(related => {
this.relatedArtworks = related;
});Display each artwork with title, image, and crafter info.
Benefits of Caching + Algorithms
- Fast: Cached results reduce database queries.
- Relevant: Weighted scoring ensures meaningful recommendations.
- Scalable: Handles thousands of artworks efficiently.
- Flexible: Can extend to include tags, user preferences, or ratings.
Conclusion
By combining graphs, Jaccard similarity, and caching, you can build a smart, fast, and scalable artwork recommendation engine. This approach can be applied to any content platform — not just artworks.





