2026-01-13 22:40:52 +07:00
|
|
|
package ru.soune.nocopy.service;
|
|
|
|
|
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
2026-01-16 14:30:45 +07:00
|
|
|
import org.springframework.data.domain.Page;
|
|
|
|
|
import org.springframework.data.domain.PageImpl;
|
|
|
|
|
import org.springframework.data.domain.Pageable;
|
2026-01-13 22:40:52 +07:00
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
import ru.soune.nocopy.dto.file.SimilarFileResponse;
|
2026-01-16 14:30:45 +07:00
|
|
|
import ru.soune.nocopy.dto.file.SimilarityFilter;
|
2026-01-13 22:40:52 +07:00
|
|
|
import ru.soune.nocopy.repository.ImageHashRepository;
|
|
|
|
|
import ru.soune.nocopy.repository.ImageSimilarityRepository;
|
|
|
|
|
import ru.soune.nocopy.repository.SimilarImageProjection;
|
|
|
|
|
|
2026-01-19 15:49:27 +07:00
|
|
|
import java.util.ArrayList;
|
2026-01-16 14:30:45 +07:00
|
|
|
import java.util.Comparator;
|
2026-01-13 22:40:52 +07:00
|
|
|
import java.util.List;
|
2026-01-16 14:30:45 +07:00
|
|
|
import java.util.stream.Collectors;
|
2026-01-13 22:40:52 +07:00
|
|
|
|
|
|
|
|
@Service
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
public class FileSimilarityService {
|
|
|
|
|
private final ImageSimilarityRepository repository;
|
2026-01-16 14:30:45 +07:00
|
|
|
|
2026-01-13 22:40:52 +07:00
|
|
|
private final ImageHashRepository hashRepository;
|
|
|
|
|
|
|
|
|
|
public List<SimilarFileResponse> findSimilarFiles(String fileId) {
|
|
|
|
|
var imageHashEntity = hashRepository.findById(fileId)
|
|
|
|
|
.orElseThrow(() -> new RuntimeException("Hash not found"));
|
|
|
|
|
|
|
|
|
|
Integer hash64Hi = imageHashEntity.getHash64Hi();
|
|
|
|
|
Integer hash64Lo = imageHashEntity.getHash64Lo();
|
|
|
|
|
|
|
|
|
|
List<SimilarImageProjection> 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()
|
2026-01-24 10:59:02 +07:00
|
|
|
.fileId(c.getId())
|
2026-01-13 22:40:52 +07:00
|
|
|
.originalFileName(c.getOriginalFileName())
|
|
|
|
|
.fileSize(c.getFileSize())
|
|
|
|
|
.hammingDistance(hamming)
|
|
|
|
|
.similarityLevel(level)
|
|
|
|
|
.build();
|
|
|
|
|
})
|
|
|
|
|
.sorted((a, b) ->
|
|
|
|
|
Integer.compare(a.getHammingDistance(), b.getHammingDistance()))
|
|
|
|
|
.toList();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 15:49:27 +07:00
|
|
|
public List<SimilarImageProjection> findDuplicatedByHash(Integer hash64Hi, Integer hash64Lo) {
|
|
|
|
|
List<SimilarImageProjection> duplicates = repository.findExactDuplicates(hash64Hi,hash64Lo);
|
|
|
|
|
|
|
|
|
|
if (duplicates.isEmpty()) {
|
|
|
|
|
return new ArrayList<>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return duplicates;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 14:30:45 +07:00
|
|
|
public Page<SimilarFileResponse> 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<SimilarImageProjection> candidates = repository.findCandidates(fileId);
|
|
|
|
|
|
|
|
|
|
List<String> similarityLevels = (filter != null && filter.getSimilarityLevels() != null)
|
|
|
|
|
? filter.getSimilarityLevels()
|
|
|
|
|
: List.of("DUPLICATE", "SIMILAR", "DIFFERENT");
|
|
|
|
|
|
|
|
|
|
List<SimilarFileResponse> 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<SimilarFileResponse> 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()
|
2026-01-24 10:59:02 +07:00
|
|
|
.fileId(similarImageProjection.getId())
|
2026-01-16 14:30:45 +07:00
|
|
|
.originalFileName(similarImageProjection.getOriginalFileName())
|
|
|
|
|
.fileSize(similarImageProjection.getFileSize())
|
|
|
|
|
.hammingDistance(hamming)
|
|
|
|
|
.similarityLevel(level)
|
|
|
|
|
.build();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-13 22:40:52 +07:00
|
|
|
private int hamming64(int aHi, int aLo, int bHi, int bLo) {
|
|
|
|
|
return Integer.bitCount(aHi ^ bHi)
|
|
|
|
|
+ Integer.bitCount(aLo ^ bLo);
|
|
|
|
|
}
|
|
|
|
|
}
|