Add publication sort while search publications service.

This commit is contained in:
Florian THIERRY
2024-03-15 22:17:27 +01:00
parent da1937cb31
commit dabd93091c
4 changed files with 265 additions and 2 deletions

View File

@@ -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();
} }
} }

View File

@@ -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;
}
}

View File

@@ -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
)
);
}
}

View File

@@ -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
} }