feat(search): auto-select a single direct person match in smart search (#763) #769

Merged
marcel merged 21 commits from feat/issue-763-nl-search-direct-match into main 2026-06-07 08:47:49 +02:00
4 changed files with 21 additions and 7 deletions
Showing only changes of commit 7eee688ce9 - Show all commits

View File

@@ -15,8 +15,12 @@ public record NlQueryInterpretation(
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
List<String> keywords,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
List<TagHint> resolvedTags,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
String rawQuery,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
boolean keywordsApplied
boolean keywordsApplied,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
boolean tagsApplied
) {
}

View File

@@ -10,12 +10,15 @@ import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.person.PersonService;
import org.raddatz.familienarchiv.tag.Tag;
import org.raddatz.familienarchiv.tag.TagOperator;
import org.raddatz.familienarchiv.tag.TagService;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.UUID;
@@ -28,10 +31,13 @@ public class NlQueryParserService {
private static final int MAX_QUERY = 500;
private static final int MAX_NAME_LENGTH = 200;
private static final int MAX_CANDIDATES = 10;
private static final int MIN_TAG_TERM = 3;
private static final int MAX_RESOLVED_TAGS = 10;
private final OllamaClient ollamaClient;
private final PersonService personService;
private final DocumentService documentService;
private final TagService tagService;
public NlSearchResponse search(String query, Pageable pageable) {
if (query == null || query.length() < MIN_QUERY || query.length() > MAX_QUERY) {
@@ -50,7 +56,7 @@ public class NlQueryParserService {
NlQueryInterpretation interpretation = new NlQueryInterpretation(
List.of(), resolution.ambiguous(),
ext.dateFrom(), ext.dateTo(),
keywords, ext.rawQuery(), false);
keywords, List.of(), ext.rawQuery(), false, false);
return new NlSearchResponse(DocumentSearchResult.of(List.of()), interpretation);
}
@@ -65,7 +71,7 @@ public class NlQueryParserService {
DocumentSearchResult docs = documentService.searchDocumentsByPersonId(
personId, ext.dateFrom(), ext.dateTo(), pageable);
NlQueryInterpretation interpretation = new NlQueryInterpretation(
resolved, List.of(), ext.dateFrom(), ext.dateTo(), keywords, ext.rawQuery(), false);
resolved, List.of(), ext.dateFrom(), ext.dateTo(), keywords, List.of(), ext.rawQuery(), false, false);
return new NlSearchResponse(docs, interpretation);
}
@@ -82,7 +88,7 @@ public class NlQueryParserService {
DocumentSearchResult docs = documentService.searchDocuments(filters, DocumentSort.DATE, "desc", pageable);
boolean keywordsApplied = !text.isBlank();
NlQueryInterpretation interpretation = new NlQueryInterpretation(
resolved, List.of(), ext.dateFrom(), ext.dateTo(), keywords, ext.rawQuery(), keywordsApplied);
resolved, List.of(), ext.dateFrom(), ext.dateTo(), keywords, List.of(), ext.rawQuery(), keywordsApplied, false);
return new NlSearchResponse(docs, interpretation);
}

View File

@@ -13,7 +13,9 @@ import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.person.PersonService;
import org.raddatz.familienarchiv.tag.Tag;
import org.raddatz.familienarchiv.tag.TagOperator;
import org.raddatz.familienarchiv.tag.TagService;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -32,6 +34,7 @@ class NlQueryParserServiceTest {
@Mock OllamaClient ollamaClient;
@Mock PersonService personService;
@Mock DocumentService documentService;
@Mock TagService tagService;
NlQueryParserService service;
@@ -40,11 +43,12 @@ class NlQueryParserServiceTest {
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
service = new NlQueryParserService(ollamaClient, personService, documentService);
service = new NlQueryParserService(ollamaClient, personService, documentService, tagService);
when(documentService.searchDocuments(any(), any(), any(), any()))
.thenReturn(DocumentSearchResult.of(List.of()));
when(documentService.searchDocumentsByPersonId(any(), any(), any(), any()))
.thenReturn(DocumentSearchResult.of(List.of()));
when(tagService.findByNameContaining(anyString())).thenReturn(List.of());
}
// --- Factory helpers ---

View File

@@ -48,7 +48,7 @@ class NlSearchControllerTest {
PersonHint hint = new PersonHint(UUID.randomUUID(), "Walter Raddatz");
NlQueryInterpretation interp = new NlQueryInterpretation(
List.of(hint), List.of(), null, null,
List.of("Krieg"), "Briefe von Walter im Krieg", true);
List.of("Krieg"), List.of(), "Briefe von Walter im Krieg", true, false);
return new NlSearchResponse(DocumentSearchResult.of(List.of()), interp);
}
@@ -77,7 +77,7 @@ class NlSearchControllerTest {
PersonHint b = new PersonHint(UUID.randomUUID(), "Walter Schmidt");
NlQueryInterpretation interp = new NlQueryInterpretation(
List.of(), List.of(a, b), null, null,
List.of(), "Briefe von Walter", false);
List.of(), List.of(), "Briefe von Walter", false, false);
NlSearchResponse resp = new NlSearchResponse(DocumentSearchResult.of(List.of()), interp);
when(nlQueryParserService.search(anyString(), any())).thenReturn(resp);