Commit 30eba60e authored by Matija Obreza's avatar Matija Obreza
Browse files

Facilitated search API endpoints

- /api/v0/search/dataset/suggest
- /api/v0/admin/elastic
parent 215966a1
/*
* 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.catalog.server.controller.api;
import org.elasticsearch.index.query.QueryBuilder;
import org.genesys.catalog.server.service.ElasticsearchFilter;
import org.genesys.catalog.service.filters.DatasetFilter;
/**
* Wrapper for {@link DatasetFilter} to produce ES queries
*
* @author Matija Obreza
*/
public class EsDatasetFilter extends DatasetFilter implements ElasticsearchFilter {
/*
* (non-Javadoc)
* @see org.genesys.catalog.server.service.ElasticsearchFilter#elasticQuery()
*/
@Override
public QueryBuilder elasticQuery() {
return null;
}
}
/*
* 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.catalog.server.controller.api.v0;
import static com.google.common.collect.Sets.newHashSet;
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 org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.genesys.blocks.model.BasicModel;
import org.genesys.blocks.model.JsonViews;
import org.genesys.catalog.exceptions.InvalidApiUsageException;
import org.genesys.catalog.model.dataset.AccessionIdentifier;
import org.genesys.catalog.model.dataset.Dataset;
import org.genesys.catalog.model.traits.Descriptor;
import org.genesys.catalog.server.controller.api.EsDatasetFilter;
import org.genesys.catalog.server.service.ElasticsearchService;
import org.genesys.common.model.Crop;
import org.genesys.common.model.Partner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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
@RequestMapping(value = { "/api/v0/search" })
public class SearchController {
private static final Logger LOG = LoggerFactory.getLogger(SearchController.class);
@Autowired(required = false)
private ElasticsearchService elasticsearch;
/**
* Get suggestions for dataset filters
* @param filters
* @param searchQuery
* @return
*/
@JsonView({ JsonViews.Minimal.class })
@PostMapping(value = "/dataset/suggest", produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, SearchResults> datasets(@RequestBody(required = false) final EsDatasetFilter 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);
Map<Class<?>, List<BasicModel>> hitsByEntity = elasticsearch.search(extraFilters, searchQuery, newHashSet(Crop.class, Partner.class,
AccessionIdentifier.class, Descriptor.class));
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("accessionIdentifier.doi"), hitsByEntity.get(AccessionIdentifier.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(EsDatasetFilter filters) {
if (filters == null) {
return null;
}
BoolQueryBuilder q = boolQuery();
if (filters.crop != null && !filters.crop.isEmpty()) {
q.must(termsQuery("crop", filters.crop));
}
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();
}
}
/**
* Wrapper for search results
*/
class SearchResults {
public List<String> filters;
public String key = "uuid";
public List<BasicModel> hits;
public static SearchResults from(String key, List<String> filters, List<BasicModel> list) {
if (list == null || list.isEmpty())
return null;
SearchResults sr = new SearchResults();
sr.filters = filters;
sr.key = key;
sr.hits = list;
return sr;
}
}
/*
* 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.catalog.server.controller.api.v0.admin;
import org.genesys.blocks.security.UserException;
import org.genesys.catalog.model.dataset.Dataset;
import org.genesys.catalog.model.traits.Descriptor;
import org.genesys.catalog.model.traits.DescriptorList;
import org.genesys.catalog.model.vocab.ControlledVocabulary;
import org.genesys.catalog.model.vocab.VocabularyTerm;
import org.genesys.catalog.server.service.ElasticsearchService;
import org.genesys.common.model.Crop;
import org.genesys.common.model.Partner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(ElasticsearchAdminController.API_BASE)
@PreAuthorize("hasRole('ADMINISTRATOR')")
public class ElasticsearchAdminController {
/** The Constant API_BASE. */
protected static final String API_BASE = "/api/v0/admin/elastic";
private static final Logger LOG = LoggerFactory.getLogger(ElasticsearchAdminController.class);
@Autowired(required = false)
private ElasticsearchService elasticsearch;
/**
* Reindex
*
* @throws UserException the user exception
*/
@PostMapping(value = "/reindex/Everything")
public void reindexEverything() {
LOG.info("Reindexing everything started.");
elasticsearch.reindex(Crop.class);
elasticsearch.reindex(Partner.class);
elasticsearch.reindex(VocabularyTerm.class);
elasticsearch.reindex(ControlledVocabulary.class);
elasticsearch.reindex(Descriptor.class);
elasticsearch.reindex(DescriptorList.class);
elasticsearch.reindex(Dataset.class);
LOG.info("Reindexing everything done.");
}
@PostMapping(value = "/reindex/{class}")
public void reindexEntity(@PathVariable("class") String className) throws Throwable {
LOG.info("Reindexing {}", className);
try {
elasticsearch.reindex(Class.forName(className));
LOG.info("Reindexing {} done.", className);
} catch (Throwable e) {
LOG.error(e.getMessage(), e);
throw e;
}
}
}
/*
* 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.catalog.server.service;
import org.elasticsearch.index.query.QueryBuilder;
/**
* Filters able to generate Elasticsearch queries
*/
public interface ElasticsearchFilter {
public QueryBuilder elasticQuery();
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment