Commit c8a6eda6 authored by Jean-Baptiste Nizet's avatar Jean-Baptiste Nizet
Browse files

doc: generate REST API documentation of search service

parent 267a655f
= RARe REST services documentation = RARe REST services documentation
:toc: left :toc: left
:source-highlighter: highlightjs :source-highlighter: highlightjs
:icons: font
== Introduction == Introduction
...@@ -109,6 +110,135 @@ include::{snippets}/harvests/list2/curl-request.adoc[] ...@@ -109,6 +110,135 @@ include::{snippets}/harvests/list2/curl-request.adoc[]
.HTTPie .HTTPie
include::{snippets}/harvests/list2/httpie-request.adoc[] include::{snippets}/harvests/list2/httpie-request.adoc[]
== Genetic Resources (aka Search)
=== Full text search
The genetic resources endpoint is used to search for genetic resources.
The simplest way to use this service is to provide a query, that will be used to do a full-text search in the
various properties (except identifiers, numeric fields and URLs) of all the genetic resources.
.Request
include::{snippets}/search/fulltext/http-request.adoc[]
.Request parameters
include::{snippets}/search/fulltext/request-parameters.adoc[]
.Curl
include::{snippets}/search/fulltext/curl-request.adoc[]
.HTTPie
include::{snippets}/search/fulltext/httpie-request.adoc[]
.Response
include::{snippets}/search/fulltext/http-response.adoc[]
.Response fields
include::{snippets}/search/fulltext/response-fields.adoc[]
=== Pagination
As you can see, you actually get back a page of results. The above example shows a search that would find only 2
matching genetic resources. In reality, most searches will find much more matching resources than that, and you might thus want to navigate to a different page.
Suppose the previous search gets back a page where `totalPages` is 254.
You can request any page between 0 and 253 included by passing the page number as an additional request parameter.
If you omit the page parameter, the requested page is 0.
.Request
include::{snippets}/search/page/http-request.adoc[]
.Request parameters
include::{snippets}/search/page/request-parameters.adoc[]
.Curl
include::{snippets}/search/page/curl-request.adoc[]
.HTTPie
include::{snippets}/search/page/httpie-request.adoc[]
.Response
include::{snippets}/search/page/http-response.adoc[]
=== Highlighting
By passing an additional `highlight=true` request parameter, you can ask the search results to be highlighted.
To be accurate, only the description of the returned genetic resources will be highlighted, by surrounding the matching
words between `<em></em>` HTML tags.
.Request
include::{snippets}/search/highlight/http-request.adoc[]
.Request parameters
include::{snippets}/search/highlight/request-parameters.adoc[]
.Curl
include::{snippets}/search/highlight/curl-request.adoc[]
.HTTPie
include::{snippets}/search/highlight/httpie-request.adoc[]
.Response
include::{snippets}/search/highlight/http-response.adoc[]
=== Aggregations
In addition to the page of genetic resources, the service can also aggregate the search results.
To request the aggregations, you must pass an additional `aggregate=true` request parameter.
The set of properties used to compute aggregations is hard-coded on the server, and is thus always the same:
`domain`, `biotopeType`, `materialType`, `countryOfOrigin` and `taxon`.
If you request aggregations, then for each property, an aggregation will be computed and the results will contain
every distinct value of the property along with the number of matching documents for each distinct value.
.Request
include::{snippets}/search/aggregate/http-request.adoc[]
.Request parameters
include::{snippets}/search/aggregate/request-parameters.adoc[]
.Curl
include::{snippets}/search/aggregate/curl-request.adoc[]
.HTTPie
include::{snippets}/search/aggregate/httpie-request.adoc[]
.Response
include::{snippets}/search/aggregate/http-response.adoc[]
.Response fields
include::{snippets}/search/aggregate/response-fields.adoc[]
=== Filtering
Based on the aggregations obtained when searching, you can execute the same search query, but filter the results
by choosing one or several values for every aggregation.
For example, if you want only the search results which have _France_ **or** _Italy_ as their country of origin,
you can pass the following additional request parameters: `coo=France&coo=Italy`.
If you want only the search results which have _France_ **or** _Italy_ as their country of origin, *and* _Plantae_
as their domain, you can pass the following additional request parameters: `coo=France&coo=Italy&domain=Plantae`.
NOTE: These additional filters are applied *after* the full-text search.
This means that they will not be highlighted in the description, and more importantly, it means that if you combine
filters with aggregation (i.e. `aggregate=true`), the aggregations will be computed *before* the filters are applied.
This is necessary, otherwise there wouldn't be any other value then _Plantae_ in the returned aggregations, and
any other country than _France_ and _Italy_, which would prevent you from knowing which additional filters you
can apply to further refine the query.
.Request
include::{snippets}/search/filter/http-request.adoc[]
.Request parameters
include::{snippets}/search/filter/request-parameters.adoc[]
.Curl
include::{snippets}/search/filter/curl-request.adoc[]
.HTTPie
include::{snippets}/search/filter/httpie-request.adoc[]
== Pillars == Pillars
=== List pillars === List pillars
......
package fr.inra.urgi.rare.search;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Arrays;
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import fr.inra.urgi.rare.dao.GeneticResourceDao;
import fr.inra.urgi.rare.dao.RareAggregation;
import fr.inra.urgi.rare.dao.SearchRefinements;
import fr.inra.urgi.rare.doc.DocumentationConfig;
import fr.inra.urgi.rare.domain.GeneticResource;
import org.elasticsearch.search.aggregations.Aggregations;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import org.springframework.restdocs.request.ParameterDescriptor;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
/**
* REST-Docs tests for {@link SearchController}
*/
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = SearchController.class, secure = false)
@Import(DocumentationConfig.class)
@AutoConfigureRestDocs
class SearchControllerDocTest {
private static final ParameterDescriptor QUERY_PARAM =
parameterWithName("query")
.description("The full text query. It can contain several words.");
private static final ParameterDescriptor PAGE_PARAM =
parameterWithName("page")
.description("The page number, starting at 0.");
private static final ParameterDescriptor HIGHLIGHT_PARAM =
parameterWithName("highlight")
.description("If true, the description is highlighted.");
private static final ParameterDescriptor AGGREGATE_PARAM =
parameterWithName("aggregate")
.description("If true, aggregations are computed and returned.");
@MockBean
private GeneticResourceDao mockGeneticResourceDao;
@Autowired
private MockMvc mockMvc;
private GeneticResource syrah;
private GeneticResource dorato;
private GeneticResource highlightedSyrah;
private GeneticResource highlightedDorato;
@BeforeEach
void prepare() {
syrah =
GeneticResource.builder()
.withPillarName("Plant")
.withDatabaseSource("Florilège")
.withPortalURL("http://florilege.arcad-project.org/fr/collections")
.withId("doi:10.15454/1.4921785297227607E12")
.withName("Syrah")
.withDescription(
"Syrah is a Vitis vinifera subsp vinifera cv. Syrah accession (number: 150Mtp0, doi:10.15454/1.4921785297227607E12) maintained by the GRAPEVINE (managed by INRA) and held by INRA. It is a maintained/maintenu accession of biological status traditional cultivar/cultivar traditionnel. This accession has phenotyping data: Doligez_et_al_2013 - Study of the genetic determinism of berry weight and seed traits in a grapevine progeny.")
.withDataURL(
"https://urgi.versailles.inra.fr/gnpis-core/#accessionCard/id=ZG9pOjEwLjE1NDU0LzEuNDkyMTc4NTI5NzIyNzYwN0UxMg==")
.withDomain("Plantae")
.withTaxon(Collections.singletonList("Vitis vinifera"))
.withFamily(Collections.singletonList("Vitaceae"))
.withGenus(Collections.singletonList("Vitis"))
.withSpecies(Collections.singletonList("Vitis vinifera"))
.build();
dorato =
GeneticResource.builder()
.withPillarName("Plant")
.withDatabaseSource("Florilège")
.withPortalURL("http://florilege.arcad-project.org/fr/collections")
.withId("doi:10.15454/1.492178535151698E12")
.withName("Grecanico dorato")
.withDescription(
"Grecanico dorato is a Vitis vinifera subsp vinifera cv. Garganega accession (number: 1310Mtp1, doi:10.15454/1.492178535151698E12) maintained by the GRAPEVINE (managed by INRA) and held by INRA. It is a maintained/maintenu accession of biological status traditional cultivar/cultivar traditionnel")
.withDataURL(
"https://urgi.versailles.inra.fr/gnpis-core/#accessionCard/id=ZG9pOjEwLjE1NDU0LzEuNDkyMTc4NTM1MTUxNjk4RTEy")
.withDomain("Plantae")
.withTaxon(Collections.singletonList("Vitis vinifera"))
.withFamily(Collections.singletonList("Vitaceae"))
.withGenus(Collections.singletonList("Vitis"))
.withSpecies(Collections.singletonList("Vitis vinifera"))
.withCountryOfCollect("Italy")
.withCollectLatitude(37.5)
.withCollectLongitude(15.099722)
.build();
highlightedSyrah =
GeneticResource.builder(syrah)
.withDescription(syrah.getDescription().replace("Vitis", "<em>Vitis</em>"))
.build();
highlightedDorato =
GeneticResource.builder(dorato)
.withDescription(dorato.getDescription().replace("Vitis", "<em>Vitis</em>"))
.build();
}
@Test
void shouldSearch() throws Exception {
PageRequest pageRequest = PageRequest.of(0, SearchController.PAGE_SIZE);
String query = "vitis";
when(mockGeneticResourceDao.search(query, false, false, SearchRefinements.EMPTY, pageRequest))
.thenReturn(new AggregatedPageImpl<>(Arrays.asList(syrah, dorato), pageRequest, 2));
mockMvc.perform(get("/api/genetic-resources").param("query", query))
.andExpect(status().isOk())
.andDo(document("search/fulltext",
requestParameters(QUERY_PARAM),
responseFields(
fieldWithPath("number").description("The number of the page, starting at 0"),
fieldWithPath("size").description("The size of the page"),
fieldWithPath("totalElements").description("The total number of genetic resources found"),
fieldWithPath("maxResults").description("The limit in terms of number of genetic resources that you can navigate to. For example, if the page size is 20, and the total number of elements is 11000, you won't be able to navigate to the last 1000 elements"),
fieldWithPath("totalPages").description("The total number of pages of genetic resources that you can navigate to"),
fieldWithPath("content").description("The array of genetic resources contained in the requested page"),
subsectionWithPath("content[]").ignored(),
fieldWithPath("aggregations").description("see the following section about aggregations"))));
}
@Test
void shouldGetSpecificPage() throws Exception {
int page = 253;
PageRequest pageRequest = PageRequest.of(page, SearchController.PAGE_SIZE);
String query = "vitis";
when(mockGeneticResourceDao.search(query, false, false, SearchRefinements.EMPTY, pageRequest))
.thenReturn(new AggregatedPageImpl<>(Arrays.asList(syrah, dorato), pageRequest, SearchController.PAGE_SIZE * page + 2));
mockMvc.perform(get("/api/genetic-resources")
.param("query", query)
.param("page", Integer.toString(page)))
.andExpect(status().isOk())
.andDo(document("search/page",
requestParameters(QUERY_PARAM, PAGE_PARAM)));
}
@Test
void shouldHighlight() throws Exception {
PageRequest pageRequest = PageRequest.of(0, SearchController.PAGE_SIZE);
String query = "vitis";
when(mockGeneticResourceDao.search(query, false, true, SearchRefinements.EMPTY, pageRequest))
.thenReturn(new AggregatedPageImpl<>(Arrays.asList(highlightedSyrah, highlightedDorato), pageRequest, 2));
mockMvc.perform(get("/api/genetic-resources")
.param("query", query)
.param("highlight", "true"))
.andExpect(status().isOk())
.andDo(document("search/highlight",
requestParameters(QUERY_PARAM, HIGHLIGHT_PARAM)));
}
@Test
void shouldSearchAndAggregate() throws Exception {
int page = 253;
PageRequest pageRequest = PageRequest.of(page, SearchController.PAGE_SIZE);
String query = "vitis";
when(mockGeneticResourceDao.search(query, true, false, SearchRefinements.EMPTY, pageRequest))
.thenReturn(new AggregatedPageImpl<>(
Arrays.asList(syrah, dorato),
pageRequest,
SearchController.PAGE_SIZE * page + 2,
new Aggregations(
Arrays.asList(new MockTermsAggregation(RareAggregation.DOMAIN.getName(),
Arrays.asList(new MockBucket("Plantae", 123),
new MockBucket("Fungi", 2))),
new MockTermsAggregation(RareAggregation.BIOTOPE.getName(),
Collections.emptyList()),
new MockTermsAggregation(RareAggregation.MATERIAL.getName(),
Collections.singletonList(new MockBucket("Genome library", 4))),
new MockTermsAggregation(RareAggregation.COUNTRY_OF_ORIGIN.getName(),
Arrays.asList(new MockBucket("France", 2431),
new MockBucket("Italy", 376))),
new MockTermsAggregation(RareAggregation.TAXON.getName(),
Arrays.asList(new MockBucket("Vitis vinifera", 4563),
new MockBucket("Vitis x interspécifique", 285)))
)
)
));
mockMvc.perform(get("/api/genetic-resources")
.param("query", query)
.param("page", Integer.toString(page))
.param("aggregate", "true"))
.andExpect(status().isOk())
.andDo(document("search/aggregate",
requestParameters(QUERY_PARAM, PAGE_PARAM, AGGREGATE_PARAM),
responseFields(
fieldWithPath("number").ignored(),
fieldWithPath("size").ignored(),
fieldWithPath("totalElements").ignored(),
fieldWithPath("maxResults").ignored(),
fieldWithPath("totalPages").ignored(),
fieldWithPath("content").ignored(),
subsectionWithPath("content[]").ignored(),
fieldWithPath("aggregations").description("The array of computed aggregations"),
fieldWithPath("aggregations[].name").description("The name of the aggregation, used as a request parameter to apply a filter for this aggregation (see later)"),
fieldWithPath("aggregations[].type")
.type(Stream.of(RareAggregation.Type.values()).map(type -> "\"" + type.name() + "\"").collect(
Collectors.joining(" or ")))
.description("The type of the aggregation, used to decide if it should be displayed using checkboxes or using a typeahead input field"),
fieldWithPath("aggregations[].buckets").description("The buckets for this aggregation. A bucket exists for each distinct value of the property"),
fieldWithPath("aggregations[].buckets[].key").description("One of the distinct values of the property"),
fieldWithPath("aggregations[].buckets[].documentCount").description("The number of documents matched by the full-text search which fall into the bucket, i.e. have this distinct value for the property"))));
}
@Test
void shouldSearchWithRefinements() throws Exception {
PageRequest pageRequest = PageRequest.of(0, SearchController.PAGE_SIZE);
String query = "vitis";
SearchRefinements expectedRefinements =
SearchRefinements.builder()
.withTerm(RareAggregation.DOMAIN, Arrays.asList("Plantae"))
.withTerm(RareAggregation.COUNTRY_OF_ORIGIN, Arrays.asList("France", "Italy"))
.build();
when(mockGeneticResourceDao.search(query, false, false, expectedRefinements, pageRequest))
.thenReturn(new AggregatedPageImpl<>(Collections.emptyList(), pageRequest, 1));
mockMvc.perform(get("/api/genetic-resources")
.param("query", query)
.param(RareAggregation.DOMAIN.getName(), "Plantae")
.param(RareAggregation.COUNTRY_OF_ORIGIN.getName(), "France", "Italy"))
.andExpect(status().isOk())
.andDo(document("search/filter",
requestParameters(
QUERY_PARAM,
parameterWithName(RareAggregation.DOMAIN.getName()).description("The accepted values for the " + RareAggregation.DOMAIN.getName() + " aggregation's corresponding property (`domain`)"),
parameterWithName(RareAggregation.COUNTRY_OF_ORIGIN.getName()).description("The accepted values for the " + RareAggregation.COUNTRY_OF_ORIGIN.getName() + " aggregation's corresponding property (`countryOfOrigin`)"))));
}
}
...@@ -106,7 +106,7 @@ class SearchControllerTest { ...@@ -106,7 +106,7 @@ class SearchControllerTest {
} }
@Test @Test
void shouldSearchWIthRefinements() throws Exception { void shouldSearchWithRefinements() throws Exception {
PageRequest pageRequest = PageRequest.of(1, SearchController.PAGE_SIZE); PageRequest pageRequest = PageRequest.of(1, SearchController.PAGE_SIZE);
String query = "pauca"; String query = "pauca";
......
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