Skip to content

Commit

Permalink
add bibframe search functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
aleksei-pronichev committed May 23, 2024
1 parent b3fbc15 commit f790b95
Show file tree
Hide file tree
Showing 31 changed files with 1,389 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ Consortium feature on module enable is defined by 'centralTenantId' tenant param
|:-------|:------------------------------|:-------------------------------------------------------------------------------------|
| GET | `/search/instances` | Search by instances and to this instance items and holding-records |
| GET | `/search/authorities` | Search by authority records |
| GET | `/search/bibframe` | Search linked data graph resource descriptions |
| GET | `/search/{recordType}/facets` | Get facets where recordType could be: instances, authorities, contributors, subjects |
| GET | ~~`/search/instances/ids`~~ | (DEPRECATED) Stream instance ids as JSON or plain text |
| GET | ~~`/search/holdings/ids`~~ | (DEPRECATED) Stream holding record ids as JSON or plain text |
Expand Down
14 changes: 14 additions & 0 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@
"user-tenants.collection.get"
]
},
{
"methods": [
"GET"
],
"pathPattern": "/search/bibframe",
"permissionsRequired": [
"search.bibframe.collection.get"
]
},
{
"methods": [
"GET"
Expand Down Expand Up @@ -525,6 +534,11 @@
"displayName": "Search - searches authorities by given query",
"description": "Searches authorities by given query"
},
{
"permissionName": "search.bibframe.collection.get",
"displayName": "Search - searches bibframe by given query",
"description": "Searches bibframe by given query"
},
{
"permissionName": "browse.call-numbers.instances.collection.get",
"displayName": "Browse - provides collections of browse items for instance by call number",
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/org/folio/search/controller/SearchController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import lombok.RequiredArgsConstructor;
import org.folio.search.domain.dto.Authority;
import org.folio.search.domain.dto.AuthoritySearchResult;
import org.folio.search.domain.dto.Bibframe;
import org.folio.search.domain.dto.BibframeSearchResult;
import org.folio.search.domain.dto.Instance;
import org.folio.search.domain.dto.InstanceSearchResult;
import org.folio.search.model.service.CqlSearchRequest;
Expand Down Expand Up @@ -47,4 +49,24 @@ public ResponseEntity<InstanceSearchResult> searchInstances(String tenantId, Str
.instances(result.getRecords())
.totalRecords(result.getTotalRecords()));
}

@Override
public ResponseEntity<BibframeSearchResult> searchBibframe(String tenant, String query, Integer limit,
Integer offset) {
var searchRequest = CqlSearchRequest.of(
Bibframe.class, tenant, query, limit, offset, true);
var result = searchService.search(searchRequest);
return ResponseEntity.ok(new BibframeSearchResult()
.searchQuery(query)
.content(result.getRecords())
.pageNumber(divPlusOneIfRemainder(offset, limit))
.totalPages(divPlusOneIfRemainder(result.getTotalRecords(), limit))
.totalRecords(result.getTotalRecords())
);
}

private int divPlusOneIfRemainder(int one, int two) {
var modulo = one % two;
return one / two + (modulo > 0 ? 1 : 0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.folio.search.cql;

import lombok.RequiredArgsConstructor;
import org.folio.search.service.setter.bibframe.BibframeIsbnProcessor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class BibframeIsbnSearchTermProcessor implements SearchTermProcessor {

private final BibframeIsbnProcessor isbnProcessor;

@Override
public String getSearchTerm(String inputTerm) {
return String.join(" ", isbnProcessor.normalizeIsbn(inputTerm));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import static org.folio.search.utils.SearchConverterUtils.getEventPayload;
import static org.folio.search.utils.SearchConverterUtils.getResourceEventId;
import static org.folio.search.utils.SearchConverterUtils.getResourceSource;
import static org.folio.search.utils.SearchUtils.BIBFRAME_RESOURCE;
import static org.folio.search.utils.SearchUtils.ID_FIELD;
import static org.folio.search.utils.SearchUtils.INSTANCE_ID_FIELD;
import static org.folio.search.utils.SearchUtils.INSTANCE_RESOURCE;
Expand Down Expand Up @@ -195,6 +196,23 @@ public void handleLocationEvents(List<ConsumerRecord<String, ResourceEvent>> con
indexResources(batch, resourceService::indexResources);
}

@KafkaListener(
id = KafkaConstants.BIBFRAME_LISTENER_ID,
containerFactory = "standardListenerContainerFactory",
groupId = "#{folioKafkaProperties.listener['bibframe'].groupId}",
concurrency = "#{folioKafkaProperties.listener['bibframe'].concurrency}",
topicPattern = "#{folioKafkaProperties.listener['bibframe'].topicPattern}")
public void handleBibframeEvents(List<ConsumerRecord<String, ResourceEvent>> consumerRecords) {
log.info("Processing bibframe events from Kafka [number of events: {}]", consumerRecords.size());
var batch = consumerRecords.stream()
.map(ConsumerRecord::value)
.map(bibframe -> bibframe.resourceName(BIBFRAME_RESOURCE).id(getResourceEventId(bibframe)))
.toList();

folioMessageBatchProcessor.consumeBatchWithFallback(batch, KAFKA_RETRY_TEMPLATE_NAME,
resourceService::indexResources, KafkaMessageListener::logFailedEvent);
}

private void indexResources(List<ResourceEvent> batch, Consumer<List<ResourceEvent>> indexConsumer) {
var batchByTenant = batch.stream().collect(Collectors.groupingBy(ResourceEvent::getTenant));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.folio.search.service.setter.bibframe;

import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toCollection;

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.folio.search.domain.dto.Bibframe;
import org.folio.search.domain.dto.BibframeContributorsInner;
import org.folio.search.domain.dto.BibframeInstancesInner;
import org.folio.search.service.setter.FieldProcessor;
import org.springframework.stereotype.Component;

@Component
public class BibframeContributorProcessor implements FieldProcessor<Bibframe, Set<String>> {

@Override
public Set<String> getFieldValue(Bibframe bibframe) {
var workContributors = ofNullable(bibframe.getContributors()).stream().flatMap(Collection::stream);
var instanceContributors = ofNullable(bibframe.getInstances()).stream().flatMap(Collection::stream)
.map(BibframeInstancesInner::getContributors).filter(Objects::nonNull).flatMap(Collection::stream);
return Stream.concat(workContributors, instanceContributors)
.filter(Objects::nonNull)
.map(BibframeContributorsInner::getName)
.filter(StringUtils::isNotBlank)
.map(String::trim)
.collect(toCollection(LinkedHashSet::new));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.folio.search.service.setter.bibframe;

import static java.util.Objects.nonNull;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toCollection;
import static org.folio.search.domain.dto.BibframeInstancesInnerIdentifiersInner.TypeEnum.ISBN;

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.folio.search.domain.dto.Bibframe;
import org.folio.search.domain.dto.BibframeInstancesInnerIdentifiersInner;
import org.folio.search.service.setter.FieldProcessor;
import org.folio.search.service.setter.instance.IsbnProcessor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class BibframeIsbnProcessor implements FieldProcessor<Bibframe, Set<String>> {

private final IsbnProcessor isbnProcessor;

@Override
public Set<String> getFieldValue(Bibframe bibframe) {
return ofNullable(bibframe.getInstances()).stream()
.flatMap(Collection::stream)
.filter(i -> nonNull(i.getIdentifiers()))
.flatMap(i -> i.getIdentifiers().stream())
.filter(i -> ISBN.equals(i.getType()))
.map(BibframeInstancesInnerIdentifiersInner::getValue)
.filter(Objects::nonNull)
.map(this::normalizeIsbn)
.flatMap(Collection::stream)
.collect(toCollection(LinkedHashSet::new));
}

public List<String> normalizeIsbn(String value) {
return isbnProcessor.normalizeIsbn(value);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.folio.search.service.setter.bibframe;

import static java.util.Objects.nonNull;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toCollection;
import static org.folio.search.domain.dto.BibframeInstancesInnerIdentifiersInner.TypeEnum.LCCN;

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.folio.search.domain.dto.Bibframe;
import org.folio.search.domain.dto.BibframeInstancesInnerIdentifiersInner;
import org.folio.search.service.setter.FieldProcessor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class BibframeLccnProcessor implements FieldProcessor<Bibframe, Set<String>> {

private final LccnNormalizer lccnNormalizer;

@Override
public Set<String> getFieldValue(Bibframe bibframe) {
return ofNullable(bibframe.getInstances()).stream()
.flatMap(Collection::stream)
.filter(i -> nonNull(i.getIdentifiers()))
.flatMap(i -> i.getIdentifiers().stream())
.filter(i -> LCCN.equals(i.getType()))
.map(BibframeInstancesInnerIdentifiersInner::getValue)
.filter(Objects::nonNull)
.map(lccnNormalizer::normalizeLccn)
.flatMap(Optional::stream)
.collect(toCollection(LinkedHashSet::new));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.folio.search.service.setter.bibframe;

import static java.util.Optional.ofNullable;

import java.util.Collection;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.folio.search.domain.dto.Bibframe;
import org.folio.search.domain.dto.BibframeInstancesInner;
import org.folio.search.domain.dto.BibframeTitlesInner;
import org.folio.search.service.setter.FieldProcessor;
import org.springframework.stereotype.Component;

@Component
public class BibframeSortTitleProcessor implements FieldProcessor<Bibframe, String> {

@Override
public String getFieldValue(Bibframe bibframe) {
var workTitles = ofNullable(bibframe.getTitles()).stream().flatMap(Collection::stream);
var instanceTitles = ofNullable(bibframe.getInstances()).stream().flatMap(Collection::stream)
.map(BibframeInstancesInner::getTitles).filter(Objects::nonNull).flatMap(Collection::stream);
return Stream.concat(workTitles, instanceTitles)
.filter(Objects::nonNull)
.map(BibframeTitlesInner::getValue)
.filter(StringUtils::isNotBlank)
.collect(Collectors.joining());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.folio.search.service.setter.bibframe;

import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toCollection;

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.folio.search.domain.dto.Bibframe;
import org.folio.search.domain.dto.BibframeInstancesInner;
import org.folio.search.domain.dto.BibframeTitlesInner;
import org.folio.search.service.setter.FieldProcessor;
import org.springframework.stereotype.Component;

@Component
public class BibframeTitleProcessor implements FieldProcessor<Bibframe, Set<String>> {

@Override
public Set<String> getFieldValue(Bibframe bibframe) {
var workTitles = ofNullable(bibframe.getTitles()).stream().flatMap(Collection::stream);
var instTitles = ofNullable(bibframe.getInstances()).stream().flatMap(Collection::stream).filter(Objects::nonNull)
.map(BibframeInstancesInner::getTitles).filter(Objects::nonNull).flatMap(Collection::stream);
return Stream.concat(workTitles, instTitles)
.filter(Objects::nonNull)
.map(BibframeTitlesInner::getValue)
.filter(StringUtils::isNotBlank)
.map(String::trim)
.collect(toCollection(LinkedHashSet::new));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.folio.search.service.setter.bibframe;

import jakarta.validation.constraints.NotNull;
import java.util.Optional;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

@Log4j2
@Service
public class LccnNormalizer {
private static final String NORMALIZED_LCCN_REGEX = "\\d{10}";
private static final char HYPHEN = '-';

/**
* Normalizes the given LCCN value and returns the normalized LCCN.
* If the given LCCN is invalid, an empty Optional is returned.
*
* @param lccn LCCN to be normalized
* @return Returns the normalized LCCN. If the given LCCN is invalid, returns an empty Optional
*/
public Optional<String> normalizeLccn(@NotNull final String lccn) {
var normalizedLccn = lccn;

// Remove white spaces
normalizedLccn = normalizedLccn.replaceAll("\\s", StringUtils.EMPTY);

// If lccn contains "/", remove it & all characters to the right of "/"
normalizedLccn = normalizedLccn.replaceAll("/.*", StringUtils.EMPTY);

// Process the serial number component of LCCN
normalizedLccn = processSerialNumber(normalizedLccn);

if (normalizedLccn.matches(NORMALIZED_LCCN_REGEX)) {
return Optional.of(normalizedLccn);
}

log.warn("LCCN is not in expected format: [{}]", lccn);
return Optional.empty();
}

/**
* Serial number is demarcated by a hyphen (fifth character in the value). Further, the serial number must be six
* digits in length. If fewer than six digits, remove the hyphen and left fill with zeroes so that there are six
* digits in the serial number.
*/
private String processSerialNumber(String lccn) {
if (lccn.length() >= 5 && lccn.charAt(4) == HYPHEN) {
var lccnParts = lccn.split(String.valueOf(HYPHEN));
if (lccnParts.length == 2) {
String prefix = lccnParts[0];
StringBuilder serialNumber = new StringBuilder(lccnParts[1]);

// Left fill the serial number with zeroes to make it six digits
while (serialNumber.length() < 6) {
serialNumber.insert(0, "0");
}

return serialNumber.insert(0, prefix).toString();
}
}
return lccn;
}
}
1 change: 1 addition & 0 deletions src/main/java/org/folio/search/utils/KafkaConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public final class KafkaConstants {
public static final String CONSORTIUM_INSTANCE_LISTENER_ID = "mod-search-consortium-instance-listener";
public static final String CLASSIFICATION_TYPE_LISTENER_ID = "mod-search-classification-type-listener";
public static final String LOCATION_LISTENER_ID = "mod-search-location-listener";
public static final String BIBFRAME_LISTENER_ID = "mod-search-bibframe-listener";

private KafkaConstants() {}
}
1 change: 1 addition & 0 deletions src/main/java/org/folio/search/utils/SearchUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class SearchUtils {
public static final String CONTRIBUTOR_RESOURCE = getResourceName(Contributor.class);
public static final String LOCATION_RESOURCE = "location";
public static final String CLASSIFICATION_TYPE_RESOURCE = "classification-type";
public static final String BIBFRAME_RESOURCE = "bibframe";

public static final String ID_FIELD = "id";
public static final String SOURCE_FIELD = "source";
Expand Down
Loading

0 comments on commit f790b95

Please sign in to comment.