refactor(search): delete Ollama* classes replaced by Nlp* equivalents

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-07 15:59:20 +02:00
committed by marcel
parent 9f78f25b0a
commit 28201e363a
5 changed files with 0 additions and 227 deletions

View File

@@ -1,5 +0,0 @@
package org.raddatz.familienarchiv.search;
public interface OllamaClient {
OllamaExtraction parse(String query);
}

View File

@@ -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<String> personNames,
String personRole,
LocalDate dateFrom,
LocalDate dateTo,
List<String> keywords,
String rawQuery
) {
}

View File

@@ -1,5 +0,0 @@
package org.raddatz.familienarchiv.search;
public interface OllamaHealthClient {
boolean isHealthy();
}

View File

@@ -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;
}

View File

@@ -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<String> 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<String, Object> 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<String> names = raw.personNames() == null ? List.of() : raw.personNames().stream()
.filter(n -> n != null && n.length() <= MAX_NAME_LENGTH)
.toList();
List<String> 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<String> personNames,
@JsonProperty("personRole") String personRole,
@JsonProperty("dateFrom") String dateFrom,
@JsonProperty("dateTo") String dateTo,
@JsonProperty("keywords") List<String> keywords
) {
}
private record OllamaGenerateRequest(
String model,
String prompt,
Object format,
boolean stream
) {
}
}