Sitemap

Graphs, Jaccard, and Caching Algorithms Explained:Boosting Art Discovery in NestJS

3 min readNov 3, 2025

--

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

  1. Fast: Cached results reduce database queries.
  2. Relevant: Weighted scoring ensures meaningful recommendations.
  3. Scalable: Handles thousands of artworks efficiently.
  4. 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.

--

--