From 741eebc276efe0d7bff508ffbb3f9d6eff1495d1 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 15 Apr 2026 15:34:00 +0200 Subject: [PATCH] feat(search): add DocumentSearchResult.withMatchData() factory with match overlay map Co-Authored-By: Claude Sonnet 4.6 --- .../dto/DocumentSearchResult.java | 23 ++++++-- .../dto/DocumentSearchResultTest.java | 53 +++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 backend/src/test/java/org/raddatz/familienarchiv/dto/DocumentSearchResultTest.java diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dto/DocumentSearchResult.java b/backend/src/main/java/org/raddatz/familienarchiv/dto/DocumentSearchResult.java index a0c4af45..b7b59d68 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/dto/DocumentSearchResult.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/dto/DocumentSearchResult.java @@ -1,16 +1,33 @@ package org.raddatz.familienarchiv.dto; +import io.swagger.v3.oas.annotations.media.Schema; import org.raddatz.familienarchiv.model.Document; import java.util.List; +import java.util.Map; +import java.util.UUID; -public record DocumentSearchResult(List documents, long total) { +public record DocumentSearchResult( + List documents, + long total, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + Map matchData +) { /** - * Creates a result where total equals the list size. + * Creates a fully-enriched result from documents and their match overlay data. + * Absent map entries (e.g. document deleted between FTS and enrichment) are safe — + * the frontend treats a missing entry as "no match data". + */ + public static DocumentSearchResult withMatchData(List documents, Map matchData) { + return new DocumentSearchResult(documents, documents.size(), matchData); + } + + /** + * Creates a result without match data — used for filter-only searches (no text query). * No pagination yet — the full matched set is always returned. * When pagination is added, total must come from a DB COUNT query, not list.size(). */ public static DocumentSearchResult of(List documents) { - return new DocumentSearchResult(documents, documents.size()); + return withMatchData(documents, Map.of()); } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/dto/DocumentSearchResultTest.java b/backend/src/test/java/org/raddatz/familienarchiv/dto/DocumentSearchResultTest.java new file mode 100644 index 00000000..3802daf9 --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/dto/DocumentSearchResultTest.java @@ -0,0 +1,53 @@ +package org.raddatz.familienarchiv.dto; + +import org.junit.jupiter.api.Test; +import org.raddatz.familienarchiv.model.Document; +import org.raddatz.familienarchiv.model.DocumentStatus; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class DocumentSearchResultTest { + + private Document doc(UUID id) { + return Document.builder() + .id(id) + .title("Test") + .originalFilename("test.pdf") + .status(DocumentStatus.UPLOADED) + .build(); + } + + @Test + void withMatchData_total_equals_list_size() { + UUID id = UUID.randomUUID(); + List docs = List.of(doc(id)); + Map matchData = Map.of(id, SearchMatchData.empty()); + + DocumentSearchResult result = DocumentSearchResult.withMatchData(docs, matchData); + + assertThat(result.total()).isEqualTo(1L); + } + + @Test + void withMatchData_exposes_match_data_map() { + UUID id = UUID.randomUUID(); + SearchMatchData data = new SearchMatchData("snippet", List.of(), false, List.of(), List.of()); + DocumentSearchResult result = DocumentSearchResult.withMatchData(List.of(doc(id)), Map.of(id, data)); + + assertThat(result.matchData()).containsKey(id); + assertThat(result.matchData().get(id).transcriptionSnippet()).isEqualTo("snippet"); + } + + @Test + void of_factory_returns_empty_match_data() { + UUID id = UUID.randomUUID(); + DocumentSearchResult result = DocumentSearchResult.of(List.of(doc(id))); + + assertThat(result.matchData()).isEmpty(); + assertThat(result.total()).isEqualTo(1L); + } +}