diff --git a/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineService.java b/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineService.java index 12fe681b..7a084205 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineService.java @@ -19,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -105,9 +106,9 @@ public class TimelineService { if (!passesYearFilter(doc.getDocumentDate(), doc.getMetaDatePrecision(), filter)) continue; letters.add(doc); } - Map rootByPrimaryTagId = resolveLetterRootTags(letters); + Map rootByDocId = resolveLetterRootTags(letters); for (Document doc : letters) { - entries.add(mapDocument(doc, rootByPrimaryTagId)); + entries.add(mapDocument(doc, rootByDocId)); } return bucket(entries); @@ -232,8 +233,8 @@ public class TimelineService { ); } - private TimelineEntryDTO mapDocument(Document doc, Map rootByPrimaryTagId) { - RootTag root = resolvePrimaryRoot(doc, rootByPrimaryTagId); + private TimelineEntryDTO mapDocument(Document doc, Map rootByDocId) { + RootTag root = rootByDocId.get(doc.getId()); return new TimelineEntryDTO( Kind.LETTER, doc.getMetaDatePrecision(), @@ -255,22 +256,27 @@ public class TimelineService { } /** - * Resolves the root tags for the letters' primary tags in one batched pass — no per-letter - * N+1: each letter contributes only its alphabetically-first assigned tag (#835), and - * {@link TagService#resolveRootTags} memoizes the ancestry walk per distinct tag. + * Resolves each letter's primary root tag in one batched pass, keyed by document id — no + * per-letter N+1: each letter contributes only its alphabetically-first assigned tag (#835), + * so the {@code min()} scan over a letter's tag set runs exactly once here (not again at map + * time), and {@link TagService#resolveRootTags} memoizes the ancestry walk per distinct tag. */ private Map resolveLetterRootTags(List letters) { - List primaryTags = letters.stream() - .map(TimelineService::primaryTag) - .filter(t -> t != null) - .toList(); - if (primaryTags.isEmpty()) return Map.of(); - return tagService.resolveRootTags(primaryTags); - } + Map primaryByDocId = new LinkedHashMap<>(); + for (Document doc : letters) { + Tag primary = primaryTag(doc); + if (primary != null) primaryByDocId.put(doc.getId(), primary); + } + if (primaryByDocId.isEmpty()) return Map.of(); - private RootTag resolvePrimaryRoot(Document doc, Map rootByPrimaryTagId) { - Tag primary = primaryTag(doc); - return primary == null ? null : rootByPrimaryTagId.get(primary.getId()); + Map rootByTagId = + tagService.resolveRootTags(new ArrayList<>(primaryByDocId.values())); + Map rootByDocId = new HashMap<>(); + primaryByDocId.forEach((docId, primary) -> { + RootTag root = rootByTagId.get(primary.getId()); + if (root != null) rootByDocId.put(docId, root); + }); + return rootByDocId; } /** A letter's primary tag: the alphabetically-first of its assigned tags by name (#835). */