From 864f44a4beb04c24b4ac4de8985e7d1d98e2ca40 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 7 Jun 2026 15:59:20 +0200 Subject: [PATCH] refactor(search): delete Ollama* classes replaced by Nlp* equivalents Co-Authored-By: Claude Sonnet 4.6 --- .../familienarchiv/search/OllamaClient.java | 5 - .../search/OllamaExtraction.java | 18 -- .../search/OllamaHealthClient.java | 5 - .../search/OllamaProperties.java | 15 -- .../search/RestClientOllamaClient.java | 184 ------------------ 5 files changed, 227 deletions(-) delete mode 100644 backend/src/main/java/org/raddatz/familienarchiv/search/OllamaClient.java delete mode 100644 backend/src/main/java/org/raddatz/familienarchiv/search/OllamaExtraction.java delete mode 100644 backend/src/main/java/org/raddatz/familienarchiv/search/OllamaHealthClient.java delete mode 100644 backend/src/main/java/org/raddatz/familienarchiv/search/OllamaProperties.java delete mode 100644 backend/src/main/java/org/raddatz/familienarchiv/search/RestClientOllamaClient.java diff --git a/backend/src/main/java/org/raddatz/familienarchiv/search/OllamaClient.java b/backend/src/main/java/org/raddatz/familienarchiv/search/OllamaClient.java deleted file mode 100644 index 8517d4df..00000000 --- a/backend/src/main/java/org/raddatz/familienarchiv/search/OllamaClient.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.raddatz.familienarchiv.search; - -public interface OllamaClient { - OllamaExtraction parse(String query); -} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/search/OllamaExtraction.java b/backend/src/main/java/org/raddatz/familienarchiv/search/OllamaExtraction.java deleted file mode 100644 index cc3dce6a..00000000 --- a/backend/src/main/java/org/raddatz/familienarchiv/search/OllamaExtraction.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.raddatz.familienarchiv.search; - -import java.time.LocalDate; -import java.util.List; - -/** - * Raw structured output from Ollama after parsing and sanitising. - * personRole is always one of "sender", "receiver", "any" — defensive parsing ensures this. - */ -record OllamaExtraction( - List personNames, - String personRole, - LocalDate dateFrom, - LocalDate dateTo, - List keywords, - String rawQuery -) { -} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/search/OllamaHealthClient.java b/backend/src/main/java/org/raddatz/familienarchiv/search/OllamaHealthClient.java deleted file mode 100644 index 9f1ad1d5..00000000 --- a/backend/src/main/java/org/raddatz/familienarchiv/search/OllamaHealthClient.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.raddatz.familienarchiv.search; - -public interface OllamaHealthClient { - boolean isHealthy(); -} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/search/OllamaProperties.java b/backend/src/main/java/org/raddatz/familienarchiv/search/OllamaProperties.java deleted file mode 100644 index 673006e7..00000000 --- a/backend/src/main/java/org/raddatz/familienarchiv/search/OllamaProperties.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.raddatz.familienarchiv.search; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Component -@ConfigurationProperties("app.ollama") -@Data -public class OllamaProperties { - private String baseUrl; - private String model; - private int timeoutSeconds = 30; - private int healthCheckTimeoutSeconds = 2; -} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/search/RestClientOllamaClient.java b/backend/src/main/java/org/raddatz/familienarchiv/search/RestClientOllamaClient.java deleted file mode 100644 index 64f08554..00000000 --- a/backend/src/main/java/org/raddatz/familienarchiv/search/RestClientOllamaClient.java +++ /dev/null @@ -1,184 +0,0 @@ -package org.raddatz.familienarchiv.search; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.extern.slf4j.Slf4j; -import org.raddatz.familienarchiv.exception.DomainException; -import org.raddatz.familienarchiv.exception.ErrorCode; -import org.springframework.http.client.JdkClientHttpRequestFactory; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClient; -import org.springframework.web.client.RestClientException; - -import java.net.http.HttpClient; -import java.time.Duration; -import java.time.LocalDate; -import java.time.Year; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.List; -import java.util.Map; -import java.util.Set; - -@Service -@Slf4j -public class RestClientOllamaClient implements OllamaClient, OllamaHealthClient { - - private static final ObjectMapper MAPPER = new ObjectMapper(); - private static final Set VALID_ROLES = Set.of("sender", "receiver", "any"); - private static final int MAX_NAME_LENGTH = 200; - private static final int MAX_KEYWORD_LENGTH = 100; - - private static final Map JSON_SCHEMA = Map.of( - "type", "object", - "required", List.of("personNames", "personRole", "keywords"), - "properties", Map.of( - "personNames", Map.of("type", "array", "items", Map.of("type", "string", "maxLength", MAX_NAME_LENGTH)), - "personRole", Map.of("type", "string", "enum", List.of("sender", "receiver", "any")), - "dateFrom", Map.of("type", List.of("string", "null"), "maxLength", 20), - "dateTo", Map.of("type", List.of("string", "null"), "maxLength", 20), - "keywords", Map.of("type", "array", "items", Map.of("type", "string", "maxLength", MAX_KEYWORD_LENGTH)) - ) - ); - - private final RestClient inferenceClient; - private final RestClient healthClient; - private final OllamaProperties props; - - public RestClientOllamaClient(OllamaProperties props) { - this.props = props; - - HttpClient inferenceHttp = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_1_1) - .connectTimeout(Duration.ofSeconds(10)) - .build(); - JdkClientHttpRequestFactory inferenceFactory = new JdkClientHttpRequestFactory(inferenceHttp); - inferenceFactory.setReadTimeout(Duration.ofSeconds(props.getTimeoutSeconds())); - this.inferenceClient = RestClient.builder() - .baseUrl(props.getBaseUrl()) - .requestFactory(inferenceFactory) - .build(); - - HttpClient healthHttp = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_1_1) - .connectTimeout(Duration.ofSeconds(props.getHealthCheckTimeoutSeconds())) - .build(); - JdkClientHttpRequestFactory healthFactory = new JdkClientHttpRequestFactory(healthHttp); - healthFactory.setReadTimeout(Duration.ofSeconds(props.getHealthCheckTimeoutSeconds())); - this.healthClient = RestClient.builder() - .baseUrl(props.getBaseUrl()) - .requestFactory(healthFactory) - .build(); - } - - @Override - public OllamaExtraction parse(String query) { - try { - OllamaGenerateRequest request = new OllamaGenerateRequest( - props.getModel(), query, JSON_SCHEMA, false); - String responseBody = inferenceClient.post() - .uri("/api/generate") - .contentType(org.springframework.http.MediaType.APPLICATION_JSON) - .body(request) - .retrieve() - .body(String.class); - return parseOllamaResponse(responseBody, query); - } catch (DomainException e) { - throw e; - } catch (Exception e) { - log.warn("Ollama inference failed: {}", e.getClass().getSimpleName()); - throw DomainException.serviceUnavailable(ErrorCode.SMART_SEARCH_UNAVAILABLE, - "Ollama unavailable: " + e.getClass().getSimpleName()); - } - } - - @Override - public boolean isHealthy() { - try { - healthClient.get().uri("/api/tags").retrieve().toBodilessEntity(); - return true; - } catch (Exception e) { - return false; - } - } - - private OllamaExtraction parseOllamaResponse(String responseBody, String rawQuery) { - try { - OllamaGenerateResponse response = MAPPER.readValue(responseBody, OllamaGenerateResponse.class); - String inner = response.response(); - if (inner == null || inner.isBlank()) { - return fallbackExtraction(rawQuery); - } - RawOllamaOutput raw = MAPPER.readValue(inner, RawOllamaOutput.class); - return toExtraction(raw, rawQuery); - } catch (Exception e) { - log.warn("Failed to parse Ollama response: {}", e.getClass().getSimpleName()); - throw DomainException.serviceUnavailable(ErrorCode.SMART_SEARCH_UNAVAILABLE, - "Failed to parse Ollama response: " + e.getClass().getSimpleName()); - } - } - - private OllamaExtraction toExtraction(RawOllamaOutput raw, String rawQuery) { - List names = raw.personNames() == null ? List.of() : raw.personNames().stream() - .filter(n -> n != null && n.length() <= MAX_NAME_LENGTH) - .toList(); - List keywords = raw.keywords() == null ? List.of() : raw.keywords().stream() - .filter(k -> k != null && k.length() <= MAX_KEYWORD_LENGTH) - .toList(); - String role = sanitiseRole(raw.personRole()); - LocalDate dateFrom = parseDate(raw.dateFrom(), true); - LocalDate dateTo = parseDate(raw.dateTo(), false); - return new OllamaExtraction(names, role, dateFrom, dateTo, keywords, rawQuery); - } - - private OllamaExtraction fallbackExtraction(String rawQuery) { - return new OllamaExtraction(List.of(), "any", null, null, List.of(), rawQuery); - } - - private String sanitiseRole(String role) { - if (role != null && VALID_ROLES.contains(role)) { - return role; - } - log.warn("Unexpected personRole from Ollama: {}", role); - return "any"; - } - - private LocalDate parseDate(String raw, boolean isFrom) { - if (raw == null || raw.isBlank()) return null; - try { - return LocalDate.parse(raw, DateTimeFormatter.ISO_LOCAL_DATE); - } catch (DateTimeParseException ignored) { - } - try { - int year = Integer.parseInt(raw.strip()); - if (year > 1000 && year < 3000) { - return isFrom ? Year.of(year).atDay(1) : Year.of(year).atMonth(12).atEndOfMonth(); - } - } catch (NumberFormatException ignored) { - } - return null; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - private record OllamaGenerateResponse(String response) { - } - - @JsonIgnoreProperties(ignoreUnknown = true) - private record RawOllamaOutput( - @JsonProperty("personNames") List personNames, - @JsonProperty("personRole") String personRole, - @JsonProperty("dateFrom") String dateFrom, - @JsonProperty("dateTo") String dateTo, - @JsonProperty("keywords") List keywords - ) { - } - - private record OllamaGenerateRequest( - String model, - String prompt, - Object format, - boolean stream - ) { - } -}