package ru.soune.nocopy.service; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import ru.soune.nocopy.dto.file.SimilarFileResponse; import ru.soune.nocopy.dto.file.SimilarityFilter; import ru.soune.nocopy.repository.ImageHashRepository; import ru.soune.nocopy.repository.ImageSimilarityRepository; import ru.soune.nocopy.repository.SimilarImageProjection; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class FileSimilarityService { private final ImageSimilarityRepository repository; private final ImageHashRepository hashRepository; public List findSimilarFiles(String fileId) { var imageHashEntity = hashRepository.findById(fileId) .orElseThrow(() -> new RuntimeException("Hash not found")); Integer hash64Hi = imageHashEntity.getHash64Hi(); Integer hash64Lo = imageHashEntity.getHash64Lo(); List candidates = repository.findCandidates(fileId); return candidates.stream() .map(c -> { Integer cHi = c.getHash64Hi(); Integer cLo = c.getHash64Lo(); int hamming = hamming64(hash64Hi, hash64Lo, cHi, cLo); String level; if (hamming <= 5) { level = "DUPLICATE"; } else if (hamming <= 12) { level = "SIMILAR"; } else { level = "DIFFERENT"; } return SimilarFileResponse.builder() .fileId(c.getId()) .originalFileName(c.getOriginalFileName()) .fileSize(c.getFileSize()) .hammingDistance(hamming) .similarityLevel(level) .build(); }) .sorted((a, b) -> Integer.compare(a.getHammingDistance(), b.getHammingDistance())) .toList(); } public List findDuplicatedByHash(Integer hash64Hi, Integer hash64Lo) { List duplicates = repository.findExactDuplicates(hash64Hi,hash64Lo); if (duplicates.isEmpty()) { return new ArrayList<>(); } return duplicates; } public Page findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable) { var imageHashEntity = hashRepository.findById(fileId) .orElseThrow(() -> new RuntimeException("Hash not found")); Integer hash64Hi = imageHashEntity.getHash64Hi(); Integer hash64Lo = imageHashEntity.getHash64Lo(); List candidates = repository.findCandidates(fileId); List similarityLevels = (filter != null && filter.getSimilarityLevels() != null) ? filter.getSimilarityLevels() : List.of("DUPLICATE", "SIMILAR", "DIFFERENT"); List allResults = candidates.stream() .map(c -> createSimilarFileResponse(c, hash64Hi, hash64Lo)) .filter(response -> similarityLevels.contains(response.getSimilarityLevel())) .sorted(Comparator.comparingInt(SimilarFileResponse::getHammingDistance)) .collect(Collectors.toList()); int total = allResults.size(); int page = (pageable != null) ? pageable.getPageNumber() : 0; int size = (pageable != null) ? pageable.getPageSize() : 20; int fromIndex = Math.min(page * size, total); int toIndex = Math.min(fromIndex + size, total); List pageContent = allResults.subList(fromIndex, toIndex); return new PageImpl<>(pageContent, pageable, total); } private SimilarFileResponse createSimilarFileResponse(SimilarImageProjection similarImageProjection, Integer hash64Hi, Integer hash64Lo) { Integer imageProjectionHash64Hi = similarImageProjection.getHash64Hi(); Integer similarImageProjectionHash64Lo = similarImageProjection.getHash64Lo(); int hamming = hamming64(hash64Hi, hash64Lo, imageProjectionHash64Hi, similarImageProjectionHash64Lo); String level; if (hamming <= 5) { level = "DUPLICATE"; } else if (hamming <= 12) { level = "SIMILAR"; } else { level = "DIFFERENT"; } return SimilarFileResponse.builder() .fileId(similarImageProjection.getId()) .originalFileName(similarImageProjection.getOriginalFileName()) .fileSize(similarImageProjection.getFileSize()) .hammingDistance(hamming) .similarityLevel(level) .build(); } private int hamming64(int aHi, int aLo, int bHi, int bLo) { return Integer.bitCount(aHi ^ bHi) + Integer.bitCount(aLo ^ bLo); } }