SearchController.java
/*
* Copyright 2018 Global Crop Diversity Trust
*
* 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.genesys.server.api.v1;
import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
import static org.elasticsearch.index.query.QueryBuilders.termsQuery;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.genesys.blocks.model.EmptyModel;
import org.genesys.blocks.model.JsonViews;
import org.genesys.server.api.ApiBaseController;
import org.genesys.server.exception.InvalidApiUsageException;
import org.genesys.server.model.Partner;
import org.genesys.server.model.dataset.Dataset;
import org.genesys.server.model.filters.DatasetFilter;
import org.genesys.server.model.genesys.AccessionRef;
import org.genesys.server.model.impl.Crop;
import org.genesys.server.model.traits.Descriptor;
import org.genesys.server.service.ElasticsearchService;
import org.genesys.server.service.ElasticsearchService.SearchResults;
import org.genesys.server.exception.SearchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.annotation.JsonView;
/**
* API to search the Catalog.
*
* @author Matija Obreza
*/
@RestController("searchApi1")
@RequestMapping(SearchController.CONTROLLER_URL)
public class SearchController extends ApiBaseController {
/** The Constant CONTROLLER_URL. */
public static final String CONTROLLER_URL = ApiBaseController.APIv1_BASE + "/search";
@Autowired(required = false)
private ElasticsearchService elasticsearch;
/**
* Full-text search across all indexed entities.
*
* @param text the search text
* @return the map
*/
@PostMapping(value = "", produces = { MediaType.APPLICATION_JSON_VALUE })
@JsonView({ JsonViews.Public.class })
public Map<String, List<? extends EmptyModel>> search(@RequestBody final String text) throws SearchException {
return elasticsearch.fullTextSearch(text);
}
/**
* Get suggestions for dataset filters .
*
* @param filters the filters
* @param searchQuery the search query
* @return the map
*/
@JsonView({ JsonViews.Minimal.class })
@PostMapping(value = "/dataset/suggest", produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, SearchResults<?>> datasets(@RequestBody(required = false) final DatasetFilter filters, @RequestParam(value = "q", required = true) String searchQuery) {
LOG.trace("Incoming {}", searchQuery);
searchQuery = sanitizeQuery(searchQuery);
LOG.info("Suggestions for datasets for: {}", searchQuery);
if (StringUtils.isBlank(searchQuery)) {
throw new InvalidApiUsageException("No search query provided");
}
QueryBuilder extraFilters = filtersFromDataset(filters);
Set<Class<? extends EmptyModel>> clazzes = Set.of(Crop.class, Partner.class, Descriptor.class);
Map<Class<? extends EmptyModel>, List<? extends EmptyModel>> hitsByEntity = elasticsearch.search(extraFilters, searchQuery, clazzes);
Map<String, SearchResults<?>> suggestions = new HashMap<>();
suggestions.put("search.group.crop", SearchResults.from("code", Arrays.asList("crop"), hitsByEntity.get(Crop.class)));
suggestions.put("search.group.partner", SearchResults.from("uuid", Arrays.asList("owner.uuid"), hitsByEntity.get(Partner.class)));
suggestions.put("search.group.accession", SearchResults.from("doi", Arrays.asList("accessionRef.doi"), hitsByEntity.get(AccessionRef.class)));
suggestions.put("search.group.descriptor", SearchResults.from("uuid", Arrays.asList("descriptor.uuid"), hitsByEntity.get(Descriptor.class)));
// Search datasets
suggestions.put("search.matches", SearchResults.from("uuid", Arrays.asList("uuid"), elasticsearch.search(extraFilters, searchQuery, Dataset.class)));
return suggestions;
}
/// Try to enhance the search by adding known Dataset filters
private QueryBuilder filtersFromDataset(DatasetFilter filters) {
if (filters == null) {
return null;
}
BoolQueryBuilder q = boolQuery();
if (filters.crops != null && !filters.crops.isEmpty()) {
q.must(termsQuery("crop", filters.crops));
}
q.boost(3.0f);
return q.hasClauses() ? q : null;
}
/**
* Sanitize incoming search query
*
* @param searchQuery incoming
* @return sanitized search query
*/
private String sanitizeQuery(String searchQuery) {
if (StringUtils.isBlank(searchQuery)) {
return null;
}
return searchQuery.replaceAll("[^\\w\\d\\s]+", "").replaceAll("\\s\\s+", " ").trim();
}
}