From 081ca05f88fcab958beaff89035212d679d0126b Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 11 Feb 2026 17:41:48 +0700 Subject: [PATCH] dev change to visual_matches engine --- .../handler/ImageFoundRequestHandler.java | 21 +- .../service/search/SearchImageService.java | 250 +++++++++++++++++ .../service/search/YandexSearchService.java | 258 ------------------ 3 files changed, 265 insertions(+), 264 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/service/search/SearchImageService.java delete mode 100644 src/main/java/ru/soune/nocopy/service/search/YandexSearchService.java diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 000f74a..574fdce 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -8,12 +8,13 @@ import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.file.ImageSearchRequest; +import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.search.SearchApiToYandexResponseMapper; -import ru.soune.nocopy.service.search.YandexSearchService; +import ru.soune.nocopy.service.search.SearchImageService; import ru.soune.nocopy.service.search.GoogleVisionSearchService; import ru.soune.nocopy.service.tariff.TariffConstants; import ru.soune.nocopy.service.tariff.TariffInfoService; @@ -30,7 +31,7 @@ public class ImageFoundRequestHandler implements RequestHandler { private final ObjectMapper objectMapper; - private final YandexSearchService yandexSearchService; + private final SearchImageService searchImageService; private final GoogleVisionSearchService googleVisionSearchService; @@ -61,9 +62,11 @@ public class ImageFoundRequestHandler implements RequestHandler { //TODO uncommited when add billing // GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId); - String yandexReverseSearchResponse; + String searchResponseYandex; + String searchResponseGoogle; try { - yandexReverseSearchResponse = yandexSearchService.searchReverseByPublicUrlWithTimeout(fileEntity); + searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image"); + searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens"); } catch (TimeoutException e) { log.warn("Search timeout for file {}, returning empty results", fileId); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), @@ -85,8 +88,14 @@ public class ImageFoundRequestHandler implements RequestHandler { int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1; + YandexSearchResponse yandexSearchResponse = mapper.mapToYandexResponse(searchResponseYandex, page, 5); + YandexSearchResponse googleSearchResponse = mapper.mapToYandexResponse(searchResponseGoogle, page, 5); + + List results = yandexSearchResponse.getImages(); + results.addAll(googleSearchResponse.getImages()); + yandexSearchResponse.setImages(results); + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), - MessageCode.SUCCESS.getDescription(), mapper.mapToYandexResponse(yandexReverseSearchResponse, page, - 10)); + MessageCode.SUCCESS.getDescription(), yandexSearchResponse); } } diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java new file mode 100644 index 0000000..a11c3d3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -0,0 +1,250 @@ +package ru.soune.nocopy.service.search; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.DeserializationFeature; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.exception.NotValidFieldException; + +import java.io.*; +import java.net.SocketTimeoutException; +import java.util.Map; +import java.util.concurrent.*; + +@Slf4j +@Service +public class SearchImageService { + + private final ObjectMapper objectMapper; + + private final OkHttpClient httpClient; + + public SearchImageService() { + this.httpClient = new OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.SECONDS) + .writeTimeout(5, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .callTimeout(20, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build(); + + this.objectMapper = new ObjectMapper(); + } + + @Value("${yandex.api-key}") + private String apiKey; + + @Value("${yandex.folder-id}") + private String folderId; + + @Value("${yandex.search-url}") + private String searchUrl; + + @Value("${searchapi.api-key:}") + private String searchApiKey; + + @Value("${server.baseurl}") + private String appBaseUrl; + + @PostConstruct + public void init() { + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + } + + public String searchReverseByPublicUrl(FileEntity fileEntity, String engine) + throws IOException, TimeoutException { + + if (!isImageFile(fileEntity)) { + throw new NotValidFieldException( + "File not image", + new BaseResponse( + 20007, + MessageCode.INVALID_FIELD.getCode(), + MessageCode.INVALID_FIELD.getDescription(), + Map.of("file_type", fileEntity.getMimeType()) + ) + ); + } + + String publicUrl = String.format("%s/api/files/public/%s", appBaseUrl, fileEntity.getId()); + + log.info("Searching reverse for image: {}", publicUrl); + + try { + return callReverseImageApiByUrl(publicUrl, engine); + } catch (SocketTimeoutException e) { + log.error("Yandex search timeout after {}", fileEntity.getId()); + throw new TimeoutException("Search timeout"); + } + } + + private boolean isImageFile(FileEntity fileEntity) { + String mimeType = fileEntity.getMimeType(); + return mimeType != null && mimeType.startsWith("image"); + } + + private String callReverseImageApiByUrl(String imageUrl, String engine) throws IOException { + + if (searchApiKey == null || searchApiKey.isBlank()) { + throw new IllegalStateException("SearchAPI key not configured"); + } + + HttpUrl url = HttpUrl.parse("https://searchapi.io/api/v1/search") + .newBuilder() + .addQueryParameter("engine", engine) + .addQueryParameter("api_key", searchApiKey) + .addQueryParameter("url", imageUrl) + .addQueryParameter("search_type", "visual_matches") + .addQueryParameter("wait", "true") + .build(); + + Request request = new Request.Builder() + .url(url) + .header("Accept", "application/json") + .header("User-Agent", "Mozilla/5.0") + .build(); + + long start = System.currentTimeMillis(); + + try (Response response = httpClient.newCall(request).execute()) { + + long duration = System.currentTimeMillis() - start; + log.info("Yandex response code={}, duration={}ms", response.code(), duration); + + if (!response.isSuccessful()) { + throw new IOException("API error: " + response.code()); + } + + ResponseBody body = response.body(); + if (body == null) { + throw new IOException("Empty response body"); + } + + return body.string(); + } + } + +// public YandexSearchResponse searchByFileEntity(FileEntity fileEntity) throws IOException { +// byte[] fileBytes; +// +// if (!isImageFile(fileEntity)) { +// log.error("File not image: {}", fileEntity.getMimeType()); +// throw new NotValidFieldException("File not image", new BaseResponse(20007, +// MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(), +// Map.of("file_type", fileEntity.getMimeType()))); +// } +// +// try { +// fileBytes = readFileFromDisk(fileEntity); +// } catch (IOException e) { +// throw new NotValidFieldException("File not found or cannot read file", new BaseResponse(20007, +// MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), +// Map.of("fileId", fileEntity.getId(), +// "filePath", fileEntity.getFilePath()))); +// } +// +// return callYandexApi(fileBytes); +// } + + +// public String searchReverseByPublicUrl(FileEntity fileEntity) throws IOException { +// if (!isImageFile(fileEntity)) { +// log.error("File not image: {}", fileEntity.getMimeType()); +// throw new NotValidFieldException("File not image", new BaseResponse(20007, +// MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(), +// Map.of("file_type", fileEntity.getMimeType()))); +// } +// +// String publicUrl = String.format("%s/api/files/public/%s", appBaseUrl, fileEntity.getId()); +// +// log.info("Searching reverse for image: {}", publicUrl); +// +// return callReverseImageApiByUrl(publicUrl); +// } + +// private byte[] readFileFromDisk(FileEntity fileEntity) throws IOException { +// Path filePath = Path.of(fileEntity.getFilePath()); +// +// if (!Files.exists(filePath)) { +// throw new IOException("File not found: " + fileEntity.getFilePath()); +// } +// +// if (!Files.isReadable(filePath)) { +// throw new IOException("Cannot read file: " + fileEntity.getFilePath()); +// } +// +// return Files.readAllBytes(filePath); +// } + +// private YandexSearchResponse callYandexApi(byte[] imageBytes) throws IOException { +// String imageBase64 = Base64.getEncoder().encodeToString(imageBytes); +// String jsonRequest = String.format("{\"folderId\":\"%s\",\"data\":\"%s\",\"page\":0,\"showSimilarImages\":true}", +// folderId, imageBase64); +// +// URL url = new URL(searchUrl); +// HttpURLConnection connection = (HttpURLConnection) url.openConnection(); +// +// try { +// connection.setRequestMethod("POST"); +// connection.setRequestProperty("Authorization", "Api-Key " + apiKey); +// connection.setRequestProperty("Content-Type", "application/json"); +// connection.setRequestProperty("Accept", "application/json"); +// connection.setConnectTimeout(30000); +// connection.setReadTimeout(30000); +// connection.setDoOutput(true); +// +// try (OutputStream os = connection.getOutputStream()) { +// byte[] input = jsonRequest.getBytes("utf-8"); +// os.write(input, 0, input.length); +// os.flush(); +// } +// +// int responseCode = connection.getResponseCode(); +// +// String responseBody; +// if (responseCode == HttpURLConnection.HTTP_OK) { +// try (BufferedReader br = new BufferedReader( +// new InputStreamReader(connection.getInputStream(), "utf-8"))) { +// responseBody = readAll(br); +// } +// } else { +// try (BufferedReader br = new BufferedReader( +// new InputStreamReader(connection.getErrorStream(), "utf-8"))) { +// responseBody = readAll(br); +// } +// throw new IOException("Error Yandex API: " + responseCode + " - " + responseBody); +// } +// +// return parseJsonResponse(responseBody); +// +// } finally { +// connection.disconnect(); +// } +// } +// private String readAll(BufferedReader reader) throws IOException { +// StringBuilder sb = new StringBuilder(); +// String line; +// while ((line = reader.readLine()) != null) { +// sb.append(line); +// } +// return sb.toString(); +// } +// +// private YandexSearchResponse parseJsonResponse(String json) { +// YandexSearchResponse response = null; +// try { +// response = objectMapper.readValue(json, YandexSearchResponse.class); +// } catch (JsonProcessingException e) { +// throw new RuntimeException(e); +// } +// +// return response; +// } +} diff --git a/src/main/java/ru/soune/nocopy/service/search/YandexSearchService.java b/src/main/java/ru/soune/nocopy/service/search/YandexSearchService.java deleted file mode 100644 index a4a4e1c..0000000 --- a/src/main/java/ru/soune/nocopy/service/search/YandexSearchService.java +++ /dev/null @@ -1,258 +0,0 @@ -package ru.soune.nocopy.service.search; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.DeserializationFeature; -import jakarta.annotation.PostConstruct; -import lombok.extern.slf4j.Slf4j; -import okhttp3.*; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import ru.soune.nocopy.dto.BaseResponse; -import ru.soune.nocopy.dto.MessageCode; -import ru.soune.nocopy.dto.file.YandexSearchResponse; -import ru.soune.nocopy.entity.file.FileEntity; -import ru.soune.nocopy.exception.NotValidFieldException; - -import java.io.*; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Base64; -import java.util.Map; -import java.util.concurrent.*; - -@Slf4j -@Service -public class YandexSearchService { - - private final ObjectMapper objectMapper; - - private final OkHttpClient httpClient; - - public YandexSearchService() { - this.httpClient = new OkHttpClient.Builder() - .connectTimeout(5, TimeUnit.SECONDS) - .writeTimeout(5, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .callTimeout(20, TimeUnit.SECONDS) - .retryOnConnectionFailure(true) - .build(); - - this.objectMapper = new ObjectMapper(); - } - - @Value("${yandex.api-key}") - private String apiKey; - - @Value("${yandex.folder-id}") - private String folderId; - - @Value("${yandex.search-url}") - private String searchUrl; - - @Value("${searchapi.api-key:}") - private String searchApiKey; - - @Value("${server.baseurl}") - private String appBaseUrl; - - @PostConstruct - public void init() { - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); - } - - public YandexSearchResponse searchByFileEntity(FileEntity fileEntity) throws IOException { - byte[] fileBytes; - - if (!isImageFile(fileEntity)) { - log.error("File not image: {}", fileEntity.getMimeType()); - throw new NotValidFieldException("File not image", new BaseResponse(20007, - MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(), - Map.of("file_type", fileEntity.getMimeType()))); - } - - try { - fileBytes = readFileFromDisk(fileEntity); - } catch (IOException e) { - throw new NotValidFieldException("File not found or cannot read file", new BaseResponse(20007, - MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), - Map.of("fileId", fileEntity.getId(), - "filePath", fileEntity.getFilePath()))); - } - - return callYandexApi(fileBytes); - } - - - public String searchReverseByPublicUrl(FileEntity fileEntity) throws IOException { - if (!isImageFile(fileEntity)) { - log.error("File not image: {}", fileEntity.getMimeType()); - throw new NotValidFieldException("File not image", new BaseResponse(20007, - MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(), - Map.of("file_type", fileEntity.getMimeType()))); - } - - String publicUrl = String.format("%s/api/files/public/%s", appBaseUrl, fileEntity.getId()); - - log.info("Searching reverse for image: {}", publicUrl); - - return callReverseImageApiByUrl(publicUrl); - } - - private boolean isImageFile(FileEntity fileEntity) { - String mimeType = fileEntity.getMimeType(); - return mimeType != null && mimeType.startsWith("image"); - } - - private byte[] readFileFromDisk(FileEntity fileEntity) throws IOException { - Path filePath = Path.of(fileEntity.getFilePath()); - - if (!Files.exists(filePath)) { - throw new IOException("File not found: " + fileEntity.getFilePath()); - } - - if (!Files.isReadable(filePath)) { - throw new IOException("Cannot read file: " + fileEntity.getFilePath()); - } - - return Files.readAllBytes(filePath); - } - - private YandexSearchResponse callYandexApi(byte[] imageBytes) throws IOException { - String imageBase64 = Base64.getEncoder().encodeToString(imageBytes); - String jsonRequest = String.format("{\"folderId\":\"%s\",\"data\":\"%s\",\"page\":0,\"showSimilarImages\":true}", - folderId, imageBase64); - - URL url = new URL(searchUrl); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - - try { - connection.setRequestMethod("POST"); - connection.setRequestProperty("Authorization", "Api-Key " + apiKey); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("Accept", "application/json"); - connection.setConnectTimeout(30000); - connection.setReadTimeout(30000); - connection.setDoOutput(true); - - try (OutputStream os = connection.getOutputStream()) { - byte[] input = jsonRequest.getBytes("utf-8"); - os.write(input, 0, input.length); - os.flush(); - } - - int responseCode = connection.getResponseCode(); - - String responseBody; - if (responseCode == HttpURLConnection.HTTP_OK) { - try (BufferedReader br = new BufferedReader( - new InputStreamReader(connection.getInputStream(), "utf-8"))) { - responseBody = readAll(br); - } - } else { - try (BufferedReader br = new BufferedReader( - new InputStreamReader(connection.getErrorStream(), "utf-8"))) { - responseBody = readAll(br); - } - throw new IOException("Error Yandex API: " + responseCode + " - " + responseBody); - } - - return parseJsonResponse(responseBody); - - } finally { - connection.disconnect(); - } - } - - public String searchReverseByPublicUrlWithTimeout(FileEntity fileEntity) - throws IOException, TimeoutException { - - if (!isImageFile(fileEntity)) { - throw new NotValidFieldException( - "File not image", - new BaseResponse( - 20007, - MessageCode.INVALID_FIELD.getCode(), - MessageCode.INVALID_FIELD.getDescription(), - Map.of("file_type", fileEntity.getMimeType()) - ) - ); - } - - String publicUrl = String.format("%s/api/files/public/%s", appBaseUrl, fileEntity.getId()); - - log.info("Searching reverse for image: {}", publicUrl); - - try { - return callReverseImageApiByUrl(publicUrl); - } catch (SocketTimeoutException e) { - log.error("Yandex search timeout after {}", fileEntity.getId()); - throw new TimeoutException("Search timeout"); - } - } - - private String callReverseImageApiByUrl(String imageUrl) throws IOException { - - if (searchApiKey == null || searchApiKey.isBlank()) { - throw new IllegalStateException("SearchAPI key not configured"); - } - - HttpUrl url = HttpUrl.parse("https://searchapi.io/api/v1/search") - .newBuilder() - .addQueryParameter("engine", "yandex_reverse_image") - .addQueryParameter("api_key", searchApiKey) - .addQueryParameter("url", imageUrl) - .addQueryParameter("search_type", "visual_matches") - .addQueryParameter("wait", "true") - .build(); - - Request request = new Request.Builder() - .url(url) - .header("Accept", "application/json") - .header("User-Agent", "Mozilla/5.0") - .build(); - - long start = System.currentTimeMillis(); - - try (Response response = httpClient.newCall(request).execute()) { - - long duration = System.currentTimeMillis() - start; - log.info("Yandex response code={}, duration={}ms", response.code(), duration); - - if (!response.isSuccessful()) { - throw new IOException("API error: " + response.code()); - } - - ResponseBody body = response.body(); - if (body == null) { - throw new IOException("Empty response body"); - } - - return body.string(); - } - } - - private String readAll(BufferedReader reader) throws IOException { - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line); - } - return sb.toString(); - } - - private YandexSearchResponse parseJsonResponse(String json) { - YandexSearchResponse response = null; - try { - response = objectMapper.readValue(json, YandexSearchResponse.class); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - - return response; - } -}