Add publication sort while search publications service.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package org.codiki.infrastructure.publication;
|
||||
|
||||
import static java.util.Collections.reverseOrder;
|
||||
import static java.util.Comparator.comparingInt;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@@ -8,6 +10,7 @@ import org.codiki.domain.publication.model.Publication;
|
||||
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
|
||||
import org.codiki.domain.publication.port.PublicationPort;
|
||||
import org.codiki.infrastructure.publication.model.PublicationEntity;
|
||||
import org.codiki.infrastructure.publication.model.PublicationSearchResult;
|
||||
import org.codiki.infrastructure.publication.repository.PublicationRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -25,7 +28,7 @@ public class PublicationJpaAdapter implements PublicationPort {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(final Publication publication) {
|
||||
public void save(Publication publication) {
|
||||
PublicationEntity newPublicationEntity = new PublicationEntity(publication);
|
||||
repository.save(newPublicationEntity);
|
||||
}
|
||||
@@ -47,6 +50,9 @@ public class PublicationJpaAdapter implements PublicationPort {
|
||||
return repository.search(adaptedCriteria)
|
||||
.stream()
|
||||
.map(PublicationEntity::toDomain)
|
||||
.map(publication -> new PublicationSearchResult(publication, criteria))
|
||||
.sorted(reverseOrder(comparingInt(PublicationSearchResult::getSearchScore)))
|
||||
.map(PublicationSearchResult::getPublication)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.codiki.infrastructure.publication.model;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.codiki.domain.publication.model.search.ComparisonType.CONTAINS;
|
||||
import static org.codiki.domain.publication.model.search.PublicationSearchField.DESCRIPTION;
|
||||
import static org.codiki.domain.publication.model.search.PublicationSearchField.TEXT;
|
||||
import static org.codiki.domain.publication.model.search.PublicationSearchField.TITLE;
|
||||
import org.codiki.domain.publication.model.Publication;
|
||||
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
|
||||
import org.codiki.domain.publication.model.search.PublicationSearchField;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class PublicationSearchResult {
|
||||
private final Publication publication;
|
||||
private final int searchScore;
|
||||
|
||||
public PublicationSearchResult(
|
||||
Publication publication,
|
||||
List<PublicationSearchCriterion> criteria
|
||||
) {
|
||||
this.publication = publication;
|
||||
|
||||
int score = 0;
|
||||
|
||||
score += computeCriterionScoreForField(TITLE, publication, criteria, 1000, 10, 3);
|
||||
score += computeCriterionScoreForField(DESCRIPTION, publication, criteria, 100, 7, 2);
|
||||
score += computeCriterionScoreForField(TEXT, publication, criteria, 100, 4, 1);
|
||||
|
||||
searchScore = score;
|
||||
}
|
||||
|
||||
private static int computeCriterionScoreForField(
|
||||
PublicationSearchField field,
|
||||
Publication publication,
|
||||
List<PublicationSearchCriterion> criteria,
|
||||
int bountyForPerfectMatch,
|
||||
int bountyForWordMatch,
|
||||
int bountyForWordContaingMatch
|
||||
) {
|
||||
return getLowercaseFieldValue(field, publication)
|
||||
.map(fieldValue ->
|
||||
criteria.stream()
|
||||
.filter(criterion -> criterion.searchType() == CONTAINS)
|
||||
.filter(criterion -> criterion.searchField() == field)
|
||||
.map(PublicationSearchCriterion::value)
|
||||
.filter(String.class::isInstance)
|
||||
.map(String.class::cast)
|
||||
.map(String::toLowerCase)
|
||||
.map(criterionValue ->
|
||||
computeCriterionScore(
|
||||
criterionValue,
|
||||
fieldValue,
|
||||
bountyForPerfectMatch,
|
||||
bountyForWordMatch,
|
||||
bountyForWordContaingMatch
|
||||
)
|
||||
)
|
||||
.mapToInt(Integer::intValue)
|
||||
.sum()
|
||||
)
|
||||
.orElse(0);
|
||||
}
|
||||
|
||||
private static Optional<String> getLowercaseFieldValue(PublicationSearchField field, Publication publication) {
|
||||
return Optional.ofNullable(
|
||||
switch (field) {
|
||||
case TITLE -> publication.title();
|
||||
case DESCRIPTION -> publication.description();
|
||||
case TEXT -> publication.text();
|
||||
default -> null;
|
||||
}
|
||||
).map(String::toLowerCase);
|
||||
}
|
||||
|
||||
private static int computeCriterionScore(
|
||||
String criterionValue,
|
||||
String publicationTitle,
|
||||
int bountyForPerfectMatch,
|
||||
int bountyForWordMatch,
|
||||
int bountyForWordContaingMatch
|
||||
) {
|
||||
int result = 0;
|
||||
|
||||
if (publicationTitle.equals(criterionValue)) {
|
||||
result = bountyForPerfectMatch;
|
||||
} else if (publicationTitle.contains(criterionValue)) {
|
||||
result = Stream.of(publicationTitle.split(" "))
|
||||
.map(titleWord -> computeWordScore(criterionValue, titleWord, bountyForWordMatch, bountyForWordContaingMatch))
|
||||
.mapToInt(Integer::intValue)
|
||||
.sum();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int computeWordScore(
|
||||
String criterionValue,
|
||||
String titleWord,
|
||||
int bountyForWordMatch,
|
||||
int bountyForWordContaingMatch
|
||||
) {
|
||||
int result = 0;
|
||||
|
||||
if (titleWord.equals(criterionValue)) {
|
||||
result = bountyForWordMatch;
|
||||
} else if (titleWord.contains(criterionValue)) {
|
||||
result = bountyForWordContaingMatch;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package org.codiki.infrastructure.publication.model;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.codiki.domain.publication.model.builder.PublicationBuilder.aPublication;
|
||||
import static org.codiki.domain.publication.model.search.ComparisonType.CONTAINS;
|
||||
import static org.codiki.domain.publication.model.search.PublicationSearchField.DESCRIPTION;
|
||||
import static org.codiki.domain.publication.model.search.PublicationSearchField.TEXT;
|
||||
import static org.codiki.domain.publication.model.search.PublicationSearchField.TITLE;
|
||||
import org.codiki.domain.publication.model.Publication;
|
||||
import org.codiki.domain.publication.model.builder.PublicationBuilder;
|
||||
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
class PublicationSearchResultTest {
|
||||
@ParameterizedTest
|
||||
@MethodSource("arguments_of_constructor_should_compute_score")
|
||||
void constructor_should_compute_score(
|
||||
String functionalRule,
|
||||
String fieldValue,
|
||||
List<PublicationSearchCriterion> criteria,
|
||||
int expectedPoints
|
||||
) {
|
||||
// given
|
||||
PublicationBuilder publicationBuilder = aPublication();
|
||||
|
||||
switch (criteria.getFirst().searchField()) {
|
||||
case TITLE -> publicationBuilder.withTitle(fieldValue);
|
||||
case DESCRIPTION -> publicationBuilder.withDescription(fieldValue);
|
||||
case TEXT -> publicationBuilder.withText(fieldValue);
|
||||
}
|
||||
|
||||
Publication publication = publicationBuilder.build();
|
||||
|
||||
// when
|
||||
PublicationSearchResult publicationSearchResult = new PublicationSearchResult(publication, criteria);
|
||||
|
||||
// then
|
||||
assertThat(publicationSearchResult.getSearchScore()).isEqualTo(expectedPoints);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> arguments_of_constructor_should_compute_score() {
|
||||
return Stream.of(
|
||||
// TITLE RULES
|
||||
Arguments.of(
|
||||
"exact match for title gives 1000 pts",
|
||||
"Exact title",
|
||||
List.of(new PublicationSearchCriterion(TITLE, CONTAINS, "Exact title")),
|
||||
1000
|
||||
),
|
||||
Arguments.of(
|
||||
"one match in title gives 10 pts",
|
||||
"Exact title",
|
||||
List.of(new PublicationSearchCriterion(TITLE, CONTAINS, "exact")),
|
||||
10
|
||||
),
|
||||
Arguments.of(
|
||||
"multiple matches in title gives 10 pts per match",
|
||||
"One super title with lot of words",
|
||||
List.of(
|
||||
new PublicationSearchCriterion(TITLE, CONTAINS, "super"),
|
||||
new PublicationSearchCriterion(TITLE, CONTAINS, "words")
|
||||
),
|
||||
20
|
||||
),
|
||||
Arguments.of(
|
||||
"one match in title gives 1 pts per match if criterion value is a title word part",
|
||||
"One super title",
|
||||
List.of(
|
||||
new PublicationSearchCriterion(TITLE, CONTAINS, "sup")
|
||||
),
|
||||
3
|
||||
),
|
||||
// DESCRIPTION RULES
|
||||
Arguments.of(
|
||||
"exact match for description gives 100 pts",
|
||||
"Exact description",
|
||||
List.of(new PublicationSearchCriterion(DESCRIPTION, CONTAINS, "Exact description")),
|
||||
100
|
||||
),
|
||||
Arguments.of(
|
||||
"one match in description gives 7 pts",
|
||||
"Exact description",
|
||||
List.of(new PublicationSearchCriterion(DESCRIPTION, CONTAINS, "exact")),
|
||||
7
|
||||
),
|
||||
Arguments.of(
|
||||
"multiple matches in description gives 7 pts per match",
|
||||
"One super description with lot of words",
|
||||
List.of(
|
||||
new PublicationSearchCriterion(DESCRIPTION, CONTAINS, "super"),
|
||||
new PublicationSearchCriterion(DESCRIPTION, CONTAINS, "words")
|
||||
),
|
||||
14
|
||||
),
|
||||
Arguments.of(
|
||||
"one match in description gives 2 pts per match if criterion value is a description word part",
|
||||
"One super description",
|
||||
List.of(
|
||||
new PublicationSearchCriterion(DESCRIPTION, CONTAINS, "sup")
|
||||
),
|
||||
2
|
||||
),
|
||||
// TEXT RULES
|
||||
Arguments.of(
|
||||
"exact match for text gives 100 pts",
|
||||
"Exact text",
|
||||
List.of(new PublicationSearchCriterion(TEXT, CONTAINS, "Exact text")),
|
||||
100
|
||||
),
|
||||
Arguments.of(
|
||||
"one match in text gives 7 pts",
|
||||
"Exact text",
|
||||
List.of(new PublicationSearchCriterion(TEXT, CONTAINS, "exact")),
|
||||
4
|
||||
),
|
||||
Arguments.of(
|
||||
"multiple matches in text gives 7 pts per match",
|
||||
"One super text with lot of words",
|
||||
List.of(
|
||||
new PublicationSearchCriterion(TEXT, CONTAINS, "super"),
|
||||
new PublicationSearchCriterion(TEXT, CONTAINS, "words")
|
||||
),
|
||||
8
|
||||
),
|
||||
Arguments.of(
|
||||
"one match in text gives 2 pts per match if criterion value is a text word part",
|
||||
"One super text",
|
||||
List.of(
|
||||
new PublicationSearchCriterion(TEXT, CONTAINS, "sup")
|
||||
),
|
||||
1
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
vars {
|
||||
url: http://localhost:8080
|
||||
publicationId: e23831a6-9cc0-4f3d-9efa-7a1cae191cb1
|
||||
bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YWQ0NjJiOC04ZjllLTRhMjYtYmI4Ni1jNzRmZWY1ZDExYjYiLCJleHAiOjE3MTA1MjE5NzN9.KgYeDYNjgM4ndv_An0fAoOdx7qHRJDXuUPnBEcn6es5kn2g-HDjgt1n_s5CD3_F13alBdR18--9dZlWV8qQmHg
|
||||
bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YWQ0NjJiOC04ZjllLTRhMjYtYmI4Ni1jNzRmZWY1ZDExYjYiLCJleHAiOjE3MTA1MjY1MDh9.DvfA5uxPMjydRaAkocimAHGo9bfJmTu7hEBIKlaMBd7Qu4XFD1OSL58u8VHnFBU2EiBitRtJRUdidERphkww0Q
|
||||
categoryId: 172fa901-3f4b-4540-92f3-1c15820e8ec9
|
||||
pictureId: 65b660b7-66bb-4e4a-a62c-fd0ca101f972
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user