From d61b3a0df324c34585c0d74af8d6fe3cd3033a71 Mon Sep 17 00:00:00 2001 From: veronikaovsyannikova <ovsyaver@fel.cvut.fel> Date: Tue, 7 Jan 2025 17:29:22 +0100 Subject: [PATCH] readme updated --- README.md | 171 ++++++++++++++++-- .../cvut/ds2/controller/HomeController.kt | 1 - .../cvut/ds2/controller/UserController.kt | 3 +- .../cassandra/UserActionLogRepository.kt | 2 +- src/main/kotlin/cvut/ds2/domain/neo/Repos.kt | 22 --- 5 files changed, 162 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index f2993d9..558436c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ # 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. + +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 @@ -10,34 +13,180 @@ Users can search for recipes, save their favorite ones, and rate/review the reci - 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. +- **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. +- **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: + +```sql +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. +```kotlin +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. +- **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. -### **MongoDB** -- **Use Case**: Search History. It is displayed in 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** +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 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. + +- **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. User can display personal recommendation by clicking button on home page. It will show recipes that are in favorites or were rated no less than 4 by user's friends or their friends. -- **Reason**: Neo4j excels at managing complex relationships. Ideal for generating recommendations based on user behavior, as it can traverse relationship graphs effectively. + +- **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. + +```cypher +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. + +```cypher +MATCH (u:User {userName: ?#{#userName}})-[rel:RATED]->(r:Recipe {recipeId: ?#{#recipeId}}) +DELETE rel +``` + +```cypher +MERGE (u:User {userName: ?#{#userName}}) +MERGE (f:User {userName: ?#{#friendName}}) +MERGE (u)-[:FRIEND]->(f) +``` + +```cypher +MATCH (u:User {userName: ?#{#userName}}), (r:Recipe {recipeId: ?#{#recipeId}}) +MERGE (u)-[:FAVORITED]->(r) +``` + +```cypher +MATCH (u:User {userName: ?#{#userName}})-[rel:FRIEND]->(f:User {userName: ?#{#friendName}}) +DELETE rel +``` + +```cypher +MATCH (u:User {userName: ?#{#userName}})-[rel:FAVORITED]->(r:Recipe {recipeId: ?#{#recipeId}}) +DELETE rel +``` + +```cypher +MATCH (u:User {userName: ?#{#userName}}), (r:Recipe {recipeId: ?#{#recipeId}}) +MERGE (u)-[rel:RATED]->(r) +SET rel.value = ?#{#value} +``` diff --git a/src/main/kotlin/cvut/ds2/controller/HomeController.kt b/src/main/kotlin/cvut/ds2/controller/HomeController.kt index e30fe84..93b3974 100644 --- a/src/main/kotlin/cvut/ds2/controller/HomeController.kt +++ b/src/main/kotlin/cvut/ds2/controller/HomeController.kt @@ -95,7 +95,6 @@ class HomeController( userActionLoggingService.logSearch(it.userName, title, categories, ingredientIds, sort) - } } } diff --git a/src/main/kotlin/cvut/ds2/controller/UserController.kt b/src/main/kotlin/cvut/ds2/controller/UserController.kt index e1679b1..1a617f6 100644 --- a/src/main/kotlin/cvut/ds2/controller/UserController.kt +++ b/src/main/kotlin/cvut/ds2/controller/UserController.kt @@ -57,8 +57,7 @@ class UserController( if (principal == null) return "redirect:/login" val user = userRepository.findByEmail(principal.username) ?: return "redirect:/login" - val userLogs = userActionLogRepository.findAll().filter { it.userId == user.userName } - .sortedByDescending { it.timestamp } + val userLogs = userActionLogRepository.findByUserId(user.userName) model.addAttribute("isLogsEmpty", userLogs.isEmpty()) model.addAttribute("username", user.userName) diff --git a/src/main/kotlin/cvut/ds2/domain/cassandra/UserActionLogRepository.kt b/src/main/kotlin/cvut/ds2/domain/cassandra/UserActionLogRepository.kt index fba953c..dd0c421 100644 --- a/src/main/kotlin/cvut/ds2/domain/cassandra/UserActionLogRepository.kt +++ b/src/main/kotlin/cvut/ds2/domain/cassandra/UserActionLogRepository.kt @@ -7,5 +7,5 @@ import java.util.* @Repository interface UserActionLogRepository : CassandraRepository<UserActionLog, UUID> { - + fun findByUserId(userId: String): List<UserActionLog> } \ No newline at end of file diff --git a/src/main/kotlin/cvut/ds2/domain/neo/Repos.kt b/src/main/kotlin/cvut/ds2/domain/neo/Repos.kt index 74a3052..cc60170 100644 --- a/src/main/kotlin/cvut/ds2/domain/neo/Repos.kt +++ b/src/main/kotlin/cvut/ds2/domain/neo/Repos.kt @@ -11,28 +11,6 @@ import org.springframework.data.repository.query.Param @Repository interface UserNodeRepository : Neo4jRepository<UserNode, String> { - @Query( - value = """ - MATCH (u:User {userName: ?#{#userName}})-[:FRIEND*1..2]->(friend:User) - OPTIONAL MATCH (friend)-[fav:FAVORITED]->(r:Recipe) - WHERE r IS NOT NULL - RETURN friend.userName AS friendName, - r.recipeId AS recipeId, - r.title AS recipeTitle, - 'favorited' AS reason - - UNION - - MATCH (u:User {userName: ?#{#userName}})-[:FRIEND*1..2]->(friend:User) - OPTIONAL MATCH (friend)-[rat:RATED]->(r2:Recipe) - WHERE r2 IS NOT NULL AND rat.value >= 4 - RETURN friend.userName AS friendName, - r2.recipeId AS recipeId, - r2.title AS recipeTitle, - 'rated ' + toString(rat.value) AS reason - """ - ) - fun getRecommendations(@Param("userName") userName: String): List<RecommendationProjection> @Query( """ -- GitLab