Skip to content

Commit

Permalink
Field of law redesign (#2342)
Browse files Browse the repository at this point in the history
- Remove blurry search and replace it with 3 separate search input fields
- Toggle between direct input and search/exploration in tree
- Highlight search results in tree
- Allow selecting field of law directly from search result list
- Display field of law component in "editable list style"

RISDEV-3581, RISDEV-5342, RISDEV-5347, RISDEV-5349, RISDEV-5344, RISDEV-5414, RISDEV-5417, RISDEV-5489, RISDEV-5541

---------

Co-authored-by: Leonie Koch <[email protected]>
  • Loading branch information
elaydis and leonie-koch authored Nov 27, 2024
1 parent 7e9e965 commit 0145335
Show file tree
Hide file tree
Showing 46 changed files with 1,763 additions and 1,273 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ public FieldOfLawController(FieldOfLawService service) {
@PreAuthorize("isAuthenticated()")
public Slice<FieldOfLaw> getFieldsOfLawBySearchQuery(
@RequestParam("q") Optional<String> searchStr,
@RequestParam("identifier") Optional<String> identifier,
@RequestParam("norm") Optional<String> norm,
@RequestParam("pg") int page,
@RequestParam("sz") int size) {
return service.getFieldsOfLawBySearchQuery(searchStr, PageRequest.of(page, size));
return service.getFieldsOfLawBySearchQuery(
identifier, searchStr, norm, PageRequest.of(page, size));
}

@GetMapping(value = "/search-by-identifier", produces = MediaType.APPLICATION_JSON_VALUE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
import de.bund.digitalservice.ris.caselaw.domain.FieldOfLawRepository;
import de.bund.digitalservice.ris.caselaw.domain.lookuptable.fieldoflaw.FieldOfLaw;
import de.bund.digitalservice.ris.caselaw.domain.lookuptable.fieldoflaw.Norm;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
Expand All @@ -22,85 +20,133 @@
@Slf4j
public class FieldOfLawService {
private static final String ROOT_ID = "root";
private static final Pattern NORMS_PATTERN = Pattern.compile("norm\\s?:\\s?\"([^\"]*)\"(.*)");

private final FieldOfLawRepository repository;

public FieldOfLawService(FieldOfLawRepository repository) {
this.repository = repository;
}

public List<FieldOfLaw> getFieldsOfLawByIdentifierSearch(Optional<String> optionalSearchStr) {
if (optionalSearchStr.isEmpty() || optionalSearchStr.get().isBlank()) {
return repository.findAllByOrderByIdentifierAsc(PageRequest.of(0, 30)).stream().toList();
}
return repository.findByIdentifier(optionalSearchStr.get().trim(), PageRequest.of(0, 30));
}

public Slice<FieldOfLaw> getFieldsOfLawBySearchQuery(
Optional<String> optionalSearchStr, Pageable pageable) {
Optional<String> identifier,
Optional<String> description,
Optional<String> norm,
Pageable pageable) {

if (optionalSearchStr.isEmpty() || optionalSearchStr.get().isBlank()) {
if (identifier.isPresent() && identifier.get().isBlank()) {
identifier = Optional.empty();
}
if (description.isPresent() && description.get().isBlank()) {
description = Optional.empty();
}
if (norm.isPresent() && norm.get().isBlank()) {
norm = Optional.empty();
}

if (identifier.isEmpty() && description.isEmpty() && norm.isEmpty()) {
return repository.findAllByOrderByIdentifierAsc(pageable);
}

return searchAndOrderByScore(optionalSearchStr.get().trim(), pageable);
return searchAndOrderByScore(description, identifier, norm, pageable);
}

private String[] splitSearchTerms(String searchStr) {
return Arrays.stream(searchStr.split("\\s+")).map(String::trim).toArray(String[]::new);
}

Slice<FieldOfLaw> searchAndOrderByScore(String searchStr, Pageable pageable) {
Matcher matcher = NORMS_PATTERN.matcher(searchStr);
String[] searchTerms;
String normStr;
private Slice<FieldOfLaw> searchAndOrderByScore(
Optional<String> description,
Optional<String> identifier,
Optional<String> norm,
Pageable pageable) {
Optional<String[]> searchTerms = description.map(this::splitSearchTerms);
Optional<String> normStr = norm.map(n -> n.trim().replaceAll("§(\\d+)", "§ $1"));

List<FieldOfLaw> unorderedList;
if (matcher.find()) {
normStr = matcher.group(1).trim().replaceAll("§(\\d+)", "§ $1");
String afterNormSearchStr = matcher.group(2).trim();
if (afterNormSearchStr.isEmpty()) {
searchTerms = null;
unorderedList = repository.findByNormStr(normStr);

if (identifier.isPresent()) {
String identifierStr = identifier.get().trim();

if (searchTerms.isEmpty() && normStr.isEmpty()) {
// Returned list is already ordered by identifier, so scoring is not necessary here
unorderedList = repository.findByIdentifier(identifierStr, pageable);
return sliceResults(unorderedList, pageable);
} else {
searchTerms = splitSearchTerms(afterNormSearchStr);
unorderedList = repository.findByNormStrAndSearchTerms(normStr, searchTerms);
unorderedList = getResultsWithIdentifier(identifierStr, searchTerms, normStr);
}
} else {
normStr = null;
searchTerms = splitSearchTerms(searchStr);
unorderedList = repository.findBySearchTerms(searchTerms);
unorderedList = getResultsWithoutIdentifier(searchTerms, normStr);
}

if (unorderedList == null || unorderedList.isEmpty()) {
// If no results found, return an empty page
if (unorderedList.isEmpty()) {
return new PageImpl<>(List.of(), pageable, 0);
}

Map<FieldOfLaw, Integer> scores = calculateScore(searchTerms, normStr, unorderedList);

List<FieldOfLaw> orderedList =
unorderedList.stream()
.sorted(
(f1, f2) -> {
int compare = scores.get(f2).compareTo(scores.get(f1));
List<FieldOfLaw> orderedList = orderResults(searchTerms, normStr, unorderedList);

if (compare == 0) {
compare = f1.identifier().compareTo(f2.identifier());
}

return compare;
})
.toList();
return sliceResults(orderedList, pageable);
}

int fromIdx = (int) pageable.getOffset();
int toIdx = (int) Math.min(pageable.getOffset() + pageable.getPageSize(), orderedList.size());
private List<FieldOfLaw> getResultsWithIdentifier(
String identifierStr, Optional<String[]> searchTerms, Optional<String> norm) {
if (searchTerms.isPresent() && norm.isPresent()) {
return repository.findByIdentifierAndSearchTermsAndNorm(
identifierStr, searchTerms.get(), norm.get());
}
if (searchTerms.isPresent()) {
return repository.findByIdentifierAndSearchTerms(identifierStr, searchTerms.get());
}
if (norm.isPresent()) {
return repository.findByIdentifierAndNorm(identifierStr, norm.get());
}
return Collections.emptyList();
}

List<FieldOfLaw> pageContent = new ArrayList<>();
if (fromIdx < toIdx) {
pageContent = orderedList.subList(fromIdx, toIdx);
private List<FieldOfLaw> getResultsWithoutIdentifier(
Optional<String[]> searchTerms, Optional<String> norm) {
if (searchTerms.isPresent() && norm.isPresent()) {
return repository.findByNormAndSearchTerms(norm.get(), searchTerms.get());
}
if (searchTerms.isPresent()) {
return repository.findBySearchTerms(searchTerms.get());
}
if (norm.isPresent()) {
return repository.findByNorm(norm.get());
}
return Collections.emptyList();
}

int totalElements = orderedList.size();
private List<FieldOfLaw> orderResults(
Optional<String[]> searchTerms, Optional<String> normStr, List<FieldOfLaw> unorderedList) {
// Calculate scores and sort the list based on the score and identifier
Map<FieldOfLaw, Integer> scores = calculateScore(searchTerms, normStr, unorderedList);
return unorderedList.stream()
.sorted(
(f1, f2) -> {
int compare = scores.get(f2).compareTo(scores.get(f1));
return compare != 0 ? compare : f1.identifier().compareTo(f2.identifier());
})
.toList();
}

return new PageImpl<>(pageContent, pageable, totalElements);
private Slice<FieldOfLaw> sliceResults(List<FieldOfLaw> orderedList, Pageable pageable) {
// Extract the correct sublist for pagination
int fromIdx = (int) pageable.getOffset();
int toIdx = (int) Math.min(pageable.getOffset() + pageable.getPageSize(), orderedList.size());
List<FieldOfLaw> pageContent = orderedList.subList(Math.max(0, fromIdx), toIdx);
return new PageImpl<>(pageContent, pageable, orderedList.size());
}

private Map<FieldOfLaw, Integer> calculateScore(
String[] searchTerms, String normStr, List<FieldOfLaw> fieldOfLaws) {
Optional<String[]> searchTerms, Optional<String> normStr, List<FieldOfLaw> fieldOfLaws) {
Map<FieldOfLaw, Integer> scores = new HashMap<>();

if (fieldOfLaws == null || fieldOfLaws.isEmpty()) {
Expand All @@ -111,14 +157,14 @@ private Map<FieldOfLaw, Integer> calculateScore(
fieldOfLaw -> {
int score = 0;

if (searchTerms != null) {
for (String searchTerm : searchTerms) {
if (searchTerms.isPresent()) {
for (String searchTerm : searchTerms.get()) {
score += getScoreContributionFromSearchTerm(fieldOfLaw, searchTerm);
}
}

if (normStr != null) {
score += getScoreContributionFromNormStr(fieldOfLaw, normStr);
if (normStr.isPresent()) {
score += getScoreContributionFromNormStr(fieldOfLaw, normStr.get());
}

scores.put(fieldOfLaw, score);
Expand All @@ -130,14 +176,8 @@ private Map<FieldOfLaw, Integer> calculateScore(
private int getScoreContributionFromSearchTerm(FieldOfLaw fieldOfLaw, String searchTerm) {
int score = 0;
searchTerm = searchTerm.toLowerCase();
String identifier =
fieldOfLaw.identifier() == null ? "" : fieldOfLaw.identifier().toLowerCase();
String text = fieldOfLaw.text() == null ? "" : fieldOfLaw.text().toLowerCase();

if (identifier.equals(searchTerm)) score += 8;
if (identifier.startsWith(searchTerm)) score += 5;
if (identifier.contains(searchTerm)) score += 2;

if (text.startsWith(searchTerm)) score += 5;
// split by whitespace and hyphen to get words
for (String textPart : text.split("[\\s-]+")) {
Expand All @@ -151,6 +191,9 @@ private int getScoreContributionFromSearchTerm(FieldOfLaw fieldOfLaw, String sea
private int getScoreContributionFromNormStr(FieldOfLaw fieldOfLaw, String normStr) {
int score = 0;
normStr = normStr.toLowerCase();
if (normStr.isBlank()) {
return score;
}
for (Norm norm : fieldOfLaw.norms()) {
String abbreviation = norm.abbreviation() == null ? "" : norm.abbreviation().toLowerCase();
String description =
Expand All @@ -163,13 +206,6 @@ private int getScoreContributionFromNormStr(FieldOfLaw fieldOfLaw, String normSt
return score;
}

public List<FieldOfLaw> getFieldsOfLawByIdentifierSearch(Optional<String> optionalSearchStr) {
if (optionalSearchStr.isEmpty() || optionalSearchStr.get().isBlank()) {
return repository.findAllByOrderByIdentifierAsc(PageRequest.of(0, 30)).stream().toList();
}
return repository.findByIdentifierSearch(optionalSearchStr.get().trim());
}

public List<FieldOfLaw> getChildrenOfFieldOfLaw(String identifier) {
if (identifier.equalsIgnoreCase(ROOT_ID)) {
return repository.getTopLevelNodes();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,11 @@ Page<FieldOfLawDTO> findAllByIdentifierStartsWithIgnoreCaseOrderByIdentifier(
+ "UPPER(f.text) LIKE UPPER(CONCAT('%', :searchTerm, '%')))")
List<FieldOfLawDTO> findAllByNotationAndIdentifierContainingIgnoreCaseOrTextContainingIgnoreCase(
String searchTerm);

@Query(
"SELECT fol FROM FieldOfLawDTO fol "
+ "WHERE fol.notation = 'NEW' AND fol.identifier "
+ "LIKE upper(concat(:searchStr, '%')) "
+ "ORDER BY fol.identifier")
List<FieldOfLawDTO> findAllByIdentifierStartsWithIgnoreCaseOrderByIdentifier(String searchStr);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
Expand All @@ -35,16 +36,19 @@
@AllArgsConstructor
@Builder(toBuilder = true)
@Entity
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Table(
schema = "incremental_migration",
uniqueConstraints = {@UniqueConstraint(columnNames = {"juris_id", "notation"})},
name = "field_of_law")
public class FieldOfLawDTO {
@Id @GeneratedValue private UUID id;

@EqualsAndHashCode.Include
@Column(unique = true, updatable = false, insertable = false)
private String identifier;

@EqualsAndHashCode.Include
@Column(updatable = false, insertable = false)
private String text;

Expand Down Expand Up @@ -104,6 +108,7 @@ public class FieldOfLawDTO {
@ToString.Include
private Integer jurisId;

@EqualsAndHashCode.Include
@Column(updatable = false)
@Enumerated(EnumType.STRING)
@ToString.Include
Expand Down
Loading

0 comments on commit 0145335

Please sign in to comment.