diff --git a/docs/architecture/c4/l3-backend-3h-search.puml b/docs/architecture/c4/l3-backend-3h-search.puml new file mode 100644 index 00000000..a0d643be --- /dev/null +++ b/docs/architecture/c4/l3-backend-3h-search.puml @@ -0,0 +1,33 @@ +@startuml +!include + +title Component Diagram: API Backend — NL Search + +Container(frontend, "Web Frontend", "SvelteKit") +ContainerDb(db, "PostgreSQL", "PostgreSQL 16") +Container(ollama, "Ollama", "ollama/ollama — port 11434 (internal only)") + +System_Boundary(backend, "API Backend (Spring Boot)") { + Component(nlCtrl, "NlSearchController", "Spring MVC — POST /api/search/nl", "REST entry point for natural language search. Enforces READ_ALL permission. Uses @AuthenticationPrincipal UserDetails to obtain the caller's email for rate limiting. Delegates to NlQueryParserService and returns NlSearchResponse.") + Component(rateLimiter, "NlSearchRateLimiter", "Spring Service", "Bucket4j + Caffeine LoadingCache keyed on user email. Allows 5 NL search requests per minute per user. Throws DomainException(SMART_SEARCH_RATE_LIMITED / HTTP 429) when the bucket is exhausted. Node-local — same caveat as LoginRateLimiter.") + Component(parserSvc, "NlQueryParserService", "Spring Service", "Orchestrates the full NL search pipeline: (1) validates query length, (2) calls OllamaClient.parse() to extract structured intent, (3) resolves each person name via PersonService.findByDisplayNameContaining(), (4) applies multi-name / personRole heuristics, (5) delegates to DocumentService.searchDocuments() or searchDocumentsByPersonId(). Returns NlSearchResponse. Never logs raw query content (PII).") + Component(ollamaClient, "RestClientOllamaClient", "Spring Service — implements OllamaClient + OllamaHealthClient", "HTTP client for the Ollama API. Uses two separate RestClient instances: inference client (30 s read timeout) and health-check client (2 s connect timeout). Calls POST /api/generate with grammar-constrained JSON schema (personNames, personRole, dateFrom, dateTo, keywords). isHealthy() polls GET /api/tags. Null-coalesces absent personNames/keywords to List.of(). Defaults unknown personRole to 'any' with a warning log. Maps timeout/5xx/parse errors to DomainException(SMART_SEARCH_UNAVAILABLE / HTTP 503).") + Component(ollamaProps, "OllamaProperties", "@ConfigurationProperties(\"app.ollama\")", "Config bean: baseUrl, model (qwen2.5:7b-instruct-q4_K_M), timeoutSeconds (default: 30), healthCheckTimeoutSeconds (default: 2).") + Component(rateLimitProps, "NlSearchRateLimitProperties", "@ConfigurationProperties(\"app.nl-search.rate-limit\")", "Config bean: maxRequestsPerMinute (default: 5).") +} + +Component(personSvc, "PersonService", "Spring Service", "See diagram 3e. findByDisplayNameContaining(fragment) delegates to PersonRepository.searchByName() — covers first+last name, alias, and name aliases via LEFT JOIN.") +Component(documentSvc, "DocumentService", "Spring Service", "See diagram 3b. searchDocuments() for keyword/sender/receiver/date queries. searchDocumentsByPersonId() for OR-semantics single-person queries (person as sender OR receiver, no keyword filter).") + +Rel(frontend, nlCtrl, "POST /api/search/nl with JSON query", "HTTP / JSON") +Rel(nlCtrl, rateLimiter, "checkAndConsume(userEmail)") +Rel(nlCtrl, parserSvc, "parse(query)") +Rel(parserSvc, ollamaClient, "parse(rawQuery) — extracts intent", "HTTP / JSON") +Rel(ollamaClient, ollama, "POST /api/generate (grammar-constrained JSON schema)", "HTTP / REST") +Rel(ollamaClient, ollama, "GET /api/tags (health check)", "HTTP / REST") +Rel(parserSvc, personSvc, "findByDisplayNameContaining(name) for each extracted name") +Rel(parserSvc, documentSvc, "searchDocuments() or searchDocumentsByPersonId()") +Rel(documentSvc, db, "JPA queries", "JDBC") +Rel(personSvc, db, "JPA queries", "JDBC") + +@enduml