Skip to content
Snippets Groups Projects
user avatar
veronikaovsyannikova authored
d61b3a0d
History

Recipe Site

Description

The platform will allow users to select ingredients they have available, and the system will suggest recipes that can be made using those ingredients.
Users can search for recipes, save their favorite ones, and rate/review the recipes. The platform will feature:

  • An ingredient filter
  • User accounts
  • Recipe browsing
  • Search history
  • Personalized recommendations based on user activity

Subject Area

The main goal of this project is to create a backend connected to multiple databases with a simple frontend.

Functional Requirements

  • FQ0: Users must be able to register and log in.
  • FQ1: Users should be able to search for recipes by selecting multiple ingredients.
  • FQ2: Users should be able to filter recipes by rating and date of publication.
  • FQ3: Users can save favorite recipes and view their history of saved and viewed recipes.
  • FQ4: The system should suggest recipes based on a user’s activity, such as previously saved recipes or highly rated recipes.
  • FQ5: Users can leave ratings and reviews on recipes.

Database System Selection

PostgreSQL

  • Use Case: Ingredients, Recipes, User Accounts, and Profiles
  • Reason: Recipes, ingredients, and user information have a structured format. Using foreign keys and indexes will ensure efficient search and filtering of recipes.

Implemented queries

In the PostgreSQL database, information about users and recipes is stored.

To find recipes based on parameters, this query is used:

SELECT r
FROM Recipe r
LEFT JOIN r.ingredients ing
WHERE
(LOWER(r.title) LIKE LOWER(CONCAT('%', :title, '%')) OR :title IS NULL)
AND (:categories IS NULL OR r.category IN :categories)
AND (:ingredientIds IS NULL OR ing.id IN :ingredientIds)
ORDER BY
CASE WHEN :sortParam = 'rating_desc' THEN r.averageRating END DESC,
CASE WHEN :sortParam = 'updated_desc' THEN r.updatedAt END DESC,
CASE WHEN :sortParam = 'updated_asc' THEN r.updatedAt END ASC,
CASE WHEN :sortParam = 'rating_asc' THEN r.averageRating END ASC

MongoDB

  • Use Case: Search History. It is displayed in the user account page.
  • Reason: MongoDB is suited for semi-structured data (e.g., user search history). It can store a flexible format of search data.

Implemented Queries

In MongoDB I save search history with provided parameters and a list of results with recipe ids.
I am using UserSearchRepository provided by Spring Boot for storing search results.

To find top3 search result I am using MongoTemplate and this query.

val agg = newAggregation(
    unwind("searches"),
    unwind("searches.results"),
    group("searches.results").count().`as`("count"),
    sort(Sort.by(Sort.Direction.DESC, "count")),
    limit(3),
    project()
        .andExpression("_id").`as`("recipeId")
        .and("count").`as`("count")
)

val results = mongoTemplate.aggregate(agg, "user_searches", Document::class.java)

return results.mappedResults.mapNotNull {
    it.getLong("recipeId")
}

Redis

  • Use Case: Caching Frequently Accessed Data. Top 3 most frequently searched recipes are cached in Redis.
  • Reason: Redis handles high-speed data access, making it ideal for session management. It can improve load times and reduce database queries by caching popular searches or recipes.

Implemented Queries

I am using search result data from MongoDB to define the top-3 recipes that are displayed in most searches. And I store these recipes in the cache.

For queries and cache management, I am using RedisTemplate provided by Spring Boot.

Cassandra

  • Use Case: Activity Log. It is accessible through the user account page.
  • Reason: Cassandra offers high write throughput, which is useful for recording every user action (recipe searches, clicks, interactions). It can efficiently handle large volumes of log entries across multiple nodes.

Implemented Queries

When a user performs actions such as adding/removing a friend, opening a recipe, adding/removing a favorite, rating a recipe, etc., the information about it is inserted into the user_actions table.
Later, I display logs for the user in their account page.

For managing user action rows, I used the CassandraRepository provided by Spring Boot.

Neo4j

  • Use Case: Personalized Recipe Recommendations. A user can display personal recommendations by clicking a button on the home page. It will show recipes that are in favorites or were rated no less than 4 by the user’s friends or their friends.
  • Reason: Neo4j excels at managing complex relationships. It is ideal for generating recommendations based on user behavior, as it can traverse relationship graphs effectively.

Implemented Queries

Query for searching recommendations for a user. Returns information about recipes that were added to favorites by the user’s friends or their friends, or rated no less than 4.

MATCH path=(u:User {userName: '$userName'})-[:FRIEND*1..2]->(friend:User)
WITH friend, size(relationships(path)) AS hopCount
MATCH (friend)-[fav:FAVORITED]->(r:Recipe)
WHERE r IS NOT NULL
WITH friend, r, hopCount,
     CASE hopCount
       WHEN 1 THEN 'friend'
       ELSE 'friend of friend'
     END AS relationshipReason
RETURN friend.userName AS friendName,
       r.recipeId      AS recipeId,
       r.title         AS recipeTitle,
       relationshipReason + ' favorited' AS reason

UNION

MATCH path=(u:User {userName: '$userName'})-[:FRIEND*1..2]->(friend:User)
WITH friend, size(relationships(path)) AS hopCount
MATCH (friend)-[rat:RATED]->(r2:Recipe)
WHERE r2 IS NOT NULL AND rat.value >= 4
WITH friend, r2, rat, hopCount,
     CASE hopCount
       WHEN 1 THEN 'friend'
       ELSE 'friend of friend'
     END AS relationshipReason
RETURN friend.userName AS friendName,
       r2.recipeId      AS recipeId,
       r2.title         AS recipeTitle,
       relationshipReason + ' rated ' + toString(rat.value) AS reason

Query for managing friendship, favorite, and rated relations, based on which recommendations are returned.

MATCH (u:User {userName: ?#{#userName}})-[rel:RATED]->(r:Recipe {recipeId: ?#{#recipeId}})
DELETE rel
MERGE (u:User {userName: ?#{#userName}})
MERGE (f:User {userName: ?#{#friendName}})
MERGE (u)-[:FRIEND]->(f)
MATCH (u:User {userName: ?#{#userName}}), (r:Recipe {recipeId: ?#{#recipeId}})
MERGE (u)-[:FAVORITED]->(r)
MATCH (u:User {userName: ?#{#userName}})-[rel:FRIEND]->(f:User {userName: ?#{#friendName}})
DELETE rel
MATCH (u:User {userName: ?#{#userName}})-[rel:FAVORITED]->(r:Recipe {recipeId: ?#{#recipeId}})
DELETE rel
MATCH (u:User {userName:  ?#{#userName}}), (r:Recipe {recipeId: ?#{#recipeId}})
MERGE (u)-[rel:RATED]->(r)
SET rel.value = ?#{#value}