Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 사진 업로드 때, 새로운 맴버가 그룹에 들어갈 때, 얼굴 인식 로직 #70

Merged
merged 14 commits into from
Aug 10, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
implementation 'commons-io:commons-io:2.6'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

Expand Down Expand Up @@ -72,10 +73,14 @@ dependencies {
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3'
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

// elasticsearch
implementation 'org.springframework.data:spring-data-elasticsearch:'
//AWS lambda
implementation 'com.amazonaws:aws-java-sdk-lambda:1.12.767'

//elasticsearch
implementation 'org.springframework.data:spring-data-elasticsearch'

}

tasks.named('test') {
useJUnitPlatform()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,9 @@
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "face_vectors")
public class FaceVector {
@Id
private String id;
@Field(type = FieldType.Long)
private String shareGroupId;
@Field(type = FieldType.Keyword)
private String keyValue;
@Field(type = FieldType.Date)
private String name;
private String date;
@Field(type = FieldType.Dense_Vector)
private List<Float> faceVector;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,12 @@
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "photos_es")
public class PhotoEs {
@Id
private String id;
@Field(type = FieldType.Long)
private Long rdsId;
private Long shareGroupId;
@Field(type = FieldType.Keyword)
private String url;
@Field(type = FieldType.Keyword)
private String name;
@Field(type = FieldType.Date)
private String createdAt;
@Field(type = FieldType.Long)
private List<Long> faceTag;
@Field(type = FieldType.Long)
private List<Long> downloadTag;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,7 @@
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "sample_face_vectors")
public class SampleFaceVector {
@Id
private String id;
@Field(type = FieldType.Long)
private Long memberId;
@Field(type = FieldType.Dense_Vector)
private List<Float> faceVector;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"shareGroupId" : {
"type" : "long"
},
"keyValue" : {
"name" : {
"type" : "keyword"
},
"createdAt" : {
Expand All @@ -23,7 +23,7 @@
},
"faceVector" : {
"type": "dense_vector",
"dims": 128,
"dims": 512,
"index": true,
"similarity" : "dot_product"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

{
"settings" : {
"index" :{
Expand All @@ -12,6 +11,9 @@
"required" : true
},
"properties" : {
"rdsId" : {
"type" : "long"
},
"shareGroupId" : {
"type" : "long"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
},
"faceVector" : {
"type": "dense_vector",
"dims": 128,
"index": true,
"dims": 512,
"index": false,
"similarity" : "dot_product"
}
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import com.umc.naoman.domain.photo.elasticsearch.document.PhotoEs;
import com.umc.naoman.domain.photo.entity.Photo;
import com.umc.naoman.global.error.BusinessException;
import com.umc.naoman.global.error.code.ElasticsearchErrorCode;
import lombok.RequiredArgsConstructor;
Expand All @@ -27,23 +28,24 @@ public class PhotoEsClientRepository {
private final ElasticsearchClient elasticsearchClient;

//사진 업로드 시 ES에 벌크로 업로드
public void savePhotoBulk(List<String> url, List<String> nameList, Long shareGroupId) {
List<PhotoEs> photoEsList = new ArrayList<>();
for(int i=0; i<url.size(); i++){
PhotoEs photoEs = PhotoEs.builder()
.shareGroupId(shareGroupId)
.url(url.get(i))
.name(nameList.get(i))
.createdAt(esTimeFormat(LocalDateTime.now()))
.build();
photoEsList.add(photoEs);
}
public void savePhotoBulk(List<Photo> photoList) {
List<PhotoEs> photoEsList = photoList.stream()
.map(photo -> PhotoEs.builder()
.rdsId(photo.getId())
.shareGroupId(photo.getShareGroup().getId())
.faceTag(new ArrayList<>())
.downloadTag(new ArrayList<>())
.url(photo.getUrl())
.name(photo.getName())
.createdAt(esTimeFormat(photo.getCreatedAt()))
.build())
.toList();
BulkRequest.Builder bulkBuilder = new BulkRequest.Builder();
for(PhotoEs photoEs :photoEsList){
bulkBuilder.operations(op ->op
.index(idx -> idx
.index("photos_es")
.routing(shareGroupId.toString())
.routing(photoEs.getShareGroupId().toString())
.document(photoEs)
)
);
Expand Down Expand Up @@ -84,7 +86,7 @@ public Page<PhotoEs> findPhotoEsByShareGroupId(Long shareGroupId, Pageable pagea
}

//특정 공유 그룹의 얼굴이 태그된 사진 검색
public Page<PhotoEs> findPhotoEsByShareGroupIdAndFaceTag(Long shareGroupId,Long faceTag, Pageable pageable) throws IOException{
public Page<PhotoEs> findPhotoEsByShareGroupIdAndFaceTag(Long shareGroupId,Long faceTag, Pageable pageable) {
SearchResponse<PhotoEs> response = null;
try{
response = elasticsearchClient.search(s->s
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.umc.naoman.domain.photo.service;

import java.util.List;

public interface FaceDetectionService {

void detectFaceUploadPhoto(List<String> photoNameList, Long shareGroupId);
void detectFaceJoinShareGroup(Long memberId, Long shareGroupId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.umc.naoman.domain.photo.service;
import com.amazonaws.services.lambda.AWSLambda;
import com.amazonaws.services.lambda.model.InvocationType;
import com.amazonaws.services.lambda.model.InvokeRequest;
import com.amazonaws.services.lambda.model.InvokeResult;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.umc.naoman.domain.shareGroup.service.ShareGroupService;
import com.umc.naoman.global.error.BusinessException;
import com.umc.naoman.global.error.code.AwsLambdaErrorCode;
import lombok.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class FaceDetectionServiceImpl implements FaceDetectionService{
redblackblossom marked this conversation as resolved.
Show resolved Hide resolved
@Value("${spring.lambda.function.detect_face_photo}")
private String detectFacePhotoLambda;
@Value("${spring.lambda.function.join_share_group}")
redblackblossom marked this conversation as resolved.
Show resolved Hide resolved
private String detectFaceShareGroupLambda;
private final AWSLambda awsLambda;
private final ObjectMapper objectMapper = new ObjectMapper();
private final ShareGroupService shareGroupService;

@Getter
@AllArgsConstructor
private class DetectFacePhotoPayload {
private List<String> nameList;
redblackblossom marked this conversation as resolved.
Show resolved Hide resolved
private List<Long> memberIdList;
private Long shareGroupId;
}

@Getter
@AllArgsConstructor
private class DetectFaceShareGroupPayload {
private Long memberId;
private Long shareGroupId;
}

@Override
public void detectFaceUploadPhoto(List<String> photoNameList, Long shareGroupId) {
List<Long> memberIdList = shareGroupService.findProfileListByShareGroupId(shareGroupId).stream()
.map(profile -> profile.getMember().getId())
.collect(Collectors.toList());
DetectFacePhotoPayload payLoad = new DetectFacePhotoPayload(photoNameList,memberIdList,shareGroupId);
String lambdaPayload = null;

try {
lambdaPayload = objectMapper.writeValueAsString(payLoad);
} catch (JsonProcessingException e) {
throw new BusinessException(AwsLambdaErrorCode.AWS_JsonProcessing_Exception,e);
}
InvokeRequest invokeRequest = new InvokeRequest()
.withInvocationType(InvocationType.Event)
.withFunctionName(detectFacePhotoLambda)
.withPayload(lambdaPayload);

awsLambda.invoke(invokeRequest);
}

@Override
public void detectFaceJoinShareGroup(Long memberId, Long shareGroupId) {
DetectFaceShareGroupPayload payLoad = new DetectFaceShareGroupPayload(memberId,shareGroupId);
String lambdaPayload = null;

try {
lambdaPayload = objectMapper.writeValueAsString(payLoad);
} catch (JsonProcessingException e) {
throw new BusinessException(AwsLambdaErrorCode.AWS_JsonProcessing_Exception,e);
}
InvokeRequest invokeRequest = new InvokeRequest()
.withInvocationType(InvocationType.Event)
redblackblossom marked this conversation as resolved.
Show resolved Hide resolved
.withFunctionName(detectFaceShareGroupLambda)
.withPayload(lambdaPayload);

awsLambda.invoke(invokeRequest);
}


}
25 changes: 25 additions & 0 deletions src/main/java/com/umc/naoman/global/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.umc.naoman.global.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {

@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(30);
executor.setKeepAliveSeconds(30);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/umc/naoman/global/config/S3Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.lambda.AWSLambda;
import com.amazonaws.services.lambda.AWSLambdaClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -32,4 +34,12 @@ public AmazonS3 amazonS3() {
.withCredentials(new AWSStaticCredentialsProvider(awsCredentialsProvider()))
.build();
}

@Bean
public AWSLambda awsLambda() {
redblackblossom marked this conversation as resolved.
Show resolved Hide resolved
return AWSLambdaClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentialsProvider()))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.umc.naoman.global.error.code;

import com.umc.naoman.global.error.ErrorCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum AwsLambdaErrorCode implements ErrorCode {
AWS_JsonProcessing_Exception(500, "EA000", "AWS Lambda JsonProcessingException 발생"),
;

private final int status;
private final String code;
private final String message;
}