Skip to content

Commit

Permalink
feat(cn-browse): Allow use of all suitable call numbers (types) for s…
Browse files Browse the repository at this point in the history
…helving order parsing on search/browse

- Make EffectiveShelvingOrderTermProcessor return list or parsed shelfKey
- Change Term building for call numbers
- Add loop for browse for all parsed shelfKeys

Closes: MSEARCH-569
  • Loading branch information
ArtHov committed Aug 31, 2023
1 parent db842bb commit a5a8e73
Show file tree
Hide file tree
Showing 14 changed files with 199 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.folio.search.cql;

import static org.folio.search.service.browse.CallNumberBrowseService.CALL_NUMBER_FIELD;
import static org.folio.search.utils.SearchQueryUtils.isBoolQuery;
import static org.folio.search.utils.SearchQueryUtils.isDisjunctionFilterQuery;
import static org.folio.search.utils.SearchQueryUtils.isFilterQuery;
Expand Down Expand Up @@ -108,6 +109,9 @@ private QueryBuilder convertToQuery(CQLNode node, String resource) {
cqlNode = cqlSortNode.getSubtree();
}
if (cqlNode instanceof CQLTermNode cqlTermNode) {
if (CALL_NUMBER_FIELD.equals(cqlTermNode.getIndex())) {
cqlTermQueryConverter.getCallNumberQuery(cqlTermNode, resource, CALL_NUMBER_FIELD);
}
return cqlTermQueryConverter.getQuery(cqlTermNode, resource);
}
if (cqlNode instanceof CQLBooleanNode cqlBooleanNode) {
Expand Down
59 changes: 50 additions & 9 deletions src/main/java/org/folio/search/cql/CqlTermQueryConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static java.util.stream.Collectors.joining;
import static org.apache.commons.lang3.StringUtils.lowerCase;
import static org.folio.search.utils.SearchUtils.ASTERISKS_SIGN;
import static org.opensearch.index.query.QueryBuilders.boolQuery;
import static org.opensearch.index.query.QueryBuilders.matchAllQuery;

import java.time.format.DateTimeFormatter;
Expand All @@ -21,6 +22,7 @@
import org.folio.search.model.metadata.PlainFieldDescription;
import org.folio.search.service.metadata.LocalSearchFieldProvider;
import org.folio.search.service.metadata.SearchFieldProvider;
import org.opensearch.index.query.BoolQueryBuilder;
import org.opensearch.index.query.QueryBuilder;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -81,20 +83,13 @@ public QueryBuilder getQuery(CQLTermNode termNode, String resource) {
var fieldName = fieldsList.size() == 1 ? fieldsList.get(0) : fieldIndex;
var optionalPlainFieldByPath = searchFieldProvider.getPlainFieldByPath(resource, fieldName);
var searchTerm = getSearchTerm(termNode.getTerm(), optionalPlainFieldByPath);
var comparator = isWildcardQuery(searchTerm) ? WILDCARD_OPERATOR : lowerCase(termNode.getRelation().getBase());

var termQueryBuilder = termQueryBuilders.get(comparator);
if (termQueryBuilder == null) {
throw new UnsupportedOperationException(String.format(
"Failed to parse CQL query. Comparator '%s' is not supported.", comparator));
}

var termQueryBuilder = getTermQueryBuilder(termNode, searchTerm);
if (CollectionUtils.isNotEmpty(fieldsList)) {
return termQueryBuilder.getQuery(searchTerm, resource, fieldsList.toArray(String[]::new));
}

var plainFieldByPath = optionalPlainFieldByPath.orElseThrow(() -> new RequestValidationException(
"Invalid search field provided in the CQL query", "field", fieldName));
var plainFieldByPath = getPlainFieldByPath(optionalPlainFieldByPath, fieldName);
var index = plainFieldByPath.getIndex();
validateIndexFormat(index, termNode);

Expand All @@ -107,6 +102,46 @@ public QueryBuilder getQuery(CQLTermNode termNode, String resource) {
: termQueryBuilder.getTermLevelQuery(searchTerm, fieldName, resource, index);
}

/**
* Provides Elasticsearch {@link BoolQueryBuilder} object for the given termNode and resource name.
*
* @param termNode - CQL term node as {@link CQLTermNode} object
* @param resource - resource name as {@link String} value
* @param fieldName - resource name as {@link String} fieldName value
* @return created Elasticsearch {@link BoolQueryBuilder} object
*/
public BoolQueryBuilder getCallNumberQuery(CQLTermNode termNode, String resource, String fieldName) {

var optionalPlainFieldByPath = searchFieldProvider.getPlainFieldByPath(resource, fieldName);
List<String> searchTerms = (List<String>) getSearchTerm(termNode.getTerm(), optionalPlainFieldByPath);

var plainFieldByPath = getPlainFieldByPath(optionalPlainFieldByPath, fieldName);
var index = plainFieldByPath.getIndex();
validateIndexFormat(index, termNode);

var boolQuery = boolQuery();
var conditions = boolQuery.should();

for (String searchTerm : searchTerms) {
var termQueryBuilder = getTermQueryBuilder(termNode, searchTerm);

conditions.add(termQueryBuilder.getTermLevelQuery(searchTerm, fieldName, resource, index));
}
return boolQuery;
}


private TermQueryBuilder getTermQueryBuilder(CQLTermNode termNode, Object searchTerm) {
var comparator = isWildcardQuery(searchTerm) ? WILDCARD_OPERATOR : lowerCase(termNode.getRelation().getBase());
var termQueryBuilder = termQueryBuilders.get(comparator);

if (termQueryBuilder == null) {
throw new UnsupportedOperationException(String.format(
"Failed to parse CQL query. Comparator '%s' is not supported.", comparator));
}
return termQueryBuilder;
}

private Object getSearchTerm(String term, Optional<PlainFieldDescription> plainFieldDescription) {
return plainFieldDescription
.map(PlainFieldDescription::getSearchTermProcessor)
Expand All @@ -115,6 +150,12 @@ private Object getSearchTerm(String term, Optional<PlainFieldDescription> plainF
.orElse(term);
}

private PlainFieldDescription getPlainFieldByPath(Optional<PlainFieldDescription> plainFieldDescription,
String fieldName) {
return plainFieldDescription.orElseThrow(() -> new RequestValidationException(
"Invalid search field provided in the CQL query", "field", fieldName));
}

private static boolean isWildcardQuery(Object query) {
return query instanceof String string && string.contains(ASTERISKS_SIGN);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static org.folio.search.utils.CallNumberUtils.normalizeEffectiveShelvingOrder;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.marc4j.callnum.CallNumber;
import org.marc4j.callnum.DeweyCallNumber;
Expand All @@ -13,18 +15,28 @@
public class EffectiveShelvingOrderTermProcessor implements SearchTermProcessor {

@Override
public String getSearchTerm(String inputTerm) {
return getValidShelfKey(new SuDocCallNumber(inputTerm))
.or(() -> getValidShelfKey(new NlmCallNumber(inputTerm)))
.or(() -> getValidShelfKey(new LCCallNumber(inputTerm)))
.or(() -> getValidShelfKey(new DeweyCallNumber(inputTerm)))
.orElse(normalizeEffectiveShelvingOrder(inputTerm))
.trim();
public List<String> getSearchTerm(String inputTerm) {

var searchTerms = new ArrayList<String>();

getValidShelfKey(searchTerms, new SuDocCallNumber(inputTerm));
getValidShelfKey(searchTerms, new NlmCallNumber(inputTerm));
getValidShelfKey(searchTerms, new LCCallNumber(inputTerm));
getValidShelfKey(searchTerms, new DeweyCallNumber(inputTerm));

if (searchTerms.isEmpty()) {
searchTerms.add(normalizeEffectiveShelvingOrder(inputTerm).trim());
}

return searchTerms;
}

private static Optional<String> getValidShelfKey(CallNumber value) {
return Optional.of(value)
private static void getValidShelfKey(List<String> searchTerms, CallNumber value) {
var term = Optional.of(value)
.filter(CallNumber::isValid)
.map(CallNumber::getShelfKey);
.map(CallNumber::getShelfKey)
.map(String::trim);

term.ifPresent(searchTerms::add);
}
}
18 changes: 18 additions & 0 deletions src/main/java/org/folio/search/model/service/BrowseContext.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.folio.search.model.service;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import lombok.Builder;
Expand Down Expand Up @@ -68,4 +69,21 @@ public boolean isAnchorIncluded() {
public int getLimit(boolean isForward) {
return isForward ? this.succeedingLimit : this.precedingLimit;
}

/**
* Checks if multiple anchors are present.
*
* @return {@code true} if multiple anchors are present, {@code false} - otherwise
*/
public boolean isMultiAnchor() {
return Arrays.stream(anchor.split(",")).count() > 1;
}
/**
* Checks if multiple anchors are present.
*
* @return {@code true} if multiple anchors are present, {@code false} - otherwise
*/
public List<String> getAnchorsList() {
return Arrays.asList(anchor.split(","));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,18 @@ private static boolean isBoolQueryWithFilters(BoolQueryBuilder boolQuery) {
}

private static String getAnchor(RangeQueryBuilder rangeQuery) {
return rangeQuery.from() != null ? (String) rangeQuery.from() : (String) rangeQuery.to();
var anchor = rangeQuery.from() != null ? rangeQuery.from() : rangeQuery.to();
if (anchor instanceof List<?>) {
return getMultipleAnchors((List) anchor);
}
return (String) anchor;
}

private static String getMultipleAnchors(List<String> anchors) {
if (anchors.size() == 1) {
return anchors.get(0);
}
return String.join(",", anchors);
}

private static String validateAndGetAnchorForBrowsingAround(BrowseRequest request, List<QueryBuilder> shouldClauses) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
import static org.folio.search.utils.CollectionUtils.mergeSafelyToList;

import java.util.ArrayList;
Expand All @@ -17,6 +18,8 @@
import org.folio.search.model.service.BrowseContext;
import org.folio.search.model.service.BrowseRequest;
import org.folio.search.repository.SearchRepository;
import org.opensearch.action.search.MultiSearchResponse;
import org.opensearch.action.search.SearchResponse;
import org.opensearch.search.builder.SearchSourceBuilder;
import org.springframework.stereotype.Service;

Expand All @@ -25,6 +28,7 @@
@RequiredArgsConstructor
public class CallNumberBrowseService extends AbstractBrowseService<CallNumberBrowseItem> {

public static final String CALL_NUMBER_FIELD = "callNumber";
private static final int ADDITIONAL_REQUEST_SIZE = 100;
private static final int ADDITIONAL_REQUEST_SIZE_MAX = 500;
private final SearchRepository searchRepository;
Expand All @@ -37,8 +41,21 @@ protected BrowseResult<CallNumberBrowseItem> browseInOneDirection(BrowseRequest
log.debug("browseInOneDirection:: by: [request: {}]", request);

var isBrowsingForward = context.isBrowsingForward();
var searchSource = callNumberBrowseQueryProvider.get(request, context, isBrowsingForward);
var searchResponse = searchRepository.search(request, searchSource);
SearchResponse searchResponse = null;

if (context.isMultiAnchor()) {
var anchors = context.getAnchorsList();
for (String anchor : anchors){
context = buildBrowseContext(context, anchor);
var searchSource = callNumberBrowseQueryProvider.get(request, context, isBrowsingForward);
searchResponse = searchRepository.search(request, searchSource);
if (isAnchorPresent(searchResponse, context))
break;
}
} else {
var searchSource = callNumberBrowseQueryProvider.get(request, context, isBrowsingForward);
searchResponse = searchRepository.search(request, searchSource);
}
var browseResult = callNumberBrowseResultConverter.convert(searchResponse, context, isBrowsingForward);
var records = browseResult.getRecords();
return new BrowseResult<CallNumberBrowseItem>()
Expand All @@ -51,12 +68,23 @@ protected BrowseResult<CallNumberBrowseItem> browseInOneDirection(BrowseRequest
@Override
protected BrowseResult<CallNumberBrowseItem> browseAround(BrowseRequest request, BrowseContext context) {
log.debug("browseAround:: by: [request: {}]", request);
MultiSearchResponse.Item[] responses = {};
var precedingQuery = callNumberBrowseQueryProvider.get(request, context,false);

if (context.isMultiAnchor()) {
var anchors = context.getAnchorsList();
for (String anchor : anchors){
context = buildBrowseContext(context, anchor);
precedingQuery = callNumberBrowseQueryProvider.get(request, context, false);

responses = getBrowseAround(request, context, precedingQuery);
if (isAnchorPresent(responses[0].getResponse(), context))
break;
}
} else {
responses = getBrowseAround(request, context, precedingQuery);
}

var precedingQuery = callNumberBrowseQueryProvider.get(request, context, false);
var succeedingQuery = callNumberBrowseQueryProvider.get(request, context, true);
var multiSearchResponse = searchRepository.msearch(request, List.of(precedingQuery, succeedingQuery));

var responses = multiSearchResponse.getResponses();
var precedingResult = callNumberBrowseResultConverter.convert(responses[0].getResponse(), context, false);
var succeedingResult = callNumberBrowseResultConverter.convert(responses[1].getResponse(), context, true);
var backwardSucceedingResult = callNumberBrowseResultConverter.convert(responses[1].getResponse(), context, false);
Expand Down Expand Up @@ -95,6 +123,14 @@ protected String getValueForBrowsing(CallNumberBrowseItem browseItem) {
return browseItem.getShelfKey();
}

private MultiSearchResponse.Item[] getBrowseAround(BrowseRequest request, BrowseContext context,
SearchSourceBuilder precedingQuery) {
var succeedingQuery = callNumberBrowseQueryProvider.get(request, context, true);
var multiSearchResponse = searchRepository.msearch(request, List.of(precedingQuery, succeedingQuery));

return multiSearchResponse.getResponses();
}

private List<CallNumberBrowseItem> additionalPrecedingRequests(BrowseRequest request,
BrowseContext context,
SearchSourceBuilder precedingQuery) {
Expand Down Expand Up @@ -122,6 +158,24 @@ private List<CallNumberBrowseItem> additionalPrecedingRequests(BrowseRequest req
return additionalPrecedingRecords;
}

private boolean isAnchorPresent(SearchResponse searchResponse, BrowseContext context) {
var items = callNumberBrowseResultConverter.
convert(searchResponse, context, true).getRecords();

return isNotEmpty(items) && StringUtils.equals(items.get(0).getShelfKey(), context.getAnchor());
}

private BrowseContext buildBrowseContext(BrowseContext context, String anchor) {
return BrowseContext.builder()
.precedingQuery(context.getPrecedingQuery())
.succeedingQuery(context.getSucceedingQuery())
.filters(context.getFilters())
.anchor(anchor)
.precedingLimit(context.getPrecedingLimit())
.succeedingLimit(context.getSucceedingLimit())
.build();
}

private static void highlightMatchingCallNumber(BrowseContext ctx,
String callNumber,
BrowseResult<CallNumberBrowseItem> result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ private static Instance instance(List<Object> data) {
.id(randomId())
.discoverySuppress(false)
.effectiveCallNumberComponents(new ItemEffectiveCallNumberComponents().callNumber(callNumber))
.effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber)))
.effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber).get(0)))
.toList();

return new Instance()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,24 @@ void browseByCallNumber_browsingVeryFirstCallNumberWithNoException() {
)));
}

@Test
void browseByCallNumber_browsingAroundWhenMultipleAnchors() {
var request = get(instanceCallNumberBrowsePath())
.param("query", prepareQuery("callNumber < {value} or callNumber >= {value}", "\"J29.2\""))
.param("limit", "5")
.param("expandAll", "true")
.param("precedingRecordsCount", "4");
var actual = parseResponse(doGet(request), CallNumberBrowseResult.class);
assertThat(actual).isEqualTo(new CallNumberBrowseResult()
.totalRecords(41).prev("GA 216 D64 541548A").next("J 229 12").items(List.of(
cnBrowseItem(instance("instance #39"), "GA 16 D64 41548A"),
cnBrowseItem(instance("instance #30"), "GA 16 G32 41557 V1"),
cnBrowseItem(instance("instance #30"), "GA 16 G32 41557 V2"),
cnBrowseItem(instance("instance #30"), "GA 16 G32 41557 V3"),
cnBrowseItem(instance("instance #47"), "J29.2", true)
)));
}

@Test
void browseByCallNumber_browsingAroundWithoutHighlightMatch() {
var request = get(instanceCallNumberBrowsePath())
Expand Down Expand Up @@ -373,7 +391,7 @@ private static Instance instance(List<Object> data) {
.id(randomId())
.discoverySuppress(false)
.effectiveCallNumberComponents(new ItemEffectiveCallNumberComponents().callNumber(callNumber))
.effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber)))
.effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber).get(0)))
.toList();

return new Instance()
Expand Down Expand Up @@ -439,6 +457,7 @@ private static List<List<Object>> callNumberBrowseInstanceData() {
List.of("instance #43", List.of("FA 42010 3546 256")),
List.of("instance #44", List.of("CE 16 B6713 X 41993")),
List.of("instance #45", List.of("CE 16 B6724 41993")),
List.of("instance #47", List.of("J29.2")),
List.of("instance #46", List.of("F PR1866.S63 V.1 C.1"))
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ private static Instance instance(List<Object> data) {
.id(randomId())
.discoverySuppress(false)
.effectiveCallNumberComponents(new ItemEffectiveCallNumberComponents().callNumber(callNumber))
.effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber)))
.effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber).get(0)))
.toList();

return new Instance()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ private static Instance instance(List<Object> data) {
.discoverySuppress(false)
.effectiveCallNumberComponents(new ItemEffectiveCallNumberComponents()
.callNumber(callNumber).typeId(data.get(2).toString()))
.effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber)))
.effectiveShelvingOrder(getShelfKeyFromCallNumber(callNumber).get(0)))
.toList();

return new Instance()
Expand Down
Loading

0 comments on commit a5a8e73

Please sign in to comment.