From 8cbecd452bd20e2045ecfb5e58e331d08ef28512 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 15 Apr 2026 15:32:26 +0200 Subject: [PATCH] feat(search): add SearchMatchData record for per-document match signals Co-Authored-By: Claude Sonnet 4.6 --- .../familienarchiv/dto/SearchMatchData.java | 48 +++++++++++++++++++ .../dto/SearchMatchDataTest.java | 44 +++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/dto/SearchMatchData.java create mode 100644 backend/src/test/java/org/raddatz/familienarchiv/dto/SearchMatchDataTest.java diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dto/SearchMatchData.java b/backend/src/main/java/org/raddatz/familienarchiv/dto/SearchMatchData.java new file mode 100644 index 00000000..7cd06d85 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/dto/SearchMatchData.java @@ -0,0 +1,48 @@ +package org.raddatz.familienarchiv.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; +import java.util.UUID; + +/** + * Match signals for a single document in a full-text search result. + * All fields are non-null except {@code transcriptionSnippet}, which is null + * when no transcription block matched the query. + */ +public record SearchMatchData( + /** + * Best-ranked matching transcription line, or null if no block matched. + */ + String transcriptionSnippet, + + /** + * Character offsets of highlighted terms within the document title. + * Empty when the title did not contribute to the match. + */ + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + List titleOffsets, + + /** + * True when the sender's name matched the query. + */ + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + boolean senderMatched, + + /** + * IDs of receiver persons whose names matched the query. + */ + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + List matchedReceiverIds, + + /** + * IDs of tags whose names matched the query. + */ + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + List matchedTagIds +) { + /** Canonical "no match data" value for a single document. */ + public static SearchMatchData empty() { + return new SearchMatchData(null, List.of(), false, List.of(), List.of()); + } +} diff --git a/backend/src/test/java/org/raddatz/familienarchiv/dto/SearchMatchDataTest.java b/backend/src/test/java/org/raddatz/familienarchiv/dto/SearchMatchDataTest.java new file mode 100644 index 00000000..8135aafa --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/dto/SearchMatchDataTest.java @@ -0,0 +1,44 @@ +package org.raddatz.familienarchiv.dto; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class SearchMatchDataTest { + + @Test + void transcription_snippet_is_nullable() { + SearchMatchData data = new SearchMatchData(null, List.of(), false, List.of(), List.of()); + + assertThat(data.transcriptionSnippet()).isNull(); + } + + @Test + void non_null_list_fields_are_empty_by_default_in_empty_factory() { + SearchMatchData data = SearchMatchData.empty(); + + assertThat(data.transcriptionSnippet()).isNull(); + assertThat(data.titleOffsets()).isEmpty(); + assertThat(data.matchedReceiverIds()).isEmpty(); + assertThat(data.matchedTagIds()).isEmpty(); + assertThat(data.senderMatched()).isFalse(); + } + + @Test + void holds_all_field_values() { + MatchOffset offset = new MatchOffset(0, 4); + SearchMatchData data = new SearchMatchData( + "schreibt dir aus dem Feld", + List.of(offset), + true, + List.of(), + List.of() + ); + + assertThat(data.transcriptionSnippet()).isEqualTo("schreibt dir aus dem Feld"); + assertThat(data.titleOffsets()).containsExactly(offset); + assertThat(data.senderMatched()).isTrue(); + } +}