SearchController.java
/*
* Copyright (c) 2007-2026 MetaSolutions AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.entrystore.rest.springboot.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.Size;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.entrystore.Entry;
import org.entrystore.repository.util.SolrSearchIndex;
import org.entrystore.rest.springboot.model.api.FacetSettingsRequestParams;
import org.entrystore.rest.springboot.model.dto.QueryResultsDto;
import org.entrystore.rest.springboot.model.exception.BadRequestException;
import org.entrystore.rest.springboot.service.SearchRateLimiter;
import org.entrystore.rest.springboot.service.SearchService;
import org.entrystore.rest.springboot.service.SolrSearchInputValidator;
import org.entrystore.rest.springboot.util.Syndication;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/search")
@Validated
@RequiredArgsConstructor
public class SearchController {
private final SearchService searchService;
private final SolrSearchInputValidator solrSearchInputValidator;
private final SearchRateLimiter searchRateLimiter;
@Operation(summary = "Searches the repository and returns entries")
@GetMapping(
params = "type=sparql"
)
public ResponseEntity<String> findEntriesSparql(
HttpServletRequest request,
@RequestParam @Size(min = 3, message = "'query' param length must be minimum 3") String query,
@RequestParam(required = false) String syndication, // feed type = rss_* or atom_*
@RequestParam(name = "urltemplate", required = false) String urlTemplate,
@RequestParam(name = "feedtitle", defaultValue = "Syndication feed of search") String feedTitle,
@RequestParam(defaultValue = MediaType.APPLICATION_JSON_VALUE) String rdfFormat,
@RequestParam(defaultValue = "en") String lang,
@RequestParam(defaultValue = "" + SearchService.DEFAULT_LIMIT) int limit
) {
searchRateLimiter.acquirePermit(request.getRemoteAddr());
limit = searchService.clampLimit(limit);
List<Entry> foundEntries = searchService.findEntriesSparql(query);
// In Restlet's SPARQL search logic, the "offset" param is accepted from the user but not used
return buildSearchResponse(request, new QueryResultsDto(foundEntries), syndication, lang, 0, limit,
urlTemplate, feedTitle, rdfFormat);
}
@Operation(
summary = "Searches the repository and returns entries",
description = "Solr search. Facet buckets carry a name and a count; facetLang narrows the buckets of a "
+ "literal facet field to one language.",
parameters = {
@Parameter(name = "facetLang", description = "BCP 47 language tag, e.g. sv or en-GB. Keeps only the "
+ "buckets of literal facet fields (metadata.predicate.literal_s.<hash> or the "
+ "metadata.predicate.literal.<hash> shorthand) whose label occurs in that language (prefix "
+ "match, so en also matches en-GB) or untagged. The selection happens in Solr before "
+ "facetLimit is applied. A bucket's count is the label's count in all languages, taken "
+ "before access filtering; it is not the number of entries in the requested language. "
+ "Candidates are gathered with a bounded overrequest ranked by their count in the "
+ "requested language, while the final order uses the total count, so a label that is rare "
+ "in that language but frequent overall can be missing from the top N entirely. Labels "
+ "longer than 256 characters are not selectable this way and are therefore absent "
+ "whenever facetLang is set. Requires facetFields.")
}
)
@GetMapping(
params = "type=solr"
)
public ResponseEntity<String> findEntriesSolr(
HttpServletRequest request,
@RequestParam @Size(min = 3, message = "'query' param length must be minimum 3") String query,
@RequestParam(required = false) String syndication, // feed type = rss_* or atom_*
@RequestParam(name = "urltemplate", required = false) String urlTemplate,
@RequestParam(name = "feedtitle", defaultValue = "Syndication feed of search") String feedTitle,
@RequestParam(defaultValue = MediaType.APPLICATION_JSON_VALUE) String rdfFormat,
@RequestParam(defaultValue = "en") String lang,
@RequestParam(defaultValue = "0") int offset,
@RequestParam(defaultValue = "" + SearchService.DEFAULT_LIMIT) int limit,
@RequestParam(required = false) String sort,
@RequestParam(required = false) String filterQuery,
FacetSettingsRequestParams facetRequest
) {
searchRateLimiter.acquirePermit(request.getRemoteAddr());
solrSearchInputValidator.validateQuery(query);
solrSearchInputValidator.validateSort(sort);
solrSearchInputValidator.validateFacetSettings(facetRequest);
if (StringUtils.isNotEmpty(syndication) && offset > 0) {
throw new BadRequestException("Query parameter 'offset' not supported with syndication");
}
if (offset < 0) {
offset = 0;
}
limit = searchService.clampLimit(limit);
List<String> filterQueries = solrSearchInputValidator.parseFilterQueries(filterQuery);
SolrSearchIndex.FacetSettings facetSettings = searchService.toFacetSettings(facetRequest);
QueryResultsDto queryResults = searchService.findEntriesSolr(query, sort, offset, limit, filterQueries, facetSettings);
return buildSearchResponse(request, queryResults, syndication, lang, offset, limit, urlTemplate, feedTitle, rdfFormat);
}
private ResponseEntity<String> buildSearchResponse(HttpServletRequest request, QueryResultsDto queryResults,
String syndication, String lang, int offset, int limit,
String urlTemplate, String feedTitle, String rdfFormat) {
String responseBody;
MediaType responseMediaType;
if (StringUtils.isNotEmpty(syndication)) {
responseBody = searchService.generateSyndication(request, queryResults.entries(), syndication, lang, limit, urlTemplate, feedTitle);
responseMediaType = Syndication.convertFeedTypeToMediaType(syndication);
} else {
responseBody = searchService.generateJson(offset, limit, queryResults, rdfFormat);
responseMediaType = MediaType.APPLICATION_JSON;
}
return ResponseEntity.ok().contentType(responseMediaType).body(responseBody);
}
}