Erweiterungen
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MongoDB document for caching LLM translations. Stores the original text,
|
||||
* all translations keyed by language code, and the insertion timestamp.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Document(collection = "translation_cache")
|
||||
public class TranslationCacheEntry {
|
||||
|
||||
@Id
|
||||
private ObjectId id;
|
||||
|
||||
/**
|
||||
* The original text that was translated.
|
||||
*/
|
||||
@Indexed(unique = true)
|
||||
@Field("source_text")
|
||||
private String sourceText;
|
||||
|
||||
/**
|
||||
* Translations keyed by language code (e.g. "de", "en", "fr").
|
||||
*/
|
||||
@Field("translations")
|
||||
private Map<String, String> translations;
|
||||
|
||||
/**
|
||||
* When this entry was inserted into the cache.
|
||||
*/
|
||||
@Indexed
|
||||
@Field("inserted_at")
|
||||
private LocalDateTime insertedAt;
|
||||
|
||||
public TranslationCacheEntry(String sourceText, Map<String, String> translations) {
|
||||
this.sourceText = sourceText;
|
||||
this.translations = translations;
|
||||
this.insertedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.assecutor.votianlt.repository;
|
||||
|
||||
import de.assecutor.votianlt.model.TranslationCacheEntry;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import org.springframework.data.mongodb.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface TranslationCacheRepository extends MongoRepository<TranslationCacheEntry, ObjectId> {
|
||||
|
||||
Optional<TranslationCacheEntry> findBySourceText(String sourceText);
|
||||
|
||||
/**
|
||||
* Returns the oldest entries (by insertedAt ASC), only projecting the id field,
|
||||
* for efficient bulk deletion.
|
||||
*/
|
||||
@Query(value = "{}", fields = "{'_id': 1}")
|
||||
List<TranslationCacheEntry> findOldestEntries(Pageable pageable);
|
||||
}
|
||||
@@ -2,7 +2,12 @@ package de.assecutor.votianlt.service;
|
||||
|
||||
import de.assecutor.votianlt.ai.service.LlmRestClient;
|
||||
import de.assecutor.votianlt.model.Language;
|
||||
import de.assecutor.votianlt.model.TranslationCacheEntry;
|
||||
import de.assecutor.votianlt.repository.TranslationCacheRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -10,35 +15,25 @@ import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Service for translating text into all available languages. Uses LLM API for
|
||||
* translations with caching.
|
||||
* translations with MongoDB-backed caching.
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class TranslationService {
|
||||
|
||||
private static final int MAX_CACHE_SIZE = 1000;
|
||||
private static final long CACHE_CLEANUP_THRESHOLD = 100_000L;
|
||||
private static final long CACHE_TARGET_SIZE = 50_000L;
|
||||
|
||||
private final LlmRestClient llmRestClient;
|
||||
private final TranslationCacheRepository cacheRepository;
|
||||
|
||||
// Cache: Original Text -> List of all translations (language -> text)
|
||||
private final Map<String, List<Translation>> translationCache;
|
||||
|
||||
public TranslationService(LlmRestClient llmRestClient) {
|
||||
public TranslationService(LlmRestClient llmRestClient, TranslationCacheRepository cacheRepository) {
|
||||
this.llmRestClient = llmRestClient;
|
||||
// LinkedHashMap with accessOrder=true for LRU behavior
|
||||
this.translationCache = new LinkedHashMap<String, List<Translation>>(MAX_CACHE_SIZE, 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<String, List<Translation>> eldest) {
|
||||
if (size() > MAX_CACHE_SIZE) {
|
||||
log.debug("[TranslationCache] Removing oldest entry, cache size: {}", size());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
this.cacheRepository = cacheRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,8 +51,8 @@ public class TranslationService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given text into all available languages. Uses cache to avoid
|
||||
* redundant LLM calls.
|
||||
* Translates the given text into all available languages. Uses MongoDB cache to
|
||||
* avoid redundant LLM calls.
|
||||
*
|
||||
* @param text
|
||||
* the text to translate
|
||||
@@ -68,63 +63,87 @@ public class TranslationService {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
List<Translation> cachedTranslations = translationCache.get(text);
|
||||
if (cachedTranslations != null) {
|
||||
// Check MongoDB cache first
|
||||
Optional<TranslationCacheEntry> cached = cacheRepository.findBySourceText(text);
|
||||
if (cached.isPresent()) {
|
||||
log.debug("[TranslationCache] Cache hit for '{}'", text);
|
||||
return cachedTranslations;
|
||||
return toTranslationList(cached.get().getTranslations());
|
||||
}
|
||||
|
||||
// Cache miss - translate via LLM
|
||||
log.debug("[TranslationCache] Cache miss for '{}', calling LLM", text);
|
||||
List<Translation> translations = translateViaLlm(text);
|
||||
|
||||
// Store in cache
|
||||
translationCache.put(text, translations);
|
||||
log.debug("[TranslationCache] Stored translations for '{}', cache size: {}", text, translationCache.size());
|
||||
|
||||
return translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates text to all languages via LLM.
|
||||
*
|
||||
* @param text
|
||||
* the text to translate
|
||||
* @return list of translations for all languages
|
||||
*/
|
||||
private List<Translation> translateViaLlm(String text) {
|
||||
List<Translation> translations = new ArrayList<>();
|
||||
|
||||
// Translate to each language
|
||||
for (Language lang : getAvailableLanguages()) {
|
||||
String translatedText = translateTextToLanguage(text, lang);
|
||||
translations.add(new Translation(lang.name().toLowerCase(), translatedText));
|
||||
// Persist in MongoDB cache
|
||||
Map<String, String> translationMap = toTranslationMap(translations);
|
||||
try {
|
||||
cacheRepository.save(new TranslationCacheEntry(text, translationMap));
|
||||
log.debug("[TranslationCache] Stored translations for '{}', cache size: {}", text,
|
||||
cacheRepository.count());
|
||||
} catch (Exception e) {
|
||||
// Duplicate key on concurrent insert is harmless - log and continue
|
||||
log.debug("[TranslationCache] Could not persist cache entry (may be duplicate): {}", e.getMessage());
|
||||
}
|
||||
|
||||
return translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a single text to the target language using LLM.
|
||||
*
|
||||
* @param text
|
||||
* the text to translate
|
||||
* @param targetLanguage
|
||||
* the target language
|
||||
* @return the translated text
|
||||
* Scheduled cleanup: if the cache exceeds 100,000 entries, deletes the oldest
|
||||
* entries until only 50,000 remain. Runs every hour.
|
||||
*/
|
||||
@Scheduled(fixedDelay = 3_600_000) // every hour
|
||||
public void cleanupCacheIfNeeded() {
|
||||
long count = cacheRepository.count();
|
||||
log.debug("[TranslationCache] Scheduled cleanup check – current size: {}", count);
|
||||
|
||||
if (count > CACHE_CLEANUP_THRESHOLD) {
|
||||
long toDelete = count - CACHE_TARGET_SIZE;
|
||||
log.info("[TranslationCache] Cache size {} exceeds threshold {}, deleting {} oldest entries", count,
|
||||
CACHE_CLEANUP_THRESHOLD, toDelete);
|
||||
|
||||
List<TranslationCacheEntry> oldest = cacheRepository.findOldestEntries(
|
||||
PageRequest.of(0, (int) toDelete, Sort.by(Sort.Direction.ASC, "inserted_at")));
|
||||
|
||||
cacheRepository.deleteAll(oldest);
|
||||
log.info("[TranslationCache] Deleted {} entries, new size: {}", oldest.size(),
|
||||
cacheRepository.count());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns cache statistics for monitoring.
|
||||
*/
|
||||
public long getCacheSize() {
|
||||
return cacheRepository.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the translation cache.
|
||||
*/
|
||||
public void clearCache() {
|
||||
cacheRepository.deleteAll();
|
||||
log.info("[TranslationCache] Cache cleared");
|
||||
}
|
||||
|
||||
// --- private helpers ---
|
||||
|
||||
private List<Translation> translateViaLlm(String text) {
|
||||
List<Translation> translations = new ArrayList<>();
|
||||
for (Language lang : getAvailableLanguages()) {
|
||||
String translatedText = translateTextToLanguage(text, lang);
|
||||
translations.add(new Translation(lang.name().toLowerCase(), translatedText));
|
||||
}
|
||||
return translations;
|
||||
}
|
||||
|
||||
private String translateTextToLanguage(String text, Language targetLanguage) {
|
||||
String languageName = getLanguageName(targetLanguage);
|
||||
|
||||
String systemPrompt = "You are a professional translator. Translate the given text to " + languageName
|
||||
+ ". Return ONLY the translated text, nothing else. Do not add quotes or explanations.";
|
||||
|
||||
try {
|
||||
String result = llmRestClient.chat(systemPrompt, text, 0.3, 500);
|
||||
|
||||
if (result != null && !result.isBlank()) {
|
||||
// Clean up the response (remove quotes if present)
|
||||
result = result.trim();
|
||||
if (result.startsWith("\"") && result.endsWith("\"")) {
|
||||
result = result.substring(1, result.length() - 1);
|
||||
@@ -134,14 +153,9 @@ public class TranslationService {
|
||||
} catch (Exception e) {
|
||||
log.warn("Translation failed for {}: {}", languageName, e.getMessage());
|
||||
}
|
||||
|
||||
// Fallback: return original text
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human-readable language name.
|
||||
*/
|
||||
private String getLanguageName(Language language) {
|
||||
return switch (language) {
|
||||
case DE -> "German";
|
||||
@@ -157,18 +171,19 @@ public class TranslationService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns cache statistics for monitoring.
|
||||
*/
|
||||
public int getCacheSize() {
|
||||
return translationCache.size();
|
||||
private static Map<String, String> toTranslationMap(List<Translation> translations) {
|
||||
Map<String, String> map = new LinkedHashMap<>();
|
||||
for (Translation t : translations) {
|
||||
map.put(t.language(), t.text());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the translation cache.
|
||||
*/
|
||||
public void clearCache() {
|
||||
translationCache.clear();
|
||||
log.info("[TranslationCache] Cache cleared");
|
||||
private static List<Translation> toTranslationList(Map<String, String> map) {
|
||||
List<Translation> list = new ArrayList<>();
|
||||
if (map != null) {
|
||||
map.forEach((lang, text) -> list.add(new Translation(lang, text)));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user