diff --git a/NEWS.md b/NEWS.md
index 73470dcc1..983e35791 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -16,6 +16,7 @@
* Prepare and populate index for classification browse ([MSEARCH-668](https://issues.folio.org/browse/MSEARCH-668))
* Instance search: Add query search option that search instances by normalized LCCN ([MSEARCH-661](https://issues.folio.org/browse/MSEARCH-661))
* Implement browse config management endpoints ([MSEARCH-674](https://issues.folio.org/browse/MSEARCH-674))
+* Implement endpoint to browse by classifications ([MSEARCH-665](https://issues.folio.org/browse/MSEARCH-665))
* Authority search: Modify query search option to search authorities by normalized LCCN ([MSEARCH-663](https://issues.folio.org/browse/MSEARCH-663))
### Bug fixes
diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json
index 42bc1876b..64978bb0f 100644
--- a/descriptors/ModuleDescriptor-template.json
+++ b/descriptors/ModuleDescriptor-template.json
@@ -183,7 +183,7 @@
},
{
"id": "browse",
- "version": "1.2",
+ "version": "1.3",
"handlers": [
{
"methods": [
@@ -197,6 +197,18 @@
"user-tenants.collection.get"
]
},
+ {
+ "methods": [
+ "GET"
+ ],
+ "pathPattern": "/browse/classification-numbers/{browseOptionId}/instances",
+ "permissionsRequired": [
+ "browse.classification-numbers.instances.collection.get"
+ ],
+ "modulePermissions": [
+ "user-tenants.collection.get"
+ ]
+ },
{
"methods": [
"GET"
@@ -469,6 +481,11 @@
"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",
+ "description": "Browse by classification number"
+ },
{
"permissionName": "browse.subjects.instances.collection.get",
"displayName": "Browse - provides collections of browse items for instance by subjects",
diff --git a/docker/.env b/docker/.env
index d8111b7bb..82039bd40 100644
--- a/docker/.env
+++ b/docker/.env
@@ -2,3 +2,6 @@ COMPOSE_PROJECT_NAME=folio-mod-search
DEBUG_PORT=5010
OKAPI_URL=http://api-mock:8080
APP_PORT=8081
+PGADMIN_DEFAULT_EMAIL=user@domain.com
+PGADMIN_DEFAULT_PASSWORD=admin
+PGADMIN_PORT=5050
\ No newline at end of file
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 08a6429a4..d18d4aec1 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -23,10 +23,14 @@ services:
JAVA_OPTIONS: -Xmx120m -Xms120m
JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:${DEBUG_PORT}
okapi.url: ${OKAPI_URL}
+ KAFKA_CONTRIBUTORS_TOPIC_PARTITIONS: 1
+ KAFKA_SUBJECTS_TOPIC_PARTITIONS: 1
+ KAFKA_CONSORTIUM_INSTANCE_TOPIC_PARTITIONS: 1
+ BROWSE_CLASSIFICATIONS_ENABLED: true
api-mock:
+ container_name: api-mock-mod-search
image: wiremock/wiremock:2.32.0
- container_name: api-mock
networks:
- mod-search-local
command:
@@ -37,7 +41,7 @@ services:
- ../src/test/resources/mappings:/home/wiremock/mappings
opensearch:
- container_name: opensearch
+ container_name: opensearch-mod-search
image: dev.folio/opensearch:1.3.2
build:
context: opensearch
@@ -56,7 +60,7 @@ services:
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
opensearch-dashboards:
- container_name: opensearch-dashboards
+ container_name: opensearch-dashboards-mod-search
image: dev.folio/opensearch-dashboards:1.3.2
build:
context: dashboards
@@ -71,7 +75,7 @@ services:
- opensearch
zookeeper:
- container_name: zookeeper
+ container_name: zookeeper-mod-search
image: wurstmeister/zookeeper:3.4.6
networks:
- mod-search-local
@@ -79,7 +83,7 @@ services:
- "2181:2181"
kafka:
- container_name: kafka
+ container_name: kafka-mod-search
image: wurstmeister/kafka:2.13-2.8.1
networks:
- mod-search-local
@@ -99,7 +103,7 @@ services:
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false"
kafka-ui:
- container_name: kafka-ui-quick-marc
+ container_name: kafka-ui-mod-search
image: provectuslabs/kafka-ui:latest
ports:
- "8080:8080"
@@ -115,7 +119,7 @@ services:
- mod-search-local
postgres:
- container_name: postgres
+ container_name: postgres-mod-search
image: postgres:12-alpine
networks:
- mod-search-local
@@ -126,6 +130,20 @@ services:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: okapi_modules
+ pgadmin:
+ container_name: pgadmin-mod-search
+ image: dpage/pgadmin4:6.7
+ networks:
+ - mod-search-local
+ ports:
+ - ${PGADMIN_PORT}:80
+ volumes:
+ - "pgadmin-data:/var/lib/pgadmin"
+ environment:
+ PGADMIN_CONFIG_SERVER_MODE: "False"
+ PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
+ PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
+
networks:
mod-search-local:
driver: bridge
@@ -133,3 +151,4 @@ networks:
volumes:
es-data: { }
db-data: { }
+ pgadmin-data: { }
diff --git a/pom.xml b/pom.xml
index 0ac8fcad4..02e386c60 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,8 +37,10 @@
8.0.0
3.1.0
1.6.0
- 35.1.2
+
+ 35.2.0
2.12.0
+
1.5.5.Final
diff --git a/src/main/java/org/folio/search/configuration/properties/SearchConfigurationProperties.java b/src/main/java/org/folio/search/configuration/properties/SearchConfigurationProperties.java
index 6b06996c3..046127711 100644
--- a/src/main/java/org/folio/search/configuration/properties/SearchConfigurationProperties.java
+++ b/src/main/java/org/folio/search/configuration/properties/SearchConfigurationProperties.java
@@ -11,7 +11,6 @@
import java.util.Set;
import lombok.Data;
import org.folio.search.domain.dto.TenantConfiguredFeature;
-import org.folio.search.model.types.ClassificationType;
import org.folio.search.model.types.IndexingDataFormat;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@@ -47,8 +46,6 @@ public class SearchConfigurationProperties {
*/
private Map searchFeatures = emptyMap();
- private Map browseClassificationTypes = emptyMap();
-
/**
* Indexing settings for different resources.
*/
diff --git a/src/main/java/org/folio/search/controller/BrowseController.java b/src/main/java/org/folio/search/controller/BrowseController.java
index 5e0ad0661..44796ccd5 100644
--- a/src/main/java/org/folio/search/controller/BrowseController.java
+++ b/src/main/java/org/folio/search/controller/BrowseController.java
@@ -4,8 +4,10 @@
import static org.folio.search.utils.SearchUtils.AUTHORITY_BROWSING_FIELD;
import static org.folio.search.utils.SearchUtils.AUTHORITY_RESOURCE;
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.CONTRIBUTOR_RESOURCE;
+import static org.folio.search.utils.SearchUtils.INSTANCE_CLASSIFICATION_RESOURCE;
import static org.folio.search.utils.SearchUtils.INSTANCE_RESOURCE;
import static org.folio.search.utils.SearchUtils.INSTANCE_SUBJECT_RESOURCE;
import static org.folio.search.utils.SearchUtils.SHELVING_ORDER_BROWSING_FIELD;
@@ -14,16 +16,21 @@
import lombok.RequiredArgsConstructor;
import org.folio.search.domain.dto.AuthorityBrowseResult;
+import org.folio.search.domain.dto.BrowseOptionType;
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.SubjectBrowseResult;
import org.folio.search.exception.RequestValidationException;
+import org.folio.search.model.BrowseResult;
import org.folio.search.model.service.BrowseRequest;
import org.folio.search.model.service.BrowseRequest.BrowseRequestBuilder;
import org.folio.search.rest.resource.BrowseApi;
import org.folio.search.service.browse.AuthorityBrowseService;
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.SubjectBrowseService;
import org.folio.search.service.consortium.TenantProvider;
@@ -42,6 +49,7 @@ public class BrowseController implements BrowseApi {
private final AuthorityBrowseService authorityBrowseService;
private final CallNumberBrowseService callNumberBrowseService;
private final ContributorBrowseService contributorBrowseService;
+ private final ClassificationBrowseService classificationBrowseService;
private final TenantProvider tenantProvider;
@Override
@@ -80,6 +88,21 @@ public ResponseEntity browseInstancesByCallNumber(String
.next(instanceByCallNumber.getNext()));
}
+ @Override
+ public ResponseEntity browseInstancesByClassificationNumber(
+ BrowseOptionType browseOptionId, String query, String tenant, Integer limit, Boolean expandAll,
+ Boolean highlightMatch, Integer precedingRecordsCount) {
+
+ var browseRequest = getBrowseRequestBuilder(query, tenant, limit, expandAll, highlightMatch, precedingRecordsCount)
+ .resource(INSTANCE_CLASSIFICATION_RESOURCE)
+ .browseOptionType(browseOptionId)
+ .targetField(CLASSIFICATION_NUMBER_BROWSING_FIELD)
+ .build();
+
+ var browseResult = classificationBrowseService.browse(browseRequest);
+ return ResponseEntity.ok(toBrowseResultDto(browseResult));
+ }
+
@Override
public ResponseEntity browseInstancesByContributor(String query, String tenant,
Integer limit, Boolean highlightMatch,
@@ -110,9 +133,17 @@ public ResponseEntity browseInstancesBySubject(String query
.next(browseResult.getNext()));
}
+ private ClassificationNumberBrowseResult toBrowseResultDto(BrowseResult result) {
+ return new ClassificationNumberBrowseResult()
+ .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) {
+ Boolean expandAll, Boolean highlightMatch,
+ Integer precedingRecordsCount) {
if (precedingRecordsCount != null && precedingRecordsCount >= limit) {
throw new RequestValidationException("Preceding records count must be less than request limit",
"precedingRecordsCount", String.valueOf(precedingRecordsCount));
diff --git a/src/main/java/org/folio/search/integration/ClassificationTypeHelper.java b/src/main/java/org/folio/search/integration/ClassificationTypeHelper.java
deleted file mode 100644
index 6966626dd..000000000
--- a/src/main/java/org/folio/search/integration/ClassificationTypeHelper.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.folio.search.integration;
-
-import static java.util.Arrays.asList;
-import static org.folio.search.client.InventoryReferenceDataClient.ReferenceDataType.CLASSIFICATION_TYPES;
-import static org.folio.search.configuration.SearchCacheNames.REFERENCE_DATA_CACHE;
-import static org.folio.search.model.client.CqlQueryParam.NAME;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import lombok.RequiredArgsConstructor;
-import org.folio.search.configuration.properties.SearchConfigurationProperties;
-import org.folio.search.model.types.ClassificationType;
-import org.springframework.cache.annotation.Cacheable;
-import org.springframework.stereotype.Component;
-
-@Component
-@RequiredArgsConstructor
-public class ClassificationTypeHelper {
-
- private final ReferenceDataService referenceDataService;
- private final SearchConfigurationProperties configurationProperties;
-
- @Cacheable(cacheNames = REFERENCE_DATA_CACHE,
- unless = "#result.isEmpty()",
- key = "@folioExecutionContext.tenantId + ':classification-types'")
- public Map getClassificationTypeMap() {
- var browseClassificationTypes = configurationProperties.getBrowseClassificationTypes();
- if (browseClassificationTypes.isEmpty()) {
- return Collections.emptyMap();
- }
- Map result = new HashMap<>();
-
- for (var typeEntry : browseClassificationTypes.entrySet()) {
- var ids = referenceDataService.fetchReferenceData(CLASSIFICATION_TYPES, NAME, asList(typeEntry.getValue()));
- ids.forEach(id -> result.put(id, typeEntry.getKey()));
- }
-
- return result;
- }
-
-}
diff --git a/src/main/java/org/folio/search/model/config/BrowseConfigEntity.java b/src/main/java/org/folio/search/model/config/BrowseConfigEntity.java
index f22ecafe7..7585bc74c 100644
--- a/src/main/java/org/folio/search/model/config/BrowseConfigEntity.java
+++ b/src/main/java/org/folio/search/model/config/BrowseConfigEntity.java
@@ -13,7 +13,7 @@
import lombok.Setter;
import lombok.ToString;
import org.folio.search.configuration.jpa.StringListConverter;
-import org.hibernate.proxy.HibernateProxy;
+import org.hibernate.Hibernate;
@Getter
@Setter
@@ -46,12 +46,8 @@ public final boolean equals(Object o) {
if (o == null) {
return false;
}
- Class> effectiveClass = o instanceof HibernateProxy
- ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass()
- : o.getClass();
- Class> thisEffectiveClass = this instanceof HibernateProxy
- ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass()
- : this.getClass();
+ Class> effectiveClass = Hibernate.getClass(o);
+ Class> thisEffectiveClass = Hibernate.getClass(this);
if (thisEffectiveClass != effectiveClass) {
return false;
}
diff --git a/src/main/java/org/folio/search/model/config/BrowseConfigId.java b/src/main/java/org/folio/search/model/config/BrowseConfigId.java
index 8336c8042..32b3d8f10 100644
--- a/src/main/java/org/folio/search/model/config/BrowseConfigId.java
+++ b/src/main/java/org/folio/search/model/config/BrowseConfigId.java
@@ -6,7 +6,7 @@
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
-import org.hibernate.proxy.HibernateProxy;
+import org.hibernate.Hibernate;
@Getter
@Embeddable
@@ -32,12 +32,8 @@ public final boolean equals(Object o) {
if (o == null) {
return false;
}
- Class> effectiveClass = o instanceof HibernateProxy
- ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass()
- : o.getClass();
- Class> thisEffectiveClass = this instanceof HibernateProxy
- ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass()
- : this.getClass();
+ Class> effectiveClass = Hibernate.getClass(o);
+ Class> thisEffectiveClass = Hibernate.getClass(this);
if (thisEffectiveClass != effectiveClass) {
return false;
}
diff --git a/src/main/java/org/folio/search/model/service/BrowseRequest.java b/src/main/java/org/folio/search/model/service/BrowseRequest.java
index 655c4a591..a5e9503b3 100644
--- a/src/main/java/org/folio/search/model/service/BrowseRequest.java
+++ b/src/main/java/org/folio/search/model/service/BrowseRequest.java
@@ -3,10 +3,11 @@
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
+import org.folio.search.domain.dto.BrowseOptionType;
import org.folio.search.model.ResourceRequest;
@Data
-@Builder()
+@Builder
@RequiredArgsConstructor(staticName = "of")
public class BrowseRequest implements ResourceRequest {
@@ -20,6 +21,11 @@ public class BrowseRequest implements ResourceRequest {
*/
private final String tenantId;
+ /**
+ * Browse option type.
+ */
+ private final BrowseOptionType browseOptionType;
+
/**
* A CQL query string with search conditions.
*/
@@ -63,7 +69,14 @@ public class BrowseRequest implements ResourceRequest {
public static BrowseRequest of(String resource, String tenantId, String query, Integer limit, String targetField,
String subField, Boolean expandAll, Boolean highlightMatch,
Integer precedingRecordsCount) {
- return new BrowseRequest(resource, tenantId, query, limit, targetField, subField, null, expandAll,
+ return new BrowseRequest(resource, tenantId, null, query, limit, targetField, subField, null, expandAll,
+ highlightMatch, precedingRecordsCount);
+ }
+
+ public static BrowseRequest of(String resource, String tenantId, BrowseOptionType optionType, String query,
+ Integer limit, String targetField, String subField, Boolean expandAll,
+ Boolean highlightMatch, Integer precedingRecordsCount) {
+ return new BrowseRequest(resource, tenantId, optionType, query, limit, targetField, subField, null, expandAll,
highlightMatch, precedingRecordsCount);
}
}
diff --git a/src/main/java/org/folio/search/service/ResourceService.java b/src/main/java/org/folio/search/service/ResourceService.java
index 4c2456aa7..e2df9aee6 100644
--- a/src/main/java/org/folio/search/service/ResourceService.java
+++ b/src/main/java/org/folio/search/service/ResourceService.java
@@ -21,6 +21,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
+import java.util.function.UnaryOperator;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@@ -44,6 +45,7 @@
import org.folio.search.service.consortium.ConsortiumTenantExecutor;
import org.folio.search.service.consortium.ConsortiumTenantService;
import org.folio.search.service.converter.MultiTenantSearchDocumentConverter;
+import org.folio.search.service.converter.preprocessor.InstanceEventPreProcessor;
import org.folio.search.service.metadata.ResourceDescriptionService;
import org.springframework.stereotype.Service;
@@ -66,6 +68,7 @@ public class ResourceService {
private final ConsortiumTenantExecutor consortiumTenantExecutor;
private final ConsortiumInstanceService consortiumInstanceService;
private final IndexNameProvider indexNameProvider;
+ private final InstanceEventPreProcessor instanceEventPreProcessor;
/**
* Saves list of resourceEvents to elasticsearch.
@@ -154,13 +157,34 @@ private Map> processIndexInstanceEvents(List preProcessEvents(List instanceEvents,
+ UnaryOperator> consortiumFunc) {
+ if (instanceEvents == null) {
+ instanceEvents = Collections.emptyList();
+ }
+ var list = instanceEvents.stream()
+ .map(event -> consortiumTenantExecutor.execute(() -> instanceEventPreProcessor.preProcess(event)))
+ .filter(Objects::nonNull)
+ .flatMap(List::stream)
+ .collect(toList());
+
+ var eventsToIndex = consortiumFunc.apply(instanceEvents);
+ if (eventsToIndex != null) {
+ list.addAll(eventsToIndex);
+ }
+ return list;
}
private Map> processDeleteInstanceEvents(List deleteEvents) {
messageProducer.prepareAndSendContributorEvents(deleteEvents);
messageProducer.prepareAndSendSubjectEvents(deleteEvents);
- return multiTenantSearchDocumentConverter.convert(consortiumInstanceService.deleteInstances(deleteEvents));
+ var list = preProcessEvents(deleteEvents, consortiumInstanceService::deleteInstances);
+ return multiTenantSearchDocumentConverter.convert(list);
}
private FolioIndexOperationResponse indexSearchDocuments(Map> eventsByResource) {
diff --git a/src/main/java/org/folio/search/service/browse/ClassificationBrowseService.java b/src/main/java/org/folio/search/service/browse/ClassificationBrowseService.java
new file mode 100644
index 000000000..f997964ab
--- /dev/null
+++ b/src/main/java/org/folio/search/service/browse/ClassificationBrowseService.java
@@ -0,0 +1,136 @@
+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.CLASSIFICATION_TYPE_ID_FIELD;
+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 lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.folio.search.domain.dto.BrowseConfig;
+import org.folio.search.domain.dto.BrowseType;
+import org.folio.search.domain.dto.ClassificationNumberBrowseItem;
+import org.folio.search.model.BrowseResult;
+import org.folio.search.model.SearchResult;
+import org.folio.search.model.index.ClassificationResource;
+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;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class ClassificationBrowseService
+ extends AbstractBrowseServiceBySearchAfter {
+
+ private final ConsortiumSearchHelper consortiumSearchHelper;
+ private final BrowseConfigServiceDecorator configService;
+
+ @Override
+ protected String getValueForBrowsing(ClassificationNumberBrowseItem browseItem) {
+ return browseItem.getClassificationNumber();
+ }
+
+ @Override
+ protected SearchSourceBuilder getAnchorSearchQuery(BrowseRequest req, BrowseContext ctx) {
+ log.debug("getAnchorSearchQuery:: by [request: {}]", req);
+ var config = configService.getConfig(BrowseType.INSTANCE_CLASSIFICATION, req.getBrowseOptionType());
+ var termQueryBuilder = getQuery(ctx, config, termQuery(req.getTargetField(), ctx.getAnchor()));
+ var query = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(ctx,
+ termQueryBuilder, req.getResource());
+ return searchSource().query(query)
+ .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(BrowseType.INSTANCE_CLASSIFICATION, 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());
+
+ return searchSource().query(query)
+ .searchAfter(new Object[] {normalizedAnchor.toLowerCase(ROOT)})
+ .sort(fieldSort(browseField).order(isBrowsingForward ? ASC : DESC))
+ .size(ctx.getLimit(isBrowsingForward) + 1)
+ .from(0);
+ }
+
+ @Override
+ protected ClassificationNumberBrowseItem getEmptyBrowseItem(BrowseContext context) {
+ return new ClassificationNumberBrowseItem()
+ .classificationNumber(context.getAnchor())
+ .totalRecords(0)
+ .isAnchor(true);
+ }
+
+ @Override
+ protected BrowseResult mapToBrowseResult(BrowseContext ctx,
+ SearchResult res,
+ boolean isAnchor) {
+ return BrowseResult.of(res)
+ .map(resource -> new ClassificationNumberBrowseItem()
+ .classificationNumber(resource.number())
+ .classificationTypeId(resource.typeId())
+ .isAnchor(isAnchor ? true : null)
+ .totalRecords(getTotalRecords(ctx, resource)));
+ }
+
+ private static 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(CLASSIFICATION_TYPE_ID_FIELD, 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);
+ }
+
+ private Integer getTotalRecords(BrowseContext ctx, ClassificationResource classificationResource) {
+ return consortiumSearchHelper.filterSubResourcesForConsortium(ctx, classificationResource,
+ ClassificationResource::instances).stream()
+ .map(InstanceSubResource::getInstanceId)
+ .distinct()
+ .map(e -> 1)
+ .reduce(0, Integer::sum);
+ }
+
+}
diff --git a/src/main/java/org/folio/search/service/config/BrowseConfigService.java b/src/main/java/org/folio/search/service/config/BrowseConfigService.java
index 6ca08db2f..75ba0fcc0 100644
--- a/src/main/java/org/folio/search/service/config/BrowseConfigService.java
+++ b/src/main/java/org/folio/search/service/config/BrowseConfigService.java
@@ -11,6 +11,7 @@
import org.folio.search.domain.dto.BrowseOptionType;
import org.folio.search.domain.dto.BrowseType;
import org.folio.search.exception.RequestValidationException;
+import org.folio.search.model.config.BrowseConfigId;
import org.folio.search.repository.BrowseConfigEntityRepository;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
@@ -26,13 +27,26 @@ public class BrowseConfigService {
private final BrowseConfigEntityRepository repository;
private final BrowseConfigMapper mapper;
- @Cacheable(cacheNames = BROWSE_CONFIG_CACHE, key = "@folioExecutionContext.tenantId + ':' + #type.value")
public BrowseConfigCollection getConfigs(@NonNull BrowseType type) {
log.debug("Fetch browse configuration [browseType: {}]", type.getValue());
return mapper.convert(repository.findByConfigId_BrowseType(type.getValue()));
}
- @CacheEvict(cacheNames = BROWSE_CONFIG_CACHE, key = "@folioExecutionContext.tenantId + ':' + #type.value")
+ @Cacheable(cacheNames = BROWSE_CONFIG_CACHE,
+ key = "@folioExecutionContext.tenantId + ':' + #type.value + ':' + #optionType.value")
+ public BrowseConfig getConfig(@NonNull BrowseType type, @NonNull BrowseOptionType optionType) {
+ var typeValue = type.getValue();
+ var optionTypeValue = optionType.getValue();
+ log.debug("Fetch browse configuration [browseType: {}, browseOptionType: {}]", typeValue, optionTypeValue);
+
+ return repository.findById(new BrowseConfigId(typeValue, optionTypeValue))
+ .map(mapper::convert)
+ .orElseThrow(() -> new IllegalStateException(
+ "Config for %s type %s must be present in database".formatted(typeValue, optionTypeValue)));
+ }
+
+ @CacheEvict(cacheNames = BROWSE_CONFIG_CACHE,
+ key = "@folioExecutionContext.tenantId + ':' + #type.value + ':' + #optionType.value")
public void upsertConfig(@NonNull BrowseType type,
@NonNull BrowseOptionType optionType,
@NonNull BrowseConfig config) {
diff --git a/src/main/java/org/folio/search/service/consortium/BrowseConfigServiceDecorator.java b/src/main/java/org/folio/search/service/consortium/BrowseConfigServiceDecorator.java
index 3583f6806..4cc41e8ec 100644
--- a/src/main/java/org/folio/search/service/consortium/BrowseConfigServiceDecorator.java
+++ b/src/main/java/org/folio/search/service/consortium/BrowseConfigServiceDecorator.java
@@ -19,6 +19,10 @@ public BrowseConfigCollection getConfigs(BrowseType type) {
return consortiumTenantExecutor.execute(() -> browseConfigService.getConfigs(type));
}
+ public BrowseConfig getConfig(BrowseType type, BrowseOptionType optionType) {
+ return consortiumTenantExecutor.execute(() -> browseConfigService.getConfig(type, optionType));
+ }
+
public void upsertConfig(BrowseType type, BrowseOptionType configId, BrowseConfig config) {
consortiumTenantExecutor.run(() -> browseConfigService.upsertConfig(type, configId, config));
}
diff --git a/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java b/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java
index fb3f93cee..a4feee7ce 100644
--- a/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java
+++ b/src/main/java/org/folio/search/service/consortium/ConsortiumSearchHelper.java
@@ -1,6 +1,7 @@
package org.folio.search.service.consortium;
import static org.folio.search.utils.SearchUtils.CONTRIBUTOR_RESOURCE;
+import static org.folio.search.utils.SearchUtils.INSTANCE_CLASSIFICATION_RESOURCE;
import static org.folio.search.utils.SearchUtils.INSTANCE_SUBJECT_RESOURCE;
import static org.folio.search.utils.SearchUtils.SHARED_FIELD_NAME;
import static org.folio.search.utils.SearchUtils.TENANT_ID_FIELD_NAME;
@@ -226,7 +227,9 @@ private boolean sharedFilterValue(TermQueryBuilder sharedQuery) {
}
private String getFieldForResource(String fieldName, String resourceName) {
- if (resourceName.equals(CONTRIBUTOR_RESOURCE) || resourceName.equals(INSTANCE_SUBJECT_RESOURCE)) {
+ if (resourceName.equals(CONTRIBUTOR_RESOURCE)
+ || resourceName.equals(INSTANCE_SUBJECT_RESOURCE)
+ || resourceName.equals(INSTANCE_CLASSIFICATION_RESOURCE)) {
return "instances." + fieldName;
}
return fieldName;
diff --git a/src/main/java/org/folio/search/service/converter/preprocessor/InstanceEventPreProcessor.java b/src/main/java/org/folio/search/service/converter/preprocessor/InstanceEventPreProcessor.java
index 4076098cd..8103a0067 100644
--- a/src/main/java/org/folio/search/service/converter/preprocessor/InstanceEventPreProcessor.java
+++ b/src/main/java/org/folio/search/service/converter/preprocessor/InstanceEventPreProcessor.java
@@ -61,12 +61,7 @@ public List preProcess(ResourceEvent event) {
return List.of(event);
}
- List events = new ArrayList<>();
- events.add(event);
-
- var classificationEvents = prepareClassificationEvents(event);
-
- events.addAll(classificationEvents);
+ var events = prepareClassificationEvents(event);
log.info("preProcess::Finished instance event pre-processing");
if (log.isDebugEnabled()) {
diff --git a/src/main/java/org/folio/search/service/setter/AbstractIdentifierProcessor.java b/src/main/java/org/folio/search/service/setter/AbstractIdentifierProcessor.java
index 283bc7e8a..f644a3a07 100644
--- a/src/main/java/org/folio/search/service/setter/AbstractIdentifierProcessor.java
+++ b/src/main/java/org/folio/search/service/setter/AbstractIdentifierProcessor.java
@@ -8,6 +8,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.folio.search.domain.dto.Identifiers;
@@ -19,12 +20,9 @@
public abstract class AbstractIdentifierProcessor implements FieldProcessor> {
private final ReferenceDataService referenceDataService;
+ @Getter
private final List identifierNames;
- public List getIdentifierNames() {
- return identifierNames;
- }
-
/**
* Returns set of filtered identifiers value from event body by specified set of types.
*
@@ -51,8 +49,7 @@ private Set fetchIdentifierIdsFromCache() {
var identifierTypeIds = referenceDataService.fetchReferenceData(IDENTIFIER_TYPES, CqlQueryParam.NAME,
getIdentifierNames());
if (identifierTypeIds.isEmpty()) {
- log.warn("Failed to provide identifiers for processor: {}]",
- this.getClass().getSimpleName());
+ log.warn("Failed to provide identifiers for [processor: {}]", this.getClass().getSimpleName());
}
return identifierTypeIds;
}
diff --git a/src/main/java/org/folio/search/service/setter/ClassificationShelvingOrderProcessor.java b/src/main/java/org/folio/search/service/setter/ClassificationShelvingOrderProcessor.java
deleted file mode 100644
index c88e02d59..000000000
--- a/src/main/java/org/folio/search/service/setter/ClassificationShelvingOrderProcessor.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package org.folio.search.service.setter;
-
-import java.util.Map;
-import java.util.function.Function;
-import lombok.RequiredArgsConstructor;
-import org.folio.search.cql.SuDocCallNumber;
-import org.folio.search.integration.ClassificationTypeHelper;
-import org.folio.search.model.index.ClassificationResource;
-import org.folio.search.model.types.ClassificationType;
-import org.folio.search.utils.CallNumberUtils;
-import org.marc4j.callnum.CallNumber;
-import org.marc4j.callnum.DeweyCallNumber;
-import org.marc4j.callnum.LCCallNumber;
-import org.marc4j.callnum.NlmCallNumber;
-import org.springframework.stereotype.Component;
-
-@Component
-@RequiredArgsConstructor
-public class ClassificationShelvingOrderProcessor implements FieldProcessor {
-
- private static final Map> CN_TYPE_TO_SHELF_KEY_GENERATOR = Map.of(
- ClassificationType.NLM, cn -> getShelfKey(new NlmCallNumber(cn)),
- ClassificationType.LC, cn -> getShelfKey(new LCCallNumber(cn)),
- ClassificationType.DEWEY, cn -> getShelfKey(new DeweyCallNumber(cn)),
- ClassificationType.SUDOC, cn -> getShelfKey(new SuDocCallNumber(cn)),
- ClassificationType.DEFAULT, CallNumberUtils::normalizeEffectiveShelvingOrder
- );
-
- private final ClassificationTypeHelper classificationTypeHelper;
-
- @Override
- public String getFieldValue(ClassificationResource eventBody) {
- var number = eventBody.number();
- var typeId = eventBody.typeId();
- var classificationByIdMap = classificationTypeHelper.getClassificationTypeMap();
- var classificationType = classificationByIdMap.getOrDefault(typeId, ClassificationType.DEFAULT);
- return CN_TYPE_TO_SHELF_KEY_GENERATOR.get(classificationType).apply(number);
- }
-
- private static String getShelfKey(CallNumber value) {
- return value.getShelfKey();
- }
-
-}
diff --git a/src/main/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessor.java
new file mode 100644
index 000000000..0245b5b8f
--- /dev/null
+++ b/src/main/java/org/folio/search/service/setter/classification/ClassificationShelvingOrderFieldProcessor.java
@@ -0,0 +1,23 @@
+package org.folio.search.service.setter.classification;
+
+import java.util.function.UnaryOperator;
+import lombok.NonNull;
+import org.folio.search.model.index.ClassificationResource;
+import org.folio.search.service.setter.FieldProcessor;
+
+public abstract class ClassificationShelvingOrderFieldProcessor
+ implements FieldProcessor {
+
+ private final UnaryOperator numberFunction;
+
+ protected ClassificationShelvingOrderFieldProcessor(@NonNull UnaryOperator numberFunction) {
+ this.numberFunction = numberFunction;
+ }
+
+ @Override
+ public String getFieldValue(ClassificationResource eventBody) {
+ var number = eventBody.number();
+ return numberFunction.apply(number);
+ }
+}
+
diff --git a/src/main/java/org/folio/search/service/setter/classification/DefaultClassificationShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/classification/DefaultClassificationShelvingOrderFieldProcessor.java
new file mode 100644
index 000000000..2f962b308
--- /dev/null
+++ b/src/main/java/org/folio/search/service/setter/classification/DefaultClassificationShelvingOrderFieldProcessor.java
@@ -0,0 +1,13 @@
+package org.folio.search.service.setter.classification;
+
+import org.folio.search.domain.dto.ShelvingOrderAlgorithmType;
+import org.folio.search.utils.ShelvingOrderCalculationHelper;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DefaultClassificationShelvingOrderFieldProcessor extends ClassificationShelvingOrderFieldProcessor {
+
+ protected DefaultClassificationShelvingOrderFieldProcessor() {
+ super(number -> ShelvingOrderCalculationHelper.calculate(number, ShelvingOrderAlgorithmType.DEFAULT));
+ }
+}
diff --git a/src/main/java/org/folio/search/service/setter/classification/DeweyClassificationShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/classification/DeweyClassificationShelvingOrderFieldProcessor.java
new file mode 100644
index 000000000..134568a48
--- /dev/null
+++ b/src/main/java/org/folio/search/service/setter/classification/DeweyClassificationShelvingOrderFieldProcessor.java
@@ -0,0 +1,12 @@
+package org.folio.search.service.setter.classification;
+
+import org.folio.search.domain.dto.ShelvingOrderAlgorithmType;
+import org.folio.search.utils.ShelvingOrderCalculationHelper;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DeweyClassificationShelvingOrderFieldProcessor extends ClassificationShelvingOrderFieldProcessor {
+ protected DeweyClassificationShelvingOrderFieldProcessor() {
+ super(number -> ShelvingOrderCalculationHelper.calculate(number, ShelvingOrderAlgorithmType.DEWEY));
+ }
+}
diff --git a/src/main/java/org/folio/search/service/setter/classification/LcClassificationShelvingOrderFieldProcessor.java b/src/main/java/org/folio/search/service/setter/classification/LcClassificationShelvingOrderFieldProcessor.java
new file mode 100644
index 000000000..e245e841a
--- /dev/null
+++ b/src/main/java/org/folio/search/service/setter/classification/LcClassificationShelvingOrderFieldProcessor.java
@@ -0,0 +1,13 @@
+package org.folio.search.service.setter.classification;
+
+import org.folio.search.domain.dto.ShelvingOrderAlgorithmType;
+import org.folio.search.utils.ShelvingOrderCalculationHelper;
+import org.springframework.stereotype.Component;
+
+@Component
+public class LcClassificationShelvingOrderFieldProcessor extends ClassificationShelvingOrderFieldProcessor {
+
+ protected LcClassificationShelvingOrderFieldProcessor() {
+ super(number -> ShelvingOrderCalculationHelper.calculate(number, ShelvingOrderAlgorithmType.LC));
+ }
+}
diff --git a/src/main/java/org/folio/search/utils/SearchUtils.java b/src/main/java/org/folio/search/utils/SearchUtils.java
index bd12b5042..49ade1819 100644
--- a/src/main/java/org/folio/search/utils/SearchUtils.java
+++ b/src/main/java/org/folio/search/utils/SearchUtils.java
@@ -20,6 +20,7 @@
import org.folio.search.domain.dto.Contributor;
import org.folio.search.domain.dto.Instance;
import org.folio.search.domain.dto.ResourceEvent;
+import org.folio.search.domain.dto.ShelvingOrderAlgorithmType;
import org.folio.search.exception.SearchOperationException;
import org.folio.search.model.ResourceRequest;
import org.folio.search.model.index.SearchDocumentBody;
@@ -45,8 +46,13 @@ public class SearchUtils {
public static final String TENANT_ID_FIELD_NAME = "tenantId";
public static final String IS_BOUND_WITH_FIELD_NAME = "isBoundWith";
public static final String CALL_NUMBER_BROWSING_FIELD = "callNumber";
+ public static final String CLASSIFICATION_NUMBER_BROWSING_FIELD = "number";
+ public static final String CLASSIFICATION_TYPE_ID_FIELD = "typeId";
public static final String TYPED_CALL_NUMBER_BROWSING_FIELD = "typedCallNumber";
public static final String SHELVING_ORDER_BROWSING_FIELD = "itemEffectiveShelvingOrder";
+ public static final String DEFAULT_SHELVING_ORDER_BROWSING_FIELD = "defaultShelvingOrder";
+ public static final String LC_SHELVING_ORDER_BROWSING_FIELD = "lcShelvingOrder";
+ public static final String DEWEY_SHELVING_ORDER_BROWSING_FIELD = "deweyShelvingOrder";
public static final String SUBJECT_BROWSING_FIELD = "value";
public static final String CONTRIBUTOR_BROWSING_FIELD = "name";
public static final String AUTHORITY_BROWSING_FIELD = "headingRef";
@@ -67,6 +73,12 @@ public class SearchUtils {
public static final String KEYWORD_FIELD_INDEX = "keyword";
public static final float CONST_SIZE_LOAD_FACTOR = 1.0f;
+ public static final Map BROWSE_FIELDS_MAP = Map.of(
+ ShelvingOrderAlgorithmType.DEFAULT, DEFAULT_SHELVING_ORDER_BROWSING_FIELD,
+ ShelvingOrderAlgorithmType.LC, LC_SHELVING_ORDER_BROWSING_FIELD,
+ ShelvingOrderAlgorithmType.DEWEY, DEWEY_SHELVING_ORDER_BROWSING_FIELD
+ );
+
//CHECKSTYLE.ON: LineLength
public static final String INSTANCE_SUBJECT_UPSERT_SCRIPT_ID = "instance_subject_upsert_script";
public static final String INSTANCE_SUBJECT_UPSERT_SCRIPT = """
diff --git a/src/main/java/org/folio/search/utils/ShelvingOrderCalculationHelper.java b/src/main/java/org/folio/search/utils/ShelvingOrderCalculationHelper.java
new file mode 100644
index 000000000..db4d26137
--- /dev/null
+++ b/src/main/java/org/folio/search/utils/ShelvingOrderCalculationHelper.java
@@ -0,0 +1,24 @@
+package org.folio.search.utils;
+
+import java.util.Locale;
+import lombok.NonNull;
+import lombok.experimental.UtilityClass;
+import org.folio.search.domain.dto.ShelvingOrderAlgorithmType;
+import org.marc4j.callnum.DeweyCallNumber;
+import org.marc4j.callnum.LCCallNumber;
+
+@UtilityClass
+public class ShelvingOrderCalculationHelper {
+
+ public static String calculate(@NonNull String input, @NonNull ShelvingOrderAlgorithmType algorithmType) {
+ return switch (algorithmType) {
+ case LC -> new LCCallNumber(input).getShelfKey().trim();
+ case DEWEY -> new DeweyCallNumber(input).getShelfKey().trim();
+ case DEFAULT -> normalize(input);
+ };
+ }
+
+ private static String normalize(String input) {
+ return input.toUpperCase(Locale.ROOT).trim();
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index f57bdbe52..95042960f 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -66,11 +66,6 @@ folio:
browse-cn-intermediate-values: ${BROWSE_CN_INTERMEDIATE_VALUES_ENABLED:true}
browse-cn-intermediate-remove-duplicates: ${BROWSE_CN_INTERMEDIATE_REMOVE_DUPLICATES:true}
browse-classifications: ${BROWSE_CLASSIFICATIONS_ENABLED:false}
- browse-classification-types:
- lc: LC,LC (local)
- nlm: NLM
- dewey: Dewey,Additional Dewey
- sudoc: SUDOC
indexing:
instance-subjects:
retry-attempts: ${INSTANCE_SUBJECTS_INDEXING_RETRY_ATTEMPTS:3}
diff --git a/src/main/resources/model/instance.json b/src/main/resources/model/instance.json
index fbd0f16ab..af9c334d7 100644
--- a/src/main/resources/model/instance.json
+++ b/src/main/resources/model/instance.json
@@ -3,9 +3,6 @@
"eventBodyJavaClass": "org.folio.search.domain.dto.Instance",
"reindexSupported": true,
"languageSourcePaths": [ "$.languages" ],
- "indexingConfiguration": {
- "eventPreProcessor": "instanceEventPreProcessor"
- },
"searchFieldModifiers": [
"itemSearchFieldModifier"
],
diff --git a/src/main/resources/model/instance_classification.json b/src/main/resources/model/instance_classification.json
index 6ed3aba42..f312e2b53 100644
--- a/src/main/resources/model/instance_classification.json
+++ b/src/main/resources/model/instance_classification.json
@@ -30,10 +30,20 @@
}
},
"searchFields": {
- "shelvingOrder": {
+ "defaultShelvingOrder": {
"type": "search",
"index": "keyword_lowercase",
- "processor": "classificationShelvingOrderProcessor"
+ "processor": "defaultClassificationShelvingOrderFieldProcessor"
+ },
+ "lcShelvingOrder": {
+ "type": "search",
+ "index": "keyword_lowercase",
+ "processor": "lcClassificationShelvingOrderFieldProcessor"
+ },
+ "deweyShelvingOrder": {
+ "type": "search",
+ "index": "keyword_lowercase",
+ "processor": "deweyClassificationShelvingOrderFieldProcessor"
}
}
}
diff --git a/src/main/resources/swagger.api/mod-search.yaml b/src/main/resources/swagger.api/mod-search.yaml
index 2974ee977..f0460663a 100644
--- a/src/main/resources/swagger.api/mod-search.yaml
+++ b/src/main/resources/swagger.api/mod-search.yaml
@@ -234,6 +234,33 @@ paths:
'500':
$ref: '#/components/responses/internalServerErrorResponse'
+ /browse/classification-numbers/{browseOptionId}/instances:
+ get:
+ operationId: browseInstancesByClassificationNumber
+ description: Provides list of instances for browsing by classification number
+ tags:
+ - browse
+ parameters:
+ - $ref: '#/components/parameters/browse-option-id'
+ - $ref: '#/components/parameters/cql-query'
+ - $ref: '#/components/parameters/browse-limit-param'
+ - $ref: '#/components/parameters/expand-all-param'
+ - $ref: '#/components/parameters/highlight-match'
+ - $ref: '#/components/parameters/preceding-records-count'
+ - $ref: '#/components/parameters/x-okapi-tenant-header'
+ responses:
+ '200':
+ description: 'Search result for browsing by call number'
+ content:
+ application/json:
+ example: examples/searchResult.sample
+ schema:
+ $ref: '#/components/schemas/classificationNumberBrowseResult'
+ '400':
+ $ref: '#/components/responses/badRequestResponse'
+ '500':
+ $ref: '#/components/responses/internalServerErrorResponse'
+
/browse/subjects/instances:
get:
operationId: browseInstancesBySubject
@@ -585,7 +612,7 @@ paths:
schema:
$ref: '#/components/schemas/browseConfigCollection'
- /browse/config/{browseType}/{browseConfigId}:
+ /browse/config/{browseType}/{browseOptionId}:
put:
operationId: putBrowseConfig
description: Update configuration for browse type
@@ -593,7 +620,7 @@ paths:
- config
parameters:
- $ref: '#/components/parameters/browse-type'
- - $ref: '#/components/parameters/browse-config-id'
+ - $ref: '#/components/parameters/browse-option-id'
requestBody:
required: true
content:
@@ -630,6 +657,8 @@ components:
$ref: schemas/response/authoritySearchResult.json
callNumberBrowseResult:
$ref: schemas/response/callNumberBrowseResult.json
+ classificationNumberBrowseResult:
+ $ref: schemas/response/classificationNumberBrowseResult.json
subjectBrowseResult:
$ref: schemas/response/subjectBrowseResult.json
contributorBrowseResult:
@@ -833,11 +862,11 @@ components:
schema:
type: boolean
default: true
- browse-config-id:
- name: browseConfigId
+ browse-option-id:
+ name: browseOptionId
in: path
required: true
- description: 'ID of browse config'
+ description: 'Browse option type'
schema:
$ref: '#/components/schemas/browseOptionType'
browse-type:
diff --git a/src/main/resources/swagger.api/schemas/response/classificationNumberBrowseItem.json b/src/main/resources/swagger.api/schemas/response/classificationNumberBrowseItem.json
new file mode 100644
index 000000000..071958581
--- /dev/null
+++ b/src/main/resources/swagger.api/schemas/response/classificationNumberBrowseItem.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Classification number browse search result item",
+ "type": "object",
+ "properties": {
+ "classificationNumber": {
+ "type": "string",
+ "description": "Classification number value to display"
+ },
+ "classificationTypeId": {
+ "type": "string",
+ "description": "Classification type ID value"
+ },
+ "isAnchor": {
+ "type": "boolean",
+ "description": "Marks if current value is anchor or not"
+ },
+ "totalRecords": {
+ "type": "integer",
+ "description": "Amount of records for the classification number value"
+ }
+ }
+}
diff --git a/src/main/resources/swagger.api/schemas/response/classificationNumberBrowseResult.json b/src/main/resources/swagger.api/schemas/response/classificationNumberBrowseResult.json
new file mode 100644
index 000000000..8eb4c1774
--- /dev/null
+++ b/src/main/resources/swagger.api/schemas/response/classificationNumberBrowseResult.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Classification number browse search response",
+ "type": "object",
+ "properties": {
+ "totalRecords": {
+ "type": "integer",
+ "description": "Amount of items to display"
+ },
+ "prev": {
+ "type": "string",
+ "description": "Previous value for browsing backward"
+ },
+ "next": {
+ "type": "string",
+ "description": "Next value for browsing forward"
+ },
+ "items": {
+ "type": "array",
+ "description": "List of classification number browse items",
+ "items": {
+ "$ref": "classificationNumberBrowseItem.json"
+ }
+ }
+ }
+}
diff --git a/src/test/java/org/folio/search/controller/BrowseClassificationConsortiumIT.java b/src/test/java/org/folio/search/controller/BrowseClassificationConsortiumIT.java
new file mode 100644
index 000000000..a054c8910
--- /dev/null
+++ b/src/test/java/org/folio/search/controller/BrowseClassificationConsortiumIT.java
@@ -0,0 +1,150 @@
+package org.folio.search.controller;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.awaitility.Durations.ONE_MINUTE;
+import static org.awaitility.Durations.ONE_SECOND;
+import static org.folio.search.model.Pair.pair;
+import static org.folio.search.support.base.ApiEndpoints.instanceClassificationBrowsePath;
+import static org.folio.search.support.base.ApiEndpoints.instanceSearchPath;
+import static org.folio.search.utils.SearchUtils.getIndexName;
+import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID;
+import static org.folio.search.utils.TestConstants.MEMBER_TENANT_ID;
+import static org.folio.search.utils.TestUtils.classificationBrowseItem;
+import static org.folio.search.utils.TestUtils.classificationBrowseResult;
+import static org.folio.search.utils.TestUtils.parseResponse;
+import static org.folio.search.utils.TestUtils.randomId;
+import static org.opensearch.index.query.QueryBuilders.matchAllQuery;
+import static org.opensearch.search.builder.SearchSourceBuilder.searchSource;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.folio.search.domain.dto.BrowseOptionType;
+import org.folio.search.domain.dto.ClassificationNumberBrowseResult;
+import org.folio.search.domain.dto.Instance;
+import org.folio.search.domain.dto.InstanceClassificationsInner;
+import org.folio.search.model.Pair;
+import org.folio.search.support.base.BaseConsortiumIntegrationTest;
+import org.folio.search.utils.SearchUtils;
+import org.folio.spring.testing.type.IntegrationTest;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.opensearch.action.search.SearchRequest;
+import org.opensearch.client.RequestOptions;
+
+@IntegrationTest
+class BrowseClassificationConsortiumIT extends BaseConsortiumIntegrationTest {
+
+ private static final String LC_TYPE_ID = "e62bbefe-adf5-4b1e-b3e7-43d877b0c91a";
+ private static final String LC2_TYPE_ID = "308c950f-8209-4f2e-9702-0c004a9f21bc";
+ private static final String DEWEY_TYPE_ID = "50524585-046b-49a1-8ca7-8d46f2a8dc19";
+ private static final Instance[] INSTANCES_MEMBER = instancesMember();
+ private static final Instance[] INSTANCES_CENTRAL = instancesCentral();
+
+ @BeforeAll
+ static void prepare() {
+ setUpTenant(CONSORTIUM_TENANT_ID);
+ setUpTenant(MEMBER_TENANT_ID);
+ saveRecords(CONSORTIUM_TENANT_ID, instanceSearchPath(), asList(INSTANCES_CENTRAL),
+ INSTANCES_CENTRAL.length,
+ instance -> inventoryApi.createInstance(CONSORTIUM_TENANT_ID, instance));
+ saveRecords(MEMBER_TENANT_ID, instanceSearchPath(), asList(INSTANCES_MEMBER),
+ INSTANCES_CENTRAL.length + INSTANCES_MEMBER.length,
+ instance -> inventoryApi.createInstance(MEMBER_TENANT_ID, instance));
+
+ await().atMost(ONE_MINUTE).pollInterval(ONE_SECOND).untilAsserted(() -> {
+ var searchRequest = new SearchRequest()
+ .source(searchSource().query(matchAllQuery()).trackTotalHits(true).from(0).size(100))
+ .indices(getIndexName(SearchUtils.INSTANCE_CLASSIFICATION_RESOURCE, CONSORTIUM_TENANT_ID));
+ var searchResponse = elasticClient.search(searchRequest, RequestOptions.DEFAULT);
+ assertThat(searchResponse.getHits().getTotalHits().value).isEqualTo(17);
+ });
+ }
+
+ @AfterAll
+ static void cleanUp() {
+ removeTenant();
+ }
+
+ @Test
+ void browseByClassification_shared() {
+ var request = get(instanceClassificationBrowsePath(BrowseOptionType.ALL))
+ .param("query", prepareQuery("number < {value} or number >= {value} and instances.shared==true",
+ "\"QD33 .O87\""))
+ .param("limit", "4")
+ .param("precedingRecordsCount", "2");
+ var actual = parseResponse(doGet(request), ClassificationNumberBrowseResult.class);
+ assertThat(actual).isEqualTo(classificationBrowseResult("HQ536 .A565 2018", null, 8, List.of(
+ classificationBrowseItem("HQ536 .A565 2018", LC2_TYPE_ID, 1),
+ classificationBrowseItem("N6679.R64 G88 2010", LC_TYPE_ID, 1),
+ classificationBrowseItem("QD33 .O87", LC_TYPE_ID, 1, true),
+ classificationBrowseItem("QD453 .M8 1961", LC_TYPE_ID, 1)
+
+ )));
+ }
+
+ @Test
+ void browseByClassification_local() {
+ var request = get(instanceClassificationBrowsePath(BrowseOptionType.ALL))
+ .param("query", prepareQuery("number < {value} or number >= {value} and instances.shared==false",
+ "\"QD33 .O87\""))
+ .param("limit", "4")
+ .param("precedingRecordsCount", "2");
+ var actual = parseResponse(doGet(request), ClassificationNumberBrowseResult.class);
+ assertThat(actual).isEqualTo(classificationBrowseResult("333.91", "SF433 .D47 2004", 11, List.of(
+ classificationBrowseItem("333.91", DEWEY_TYPE_ID, 1),
+ classificationBrowseItem("372.4", DEWEY_TYPE_ID, 1),
+ classificationBrowseItem("QD33 .O87", LC_TYPE_ID, 1, true),
+ classificationBrowseItem("SF433 .D47 2004", LC_TYPE_ID, 1)
+ )));
+ }
+
+ private static Instance[] instancesCentral() {
+ return classificationBrowseInstanceData().subList(0, 5).stream()
+ .map(BrowseClassificationConsortiumIT::instance)
+ .toArray(Instance[]::new);
+ }
+
+ private static Instance[] instancesMember() {
+ return classificationBrowseInstanceData().subList(5, 10).stream()
+ .map(BrowseClassificationConsortiumIT::instance)
+ .toArray(Instance[]::new);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Instance instance(List