diff --git a/ticketmate-ai/src/main/java/com/ticketmate/backend/ai/application/service/VertexAiEmbeddingService.java b/ticketmate-ai/src/main/java/com/ticketmate/backend/ai/application/service/VertexAiEmbeddingService.java index e1143749..d1cefc61 100644 --- a/ticketmate-ai/src/main/java/com/ticketmate/backend/ai/application/service/VertexAiEmbeddingService.java +++ b/ticketmate-ai/src/main/java/com/ticketmate/backend/ai/application/service/VertexAiEmbeddingService.java @@ -40,23 +40,13 @@ public class VertexAiEmbeddingService { * @param embeddingType CONCERT, AGENT, SEARCH */ @Transactional - @Caching( - cacheable = @Cacheable( - value = "embeddings", - key = "T(com.ticketmate.backend.common.core.util.CommonUtil)" - + ".normalizeAndRemoveSpecialCharacters(#text)" - + "+':' + #embeddingType", - condition = "#targetId == null", - unless = "#result == null" - ) - ) public Embedding fetchOrGenerateEmbedding(UUID targetId, String text, EmbeddingType embeddingType) { // 텍스트 정규화 String normalizeText = CommonUtil.normalizeAndRemoveSpecialCharacters(text); // 검색 모드: 캐시나 DB 조회 후 없으면 생성 return embeddingRepository.findByTextAndEmbeddingType(normalizeText, embeddingType) - .orElseGet(() -> createAndSaveEmbedding(targetId, normalizeText, embeddingType)); + .orElseGet(() -> createAndSaveEmbedding(targetId, normalizeText, embeddingType)); } /** @@ -67,11 +57,11 @@ private Embedding createAndSaveEmbedding(UUID targetId, String normalizedText, E float[] vector = extractVector(generateEmbedding(normalizedText)); return embeddingRepository.save(Embedding.builder() - .targetId(targetId) - .text(normalizedText) - .embeddingVector(vector) - .embeddingType(type) - .build()); + .targetId(targetId) + .text(normalizedText) + .embeddingVector(vector) + .embeddingType(type) + .build()); } /** @@ -83,9 +73,9 @@ private Embedding createAndSaveEmbedding(UUID targetId, String normalizedText, E private EmbedContentResponse generateEmbedding(String text) { try { return genAiClient.models.embedContent( - googleGenAIProperties.model(), - text, - EmbedContentConfig.builder().build() + googleGenAIProperties.model(), + text, + EmbedContentConfig.builder().build() ); } catch (ClientException e) { log.error("Vertex AI 임베딩 API 호출 실패: {}", e.getMessage()); @@ -101,9 +91,9 @@ private EmbedContentResponse generateEmbedding(String text) { */ private float[] extractVector(EmbedContentResponse response) { List embeddingValues = response.embeddings() - .flatMap(list -> list.stream().findFirst()) - .flatMap(ContentEmbedding::values) - .orElseThrow(() -> new CustomException(ErrorCode.EMBEDDING_DATA_NOT_FOUND)); + .flatMap(list -> list.stream().findFirst()) + .flatMap(ContentEmbedding::values) + .orElseThrow(() -> new CustomException(ErrorCode.EMBEDDING_DATA_NOT_FOUND)); int size = embeddingValues.size(); float[] vector = new float[size]; for (int i = 0; i < size; i++) { diff --git a/ticketmate-ai/src/main/java/com/ticketmate/backend/ai/infrastructure/repository/EmbeddingRepository.java b/ticketmate-ai/src/main/java/com/ticketmate/backend/ai/infrastructure/repository/EmbeddingRepository.java index 8df7861b..45f2cb1b 100644 --- a/ticketmate-ai/src/main/java/com/ticketmate/backend/ai/infrastructure/repository/EmbeddingRepository.java +++ b/ticketmate-ai/src/main/java/com/ticketmate/backend/ai/infrastructure/repository/EmbeddingRepository.java @@ -25,6 +25,7 @@ public interface EmbeddingRepository extends JpaRepository { */ @Query(value = "SELECT target_id FROM embedding e " + "WHERE e.embedding_type = 'CONCERT' " + + "AND (e.embedding_vector <-> CAST(:vector AS vector)) <= 0.90 " + "AND EXISTS " + "(SELECT 1 FROM ticket_open_date t " + "JOIN concert c ON t.concert_concert_id = c.concert_id " @@ -43,6 +44,7 @@ public interface EmbeddingRepository extends JpaRepository { */ @Query(value = "SELECT target_id FROM embedding " + "WHERE embedding_type = 'AGENT' " + + "AND (embedding_vector <-> CAST(:vector AS vector)) <= 0.90 " + "ORDER BY embedding_vector <-> CAST(:vector AS vector) LIMIT :limit", nativeQuery = true) List findNearestAgentEmbeddings(@Param("vector") float[] vector, @Param("limit") int limit); diff --git a/ticketmate-search/src/main/java/com/ticketmate/backend/search/infrastructure/repository/SearchRepositoryImpl.java b/ticketmate-search/src/main/java/com/ticketmate/backend/search/infrastructure/repository/SearchRepositoryImpl.java index d561f08d..abb1d3b3 100644 --- a/ticketmate-search/src/main/java/com/ticketmate/backend/search/infrastructure/repository/SearchRepositoryImpl.java +++ b/ticketmate-search/src/main/java/com/ticketmate/backend/search/infrastructure/repository/SearchRepositoryImpl.java @@ -50,44 +50,44 @@ public List findConcertDetailsByIds(List concertIds) { return Collections.emptyList(); } return queryFactory - .select(Projections.constructor(ConcertSearchInfo.class, - CONCERT.concertId, - CONCERT.concertName, - CONCERT_HALL.concertHallName, - // 선예매 오픈일 - Expressions.dateTimeTemplate( - Instant.class, - "min({0})", - new CaseBuilder() - .when(TICKET_OPEN_DATE.ticketOpenType.eq(TicketOpenType.PRE_OPEN)) - .then(TICKET_OPEN_DATE.openDate) - .otherwise((Instant) null) - ).as("ticketPreOpenDate"), - // 일반 예매 오픈일 - Expressions.dateTimeTemplate( - Instant.class, - "min({0})", - new CaseBuilder() - .when(TICKET_OPEN_DATE.ticketOpenType.eq(TicketOpenType.GENERAL_OPEN)) - .then(TICKET_OPEN_DATE.openDate) - .otherwise((Instant) null) - ).as("ticketGeneralOpenDate"), - CONCERT_DATE.performanceDate.min().as("startDate"), - CONCERT_DATE.performanceDate.max().as("endDate"), - CONCERT.concertThumbnailStoredPath, - Expressions.constant(0.0) - )) - .from(CONCERT) - .leftJoin(CONCERT.concertHall, CONCERT_HALL) - .join(CONCERT_DATE).on(CONCERT.eq(CONCERT_DATE.concert)) - .join(TICKET_OPEN_DATE).on(CONCERT.eq(TICKET_OPEN_DATE.concert)) - .where(CONCERT.concertId.in(concertIds)) - .groupBy(CONCERT.concertId, - CONCERT.concertName, - CONCERT_HALL.concertHallName, - CONCERT.concertThumbnailStoredPath - ) - .fetch(); + .select(Projections.constructor(ConcertSearchInfo.class, + CONCERT.concertId, + CONCERT.concertName, + CONCERT_HALL.concertHallName, + // 선예매 오픈일 + Expressions.dateTimeTemplate( + Instant.class, + "min({0})", + new CaseBuilder() + .when(TICKET_OPEN_DATE.ticketOpenType.eq(TicketOpenType.PRE_OPEN)) + .then(TICKET_OPEN_DATE.openDate) + .otherwise((Instant) null) + ).as("ticketPreOpenDate"), + // 일반 예매 오픈일 + Expressions.dateTimeTemplate( + Instant.class, + "min({0})", + new CaseBuilder() + .when(TICKET_OPEN_DATE.ticketOpenType.eq(TicketOpenType.GENERAL_OPEN)) + .then(TICKET_OPEN_DATE.openDate) + .otherwise((Instant) null) + ).as("ticketGeneralOpenDate"), + CONCERT_DATE.performanceDate.min().as("startDate"), + CONCERT_DATE.performanceDate.max().as("endDate"), + CONCERT.concertThumbnailStoredPath, + Expressions.constant(0.0) + )) + .from(CONCERT) + .leftJoin(CONCERT.concertHall, CONCERT_HALL) + .join(CONCERT_DATE).on(CONCERT.eq(CONCERT_DATE.concert)) + .join(TICKET_OPEN_DATE).on(CONCERT.eq(TICKET_OPEN_DATE.concert)) + .where(CONCERT.concertId.in(concertIds)) + .groupBy(CONCERT.concertId, + CONCERT.concertName, + CONCERT_HALL.concertHallName, + CONCERT.concertThumbnailStoredPath + ) + .fetch(); } /** @@ -102,20 +102,20 @@ public List findAgentDetailsByIds(List agentIds) { return Collections.emptyList(); } return queryFactory - .select(Projections.constructor(AgentSearchInfo.class, - MEMBER.memberId, - MEMBER.nickname, - MEMBER.profileImgStoredPath, - PORTFOLIO.portfolioDescription, - AGENT_PERFORMANCE_SUMMARY.averageRating, - AGENT_PERFORMANCE_SUMMARY.reviewCount, - Expressions.constant(0.0) - )) - .from(MEMBER) - .leftJoin(PORTFOLIO).on(PORTFOLIO.member.eq(MEMBER)) - .innerJoin(AGENT_PERFORMANCE_SUMMARY).on(MEMBER.eq(AGENT_PERFORMANCE_SUMMARY.agent)) - .where(MEMBER.memberId.in(agentIds)) - .fetch(); + .select(Projections.constructor(AgentSearchInfo.class, + MEMBER.memberId, + MEMBER.nickname, + MEMBER.profileImgStoredPath, + PORTFOLIO.portfolioDescription, + AGENT_PERFORMANCE_SUMMARY.averageRating, + AGENT_PERFORMANCE_SUMMARY.reviewCount, + Expressions.constant(0.0) + )) + .from(MEMBER) + .leftJoin(PORTFOLIO).on(PORTFOLIO.member.eq(MEMBER)) + .innerJoin(AGENT_PERFORMANCE_SUMMARY).on(MEMBER.eq(AGENT_PERFORMANCE_SUMMARY.agent)) + .where(MEMBER.memberId.in(agentIds)) + .fetch(); } /** @@ -129,21 +129,21 @@ public List findAgentDetailsByIds(List agentIds) { public List findAgentIdsByKeyword(String keyword, int limit) { // 동적 WHERE 절 조합 BooleanExpression whereClause = QueryDslUtil.allOf( - MEMBER.memberType.eq(MemberType.AGENT), - QueryDslUtil.anyOf( - QueryDslUtil.likeIgnoreCase(MEMBER.nickname, keyword), - QueryDslUtil.likeIgnoreCase(PORTFOLIO.portfolioDescription, keyword) - ) + MEMBER.memberType.eq(MemberType.AGENT), + QueryDslUtil.anyOf( + QueryDslUtil.likeIgnoreCase(MEMBER.nickname, keyword), + QueryDslUtil.likeIgnoreCase(PORTFOLIO.portfolioDescription, keyword) + ) ); return queryFactory - .select(MEMBER.memberId) - .from(MEMBER) - .innerJoin(PORTFOLIO) - .on((PORTFOLIO.member).eq(MEMBER)) - .where(whereClause) - .limit(limit) - .fetch(); + .select(MEMBER.memberId) + .from(MEMBER) + .innerJoin(PORTFOLIO) + .on((PORTFOLIO.member).eq(MEMBER)) + .where(whereClause) + .limit(limit) + .fetch(); } /**