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