Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cn-browse): Implement browsing endpoint for Call Numbers #712

Merged
merged 1 commit into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Call Numbers Browse: Implement Database Structure and Logic for Managing Call Numbers ([MSEARCH-862](https://folio-org.atlassian.net/browse/MSEARCH-862))
* Call Numbers Browse: Implement Call Number Browse Config ([MSEARCH-863](https://folio-org.atlassian.net/browse/MSEARCH-863))
* Call Numbers Browse: Implement Indexing and Re-indexing Mechanisms for Call-Numbers ([MSEARCH-864](https://folio-org.atlassian.net/browse/MSEARCH-864))
* Call Numbers Browse: Implement Browsing Endpoint for Call-Numbers ([MSEARCH-865](https://folio-org.atlassian.net/browse/MSEARCH-865))

### Bug fixes
* Remove shelving order calculation for local call-number types
Expand Down
19 changes: 18 additions & 1 deletion descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@
},
{
"id": "browse",
"version": "1.4",
"version": "1.5",
"handlers": [
{
"methods": [
Expand All @@ -400,6 +400,18 @@
"user-tenants.collection.get"
]
},
{
"methods": [
"GET"
],
"pathPattern": "/browse/call-numbers/{browseOptionId}/instances",
"permissionsRequired": [
"browse.call-numbers-new.instances.collection.get"
],
"modulePermissions": [
"user-tenants.collection.get"
]
},
{
"methods": [
"GET"
Expand Down Expand Up @@ -727,6 +739,11 @@
"displayName": "Browse - provides collections of browse items for instance by call number",
"description": "Browse instances by given query"
},
{
"permissionName": "browse.call-numbers-new.instances.collection.get",
"displayName": "Browse - provides collections of browse items for instance by call number",
"description": "Browse instances by given query"
},
{
"permissionName": "browse.classification-numbers.instances.collection.get",
"displayName": "Browse - provides collections of browse by classification number",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public void addFormatters(FormatterRegistry registry) {
private static final class StringToRecordTypeEnumConverter implements Converter<String, RecordType> {
@Override
public RecordType convert(String source) {
return RecordType.valueOf(source.toUpperCase());
return RecordType.valueOf(source.toUpperCase().replace('-', '_'));
}
}

Expand Down
53 changes: 42 additions & 11 deletions src/main/java/org/folio/search/controller/BrowseController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,29 @@
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.folio.search.model.types.ResourceType.AUTHORITY;
import static org.folio.search.model.types.ResourceType.INSTANCE;
import static org.folio.search.model.types.ResourceType.INSTANCE_CALL_NUMBER;
import static org.folio.search.model.types.ResourceType.INSTANCE_CLASSIFICATION;
import static org.folio.search.model.types.ResourceType.INSTANCE_CONTRIBUTOR;
import static org.folio.search.model.types.ResourceType.INSTANCE_SUBJECT;
import static org.folio.search.utils.SearchUtils.AUTHORITY_BROWSING_FIELD;
import static org.folio.search.utils.SearchUtils.CALL_NUMBER_BROWSING_FIELD;
import static org.folio.search.utils.SearchUtils.CLASSIFICATION_NUMBER_BROWSING_FIELD;
import static org.folio.search.utils.SearchUtils.CONTRIBUTOR_BROWSING_FIELD;
import static org.folio.search.utils.SearchUtils.LEGACY_CALL_NUMBER_BROWSING_FIELD;
import static org.folio.search.utils.SearchUtils.SHELVING_ORDER_BROWSING_FIELD;
import static org.folio.search.utils.SearchUtils.SUBJECT_BROWSING_FIELD;
import static org.folio.search.utils.SearchUtils.TYPED_CALL_NUMBER_BROWSING_FIELD;

import lombok.RequiredArgsConstructor;
import org.folio.search.domain.dto.AuthorityBrowseResult;
import org.folio.search.domain.dto.BrowseOptionType;
import org.folio.search.domain.dto.CallNumberBrowseItem;
import org.folio.search.domain.dto.CallNumberBrowseResult;
import org.folio.search.domain.dto.CallNumberType;
import org.folio.search.domain.dto.ClassificationNumberBrowseItem;
import org.folio.search.domain.dto.ClassificationNumberBrowseResult;
import org.folio.search.domain.dto.ContributorBrowseResult;
import org.folio.search.domain.dto.LegacyCallNumberBrowseResult;
import org.folio.search.domain.dto.SubjectBrowseResult;
import org.folio.search.exception.RequestValidationException;
import org.folio.search.model.BrowseResult;
Expand All @@ -32,6 +36,7 @@
import org.folio.search.service.browse.CallNumberBrowseService;
import org.folio.search.service.browse.ClassificationBrowseService;
import org.folio.search.service.browse.ContributorBrowseService;
import org.folio.search.service.browse.LegacyCallNumberBrowseService;
import org.folio.search.service.browse.SubjectBrowseService;
import org.folio.search.service.consortium.TenantProvider;
import org.springframework.http.ResponseEntity;
Expand All @@ -47,9 +52,10 @@ public class BrowseController implements BrowseApi {

private final SubjectBrowseService subjectBrowseService;
private final AuthorityBrowseService authorityBrowseService;
private final CallNumberBrowseService callNumberBrowseService;
private final LegacyCallNumberBrowseService legacyCallNumberBrowseService;
private final ContributorBrowseService contributorBrowseService;
private final ClassificationBrowseService classificationBrowseService;
private final CallNumberBrowseService callNumberBrowseService;
private final TenantProvider tenantProvider;

@Override
Expand All @@ -67,20 +73,36 @@ public ResponseEntity<AuthorityBrowseResult> browseAuthorities(String query, Str
}

@Override
public ResponseEntity<CallNumberBrowseResult> browseInstancesByCallNumber(String query, String tenant,
Integer limit, Boolean expandAll,
Boolean highlightMatch,
Integer precedingRecordsCount,
CallNumberType callNumberType) {
public ResponseEntity<CallNumberBrowseResult> browseInstancesByCallNumber(BrowseOptionType browseOptionId,
String query, String tenant,
Integer limit, Boolean highlightMatch,
Integer precedingRecordsCount) {
var browseRequest = getBrowseRequestBuilder(query, tenant, limit, false, highlightMatch, precedingRecordsCount)
.resource(INSTANCE_CALL_NUMBER)
.browseOptionType(browseOptionId)
.targetField(CALL_NUMBER_BROWSING_FIELD)
.build();

var browseResult = callNumberBrowseService.browse(browseRequest);
return ResponseEntity.ok(toCallNumberBrowseResultDto(browseResult));
}

@Override
public ResponseEntity<LegacyCallNumberBrowseResult> browseInstancesByCallNumberLegacy(String query, String tenant,
Integer limit,
Boolean expandAll,
Boolean highlightMatch,
Integer precedingRecordsCount,
CallNumberType callNumberType) {
var browseRequest = getBrowseRequestBuilder(query, tenant, limit, expandAll, highlightMatch, precedingRecordsCount)
.resource(INSTANCE)
.targetField(SHELVING_ORDER_BROWSING_FIELD)
.subField(callNumberType == null ? CALL_NUMBER_BROWSING_FIELD : TYPED_CALL_NUMBER_BROWSING_FIELD)
.subField(callNumberType == null ? LEGACY_CALL_NUMBER_BROWSING_FIELD : TYPED_CALL_NUMBER_BROWSING_FIELD)
.refinedCondition(callNumberType != null ? callNumberType.getValue() : null)
.build();

var instanceByCallNumber = callNumberBrowseService.browse(browseRequest);
return ResponseEntity.ok(new CallNumberBrowseResult()
var instanceByCallNumber = legacyCallNumberBrowseService.browse(browseRequest);
return ResponseEntity.ok(new LegacyCallNumberBrowseResult()
.items(instanceByCallNumber.getRecords())
.totalRecords(instanceByCallNumber.getTotalRecords())
.prev(instanceByCallNumber.getPrev())
Expand All @@ -99,7 +121,7 @@ public ResponseEntity<ClassificationNumberBrowseResult> browseInstancesByClassif
.build();

var browseResult = classificationBrowseService.browse(browseRequest);
return ResponseEntity.ok(toBrowseResultDto(browseResult));
return ResponseEntity.ok(toClassificationBrowseResultDto(browseResult));
}

@Override
Expand Down Expand Up @@ -132,14 +154,23 @@ public ResponseEntity<SubjectBrowseResult> browseInstancesBySubject(String query
.next(browseResult.getNext()));
}

private ClassificationNumberBrowseResult toBrowseResultDto(BrowseResult<ClassificationNumberBrowseItem> result) {
private ClassificationNumberBrowseResult toClassificationBrowseResultDto(
BrowseResult<ClassificationNumberBrowseItem> result) {
return new ClassificationNumberBrowseResult()
.totalRecords(result.getTotalRecords())
.items(result.getRecords())
.prev(result.getPrev())
.next(result.getNext());
}

private CallNumberBrowseResult toCallNumberBrowseResultDto(BrowseResult<CallNumberBrowseItem> result) {
return new CallNumberBrowseResult()
.totalRecords(result.getTotalRecords())
.items(result.getRecords())
.prev(result.getPrev())
.next(result.getNext());
}

private BrowseRequestBuilder getBrowseRequestBuilder(String query, String tenant, Integer limit,
Boolean expandAll, Boolean highlightMatch,
Integer precedingRecordsCount) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public class FacetsController implements FacetsApi {
RecordType.AUTHORITIES, ResourceType.AUTHORITY,
RecordType.CONTRIBUTORS, ResourceType.INSTANCE_CONTRIBUTOR,
RecordType.SUBJECTS, ResourceType.INSTANCE_SUBJECT,
RecordType.CLASSIFICATIONS, ResourceType.INSTANCE_CLASSIFICATION
RecordType.CLASSIFICATIONS, ResourceType.INSTANCE_CLASSIFICATION,
RecordType.CALL_NUMBERS, ResourceType.INSTANCE_CALL_NUMBER
);

private final FacetService facetService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,6 @@ public boolean isAnchorIncluded(boolean isForward) {
return isForward ? this.succeedingQuery.includeLower() : this.precedingQuery.includeUpper();
}

/**
* Checks if anchor is included in the range query or not.
*
* @return {@code true} if anchor is included, {@code false} - otherwise
*/
public boolean isAnchorIncluded() {
return isAnchorIncluded(true);
}

/**
* Returns limit for browsing.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ private void logBrowseRequest(BrowseRequest request, String functionName) {
}

private void logMultiSearchRequest(BrowseRequest request, int size) {
log.info("browseAround:: Attempting to multi-search request [tenant: {}, searchSource.size: {}]",
log.debug("browseAround:: Attempting to multi-search request [tenant: {}, searchSource.size: {}]",
request.getTenantId(), size);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package org.folio.search.service.browse;

import static java.util.Locale.ROOT;
import static org.folio.search.utils.SearchUtils.BROWSE_FIELDS_MAP;
import static org.folio.search.utils.SearchUtils.DEFAULT_SHELVING_ORDER_BROWSING_FIELD;
import static org.opensearch.index.query.QueryBuilders.boolQuery;
import static org.opensearch.index.query.QueryBuilders.matchAllQuery;
import static org.opensearch.index.query.QueryBuilders.termQuery;
import static org.opensearch.search.builder.SearchSourceBuilder.searchSource;
import static org.opensearch.search.sort.SortBuilders.fieldSort;
import static org.opensearch.search.sort.SortOrder.ASC;
import static org.opensearch.search.sort.SortOrder.DESC;

import java.util.Set;
import java.util.function.Function;
import lombok.extern.log4j.Log4j2;
import org.folio.search.domain.dto.BrowseConfig;
import org.folio.search.domain.dto.BrowseType;
import org.folio.search.model.index.InstanceSubResource;
import org.folio.search.model.service.BrowseContext;
import org.folio.search.model.service.BrowseRequest;
import org.folio.search.service.consortium.BrowseConfigServiceDecorator;
import org.folio.search.service.consortium.ConsortiumSearchHelper;
import org.folio.search.utils.ShelvingOrderCalculationHelper;
import org.opensearch.index.query.QueryBuilder;
import org.opensearch.index.query.TermQueryBuilder;
import org.opensearch.search.builder.SearchSourceBuilder;

@Log4j2
public abstract class AbstractShelvingOrderBrowseServiceBySearchAfter<T, R>
extends AbstractBrowseServiceBySearchAfter<T, R> {

private final ConsortiumSearchHelper consortiumSearchHelper;
private final BrowseConfigServiceDecorator configService;

protected AbstractShelvingOrderBrowseServiceBySearchAfter(ConsortiumSearchHelper consortiumSearchHelper,
BrowseConfigServiceDecorator configService) {
this.consortiumSearchHelper = consortiumSearchHelper;
this.configService = configService;
}

@Override
protected SearchSourceBuilder getAnchorSearchQuery(BrowseRequest req, BrowseContext ctx) {
log.debug("getAnchorSearchQuery:: by [request: {}]", req);
var config = configService.getConfig(getBrowseType(), req.getBrowseOptionType());

var browseField = getBrowseField(config);
var termQueryBuilder = getQuery(ctx, config, termQuery(req.getTargetField(), ctx.getAnchor()));
var query = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(ctx, termQueryBuilder, req.getResource());
var sortOrder = ctx.isBrowsingForward() ? ASC : DESC;
return searchSource().query(query)
.sort(fieldSort(browseField).order(sortOrder))
.sort(fieldSort(req.getTargetField()).order(sortOrder))
.size(ctx.getLimit(ctx.isBrowsingForward()))
.from(0);
}

@Override
protected SearchSourceBuilder getSearchQuery(BrowseRequest req, BrowseContext ctx, boolean isBrowsingForward) {
log.debug("getSearchQuery:: by [request: {}, isBrowsingForward: {}]", req, isBrowsingForward);
var config = configService.getConfig(getBrowseType(), req.getBrowseOptionType());

var browseField = getBrowseField(config);
var normalizedAnchor = ShelvingOrderCalculationHelper.calculate(ctx.getAnchor(), config.getShelvingAlgorithm());
var query = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(ctx, getQuery(ctx, config, null),
req.getResource());

var sortOrder = isBrowsingForward ? ASC : DESC;
return searchSource().query(query)
.searchAfter(new Object[] {normalizedAnchor.toLowerCase(ROOT), ctx.getAnchor().toLowerCase(ROOT)})
.sort(fieldSort(browseField).order(sortOrder))
.sort(fieldSort(req.getTargetField()).order(sortOrder))
.size(ctx.getLimit(isBrowsingForward) + 1)
.from(0);
}

/**
* Returns the type of browsing supported by this service.
*
* @return the {@link BrowseType} for this service
*/
protected abstract BrowseType getBrowseType();

/**
* Returns the field name used for type identification in the browse service.
*
* @return the type ID field name
*/
protected abstract String getTypeIdField();

protected Integer getTotalRecords(BrowseContext ctx, R resource, Function<R, Set<InstanceSubResource>> func) {
return consortiumSearchHelper.filterSubResourcesForConsortium(ctx, resource, func)
.stream()
.map(InstanceSubResource::getCount)
.reduce(0, Integer::sum);
}

private QueryBuilder getQuery(BrowseContext ctx, BrowseConfig config, TermQueryBuilder anchorQuery) {
var typeIds = config.getTypeIds();
if (config.getTypeIds().isEmpty() && ctx.getFilters().isEmpty()) {
if (anchorQuery != null) {
return anchorQuery;
}
return matchAllQuery();
} else {
var boolQueryMain = boolQuery();
if (!config.getTypeIds().isEmpty()) {
var boolQuery = boolQuery();
for (var typeId : typeIds) {
boolQuery.should(termQuery(getTypeIdField(), typeId.toString()));
}
boolQueryMain.must(boolQuery);
}
if (!ctx.getFilters().isEmpty()) {
ctx.getFilters().forEach(boolQueryMain::filter);
}
if (anchorQuery != null) {
boolQueryMain.must(anchorQuery);
}
return boolQueryMain;
}
}

private static String getBrowseField(BrowseConfig config) {
return BROWSE_FIELDS_MAP.getOrDefault(config.getShelvingAlgorithm(), DEFAULT_SHELVING_ORDER_BROWSING_FIELD);
}

}
Loading
Loading