You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
318 lines
13 KiB
Java
318 lines
13 KiB
Java
package at.procon.ted.controller;
|
|
|
|
import at.procon.dip.runtime.condition.ConditionalOnRuntimeMode;
|
|
import at.procon.dip.runtime.config.RuntimeMode;
|
|
import at.procon.ted.model.dto.DocumentDtos.*;
|
|
import at.procon.ted.model.entity.ContractNature;
|
|
import at.procon.ted.model.entity.NoticeType;
|
|
import at.procon.ted.model.entity.ProcedureType;
|
|
import at.procon.ted.service.SearchService;
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
import io.swagger.v3.oas.annotations.Parameter;
|
|
import io.swagger.v3.oas.annotations.media.Content;
|
|
import io.swagger.v3.oas.annotations.media.Schema;
|
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.format.annotation.DateTimeFormat;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
import java.time.LocalDate;
|
|
import java.time.OffsetDateTime;
|
|
import java.util.List;
|
|
import java.util.UUID;
|
|
|
|
/**
|
|
* REST API controller for searching and retrieving TED procurement documents.
|
|
*
|
|
* Provides endpoints for:
|
|
* - Structured search with filters (country, type, dates, etc.)
|
|
* - Semantic search using natural language queries
|
|
* - Document retrieval by ID or publication ID
|
|
* - Statistics and metadata
|
|
*
|
|
* @author Martin.Schweitzer@procon.co.at and claude.ai
|
|
*/
|
|
@RestController
|
|
@RequestMapping("/v1/documents")
|
|
@RequiredArgsConstructor
|
|
@Slf4j
|
|
@ConditionalOnRuntimeMode(RuntimeMode.LEGACY)
|
|
@Tag(name = "Documents", description = "TED Procurement Document Search API")
|
|
public class DocumentController {
|
|
|
|
private final SearchService searchService;
|
|
|
|
/**
|
|
* Search documents with structured and/or semantic filters.
|
|
*/
|
|
@GetMapping("/search")
|
|
@Operation(
|
|
summary = "Search procurement documents",
|
|
description = "Search documents using structured filters (country, type, dates) and/or semantic search with natural language queries"
|
|
)
|
|
@ApiResponses(value = {
|
|
@ApiResponse(responseCode = "200", description = "Search results returned successfully",
|
|
content = @Content(schema = @Schema(implementation = SearchResponse.class))),
|
|
@ApiResponse(responseCode = "400", description = "Invalid search parameters")
|
|
})
|
|
public ResponseEntity<SearchResponse> searchDocuments(
|
|
@Parameter(description = "Country code (ISO 3166-1 alpha-3, e.g., POL, DEU, FRA)")
|
|
@RequestParam(required = false) String countryCode,
|
|
|
|
@Parameter(description = "Multiple country codes")
|
|
@RequestParam(required = false) List<String> countryCodes,
|
|
|
|
@Parameter(description = "Notice type filter")
|
|
@RequestParam(required = false) NoticeType noticeType,
|
|
|
|
@Parameter(description = "Contract nature filter")
|
|
@RequestParam(required = false) ContractNature contractNature,
|
|
|
|
@Parameter(description = "Procedure type filter")
|
|
@RequestParam(required = false) ProcedureType procedureType,
|
|
|
|
@Parameter(description = "CPV code prefix (e.g., '33' for medical supplies)")
|
|
@RequestParam(required = false) String cpvPrefix,
|
|
|
|
@Parameter(description = "NUTS region code")
|
|
@RequestParam(required = false) String nutsCode,
|
|
|
|
@Parameter(description = "Publication date from (inclusive)")
|
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate publicationDateFrom,
|
|
|
|
@Parameter(description = "Publication date to (inclusive)")
|
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate publicationDateTo,
|
|
|
|
@Parameter(description = "Only documents with submission deadline after this date")
|
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime submissionDeadlineAfter,
|
|
|
|
@Parameter(description = "Filter by EU funding status")
|
|
@RequestParam(required = false) Boolean euFunded,
|
|
|
|
@Parameter(description = "Search in buyer name (case-insensitive)")
|
|
@RequestParam(required = false) String buyerNameContains,
|
|
|
|
@Parameter(description = "Search in project title (case-insensitive)")
|
|
@RequestParam(required = false) String projectTitleContains,
|
|
|
|
@Parameter(description = "Natural language semantic search query")
|
|
@RequestParam(required = false) String q,
|
|
|
|
@Parameter(description = "Similarity threshold for semantic search (0.0-1.0)")
|
|
@RequestParam(required = false, defaultValue = "0.7") Double similarityThreshold,
|
|
|
|
@Parameter(description = "Page number (0-based)")
|
|
@RequestParam(required = false, defaultValue = "0") Integer page,
|
|
|
|
@Parameter(description = "Page size (max 100)")
|
|
@RequestParam(required = false, defaultValue = "20") Integer size,
|
|
|
|
@Parameter(description = "Sort field (publicationDate, submissionDeadline, buyerName, projectTitle)")
|
|
@RequestParam(required = false, defaultValue = "publicationDate") String sortBy,
|
|
|
|
@Parameter(description = "Sort direction (asc, desc)")
|
|
@RequestParam(required = false, defaultValue = "desc") String sortDirection
|
|
) {
|
|
SearchRequest request = SearchRequest.builder()
|
|
.countryCode(countryCode)
|
|
.countryCodes(countryCodes)
|
|
.noticeType(noticeType)
|
|
.contractNature(contractNature)
|
|
.procedureType(procedureType)
|
|
.cpvPrefix(cpvPrefix)
|
|
.nutsCode(nutsCode)
|
|
.publicationDateFrom(publicationDateFrom)
|
|
.publicationDateTo(publicationDateTo)
|
|
.submissionDeadlineAfter(submissionDeadlineAfter)
|
|
.euFunded(euFunded)
|
|
.buyerNameContains(buyerNameContains)
|
|
.projectTitleContains(projectTitleContains)
|
|
.semanticQuery(q)
|
|
.similarityThreshold(similarityThreshold)
|
|
.page(page)
|
|
.size(size)
|
|
.sortBy(sortBy)
|
|
.sortDirection(sortDirection)
|
|
.build();
|
|
|
|
log.debug("Search request: {}", request);
|
|
SearchResponse response = searchService.search(request);
|
|
return ResponseEntity.ok(response);
|
|
}
|
|
|
|
/**
|
|
* Search documents using POST with request body.
|
|
* Useful for complex queries with many parameters.
|
|
*/
|
|
@PostMapping("/search")
|
|
@Operation(
|
|
summary = "Search procurement documents (POST)",
|
|
description = "Search documents using a JSON request body for complex queries"
|
|
)
|
|
public ResponseEntity<SearchResponse> searchDocumentsPost(@RequestBody SearchRequest request) {
|
|
log.debug("Search request (POST): {}", request);
|
|
SearchResponse response = searchService.search(request);
|
|
return ResponseEntity.ok(response);
|
|
}
|
|
|
|
/**
|
|
* Get document by internal UUID.
|
|
*/
|
|
@GetMapping("/{id}")
|
|
@Operation(
|
|
summary = "Get document by ID",
|
|
description = "Retrieve full document details by internal UUID"
|
|
)
|
|
@ApiResponses(value = {
|
|
@ApiResponse(responseCode = "200", description = "Document found",
|
|
content = @Content(schema = @Schema(implementation = DocumentDetail.class))),
|
|
@ApiResponse(responseCode = "404", description = "Document not found")
|
|
})
|
|
public ResponseEntity<DocumentDetail> getDocument(
|
|
@Parameter(description = "Document UUID") @PathVariable UUID id) {
|
|
return searchService.getDocumentDetail(id)
|
|
.map(ResponseEntity::ok)
|
|
.orElse(ResponseEntity.notFound().build());
|
|
}
|
|
|
|
/**
|
|
* Get document by TED publication ID.
|
|
*/
|
|
@GetMapping("/publication/{publicationId}")
|
|
@Operation(
|
|
summary = "Get document by publication ID",
|
|
description = "Retrieve document by TED publication ID (e.g., '00786665-2025')"
|
|
)
|
|
@ApiResponses(value = {
|
|
@ApiResponse(responseCode = "200", description = "Document found"),
|
|
@ApiResponse(responseCode = "404", description = "Document not found")
|
|
})
|
|
public ResponseEntity<DocumentDetail> getDocumentByPublicationId(
|
|
@Parameter(description = "TED Publication ID") @PathVariable String publicationId) {
|
|
return searchService.getDocumentByPublicationId(publicationId)
|
|
.map(ResponseEntity::ok)
|
|
.orElse(ResponseEntity.notFound().build());
|
|
}
|
|
|
|
/**
|
|
* Get documents with upcoming submission deadlines.
|
|
*/
|
|
@GetMapping("/upcoming-deadlines")
|
|
@Operation(
|
|
summary = "Get documents with upcoming deadlines",
|
|
description = "List documents with submission deadlines in the future, sorted by deadline"
|
|
)
|
|
public ResponseEntity<List<DocumentSummary>> getUpcomingDeadlines(
|
|
@Parameter(description = "Maximum number of results")
|
|
@RequestParam(required = false, defaultValue = "20") Integer limit) {
|
|
List<DocumentSummary> documents = searchService.getUpcomingDeadlines(Math.min(limit, 100));
|
|
return ResponseEntity.ok(documents);
|
|
}
|
|
|
|
/**
|
|
* Get collection statistics.
|
|
*/
|
|
@GetMapping("/statistics")
|
|
@Operation(
|
|
summary = "Get collection statistics",
|
|
description = "Retrieve statistics about the document collection including counts by country, type, and vectorization status"
|
|
)
|
|
public ResponseEntity<StatisticsResponse> getStatistics() {
|
|
StatisticsResponse stats = searchService.getStatistics();
|
|
return ResponseEntity.ok(stats);
|
|
}
|
|
|
|
/**
|
|
* Get list of all countries in the collection.
|
|
*/
|
|
@GetMapping("/metadata/countries")
|
|
@Operation(
|
|
summary = "Get available countries",
|
|
description = "List all distinct country codes present in the collection"
|
|
)
|
|
public ResponseEntity<List<String>> getCountries() {
|
|
List<String> countries = searchService.getDistinctCountries();
|
|
return ResponseEntity.ok(countries);
|
|
}
|
|
|
|
/**
|
|
* Get available notice types.
|
|
*/
|
|
@GetMapping("/metadata/notice-types")
|
|
@Operation(
|
|
summary = "Get available notice types",
|
|
description = "List all notice type enum values"
|
|
)
|
|
public ResponseEntity<NoticeType[]> getNoticeTypes() {
|
|
return ResponseEntity.ok(NoticeType.values());
|
|
}
|
|
|
|
/**
|
|
* Get available contract natures.
|
|
*/
|
|
@GetMapping("/metadata/contract-natures")
|
|
@Operation(
|
|
summary = "Get available contract natures",
|
|
description = "List all contract nature enum values"
|
|
)
|
|
public ResponseEntity<ContractNature[]> getContractNatures() {
|
|
return ResponseEntity.ok(ContractNature.values());
|
|
}
|
|
|
|
/**
|
|
* Get available procedure types.
|
|
*/
|
|
@GetMapping("/metadata/procedure-types")
|
|
@Operation(
|
|
summary = "Get available procedure types",
|
|
description = "List all procedure type enum values"
|
|
)
|
|
public ResponseEntity<ProcedureType[]> getProcedureTypes() {
|
|
return ResponseEntity.ok(ProcedureType.values());
|
|
}
|
|
|
|
/**
|
|
* Semantic search endpoint - convenience method for natural language queries.
|
|
*/
|
|
@GetMapping("/semantic-search")
|
|
@Operation(
|
|
summary = "Semantic search",
|
|
description = "Search documents using natural language query with vector similarity"
|
|
)
|
|
public ResponseEntity<SearchResponse> semanticSearch(
|
|
@Parameter(description = "Natural language search query", required = true)
|
|
@RequestParam String query,
|
|
|
|
@Parameter(description = "Minimum similarity score (0.0-1.0)")
|
|
@RequestParam(required = false, defaultValue = "0.7") Double threshold,
|
|
|
|
@Parameter(description = "Country code filter")
|
|
@RequestParam(required = false) String countryCode,
|
|
|
|
@Parameter(description = "Notice type filter")
|
|
@RequestParam(required = false) NoticeType noticeType,
|
|
|
|
@Parameter(description = "Page number")
|
|
@RequestParam(required = false, defaultValue = "0") Integer page,
|
|
|
|
@Parameter(description = "Page size")
|
|
@RequestParam(required = false, defaultValue = "20") Integer size
|
|
) {
|
|
SearchRequest request = SearchRequest.builder()
|
|
.semanticQuery(query)
|
|
.similarityThreshold(threshold)
|
|
.countryCode(countryCode)
|
|
.noticeType(noticeType)
|
|
.page(page)
|
|
.size(size)
|
|
.build();
|
|
|
|
SearchResponse response = searchService.search(request);
|
|
return ResponseEntity.ok(response);
|
|
}
|
|
}
|