refactor(documents): split getDensity into resolve/load/aggregate (#385)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-08 11:44:37 +02:00
parent 86de118d63
commit 5cd6ecc624
2 changed files with 32 additions and 15 deletions

View File

@@ -19,4 +19,9 @@ public record DocumentDensityResult(
List<MonthBucket> buckets, List<MonthBucket> buckets,
LocalDate minDate, LocalDate minDate,
LocalDate maxDate 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);
}
}

View File

@@ -147,29 +147,41 @@ public class DocumentService {
* parallel native-query path. * parallel native-query path.
*/ */
public DocumentDensityResult getDensity(DensityFilters filters) { public DocumentDensityResult getDensity(DensityFilters filters) {
String text = filters.text(); List<UUID> ftsIds = resolveFtsIds(filters.text());
boolean hasText = StringUtils.hasText(text); if (ftsIds != null && ftsIds.isEmpty()) {
List<UUID> rankedIds = null; return DocumentDensityResult.empty();
if (hasText) {
rankedIds = documentRepository.findRankedIdsByFts(text);
if (rankedIds.isEmpty()) {
return new DocumentDensityResult(List.of(), null, null);
}
} }
List<LocalDate> 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<UUID> 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<LocalDate> loadFilteredDates(DensityFilters filters, List<UUID> ftsIds) {
boolean hasFts = ftsIds != null;
Specification<Document> spec = buildSearchSpec( Specification<Document> spec = buildSearchSpec(
hasText, rankedIds, null, null, hasFts, ftsIds, null, null,
filters.sender(), filters.receiver(), filters.sender(), filters.receiver(),
filters.tags(), filters.tagQ(), filters.tags(), filters.tagQ(),
filters.status(), filters.tagOperator()); filters.status(), filters.tagOperator());
return documentRepository.findAll(spec).stream()
List<LocalDate> dates = documentRepository.findAll(spec).stream()
.map(Document::getDocumentDate) .map(Document::getDocumentDate)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.toList(); .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<LocalDate> dates) {
if (dates.isEmpty()) return DocumentDensityResult.empty();
Map<String, Integer> counts = new java.util.TreeMap<>(); Map<String, Integer> counts = new java.util.TreeMap<>();
for (LocalDate d : dates) { for (LocalDate d : dates) {
counts.merge(YearMonth.from(d).toString(), 1, Integer::sum); counts.merge(YearMonth.from(d).toString(), 1, Integer::sum);