diff --git a/persistence-service-v1/persistence-service-external-api-impl-v1/src/main/java/com/iqser/red/persistence/service/v1/external/api/impl/controller/ImageSimilaritySearchController.java b/persistence-service-v1/persistence-service-external-api-impl-v1/src/main/java/com/iqser/red/persistence/service/v1/external/api/impl/controller/ImageSimilaritySearchController.java new file mode 100644 index 000000000..62998a489 --- /dev/null +++ b/persistence-service-v1/persistence-service-external-api-impl-v1/src/main/java/com/iqser/red/persistence/service/v1/external/api/impl/controller/ImageSimilaritySearchController.java @@ -0,0 +1,51 @@ +package com.iqser.red.persistence.service.v1.external.api.impl.controller; + +import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.GET_SIMILAR_IMAGES; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.iqser.red.service.persistence.management.v1.processor.service.ImageSimilarityService; +import com.iqser.red.service.persistence.service.v1.api.external.resource.ImageSimilaritySearchResource; +import com.iqser.red.service.persistence.service.v1.api.shared.model.image.ImageSimilaritySearchRequest; +import com.iqser.red.service.persistence.service.v1.api.shared.model.image.ImageSimilaritySearchResponse; +import com.iqser.red.service.persistence.service.v1.api.shared.mongo.document.ImageDocument; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +@RestController +@RequiredArgsConstructor +@Slf4j +public class ImageSimilaritySearchController implements ImageSimilaritySearchResource { + + private final ImageSimilarityService imageSimilarityService; + + + @SneakyThrows + @PreAuthorize("hasAuthority('" + GET_SIMILAR_IMAGES + "')") + public ResponseEntity getSimilarImages(@RequestBody ImageSimilaritySearchRequest imageSimilaritySearchRequest) { + + log.info("received similiar image search request {}", imageSimilaritySearchRequest); + List similarImages = this.imageSimilarityService.findSimilarImages(imageSimilaritySearchRequest.getCentroId(), + imageSimilaritySearchRequest.getDistance(), + imageSimilaritySearchRequest.getScope()); + List similarImagesIds = new ArrayList<>(); + List similarImagesThumbnails = new ArrayList<>(); + similarImages.stream() + .forEach(doc -> { + similarImagesIds.add(doc.getImageId()); + similarImagesThumbnails.add(doc.getThumbnail()); + }); + ImageSimilaritySearchResponse imageSimilaritySearchResponse = new ImageSimilaritySearchResponse(similarImagesIds, similarImagesThumbnails); + return new ResponseEntity<>(imageSimilaritySearchResponse, HttpStatus.OK); + } + +} diff --git a/persistence-service-v1/persistence-service-external-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/external/resource/ImageSimilaritySearchResource.java b/persistence-service-v1/persistence-service-external-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/external/resource/ImageSimilaritySearchResource.java new file mode 100644 index 000000000..a8795cdf3 --- /dev/null +++ b/persistence-service-v1/persistence-service-external-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/external/resource/ImageSimilaritySearchResource.java @@ -0,0 +1,28 @@ +package com.iqser.red.service.persistence.service.v1.api.external.resource; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +import com.iqser.red.service.persistence.service.v1.api.shared.model.image.ImageSimilaritySearchRequest; +import com.iqser.red.service.persistence.service.v1.api.shared.model.image.ImageSimilaritySearchResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +public interface ImageSimilaritySearchResource { + + String IMAGE_SIMILARITY_SEARCH_PATH = ExternalApi.BASE_PATH + "/imageSimilaritySearch"; + + + @ResponseStatus(value = HttpStatus.OK) + @Operation(summary = "Gets similiar images to given image", description = "None") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "404", description = "Not found")}) + @PostMapping(value = IMAGE_SIMILARITY_SEARCH_PATH, produces = MediaType.APPLICATION_JSON_VALUE) + ResponseEntity getSimilarImages(@RequestBody ImageSimilaritySearchRequest imageSimilaritySearchRequest); + +} diff --git a/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/roles/ActionRoles.java b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/roles/ActionRoles.java index 051962562..cb81b38d3 100644 --- a/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/roles/ActionRoles.java +++ b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/roles/ActionRoles.java @@ -59,6 +59,10 @@ public final class ActionRoles { public static final String READ_GENERAL_CONFIGURATION = "red-read-general-configuration"; public static final String WRITE_GENERAL_CONFIGURATION = "red-write-general-configuration"; + // IMAGE SIMILIARITY SEARCH + + public static final String GET_SIMILAR_IMAGES ="red-get-similiar-images"; + // Preferences public static final String MANAGE_USER_PREFERENCES = "red-manage-user-preferences"; diff --git a/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/ImageSimilarityService.java b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/ImageSimilarityService.java new file mode 100644 index 000000000..983c3951d --- /dev/null +++ b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/ImageSimilarityService.java @@ -0,0 +1,93 @@ +package com.iqser.red.service.persistence.management.v1.processor.service; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iqser.red.service.persistence.service.v1.api.shared.model.utils.Scope; +import com.iqser.red.service.persistence.service.v1.api.shared.mongo.document.ImageDocument; +import com.iqser.red.service.persistence.service.v1.api.shared.mongo.service.ImageMongoService; +import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.DocumentStructure; +import com.knecon.fforesight.service.layoutparser.internal.api.data.redaction.NodeType; +import com.knecon.fforesight.tenantcommons.TenantContext; + +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class ImageSimilarityService { + + @Autowired + ImageMongoService imageMongoService; + @Autowired + FileManagementStorageService fileManagementStorageService; + @Autowired + ObjectMapper objectMapper; + + + public void saveImages(String templateId, String dossierId, String fileId, String storageId) throws IOException { + + List imageDocuments = new ArrayList<>(); + try (InputStream inputStream = fileManagementStorageService.getObject(TenantContext.getTenantId(), storageId)) { + DocumentStructure documentStructure = objectMapper.readValue(inputStream, DocumentStructure.class); + documentStructure.streamAllEntries() + .filter(entry -> entry.getType().equals(NodeType.IMAGE)) + .forEach(i -> { + Map properties = i.getProperties(); + ImageDocument imageDocument = new ImageDocument(); + imageDocument.setImageId(properties.get("id")); + imageDocument.setFeatureVector(parseRepresentationVector(properties.get("representationHash"))); + imageDocument.setTemplateId(templateId); + imageDocument.setDossierId(dossierId); + imageDocument.setFileId(fileId); + imageDocuments.add(imageDocument); + + }); + } + if (imageDocuments.isEmpty()) { + return; + } + imageMongoService.saveImages(imageDocuments); + } + + + public List findSimilarImages(String centroId, double distance, Scope scope) throws Exception { + + ImageDocument centroImage = this.imageMongoService.findById(centroId); + log.info("image received with id {}: {}", centroId, centroImage); + List similarImages = this.imageMongoService.findSimilarImages(centroImage, distance, scope); + log.info("received similar images: {}", similarImages); + return similarImages.stream() + .collect(Collectors.toList()); + } + + + public static double[] parseRepresentationVector(String representationHash) { + + double[] doubleArray = new double[representationHash.length()]; + + for (int i = 0; i < representationHash.length(); i++) { + char c = representationHash.charAt(i); + if (Character.isDigit(c)) { + // Convert numeric characters directly to their numeric values + doubleArray[i] = Character.getNumericValue(c); + } else if (Character.isLetter(c)) { + // Convert alphabetic characters to their position in the alphabet + // 'A' or 'a' -> 10, 'B' or 'b' -> 11, ..., 'F' or 'f' -> 15 + doubleArray[i] = 10 + Character.toUpperCase(c) - 'A'; + } else { + throw new IllegalArgumentException("Invalid character in input string: " + c); + } + } + + return doubleArray; + } + +} \ No newline at end of file diff --git a/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/queue/ImageMessageReceiver.java b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/queue/ImageMessageReceiver.java index 00597e9b2..a1807c6b5 100644 --- a/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/queue/ImageMessageReceiver.java +++ b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/queue/ImageMessageReceiver.java @@ -15,9 +15,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.iqser.red.service.persistence.management.v1.processor.configuration.MessagingConfiguration; import com.iqser.red.service.persistence.management.v1.processor.service.FileStatusProcessingUpdateService; import com.iqser.red.service.persistence.management.v1.processor.service.FileStatusService; -import com.iqser.red.service.persistence.management.v1.processor.settings.FileManagementServiceSettings; import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileErrorInfo; -import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType; import io.micrometer.observation.ObservationRegistry; import lombok.RequiredArgsConstructor; @@ -43,11 +41,13 @@ public class ImageMessageReceiver { String dossierId = imageResponse.path("dossierId").asText(); String fileId = imageResponse.path("fileId").asText(); + addFileIdToTrace(fileId); fileStatusService.setStatusAnalyse(dossierId, fileId, false); log.info("Received message from {} for dossierId {} and fileId {}", MessagingConfiguration.IMAGE_SERVICE_RESPONSE_QUEUE, dossierId, fileId); + } diff --git a/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/queue/LayoutParsingFinishedMessageReceiver.java b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/queue/LayoutParsingFinishedMessageReceiver.java index 4ac94ca82..7035abacc 100644 --- a/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/queue/LayoutParsingFinishedMessageReceiver.java +++ b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/queue/LayoutParsingFinishedMessageReceiver.java @@ -1,16 +1,25 @@ package com.iqser.red.service.persistence.management.v1.processor.service.queue; +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Service; + import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.exc.ValueInstantiationException; import com.iqser.red.service.persistence.management.v1.processor.configuration.MessagingConfiguration; import com.iqser.red.service.persistence.management.v1.processor.migration.SaasMigrationService; import com.iqser.red.service.persistence.management.v1.processor.model.websocket.AnalyseStatus; import com.iqser.red.service.persistence.management.v1.processor.service.FileStatusProcessingUpdateService; import com.iqser.red.service.persistence.management.v1.processor.service.FileStatusService; +import com.iqser.red.service.persistence.management.v1.processor.service.ImageSimilarityService; import com.iqser.red.service.persistence.management.v1.processor.service.WebsocketService; import com.iqser.red.service.persistence.management.v1.processor.service.layoutparsing.LayoutParsingRequestIdentifierService; import com.iqser.red.service.persistence.management.v1.processor.service.persistence.SaasMigrationStatusPersistenceService; +import com.iqser.red.service.persistence.management.v1.processor.utils.StorageIdUtils; import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileErrorInfo; +import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType; import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingFinishedEvent; import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingQueueNames; import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingRequest; @@ -19,13 +28,6 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.rabbit.annotation.RabbitListener; -import org.springframework.stereotype.Service; - -import java.time.OffsetDateTime; -import java.time.temporal.ChronoUnit; - @Slf4j @Service @RequiredArgsConstructor @@ -37,6 +39,7 @@ public class LayoutParsingFinishedMessageReceiver { private final LayoutParsingRequestIdentifierService layoutParsingRequestIdentifierService; private final SaasMigrationStatusPersistenceService saasMigrationStatusPersistenceService; private final SaasMigrationService saasMigrationService; + private final ImageSimilarityService imageSimilarityService; private final WebsocketService websocketService; @@ -50,8 +53,10 @@ public class LayoutParsingFinishedMessageReceiver { return; } + var templateId = ""; var dossierId = layoutParsingRequestIdentifierService.parseDossierId(response.identifier()); var fileId = layoutParsingRequestIdentifierService.parseFileId(response.identifier()); + var storageId = StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_STRUCTURE); fileStatusService.setStatusAnalyse(layoutParsingRequestIdentifierService.parseDossierId(response.identifier()), layoutParsingRequestIdentifierService.parseFileId(response.identifier()), @@ -59,6 +64,7 @@ public class LayoutParsingFinishedMessageReceiver { fileStatusService.updateLayoutProcessedTime(layoutParsingRequestIdentifierService.parseFileId(response.identifier())); websocketService.sendAnalysisEvent(dossierId, fileId, AnalyseStatus.LAYOUT_UPDATE, fileStatusService.getStatus(fileId).getNumberOfAnalyses() + 1); + imageSimilarityService.saveImages(templateId, dossierId, fileId, storageId); log.info("Received message {} in {}", response, LayoutParsingQueueNames.LAYOUT_PARSING_FINISHED_EVENT_QUEUE); } diff --git a/persistence-service-v1/persistence-service-shared-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/model/image/ImageSimilaritySearchRequest.java b/persistence-service-v1/persistence-service-shared-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/model/image/ImageSimilaritySearchRequest.java new file mode 100644 index 000000000..418d51d09 --- /dev/null +++ b/persistence-service-v1/persistence-service-shared-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/model/image/ImageSimilaritySearchRequest.java @@ -0,0 +1,31 @@ +package com.iqser.red.service.persistence.service.v1.api.shared.model.image; + +import com.iqser.red.service.persistence.service.v1.api.shared.model.utils.Scope; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.ToString; + +@Data +@ToString +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Image Similiarity Search request containing information about centroId, distance and scope") +public class ImageSimilaritySearchRequest { + + @NonNull + @Schema(description = "id of the central image") + private String centroId; + @NonNull + @Schema(description = "the manhattan distance used in the similiarity search") + private double distance; + @NonNull + @Schema(description = "the scope for the similiarity search") + private Scope scope; + +} diff --git a/persistence-service-v1/persistence-service-shared-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/model/image/ImageSimilaritySearchResponse.java b/persistence-service-v1/persistence-service-shared-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/model/image/ImageSimilaritySearchResponse.java new file mode 100644 index 000000000..03db80643 --- /dev/null +++ b/persistence-service-v1/persistence-service-shared-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/model/image/ImageSimilaritySearchResponse.java @@ -0,0 +1,28 @@ +package com.iqser.red.service.persistence.service.v1.api.shared.model.image; + +import java.util.ArrayList; +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Schema(description = "Object containing the result of the image similiarity search") +public class ImageSimilaritySearchResponse { + + @Builder.Default + @Schema(description = "list of ids of similiar images") + List similarImagesIds = new ArrayList<>(); + + @Builder.Default + @Schema(description = "list of ids of similiar images") + List similarImagesThumbnails = new ArrayList<>(); + +} diff --git a/persistence-service-v1/persistence-service-shared-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/model/utils/Scope.java b/persistence-service-v1/persistence-service-shared-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/model/utils/Scope.java new file mode 100644 index 000000000..f9a881f19 --- /dev/null +++ b/persistence-service-v1/persistence-service-shared-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/model/utils/Scope.java @@ -0,0 +1,14 @@ +package com.iqser.red.service.persistence.service.v1.api.shared.model.utils; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Scope { + private String templateId; + private String dossierId; + private String fileId; +} diff --git a/persistence-service-v1/persistence-service-shared-mongo-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/mongo/document/ImageDocument.java b/persistence-service-v1/persistence-service-shared-mongo-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/mongo/document/ImageDocument.java new file mode 100644 index 000000000..609226afc --- /dev/null +++ b/persistence-service-v1/persistence-service-shared-mongo-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/mongo/document/ImageDocument.java @@ -0,0 +1,40 @@ +package com.iqser.red.service.persistence.service.v1.api.shared.mongo.document; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@ToString +@Document(collection = "images") +public class ImageDocument { + + @Indexed + private double[] featureVector; + + private String label; + + @Id + @EqualsAndHashCode.Include + private String imageId; + + private String templateId; + + private String dossierId; + + private String fileId; + + private Byte thumbnail; + +} diff --git a/persistence-service-v1/persistence-service-shared-mongo-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/mongo/repository/ImageDocumentRepository.java b/persistence-service-v1/persistence-service-shared-mongo-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/mongo/repository/ImageDocumentRepository.java new file mode 100644 index 000000000..1dbde7d6f --- /dev/null +++ b/persistence-service-v1/persistence-service-shared-mongo-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/mongo/repository/ImageDocumentRepository.java @@ -0,0 +1,12 @@ +package com.iqser.red.service.persistence.service.v1.api.shared.mongo.repository; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import com.iqser.red.service.persistence.service.v1.api.shared.mongo.document.ImageDocument; + +@Repository +public interface ImageDocumentRepository extends MongoRepository { + // Additional query methods can be added here + +} diff --git a/persistence-service-v1/persistence-service-shared-mongo-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/mongo/service/ImageMongoService.java b/persistence-service-v1/persistence-service-shared-mongo-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/mongo/service/ImageMongoService.java new file mode 100644 index 000000000..1abbc9f98 --- /dev/null +++ b/persistence-service-v1/persistence-service-shared-mongo-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/shared/mongo/service/ImageMongoService.java @@ -0,0 +1,103 @@ +package com.iqser.red.service.persistence.service.v1.api.shared.mongo.service; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.stereotype.Service; + +import com.iqser.red.service.persistence.service.v1.api.shared.model.utils.Scope; +import com.iqser.red.service.persistence.service.v1.api.shared.mongo.document.ImageDocument; +import com.iqser.red.service.persistence.service.v1.api.shared.mongo.repository.ImageDocumentRepository; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class ImageMongoService { + + private MongoOperations mongoOperations; + private final ImageDocumentRepository imageDocumentRepository; + + + public ImageMongoService(MongoOperations mongoOperations, ImageDocumentRepository imageDocumentRepository) { + + this.mongoOperations = mongoOperations; + + this.imageDocumentRepository = imageDocumentRepository; + } + + + public ImageDocument findById(String imageId) { + + Optional image = this.imageDocumentRepository.findById(imageId); + if (image.isPresent()) { + return image.get(); + } + return null; + } + + + public List findSimilarImages(ImageDocument centralImage, double maxDistance, Scope scope) { + + double[] centralFeatureVector = centralImage.getFeatureVector(); + + List allImages = this.imageDocumentRepository.findAll(); + + return allImages.stream() + .filter(image -> !image.getImageId().equals(centralImage.getImageId())) + .filter(image -> filterForScope(image, scope)) + .map(image -> new ImageDistancePair(image, calculateManhattanDistance(image.getFeatureVector(), centralFeatureVector))) + .filter(image -> image.getDistance() <= maxDistance) + .map(ImageDistancePair::getImageDocument) + .collect(Collectors.toList()); + } + + + private boolean filterForScope(ImageDocument image, Scope scope) { + + if (scope.getTemplateId() != null && scope.getTemplateId().length() > 0) { + return image.getTemplateId() != null && image.getTemplateId().equals(scope.getTemplateId()); + } else if (scope.getDossierId() != null && scope.getDossierId().length() > 0) { + return image.getDossierId() != null && image.getDossierId().equals(scope.getDossierId()); + } else if (scope.getFileId() != null && scope.getFileId().length() > 0) { + return image.getFileId() != null && image.getFileId().equals(scope.getFileId()); + } + return true; + } + + + private double calculateManhattanDistance(double[] vector1, double[] vector2) { + + double distance = 0.0; + for (int i = 0; i < vector1.length; i++) { + distance += Math.abs(vector1[i] - vector2[i]); + } + return distance; + } + + + public void saveImages(List imageDocuments) { + + imageDocumentRepository.saveAll(imageDocuments); + } + + + @Getter + private static class ImageDistancePair { + + private final ImageDocument imageDocument; + private final double distance; + + + ImageDistancePair(ImageDocument imageDocument, double distance) { + + this.imageDocument = imageDocument; + this.distance = distance; + } + + } + +}