Skip to content

Commit

Permalink
Added pagination for search and list apis including both of elastic a…
Browse files Browse the repository at this point in the history
…nd native search
  • Loading branch information
holashchand committed May 6, 2024
1 parent a23865b commit 810b7bf
Show file tree
Hide file tree
Showing 16 changed files with 201 additions and 96 deletions.
2 changes: 1 addition & 1 deletion docker-compose-v1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: '2.4'

services:
es:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.13
image: docker.elastic.co/elasticsearch/elasticsearch:6.8.23
volumes:
- ./${ES_DIR-es-data}:/usr/share/elasticsearch/data/*
environment:
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: '2.4'

services:
es:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.13
image: docker.elastic.co/elasticsearch/elasticsearch:6.8.23
volumes:
- ./${ES_DIR-es-data}:/usr/share/elasticsearch/data/*
environment:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@ Feature: Registry api tests
And header Authorization = student_token
When method get
Then status 200
And response[0].osid.length > 0
And response.data[0].osid.length > 0
33 changes: 17 additions & 16 deletions java/apitest/src/test/java/e2e/registry/registry.feature
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ Feature: Registry api tests
And header Authorization = student_token
When method get
Then status 200
And response[0].osid.length > 0
* def studentOsid = response[0].osid
And response.data[0].osid.length > 0
* def studentOsid = response.data[0].osid
# update student info
Given url registryUrl
And path 'api/v1/Student/' + studentOsid
Expand All @@ -168,7 +168,7 @@ Feature: Registry api tests
And header Authorization = student_token
When method get
Then status 200
And response[0].name == "xyz"
And response.data[0].name == "xyz"
# get student
Given url registryUrl
And path 'api/v1/Student/search'
Expand All @@ -177,8 +177,8 @@ Feature: Registry api tests
Then status 200
* print response
And response.length == 1
And match response[0].contact == '#notpresent'
And match response[0].favoriteSubject == '#notpresent'
And match response.data[0].contact == '#notpresent'
And match response.data[0].favoriteSubject == '#notpresent'
# delete student info
Given url registryUrl
And path 'api/v1/Student/' + studentOsid
Expand All @@ -191,7 +191,8 @@ Feature: Registry api tests
And path 'api/v1/Student'
And header Authorization = student_token
When method get
Then status 404
Then status 200
And response.totalCount == 0

@env=async
Scenario: Create a teacher schema and create teacher entity asynchronously
Expand Down Expand Up @@ -240,17 +241,17 @@ Feature: Registry api tests
When method post
Then status 200
* print response
And response.length == 1
And response[0].contact == '#notpresent'
And response.totalCount == 1
And response.data[0].contact == '#notpresent'
# get teacher info
Given url registryUrl
And path 'api/v1/Teacher/search'
And request {"filters":{ "name": { "endsWith": "abc" }}}
When method post
Then status 200
* print response
And response.length == 1
And response[0].contact == '#notpresent'
And response.totalCount == 1
And response.data[0].contact == '#notpresent'

@envnot=fusionauth
Scenario: Create Board and invite institutes
Expand Down Expand Up @@ -316,7 +317,7 @@ Feature: Registry api tests
And header Authorization = board_token
When method get
Then status 200
And response[0].osid.length > 0
And response.data[0].osid.length > 0

# invite institute with token
Given url registryUrl
Expand Down Expand Up @@ -347,9 +348,9 @@ Feature: Registry api tests
And header Authorization = institute_token
When method get
Then status 200
And response[0].osid.length > 0
* def instituteOsid = response[0].osid
* def address = response[0].address
And response.data[0].osid.length > 0
* def instituteOsid = response.data[0].osid
* def address = response.data[0].address

# update property of institute
Given url registryUrl
Expand All @@ -366,8 +367,8 @@ Feature: Registry api tests
And header Authorization = institute_token
When method get
Then status 200
And assert response[0].address[0].phoneNo.length == 1
And assert response[0].address[0].phoneNo[0] == "444"
And assert response.data[0].address[0].phoneNo.length == 1
And assert response.data[0].address[0].phoneNo[0] == "444"

@envnot=fusionauth
Scenario: write a api test, to test the schema not found error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import dev.sunbirdrc.pojos.ComponentHealthInfo;
import dev.sunbirdrc.pojos.Filter;
import dev.sunbirdrc.pojos.FilterOperators;
Expand Down Expand Up @@ -49,8 +50,7 @@
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;

import static dev.sunbirdrc.registry.middleware.util.Constants.CONNECTION_FAILURE;
import static dev.sunbirdrc.registry.middleware.util.Constants.SUNBIRD_ELASTIC_SERVICE_NAME;
import static dev.sunbirdrc.registry.middleware.util.Constants.*;

public class ElasticServiceImpl implements IElasticService {
private static Map<String, RestHighLevelClient> esClient = new HashMap<String, RestHighLevelClient>();
Expand Down Expand Up @@ -283,21 +283,25 @@ public JsonNode search(String index, SearchQuery searchQuery) throws IOException
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
.query(query)
.size(searchQuery.getLimit())
.from(searchQuery.getOffset());
.from(searchQuery.getOffset())
.trackTotalHits(true);
SearchRequest searchRequest = new SearchRequest(index).source(sourceBuilder);
ArrayNode resultArray = JsonNodeFactory.instance.arrayNode();
ObjectNode resultNode = JsonNodeFactory.instance.objectNode();
ArrayNode dataArray = JsonNodeFactory.instance.arrayNode();
ObjectMapper mapper = new ObjectMapper();
SearchResponse searchResponse = getClient(index).search(searchRequest, RequestOptions.DEFAULT);
for (SearchHit hit : searchResponse.getHits()) {
JsonNode node = mapper.readValue(hit.getSourceAsString(), JsonNode.class);
// TODO: Add draft mode condition
if(node.get("_status") == null || node.get("_status").asBoolean()) {
resultArray.add(node);
}
SearchResponse searchResponse = getClient(index).search(searchRequest, RequestOptions.DEFAULT);
for (SearchHit hit : searchResponse.getHits()) {
JsonNode node = mapper.readValue(hit.getSourceAsString(), JsonNode.class);
// TODO: Add draft mode condition
if(node.get(STATUS_KEYWORD) == null || node.get(STATUS_KEYWORD).asBoolean()) {
dataArray.add(node);
}
logger.debug("Total search records found " + resultArray.size());
}
resultNode.set(ENTITY_LIST, dataArray);
resultNode.put(TOTAL_COUNT, searchResponse.getHits().getTotalHits());
logger.debug("Total search records found " + dataArray.size());

return resultArray;
return resultNode;

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public class Constants {
public static final String END_DATE="endDate";
public static final String LIMIT="limit";
public static final String OFFSET="offset";
public static final String NEXT_PAGE="nextPage";
public static final String PREV_PAGE="prevPage";
public static final String TOTAL_COUNT="totalCount";
public static final String ENTITY_LIST="data";

// JSON LD specific
public static final String CONTEXT_KEYWORD = "@context";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand All @@ -30,7 +31,8 @@
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.NestedExceptionUtils;

import static dev.sunbirdrc.registry.middleware.util.Constants.*;

public class JSONUtil {

Expand Down Expand Up @@ -545,4 +547,28 @@ public static JsonNode extractPropertyDataFromEntity(JsonNode entityNode, Map<St
}
return result;
}

public static ObjectNode getSearchPageUrls(JsonNode inputNode, long defaultLimit, long defaultOffset, long totalCount, String url) throws IOException {
ObjectNode result = JsonNodeFactory.instance.objectNode();
JsonNode searchNode = objectMapper.readTree(inputNode.toString());
long limit = searchNode.get(LIMIT) == null ? defaultLimit : searchNode.get(LIMIT).asLong(defaultLimit);
long offset = searchNode.get(OFFSET) == null ? defaultOffset : searchNode.get(OFFSET).asLong(defaultOffset);
((ObjectNode) searchNode).set(OFFSET, JsonNodeFactory.instance.numberNode(offset - limit));
String prevPageToken = Base64.getEncoder().encodeToString(searchNode.toString().getBytes(StandardCharsets.UTF_8));
((ObjectNode) searchNode).set(OFFSET, JsonNodeFactory.instance.numberNode(offset + limit));
String nextPageToken = Base64.getEncoder().encodeToString(searchNode.toString().getBytes(StandardCharsets.UTF_8));
if(offset - limit >=0) result.put(PREV_PAGE, url + "?search=" + prevPageToken);
if(offset + limit <= totalCount) result.put(NEXT_PAGE, url + "?search=" + nextPageToken);
return result;
}

public static ObjectNode parseSearchToken(String endcodedValue) {
try {
byte[] decoded = Base64.getDecoder().decode(endcodedValue);
return (ObjectNode) objectMapper.readTree(decoded);
} catch (Exception ignored) {
logger.warn("Unable to parse next page token");
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@
import java.util.ArrayList;

import static dev.sunbirdrc.registry.Constants.Schema;
import static dev.sunbirdrc.registry.middleware.util.Constants.ENTITY_TYPE;
import static dev.sunbirdrc.registry.middleware.util.Constants.FILTERS;
import static dev.sunbirdrc.registry.middleware.util.Constants.*;

@Component
public class SchemaLoader implements ApplicationListener<ContextRefreshedEvent> {
Expand All @@ -46,14 +45,14 @@ private void loadSchemasFromDB() {
objectNode.set(FILTERS, JsonNodeFactory.instance.objectNode());
try {
JsonNode searchResults = searchService.search(objectNode, "");
for (JsonNode schemaNode : searchResults.get(Schema)) {
for (JsonNode schemaNode : searchResults.get(Schema).get(ENTITY_LIST)) {
try {
schemaService.addSchema(JsonNodeFactory.instance.objectNode().set(Schema, schemaNode));
} catch (Exception e) {
logger.error("Failed loading schema to definition manager: {}", ExceptionUtils.getStackTrace(e));
}
}
logger.error("Loaded {} schema from DB", searchResults.get(Schema).size());
logger.error("Loaded {} schema from DB", searchResults.get(Schema).get(TOTAL_COUNT));
} catch (IOException e) {
logger.error("Exception occurred while loading schema from db: {}", ExceptionUtils.getStackTrace(e));
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public ResponseEntity<Response> searchEntity(@RequestHeader HttpHeaders header)

try {
watch.start("RegistryController.searchEntity");
JsonNode result = registryHelper.searchEntity(payload, null);
JsonNode result = registryHelper.searchEntity(payload, null, false);

response.setResult(result);
responseParams.setStatus(Response.Status.SUCCESSFUL);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.BadRequestException;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

import static dev.sunbirdrc.registry.Constants.*;
import static dev.sunbirdrc.registry.helper.RegistryHelper.ServiceNotEnabledResponse;
import static dev.sunbirdrc.registry.middleware.util.Constants.ENTITY_TYPE;
import static dev.sunbirdrc.registry.middleware.util.Constants.TOTAL_COUNT;

@RestController
public class RegistryEntityController extends AbstractController {
Expand Down Expand Up @@ -83,6 +85,10 @@ public class RegistryEntityController extends AbstractController {
boolean securityEnabled;
@Value("${certificate.enableExternalTemplates:false}")
boolean externalTemplatesEnabled;
@Value("${search.offset:0}")
private int searchOffset;
@Value("${search.limit:2000}")
private int searchLimit;

@RequestMapping(value = "/api/v1/{entityName}/invite", method = RequestMethod.POST)
public ResponseEntity<Object> invite(
Expand Down Expand Up @@ -191,8 +197,11 @@ public ResponseEntity<Object> deleteEntity(
}
}

@RequestMapping(value = "/api/v1/{entityName}/search", method = RequestMethod.POST)
public ResponseEntity<Object> searchEntity(@PathVariable String entityName, @RequestHeader HttpHeaders header, @RequestBody ObjectNode searchNode) {
@RequestMapping(value = "/api/v1/{entityName}/search", method = {RequestMethod.POST, RequestMethod.GET})
public ResponseEntity<Object> searchEntity(@PathVariable String entityName,
HttpServletRequest request,
@RequestHeader HttpHeaders header, @RequestBody(required = false) ObjectNode searchNode,
@RequestParam(value = "search", required = false) String searchQueryString) {

ResponseParams responseParams = new ResponseParams();
Response response = new Response(Response.API_ID.SEARCH, "OK", responseParams);
Expand All @@ -201,12 +210,21 @@ public ResponseEntity<Object> searchEntity(@PathVariable String entityName, @Req
watch.start("RegistryController.searchEntity");
ArrayNode entity = JsonNodeFactory.instance.arrayNode();
entity.add(entityName);
if(searchNode == null && (searchQueryString == null || searchQueryString.isEmpty())) {
throw new BadRequestException("Search Request body not found");
}
if(searchNode == null) {
searchNode = JsonNodeFactory.instance.objectNode();
registryHelper.addSearchTokenToQuery(searchQueryString, searchNode);
}
searchNode.set(ENTITY_TYPE, entity);
checkEntityNameInDefinitionManager(entityName);
if (definitionsManager.getDefinition(entityName).getOsSchemaConfiguration().getEnableSearch()) {
JsonNode result = registryHelper.searchEntity(searchNode, null);
JsonNode result = registryHelper.searchEntity(searchNode, null, false).get(entityName);
ObjectNode pageUrls = JSONUtil.getSearchPageUrls(searchNode, searchLimit, searchOffset, result.get(TOTAL_COUNT).asLong(), request.getRequestURL().toString());
((ObjectNode) result).setAll(pageUrls);
watch.stop("RegistryController.searchEntity");
return new ResponseEntity<>(result.get(entityName), HttpStatus.OK);
return new ResponseEntity<>(result, HttpStatus.OK);
} else {
watch.stop("RegistryController.searchEntity");
logger.error("Searching on entity {} not allowed", entityName);
Expand Down Expand Up @@ -669,17 +687,21 @@ private JsonNode getEntityJsonNode(@PathVariable String entityName, @PathVariabl

@RequestMapping(value = "/api/v1/{entityName}", method = RequestMethod.GET)
public ResponseEntity<Object> getEntityByToken(@PathVariable String entityName, HttpServletRequest request,
@RequestHeader(required = false) String viewTemplateId) throws RecordNotFoundException {
@RequestHeader(required = false) String viewTemplateId,
@RequestParam(value = "search", required = false) String searchToken) throws RecordNotFoundException {
ResponseParams responseParams = new ResponseParams();
Response response = new Response(Response.API_ID.GET, "OK", responseParams);
try {
checkEntityNameInDefinitionManager(entityName);
String userId = registryHelper.getUserId(entityName);
if (!Strings.isEmpty(userId)) {
JsonNode responseFromDb = registryHelper.searchEntitiesByUserId(entityName, userId, viewTemplateId);
JsonNode entities = responseFromDb.get(entityName);
if (!entities.isEmpty()) {
return new ResponseEntity<>(entities, HttpStatus.OK);
JsonNode searchQuery = registryHelper.searchQueryByUserId(entityName, userId, searchToken, viewTemplateId);
JsonNode responseFromDb = registryHelper.searchEntity(searchQuery, userId, true);
JsonNode results = responseFromDb.get(entityName);
if (!results.isEmpty()) {
ObjectNode pageUrls = JSONUtil.getSearchPageUrls(searchQuery, searchLimit, searchOffset, results.get(TOTAL_COUNT).asLong(), request.getRequestURL().toString());
((ObjectNode) results).setAll(pageUrls);
return new ResponseEntity<>(results, HttpStatus.OK);
} else {
responseParams.setErrmsg("No record found");
responseParams.setStatus(Response.Status.UNSUCCESSFUL);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static dev.sunbirdrc.registry.middleware.util.Constants.ENTITY_LIST;
import static dev.sunbirdrc.registry.middleware.util.Constants.TOTAL_COUNT;
import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.has;
import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.hasNot;

Expand All @@ -45,10 +47,15 @@ public JsonNode search(Graph graphFromStore, SearchQuery searchQuery, boolean ex
GraphTraversal<Vertex, Vertex> parentTraversal = resultGraphTraversal.asAdmin();

resultGraphTraversal = getFilteredResultTraversal(resultGraphTraversal, filterList)
.or(hasNot(Constants.STATUS_KEYWORD), has(Constants.STATUS_KEYWORD, Constants.STATUS_ACTIVE))
.range(offset, offset + searchQuery.getLimit()).limit(searchQuery.getLimit());
.or(hasNot(Constants.STATUS_KEYWORD), has(Constants.STATUS_KEYWORD, Constants.STATUS_ACTIVE));
GraphTraversal<Vertex, Vertex> resultGraphTraversalCount = resultGraphTraversal.asAdmin().clone();
resultGraphTraversal.range(offset, offset + searchQuery.getLimit());
JsonNode result = getResult(graphFromStore, resultGraphTraversal, parentTraversal, expandInternal);
resultNode.set(entity, result);
ObjectNode response = JsonNodeFactory.instance.objectNode();
response.set(ENTITY_LIST, result);
long count = resultGraphTraversalCount.count().next();
response.set(TOTAL_COUNT, JsonNodeFactory.instance.numberNode(count));
resultNode.set(entity, response);
}

return resultNode;
Expand Down
Loading

0 comments on commit 810b7bf

Please sign in to comment.