Skip to content

Commit

Permalink
feat(consortium-search): Implement consolidated items/holdings search (
Browse files Browse the repository at this point in the history
…#586)

* feat(consortium-search): Implement consolidated items/holdings search

- Implement endpoints by id
- Implement batch endpoints by ids

Implements: MSEARCH-759

---------

Co-authored-by: Mukhiddin Yusupov <[email protected]>
  • Loading branch information
viacheslavkol and mukhiddin-yusuf authored May 30, 2024
1 parent 9a72972 commit 088759a
Show file tree
Hide file tree
Showing 25 changed files with 836 additions and 16 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* Facets: add support for instance classification facets ([MSEARCH-606](https://issues.folio.org/browse/MSEARCH-606))
* Return Unified List of Inventory Locations in a Consortium ([MSEARCH-681](https://folio-org.atlassian.net/browse/MSEARCH-681))
* Remove ability to match on LCCN searches without a prefix ([MSEARCH-752](https://folio-org.atlassian.net/browse/MSEARCH-752))
* Search consolidated items/holdings data in consortium ([MSEARCH-759](https://folio-org.atlassian.net/browse/MSEARCH-759))

### Bug fixes
* Do not delete kafka topics if collection topic is enabled ([MSEARCH-725](https://folio-org.atlassian.net/browse/MSEARCH-725))
Expand Down
58 changes: 58 additions & 0 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,54 @@
"modulePermissions": [
"user-tenants.collection.get"
]
},
{
"methods": [
"GET"
],
"pathPattern": "/search/consortium/holding/{id}",
"permissionsRequired": [
"consortium-search.holdings.item.get"
],
"modulePermissions": [
"user-tenants.collection.get"
]
},
{
"methods": [
"GET"
],
"pathPattern": "/search/consortium/item/{id}",
"permissionsRequired": [
"consortium-search.items.item.get"
],
"modulePermissions": [
"user-tenants.collection.get"
]
},
{
"methods": [
"POST"
],
"pathPattern": "/search/consortium/batch/items",
"permissionsRequired": [
"consortium-search.items.collection.get"
],
"modulePermissions": [
"user-tenants.collection.get"
]
},
{
"methods": [
"POST"
],
"pathPattern": "/search/consortium/batch/holdings",
"permissionsRequired": [
"consortium-search.holdings.collection.get"
],
"modulePermissions": [
"user-tenants.collection.get"
]
}
]
},
Expand Down Expand Up @@ -648,6 +696,16 @@
"displayName": "Consortium Search - fetch items records",
"description": "Returns items records in consortium"
},
{
"permissionName": "consortium-search.holdings.item.get",
"displayName": "Consortium Search - fetch holdings record",
"description": "Returns holdings record in consortium"
},
{
"permissionName": "consortium-search.items.item.get",
"displayName": "Consortium Search - fetch item record",
"description": "Returns item record in consortium"
},
{
"permissionName": "consortium-search.locations.collection.get",
"displayName": "Consortium Search - fetch locations records",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
package org.folio.search.controller;

import static org.folio.search.utils.SearchUtils.INSTANCE_HOLDING_FIELD_NAME;
import static org.folio.search.utils.SearchUtils.INSTANCE_ITEM_FIELD_NAME;

import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.folio.search.domain.dto.BatchIdsDto;
import org.folio.search.domain.dto.ConsortiumHolding;
import org.folio.search.domain.dto.ConsortiumHoldingCollection;
import org.folio.search.domain.dto.ConsortiumItem;
import org.folio.search.domain.dto.ConsortiumItemCollection;
import org.folio.search.domain.dto.ConsortiumLocationCollection;
import org.folio.search.domain.dto.Instance;
import org.folio.search.domain.dto.SortOrder;
import org.folio.search.exception.RequestValidationException;
import org.folio.search.model.service.ConsortiumSearchContext;
import org.folio.search.model.service.CqlSearchRequest;
import org.folio.search.model.types.ResourceType;
import org.folio.search.rest.resource.SearchConsortiumApi;
import org.folio.search.service.consortium.ConsortiumInstanceSearchService;
import org.folio.search.service.consortium.ConsortiumInstanceService;
import org.folio.search.service.consortium.ConsortiumLocationService;
import org.folio.search.service.consortium.ConsortiumTenantService;
Expand All @@ -18,6 +31,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Log4j2
@Validated
@RestController
@RequiredArgsConstructor
Expand All @@ -30,13 +44,14 @@ public class SearchConsortiumController implements SearchConsortiumApi {
private final ConsortiumTenantService consortiumTenantService;
private final ConsortiumInstanceService instanceService;
private final ConsortiumLocationService locationService;
private final ConsortiumInstanceSearchService searchService;

@Override
public ResponseEntity<ConsortiumHoldingCollection> getConsortiumHoldings(String tenantHeader, String instanceId,
String tenantId, Integer limit,
Integer offset, String sortBy,
SortOrder sortOrder) {
checkAllowance(tenantHeader);
verifyAndGetTenant(tenantHeader);
var context = ConsortiumSearchContext.builderFor(ResourceType.HOLDINGS)
.filter("instanceId", instanceId)
.filter("tenantId", tenantId)
Expand All @@ -53,7 +68,7 @@ public ResponseEntity<ConsortiumItemCollection> getConsortiumItems(String tenant
String holdingsRecordId, String tenantId,
Integer limit, Integer offset, String sortBy,
SortOrder sortOrder) {
checkAllowance(tenantHeader);
verifyAndGetTenant(tenantHeader);
var context = ConsortiumSearchContext.builderFor(ResourceType.ITEM)
.filter("instanceId", instanceId)
.filter("tenantId", tenantId)
Expand All @@ -73,7 +88,7 @@ public ResponseEntity<ConsortiumLocationCollection> getConsortiumLocations(Strin
Integer offset,
String sortBy,
SortOrder sortOrder) {
checkAllowance(tenantHeader);
verifyAndGetTenant(tenantHeader);
var result = locationService.fetchLocations(tenantHeader, tenantId, limit, offset, sortBy, sortOrder);

return ResponseEntity.ok(new
Expand All @@ -82,11 +97,72 @@ public ResponseEntity<ConsortiumLocationCollection> getConsortiumLocations(Strin
.totalRecords(result.getTotalRecords()));
}

private void checkAllowance(String tenantHeader) {
@Override
public ResponseEntity<ConsortiumHolding> getConsortiumHolding(UUID id, String tenantHeader) {
var tenant = verifyAndGetTenant(tenantHeader);
var holdingId = id.toString();
var searchRequest = idCqlRequest(tenant, INSTANCE_HOLDING_FIELD_NAME, holdingId);

var result = searchService.getConsortiumHolding(holdingId, searchRequest);
return ResponseEntity.ok(result);
}

@Override
public ResponseEntity<ConsortiumItem> getConsortiumItem(UUID id, String tenantHeader) {
var tenant = verifyAndGetTenant(tenantHeader);
var itemId = id.toString();
var searchRequest = idCqlRequest(tenant, INSTANCE_ITEM_FIELD_NAME, itemId);

var result = searchService.getConsortiumItem(itemId, searchRequest);
return ResponseEntity.ok(result);
}

@Override
public ResponseEntity<ConsortiumHoldingCollection> fetchConsortiumBatchHoldings(String tenantHeader,
BatchIdsDto batchIdsDto) {
var tenant = verifyAndGetTenant(tenantHeader);
var holdingIds = batchIdsDto.getIds().stream().map(UUID::toString).collect(Collectors.toSet());
var searchRequest = idsCqlRequest(tenant, INSTANCE_HOLDING_FIELD_NAME, holdingIds);

var result = searchService.fetchConsortiumBatchHoldings(searchRequest, holdingIds);
return ResponseEntity.ok(result);
}

@Override
public ResponseEntity<ConsortiumItemCollection> fetchConsortiumBatchItems(String tenantHeader,
BatchIdsDto batchIdsDto) {
if (batchIdsDto.getIds().isEmpty()) {
return ResponseEntity
.ok(new ConsortiumItemCollection());
}

var tenant = verifyAndGetTenant(tenantHeader);
var itemIds = batchIdsDto.getIds().stream().map(UUID::toString).collect(Collectors.toSet());
var searchRequest = idsCqlRequest(tenant, INSTANCE_ITEM_FIELD_NAME, itemIds);

var result = searchService.fetchConsortiumBatchItems(searchRequest, itemIds);
return ResponseEntity.ok(result);
}

private String verifyAndGetTenant(String tenantHeader) {
var centralTenant = consortiumTenantService.getCentralTenant(tenantHeader);
if (centralTenant.isEmpty() || !centralTenant.get().equals(tenantHeader)) {
throw new RequestValidationException(REQUEST_NOT_ALLOWED_MSG, XOkapiHeaders.TENANT, tenantHeader);
}
return centralTenant.get();
}

private CqlSearchRequest<Instance> idCqlRequest(String tenant, String fieldName, String id) {
var query = fieldName + ".id=" + id;
return CqlSearchRequest.of(Instance.class, tenant, query, 1, 0, true, false, true);
}

private CqlSearchRequest<Instance> idsCqlRequest(String tenant, String fieldName, Set<String> ids) {
var query = ids.stream()
.map((fieldName + ".id=%s")::formatted)
.collect(Collectors.joining(" or "));

return CqlSearchRequest.of(Instance.class, tenant, query, ids.size(), 0, true, false, true);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.folio.search.converter;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.folio.search.domain.dto.ConsortiumHolding;
import org.folio.search.domain.dto.Holding;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ConsortiumHoldingMapper {

public static ConsortiumHolding toConsortiumHolding(String instanceId, Holding holding) {
return new ConsortiumHolding()
.id(holding.getId())
.hrid(holding.getHrid())
.tenantId(holding.getTenantId())
.instanceId(instanceId)
.callNumberPrefix(holding.getCallNumberPrefix())
.callNumber(holding.getCallNumber())
.callNumberSuffix(holding.getCallNumberSuffix())
.copyNumber(holding.getCopyNumber())
.permanentLocationId(holding.getPermanentLocationId())
.discoverySuppress(holding.getDiscoverySuppress() != null && holding.getDiscoverySuppress());
}
}
20 changes: 20 additions & 0 deletions src/main/java/org/folio/search/converter/ConsortiumItemMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.folio.search.converter;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.folio.search.domain.dto.ConsortiumItem;
import org.folio.search.domain.dto.Item;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ConsortiumItemMapper {

public static ConsortiumItem toConsortiumItem(String instanceId, Item item) {
return new ConsortiumItem()
.id(item.getId())
.hrid(item.getHrid())
.tenantId(item.getTenantId())
.instanceId(instanceId)
.holdingsRecordId(item.getHoldingsRecordId())
.barcode(item.getBarcode());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,16 @@ public SearchSourceBuilder convert(String query, String resource) {
* @return search source as {@link SearchSourceBuilder} object with query and sorting conditions
*/
public SearchSourceBuilder convertForConsortia(String query, String resource) {
return convertForConsortia(query, resource, false);
}

public SearchSourceBuilder convertForConsortia(String query, String resource, boolean consortiumConsolidated) {
var sourceBuilder = convert(query, resource);
var queryBuilder = consortiumSearchHelper.filterQueryForActiveAffiliation(sourceBuilder.query(), resource);
if (consortiumConsolidated) {
return sourceBuilder;
}

var queryBuilder = consortiumSearchHelper.filterQueryForActiveAffiliation(sourceBuilder.query(), resource);
return sourceBuilder.query(queryBuilder);
}

Expand Down
17 changes: 15 additions & 2 deletions src/main/java/org/folio/search/model/service/CqlSearchRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ public class CqlSearchRequest<T> implements ResourceRequest {
*/
private final Boolean includeNumberOfTitles;

/**
* Doesn't affect non-consortium. true means include all records, false means filter for active affiliation.
*/
private final Boolean consortiumConsolidated;

/**
* Creates {@link CqlSearchRequest} object for given variables.
*
Expand All @@ -64,14 +69,22 @@ public class CqlSearchRequest<T> implements ResourceRequest {
* @param expandAll - whether to return only response properties or entire record
* @param <R> - generic type for {@link CqlSearchRequest} object.
* @param includeNumberOfTitles - indicates whether the number of titles should be counted.
* @param consortiumConsolidated - indicates whether to return consortium consolidated records.
* @return created {@link CqlSearchRequest} object
*/
public static <R> CqlSearchRequest<R> of(Class<R> resourceClass, String tenantId, String query,
Integer limit, Integer offset, Boolean expandAll,
Boolean includeNumberOfTitles) {
Boolean includeNumberOfTitles, Boolean consortiumConsolidated) {
var resource = SearchUtils.getResourceName(resourceClass);
return new CqlSearchRequest<>(resource, resourceClass, tenantId, query, limit, offset, expandAll,
includeNumberOfTitles);
includeNumberOfTitles, consortiumConsolidated);
}

public static <R> CqlSearchRequest<R> of(Class<R> resourceClass, String tenantId, String query,
Integer limit, Integer offset, Boolean expandAll,
Boolean includeNumberOfTitles) {
return CqlSearchRequest.of(resourceClass, tenantId, query, limit, offset, expandAll,
includeNumberOfTitles, false);
}

public static <R> CqlSearchRequest<R> of(Class<R> resourceClass, String tenantId, String query,
Expand Down
8 changes: 3 additions & 5 deletions src/main/java/org/folio/search/service/SearchService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.apache.commons.lang3.BooleanUtils.isFalse;
import static org.folio.search.model.types.ResponseGroupType.SEARCH;
import static org.folio.search.utils.SearchUtils.buildPreferenceKey;

import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -54,7 +55,8 @@ public <T> SearchResult<T> search(CqlSearchRequest<T> request) {
}
var resource = request.getResource();
var requestTimeout = searchQueryConfiguration.getRequestTimeout();
var queryBuilder = cqlSearchQueryConverter.convertForConsortia(request.getQuery(), resource)
var queryBuilder = cqlSearchQueryConverter.convertForConsortia(request.getQuery(), resource,
request.getConsortiumConsolidated())
.from(request.getOffset())
.size(request.getLimit())
.trackTotalHits(true)
Expand All @@ -76,10 +78,6 @@ public <T> SearchResult<T> search(CqlSearchRequest<T> request) {
return searchResult;
}

private String buildPreferenceKey(String tenantId, String resource, String query) {
return tenantId + "-" + resource + "-" + query;
}

private <T> void searchResultPostProcessing(Class<?> resourceClass, boolean includeNumberOfTitles,
SearchResult<T> searchResult) {
if (Objects.isNull(resourceClass)) {
Expand Down
Loading

0 comments on commit 088759a

Please sign in to comment.