Skip to content

Commit

Permalink
feat(consortium-search): Implement consolidated items/holdings search
Browse files Browse the repository at this point in the history
- Implement endpoints by id
- Implement batch endpoints by ids

Implements: MSEARCH-759
  • Loading branch information
viacheslavkol committed May 22, 2024
1 parent 13e1fac commit c5f5ca5
Show file tree
Hide file tree
Showing 23 changed files with 680 additions and 168 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
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
package org.folio.search.controller;

import static org.apache.commons.collections4.CollectionUtils.isEmpty;

import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.folio.search.domain.dto.BatchHoldingIdsDto;
import org.folio.search.domain.dto.BatchItemIdsDto;
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;
Expand All @@ -22,7 +17,7 @@
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.SearchService;
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 @@ -45,7 +40,7 @@ public class SearchConsortiumController implements SearchConsortiumApi {
private final ConsortiumTenantService consortiumTenantService;
private final ConsortiumInstanceService instanceService;
private final ConsortiumLocationService locationService;
private final SearchService searchService;
private final ConsortiumInstanceSearchService searchService;

@Override
public ResponseEntity<ConsortiumHoldingCollection> getConsortiumHoldings(String tenantHeader, String instanceId,
Expand All @@ -64,34 +59,6 @@ public ResponseEntity<ConsortiumHoldingCollection> getConsortiumHoldings(String
return ResponseEntity.ok(instanceService.fetchHoldings(context));
}

@Override
public ResponseEntity<ConsortiumHolding> getConsortiumHolding(UUID id, String tenantHeader) {
var tenant = verifyAndGetTenant(tenantHeader);
var holdingId = id.toString();
var query = "holdings.id=" + holdingId;
var searchRequest = CqlSearchRequest.of(Instance.class, tenant, query, 1, 0, true);
var result = searchService.search(searchRequest);

if (isEmpty(result.getRecords()) || isEmpty(result.getRecords().iterator().next().getHoldings())) {
return ResponseEntity.ok(new ConsortiumHolding());
}

var instance = result.getRecords().iterator().next();
var holding = instance.getHoldings().stream()
.filter(hol -> Objects.equals(holdingId, hol.getId()))
.findFirst().orElse(null);

if (holding == null) {
return ResponseEntity.ok(new ConsortiumHolding());
}

return ResponseEntity.ok(new ConsortiumHolding()
.id(holdingId)
.tenantId(holding.getTenantId())
.instanceId(instance.getId())
);
}

@Override
public ResponseEntity<ConsortiumItemCollection> getConsortiumItems(String tenantHeader, String instanceId,
String holdingsRecordId, String tenantId,
Expand Down Expand Up @@ -126,95 +93,60 @@ public ResponseEntity<ConsortiumLocationCollection> getConsortiumLocations(Strin
.totalRecords(result.getTotalRecords()));
}

@Override
public ResponseEntity<ConsortiumHolding> getConsortiumHolding(UUID id, String tenantHeader) {
var tenant = verifyAndGetTenant(tenantHeader);
var holdingId = id.toString();
var query = "holdings.id=" + holdingId;
var searchRequest = CqlSearchRequest.of(Instance.class, tenant, query, 1, 0, true, false, true);

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

@Override
public ResponseEntity<ConsortiumItem> getConsortiumItem(UUID itemId, String tenantHeader) {
var tenant = verifyAndGetTenant(tenantHeader);
var query = "items.id=" + itemId.toString();
var searchRequest = CqlSearchRequest.of(Instance.class, tenant, query, 1, 0, true);
var result = searchService.search(searchRequest);
var searchRequest = CqlSearchRequest.of(Instance.class, tenant, query, 1, 0, true, false, true);

if (isEmpty(result.getRecords()) || isEmpty(result.getRecords().iterator().next().getItems())) {
return ResponseEntity.ok(new ConsortiumItem());
}

var instance = result.getRecords().iterator().next();
var item = instance.getItems().stream()
.filter(it -> Objects.equals(itemId.toString(), it.getId()))
.findFirst().orElse(null);
var result = searchService.getConsortiumItem(itemId.toString(), searchRequest);
return ResponseEntity.ok(result);
}

if (item == null) {
return ResponseEntity.ok(new ConsortiumItem());
}
@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 query = batchIdsDto.getIds().stream()
.map(UUID::toString)
.map("holdings.id = %s"::formatted)
.collect(Collectors.joining(" or "));
var searchRequest = CqlSearchRequest.of(Instance.class, tenant, query, holdingIds.size(), 0, true, false, true);

return ResponseEntity.ok(new ConsortiumItem()
.id(itemId.toString())
.tenantId(item.getTenantId())
.instanceId(instance.getId())
.holdingsRecordId(item.getHoldingsRecordId())
);
var result = searchService.fetchConsortiumBatchHoldings(searchRequest, holdingIds);
return ResponseEntity.ok(result);
}

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

var tenant = verifyAndGetTenant(tenantHeader);
var itemIds = batchItemIdsDto.getIds().stream().map(UUID::toString).collect(Collectors.toSet());
var query = batchItemIdsDto.getIds().stream()
var itemIds = batchIdsDto.getIds().stream().map(UUID::toString).collect(Collectors.toSet());
var query = batchIdsDto.getIds().stream()
.map(UUID::toString)
.map("items.id = %s"::formatted)
.collect(Collectors.joining(" or "));
var searchRequest = CqlSearchRequest.of(Instance.class, tenant, query, 1000, 0, true);
var result = searchService.search(searchRequest);
var consortiumItems = result.getRecords().stream()
.map(instance ->
instance.getItems().stream()
.filter(item -> itemIds.contains(item.getId()))
.findFirst()
.map(item -> new ConsortiumItem()
.id(item.getId())
.tenantId(item.getTenantId())
.instanceId(instance.getId())
.holdingsRecordId(item.getHoldingsRecordId()))
)
.filter(Optional::isPresent)
.map(Optional::get)
.toList();

return ResponseEntity
.ok(new ConsortiumItemCollection().items(consortiumItems).totalRecords(result.getTotalRecords()));
}
var searchRequest = CqlSearchRequest.of(Instance.class, tenant, query, itemIds.size(), 0, true, false, true);

@Override
public ResponseEntity<ConsortiumHoldingCollection> fetchConsortiumBatchHoldings(String tenantHeader,
BatchHoldingIdsDto holdingIdsDto) {
var tenant = verifyAndGetTenant(tenantHeader);
var holdingIds = holdingIdsDto.getIds().stream().map(UUID::toString).collect(Collectors.toSet());
var query = holdingIdsDto.getIds().stream()
.map(UUID::toString)
.map("holdings.id = %s"::formatted)
.collect(Collectors.joining(" or "));
var searchRequest = CqlSearchRequest.of(Instance.class, tenant, query, 1000, 0, true);
var result = searchService.search(searchRequest);
var consortiumHoldings = result.getRecords().stream()
.map(instance ->
instance.getHoldings().stream()
.filter(holding -> holdingIds.contains(holding.getId()))
.findFirst()
.map(holding -> new ConsortiumHolding()
.id(holding.getId())
.tenantId(holding.getTenantId())
.instanceId(instance.getId()))
)
.filter(Optional::isPresent)
.map(Optional::get)
.toList();
return ResponseEntity
.ok(new ConsortiumHoldingCollection().holdings(consortiumHoldings).totalRecords(result.getTotalRecords()));
var result = searchService.fetchConsortiumBatchItems(searchRequest, itemIds);
return ResponseEntity.ok(result);
}

private String verifyAndGetTenant(String tenantHeader) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.folio.search.converter;

import org.folio.search.domain.dto.ConsortiumHolding;
import org.folio.search.domain.dto.Holding;
import org.springframework.stereotype.Component;

@Component
public class ConsortiumHoldingMapper {

public ConsortiumHolding map(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());
}
}
19 changes: 19 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,19 @@
package org.folio.search.converter;

import org.folio.search.domain.dto.ConsortiumItem;
import org.folio.search.domain.dto.Item;
import org.springframework.stereotype.Component;

@Component
public class ConsortiumItemMapper {

public ConsortiumItem map(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 c5f5ca5

Please sign in to comment.