Add publication sort while search publications service.
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
package org.codiki.infrastructure.publication;
|
package org.codiki.infrastructure.publication;
|
||||||
|
|
||||||
|
import static java.util.Collections.reverseOrder;
|
||||||
|
import static java.util.Comparator.comparingInt;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
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.model.search.PublicationSearchCriterion;
|
||||||
import org.codiki.domain.publication.port.PublicationPort;
|
import org.codiki.domain.publication.port.PublicationPort;
|
||||||
import org.codiki.infrastructure.publication.model.PublicationEntity;
|
import org.codiki.infrastructure.publication.model.PublicationEntity;
|
||||||
|
import org.codiki.infrastructure.publication.model.PublicationSearchResult;
|
||||||
import org.codiki.infrastructure.publication.repository.PublicationRepository;
|
import org.codiki.infrastructure.publication.repository.PublicationRepository;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -25,7 +28,7 @@ public class PublicationJpaAdapter implements PublicationPort {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void save(final Publication publication) {
|
public void save(Publication publication) {
|
||||||
PublicationEntity newPublicationEntity = new PublicationEntity(publication);
|
PublicationEntity newPublicationEntity = new PublicationEntity(publication);
|
||||||
repository.save(newPublicationEntity);
|
repository.save(newPublicationEntity);
|
||||||
}
|
}
|
||||||
@@ -47,6 +50,9 @@ public class PublicationJpaAdapter implements PublicationPort {
|
|||||||
return repository.search(adaptedCriteria)
|
return repository.search(adaptedCriteria)
|
||||||
.stream()
|
.stream()
|
||||||
.map(PublicationEntity::toDomain)
|
.map(PublicationEntity::toDomain)
|
||||||
|
.map(publication -> new PublicationSearchResult(publication, criteria))
|
||||||
|
.sorted(reverseOrder(comparingInt(PublicationSearchResult::getSearchScore)))
|
||||||
|
.map(PublicationSearchResult::getPublication)
|
||||||
.toList();
|
.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 {
|
vars {
|
||||||
url: http://localhost:8080
|
url: http://localhost:8080
|
||||||
publicationId: e23831a6-9cc0-4f3d-9efa-7a1cae191cb1
|
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
|
categoryId: 172fa901-3f4b-4540-92f3-1c15820e8ec9
|
||||||
pictureId: 65b660b7-66bb-4e4a-a62c-fd0ca101f972
|
pictureId: 65b660b7-66bb-4e4a-a62c-fd0ca101f972
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user