diff --git a/src/main/java/org/example/recipe_match_backend/domain/recipe/service/RecipeService.java b/src/main/java/org/example/recipe_match_backend/domain/recipe/service/RecipeService.java index cc57533..91ffd42 100644 --- a/src/main/java/org/example/recipe_match_backend/domain/recipe/service/RecipeService.java +++ b/src/main/java/org/example/recipe_match_backend/domain/recipe/service/RecipeService.java @@ -449,8 +449,8 @@ public RecipeResponse find(Long recipeId,String uid){ SearchHistory searchHistory = SearchHistory.builder().user(user).recipe(recipe).categoryType(recipe.getCategory()).build(); - if(user.getSearchHistories().size() >= 5){ - SearchHistory history = user.getSearchHistories().remove(0); + if(user.getSearchHistories().size() >= 7){ + SearchHistory history = user.getSearchHistories().removeFirst(); recipe.getSearchHistories().remove(history); } user.getSearchHistories().add(searchHistory); diff --git a/src/main/java/org/example/recipe_match_backend/domain/searchhistory/repository/SearchHistoryRepositoryImpl.java b/src/main/java/org/example/recipe_match_backend/domain/searchhistory/repository/SearchHistoryRepositoryImpl.java index c165164..2d3d64b 100644 --- a/src/main/java/org/example/recipe_match_backend/domain/searchhistory/repository/SearchHistoryRepositoryImpl.java +++ b/src/main/java/org/example/recipe_match_backend/domain/searchhistory/repository/SearchHistoryRepositoryImpl.java @@ -1,5 +1,8 @@ package org.example.recipe_match_backend.domain.searchhistory.repository; +import ch.qos.logback.core.rolling.helper.IntegerTokenConverter; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.CaseBuilder; import com.querydsl.core.types.dsl.Expressions; @@ -7,18 +10,22 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import lombok.extern.slf4j.Slf4j; +import org.example.recipe_match_backend.domain.ingredient.domain.Ingredient; import org.example.recipe_match_backend.domain.recipe.domain.QRecipe; import org.example.recipe_match_backend.domain.recipe.domain.Recipe; import org.example.recipe_match_backend.domain.recipe.domain.RecipeIngredient; import org.example.recipe_match_backend.domain.recipe.domain.RecipeTool; import org.example.recipe_match_backend.domain.searchhistory.dto.request.SearchHistoryRequest; +import org.example.recipe_match_backend.domain.tool.domain.Tool; import org.example.recipe_match_backend.domain.user.domain.User; import org.example.recipe_match_backend.domain.user.repository.UserRepository; import org.example.recipe_match_backend.type.AllergyType; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +import java.sql.Array; +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; import static org.example.recipe_match_backend.domain.recipe.domain.QRecipeIngredient.recipeIngredient; import static org.example.recipe_match_backend.domain.recipe.domain.QRecipeTool.recipeTool; @@ -39,7 +46,7 @@ public SearchHistoryRepositoryImpl(EntityManager em, UserRepository userReposito @Override public List recommend(SearchHistoryRequest request) { - NumberExpression scoreExpr = buildScoreExpr(request); + NumberExpression scoreExpr = buildScoreExpr(request); Optional optionalUser = userRepository.findByUid(request.getUid()); @@ -53,17 +60,6 @@ public List recommend(SearchHistoryRequest request) { .orderBy(scoreExpr.desc()) .limit(5) .fetch(); - - /**List recipes= queryFactory - .select(recipe) - .from(recipe) - .where(optionalUser.map(u -> allergiesContainAny(u.getAllergies())).orElse(null)) - .orderBy(scoreExpr.desc()) - .fetch(); - - return recipes.stream() - .limit(5) - .toList();**/ } private BooleanExpression allergiesContainAny(List allergies) { @@ -84,38 +80,138 @@ private BooleanExpression duplicateAny(List recipes){ return recipe.notIn(recipes); } - private NumberExpression buildScoreExpr(SearchHistoryRequest request){ - NumberExpression scoreExpr = Expressions.numberTemplate(Integer.class, "0"); + private NumberExpression buildScoreExpr(SearchHistoryRequest request){ + NumberExpression scoreExpr = Expressions.numberTemplate(Double.class, "0"); scoreExpr = scoreExpr.add(new CaseBuilder() .when(request.getCategoryTypes() != null ? recipe.category.in(request.getCategoryTypes()) : Expressions.FALSE) - .then(4) - .otherwise(0)); + .then(3.0) + .otherwise(0.0)); scoreExpr = scoreExpr.add(new CaseBuilder() .when(request.getDifficultyTypes() != null ? recipe.difficulty.in(request.getDifficultyTypes()):Expressions.FALSE) - .then(2) - .otherwise(0)); + .then(1.0) + .otherwise(0.0)); + + NumberExpression jaccardScore = similarity(request); - for (RecipeIngredient recipeIngredient : request.getRecipeIngredients()) { - scoreExpr = scoreExpr.add( + scoreExpr = scoreExpr.add(jaccardScore); + + return scoreExpr; + } + + private NumberExpression similarity(SearchHistoryRequest request) { + + NumberExpression score = Expressions.numberTemplate(Double.class, "0"); + + //레시피 재료에 대한 자카드 유사도 + Set commonIngredientsToExclude = Set.of( + "소금", "물", "후춧가루", "간장", "설탕", "참기름","고춧가루","국간장","식용유", + "다진마늘","대파","물엿","깨소금","다진파","생강즙","고추장","된장", + "마늘","초고추장","양념간장","청주","식초","배즙","양파즙","양념장","녹말","무명실", + "파슬리가루","생강","통깨","깨","월계수잎","실파","다진생강","진간장", + "겨자", "겨자잎", "계핏가루", "다진양파", "들기름", "레몬즙","마늘종", + "버터", "슈가파우더", "쌈장", "연겨자", "올리브오일", "전분", + "통후추", "파마산치즈", "파슬리", "황설탕", "후추", "흑설탕", "흰설탕" + ); + + List userIngredients = request + .getRecipeIngredients() + .stream() + .map(RecipeIngredient::getIngredient) + .filter(ingredient -> !commonIngredientsToExclude.contains(ingredient.getIngredientName())) + .toList(); + + List MainIngredients = new ArrayList<>(Arrays.asList( + "돼지고기","쇠고기","닭","닭고기","가리비", "가재새우","검은껍질홍합", "고등어", + "꼴뚜기", "꽁치", "꽃게", "낙지","문어", "바지락", + "북어", "새우", "생새우", "생태", "연어", "오징어", + "재첩", "조개살", "조기", "중새우살", "쭈꾸미", "홍합", "훈제연어", + "감자", "배추", "송이버섯", "오이", "호박","청포묵","팥","두부" + )); + + //NumberExpression IngredientCount = recipe.recipeIngredients.size().doubleValue(); + //NumberExpression recipeIngredientCount = Expressions.numberTemplate(Double.class, "{0}", IngredientCount); + NumberExpression recipeIngredientCount =recipe.recipeIngredients.size().doubleValue(); + + score = score.add(similarityCalculate( + userIngredients, + MainIngredients, + recipeIngredientCount, + ing -> recipe.recipeIngredients.any().ingredient.eq(ing), + (ing, main) -> recipe.recipeIngredients.any().ingredient.eq(ing).and(recipe.recipeIngredients.any().ingredient.ingredientName.eq(main)), + 30.0)); + + List userTools = request + .getRecipeTools() + .stream() + .map(RecipeTool::getTool) + .toList(); + + List MainTools = new ArrayList<>(Arrays.asList( + "그릴", "김밥매트", "냄비", "돌솥", "뚝배기", "발효통", "밥솥", "석쇠", "솥", "스팀기","압력밥솥", "압력솥", + "오븐", "원형 틀", "유부틀", "전골냄비", "찜통", "튀김용 냄비", "튀김팬", "팬", "프라이팬" + )); + + //NumberExpression ToolCount = recipe.recipeTools.size().doubleValue(); + //NumberExpression recipeToolCount = Expressions.numberTemplate(Double.class, "{0}", ToolCount); + NumberExpression recipeToolCount = recipe.recipeTools.size().doubleValue();; + + score = score.add(similarityCalculate( + userTools, + MainTools, + recipeToolCount, + tool -> recipe.recipeTools.any().tool.eq(tool), + (tool,main) -> recipe.recipeTools.any().tool.eq(tool).and(recipe.recipeTools.any().tool.toolName.eq(main)), + 10.0)); + + return score; + + } + + private NumberExpression similarityCalculate(List userValues,// 사용자 입력 값 (Ingredient, Tool 등) + List userMain, + Expression recipeSizeExpr,// recipe.recipeIngredients / recipeTools 개수 + Function matchCondition,// any().ingredient.eq(x) 또는 any().tool.eq(x) + BiFunction matchMain, + double weight){ + + NumberExpression count = Expressions.numberTemplate(Double.class, "0"); + NumberExpression matchCount = Expressions.numberTemplate(Double.class, "0"); + NumberExpression unionCount = Expressions.numberTemplate(Double.class, String.valueOf(userValues.size())); + + List> expressions = new ArrayList<>(); + + for(T userValue: userValues){ + for (F main: userMain) { + count = count.add( + new CaseBuilder() + .when(matchMain.apply(userValue,main)) + .then(2.0) + .otherwise(0.0) + ); + } + matchCount = matchCount.add( new CaseBuilder() - .when(recipe.recipeIngredients.any().ingredient.eq(recipeIngredient.getIngredient())) - .then(2) - .otherwise(0) + .when(matchCondition.apply(userValue)) + .then(1.0) + .otherwise(0.0) ); - } - - for(RecipeTool recipeTool: request.getRecipeTools()){ - scoreExpr = scoreExpr.add( + count = count.add( new CaseBuilder() - .when(recipe.recipeTools.any().tool.eq(recipeTool.getTool())) - .then(1) - .otherwise(0) + .when(matchCondition.apply(userValue)) + .then(1.0) + .otherwise(0.0) ); } - return scoreExpr; + unionCount = unionCount.add(recipeSizeExpr).subtract(matchCount); + + return Expressions.numberTemplate( + Double.class, + "({0} * {1}) / nullif({2}, 0)", + count, Expressions.constant(weight), unionCount + ); } }