From fea837b345b30f6d687327fe612eff3e269c730e Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 9 May 2026 14:06:24 +0200 Subject: [PATCH] refactor(fts): add FtsHit/FtsPage records; rename findRankedIdsByFts -> findAllMatchingIdsByFts Co-Authored-By: Claude Sonnet 4.6 --- .../document/DocumentRepository.java | 3 +- .../document/DocumentService.java | 6 ++-- .../familienarchiv/document/FtsHit.java | 6 ++++ .../familienarchiv/document/FtsPage.java | 6 ++++ .../document/DocumentFtsTest.java | 34 +++++++++---------- .../document/DocumentServiceSortTest.java | 6 ++-- .../document/DocumentServiceTest.java | 8 ++--- 7 files changed, 41 insertions(+), 28 deletions(-) create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/document/FtsHit.java create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/document/FtsPage.java diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentRepository.java index a110d22c..66ed5d4a 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentRepository.java @@ -100,7 +100,8 @@ public interface DocumentRepository extends JpaRepository, JpaSp ORDER BY ts_rank(d.search_vector, q.pq) DESC, d.meta_date DESC NULLS LAST """) - List findRankedIdsByFts(@Param("query") String query); + // Unpaged path — use findFtsPageRaw for paginated search results + List findAllMatchingIdsByFts(@Param("query") String query); /** * Returns match-enrichment data for a set of documents identified by their IDs. 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 31e2e85d..cc126b37 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java @@ -162,7 +162,7 @@ public class DocumentService { */ private List resolveFtsIds(String text) { if (!StringUtils.hasText(text)) return null; - return documentRepository.findRankedIdsByFts(text); + return documentRepository.findAllMatchingIdsByFts(text); } /** Loads matching documents and projects to non-null {@link LocalDate}s. */ @@ -485,7 +485,7 @@ public class DocumentService { boolean hasText = StringUtils.hasText(text); List rankedIds = null; if (hasText) { - rankedIds = documentRepository.findRankedIdsByFts(text); + rankedIds = documentRepository.findAllMatchingIdsByFts(text); if (rankedIds.isEmpty()) return List.of(); } @@ -648,7 +648,7 @@ public class DocumentService { List rankedIds = null; if (hasText) { - rankedIds = documentRepository.findRankedIdsByFts(text); + rankedIds = documentRepository.findAllMatchingIdsByFts(text); if (rankedIds.isEmpty()) return DocumentSearchResult.of(List.of()); } diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/FtsHit.java b/backend/src/main/java/org/raddatz/familienarchiv/document/FtsHit.java new file mode 100644 index 00000000..fd38250d --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/FtsHit.java @@ -0,0 +1,6 @@ +package org.raddatz.familienarchiv.document; + +import java.util.UUID; + +/** A single document hit from a paginated FTS query — id and its ts_rank score. */ +record FtsHit(UUID id, double rank) {} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/FtsPage.java b/backend/src/main/java/org/raddatz/familienarchiv/document/FtsPage.java new file mode 100644 index 00000000..a1ce7bf5 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/FtsPage.java @@ -0,0 +1,6 @@ +package org.raddatz.familienarchiv.document; + +import java.util.List; + +/** One page of FTS results — the ranked hit list for this page and the total match count. */ +record FtsPage(List hits, long total) {} diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentFtsTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentFtsTest.java index 2ffec47c..758a2ad2 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentFtsTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentFtsTest.java @@ -69,7 +69,7 @@ class DocumentFtsTest { documentRepository.saveAndFlush(document("Alter Brief")); em.clear(); - List ids = documentRepository.findRankedIdsByFts("Brief"); + List ids = documentRepository.findAllMatchingIdsByFts("Brief"); assertThat(ids).hasSize(1); } @@ -79,7 +79,7 @@ class DocumentFtsTest { documentRepository.saveAndFlush(document("Alter Brief")); em.clear(); - List ids = documentRepository.findRankedIdsByFts("Briefe"); + List ids = documentRepository.findAllMatchingIdsByFts("Briefe"); assertThat(ids).hasSize(1); } @@ -89,7 +89,7 @@ class DocumentFtsTest { documentRepository.saveAndFlush(document("Ein furchtbarer Brief")); em.clear(); - List ids = documentRepository.findRankedIdsByFts("furchtb"); + List ids = documentRepository.findAllMatchingIdsByFts("furchtb"); assertThat(ids).hasSize(1); } @@ -99,7 +99,7 @@ class DocumentFtsTest { documentRepository.saveAndFlush(document("Familienfoto")); em.clear(); - List ids = documentRepository.findRankedIdsByFts("Brief"); + List ids = documentRepository.findAllMatchingIdsByFts("Brief"); assertThat(ids).isEmpty(); } @@ -115,7 +115,7 @@ class DocumentFtsTest { em.flush(); em.clear(); - List ids = documentRepository.findRankedIdsByFts("schreiben"); + List ids = documentRepository.findAllMatchingIdsByFts("schreiben"); assertThat(ids).contains(doc.getId()); } @@ -125,14 +125,14 @@ class DocumentFtsTest { Document doc = documentRepository.saveAndFlush(document("Leeres Dokument")); em.clear(); - assertThat(documentRepository.findRankedIdsByFts("Grundbuch")).isEmpty(); + assertThat(documentRepository.findAllMatchingIdsByFts("Grundbuch")).isEmpty(); UUID annotationId = annotation(doc.getId()); blockRepository.saveAndFlush(block(doc.getId(), annotationId, "Grundbuch Eintrag 1923", 0)); em.flush(); em.clear(); - assertThat(documentRepository.findRankedIdsByFts("Grundbuch")).contains(doc.getId()); + assertThat(documentRepository.findAllMatchingIdsByFts("Grundbuch")).contains(doc.getId()); } @Test @@ -144,13 +144,13 @@ class DocumentFtsTest { em.flush(); em.clear(); - assertThat(documentRepository.findRankedIdsByFts("Grundbuch")).contains(doc.getId()); + assertThat(documentRepository.findAllMatchingIdsByFts("Grundbuch")).contains(doc.getId()); blockRepository.deleteById(block.getId()); em.flush(); em.clear(); - assertThat(documentRepository.findRankedIdsByFts("Grundbuch")).doesNotContain(doc.getId()); + assertThat(documentRepository.findAllMatchingIdsByFts("Grundbuch")).doesNotContain(doc.getId()); } // ─── Ranking ─────────────────────────────────────────────────────────────── @@ -166,7 +166,7 @@ class DocumentFtsTest { em.flush(); em.clear(); - List ids = documentRepository.findRankedIdsByFts("Grundbuch"); + List ids = documentRepository.findAllMatchingIdsByFts("Grundbuch"); assertThat(ids).hasSize(2); assertThat(ids.get(0)).isEqualTo(docA.getId()); @@ -179,7 +179,7 @@ class DocumentFtsTest { documentRepository.saveAndFlush(document("Ein Brief von der Oma")); em.clear(); - List ids = documentRepository.findRankedIdsByFts("der die das und"); + List ids = documentRepository.findAllMatchingIdsByFts("der die das und"); assertThat(ids).isEmpty(); } @@ -195,7 +195,7 @@ class DocumentFtsTest { em.flush(); em.clear(); - List ids = documentRepository.findRankedIdsByFts("Wille"); + List ids = documentRepository.findAllMatchingIdsByFts("Wille"); assertThat(ids).contains(doc.getId()); } @@ -205,7 +205,7 @@ class DocumentFtsTest { documentRepository.saveAndFlush(document("Brief")); em.clear(); - assertThatNoException().isThrownBy(() -> documentRepository.findRankedIdsByFts("(((")); + assertThatNoException().isThrownBy(() -> documentRepository.findAllMatchingIdsByFts("(((")); } // ─── Weight C: sender/receiver names ─────────────────────────────────────── @@ -223,7 +223,7 @@ class DocumentFtsTest { em.flush(); em.clear(); - List ids = documentRepository.findRankedIdsByFts("Schmidt"); + List ids = documentRepository.findAllMatchingIdsByFts("Schmidt"); assertThat(ids).contains(doc.getId()); } @@ -241,7 +241,7 @@ class DocumentFtsTest { em.flush(); em.clear(); - List ids = documentRepository.findRankedIdsByFts("Raddatz"); + List ids = documentRepository.findAllMatchingIdsByFts("Raddatz"); assertThat(ids).contains(doc.getId()); } @@ -260,7 +260,7 @@ class DocumentFtsTest { em.flush(); em.clear(); - List ids = documentRepository.findRankedIdsByFts("Familiengeschichte"); + List ids = documentRepository.findAllMatchingIdsByFts("Familiengeschichte"); assertThat(ids).hasSize(1); } @@ -278,7 +278,7 @@ class DocumentFtsTest { em.flush(); em.clear(); - List rankedIds = documentRepository.findRankedIdsByFts("Grundbuch"); + List rankedIds = documentRepository.findAllMatchingIdsByFts("Grundbuch"); Specification spec = Specification.where(hasIds(rankedIds)) .and(hasStatus(DocumentStatus.UPLOADED)); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceSortTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceSortTest.java index 80e1e4fa..5bff7363 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceSortTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceSortTest.java @@ -58,7 +58,7 @@ class DocumentServiceSortTest { .documentDate(LocalDate.of(1960, 1, 1)).build(); // FTS returns id1 first (higher rank), id2 second - when(documentRepository.findRankedIdsByFts("Brief")).thenReturn(List.of(id1, id2)); + when(documentRepository.findAllMatchingIdsByFts("Brief")).thenReturn(List.of(id1, id2)); // findAll(spec, pageable) — the correct date path — returns date-DESC order when(documentRepository.findAll(any(Specification.class), any(Pageable.class))) .thenReturn(new PageImpl<>(List.of(newer, older))); @@ -81,7 +81,7 @@ class DocumentServiceSortTest { Document doc1 = Document.builder().id(id1).title("Brief").status(DocumentStatus.UPLOADED).build(); Document doc2 = Document.builder().id(id2).title("Brief").status(DocumentStatus.UPLOADED).build(); - when(documentRepository.findRankedIdsByFts("Brief")).thenReturn(List.of(id1, id2)); + when(documentRepository.findAllMatchingIdsByFts("Brief")).thenReturn(List.of(id1, id2)); when(documentRepository.findAll(any(Specification.class))) .thenReturn(List.of(doc2, doc1)); // unordered from DB @@ -100,7 +100,7 @@ class DocumentServiceSortTest { Document doc1 = Document.builder().id(id1).title("Brief").status(DocumentStatus.UPLOADED).build(); Document doc2 = Document.builder().id(id2).title("Brief").status(DocumentStatus.UPLOADED).build(); - when(documentRepository.findRankedIdsByFts("Brief")).thenReturn(List.of(id1, id2)); + when(documentRepository.findAllMatchingIdsByFts("Brief")).thenReturn(List.of(id1, id2)); when(documentRepository.findAll(any(Specification.class))) .thenReturn(List.of(doc2, doc1)); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceTest.java index 99198452..9be822b1 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentServiceTest.java @@ -1620,7 +1620,7 @@ class DocumentServiceTest { // chr(1)=\u0001 marks start, chr(2)=\u0002 marks end of highlighted term List rows = Collections.singletonList(new Object[]{docId, "\u0001Brief\u0002 an Anna", null, false, null, null, null}); - when(documentRepository.findRankedIdsByFts("Brief")).thenReturn(List.of(docId)); + when(documentRepository.findAllMatchingIdsByFts("Brief")).thenReturn(List.of(docId)); when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class))) .thenReturn(List.of(doc)); when(documentRepository.findEnrichmentData(any(), eq("Brief"))).thenReturn(rows); @@ -1654,7 +1654,7 @@ class DocumentServiceTest { String snippetHeadline = "Hier ist der \u0001Brief\u0002 aus Berlin"; List rows = Collections.singletonList(new Object[]{docId, "Dok", snippetHeadline, false, null, null, null}); - when(documentRepository.findRankedIdsByFts("Brief")).thenReturn(List.of(docId)); + when(documentRepository.findAllMatchingIdsByFts("Brief")).thenReturn(List.of(docId)); when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class))) .thenReturn(List.of(doc)); when(documentRepository.findEnrichmentData(any(), eq("Brief"))).thenReturn(rows); @@ -2202,7 +2202,7 @@ class DocumentServiceTest { @Test void findIdsForFilter_returnsEmpty_whenFtsHasNoMatches() { - when(documentRepository.findRankedIdsByFts("xyz")).thenReturn(List.of()); + when(documentRepository.findAllMatchingIdsByFts("xyz")).thenReturn(List.of()); List result = documentService.findIdsForFilter( "xyz", null, null, null, null, null, null, null, null); @@ -2386,7 +2386,7 @@ class DocumentServiceTest { @Test void getDensity_shortCircuits_whenFtsReturnsNoMatches() { - when(documentRepository.findRankedIdsByFts("xyz")).thenReturn(List.of()); + when(documentRepository.findAllMatchingIdsByFts("xyz")).thenReturn(List.of()); DocumentDensityResult result = documentService.getDensity( new DensityFilters("xyz", null, null, null, null, null, null));