diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentDensityResult.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentDensityResult.java index 521feca5..8e695589 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentDensityResult.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentDensityResult.java @@ -19,4 +19,9 @@ public record DocumentDensityResult( List buckets, LocalDate minDate, LocalDate maxDate -) {} +) { + /** The "no documents match the filter" result, with no buckets and null date bounds. */ + public static DocumentDensityResult empty() { + return new DocumentDensityResult(List.of(), null, null); + } +} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java index 3f0d5ece..cca184d8 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java @@ -147,29 +147,41 @@ public class DocumentService { * parallel native-query path. */ public DocumentDensityResult getDensity(DensityFilters filters) { - String text = filters.text(); - boolean hasText = StringUtils.hasText(text); - List rankedIds = null; - if (hasText) { - rankedIds = documentRepository.findRankedIdsByFts(text); - if (rankedIds.isEmpty()) { - return new DocumentDensityResult(List.of(), null, null); - } + List ftsIds = resolveFtsIds(filters.text()); + if (ftsIds != null && ftsIds.isEmpty()) { + return DocumentDensityResult.empty(); } + List dates = loadFilteredDates(filters, ftsIds); + return aggregateByMonth(dates); + } + + /** + * Returns the FTS-ranked document IDs when {@code text} is non-blank, or {@code null} + * when no full-text query is active. An empty list means the FTS query ran but + * matched zero documents — the caller short-circuits on that signal. + */ + private List resolveFtsIds(String text) { + if (!StringUtils.hasText(text)) return null; + return documentRepository.findRankedIdsByFts(text); + } + + /** Loads matching documents and projects to non-null {@link LocalDate}s. */ + private List loadFilteredDates(DensityFilters filters, List ftsIds) { + boolean hasFts = ftsIds != null; Specification spec = buildSearchSpec( - hasText, rankedIds, null, null, + hasFts, ftsIds, null, null, filters.sender(), filters.receiver(), filters.tags(), filters.tagQ(), filters.status(), filters.tagOperator()); - - List dates = documentRepository.findAll(spec).stream() + return documentRepository.findAll(spec).stream() .map(Document::getDocumentDate) .filter(Objects::nonNull) .toList(); - if (dates.isEmpty()) { - return new DocumentDensityResult(List.of(), null, null); - } + } + /** Buckets {@code dates} into one {@link MonthBucket} per YYYY-MM and computes min/max. */ + private DocumentDensityResult aggregateByMonth(List dates) { + if (dates.isEmpty()) return DocumentDensityResult.empty(); Map counts = new java.util.TreeMap<>(); for (LocalDate d : dates) { counts.merge(YearMonth.from(d).toString(), 1, Integer::sum);