fix: 鱼类字典相似度查询优化
This commit is contained in:
parent
687ec712e0
commit
f0dcf99d28
@ -74,107 +74,231 @@ public class SdFishDictoryBServiceImpl extends ServiceImpl<SdFishDictoryBMapper,
|
||||
}
|
||||
|
||||
int resultLimit = (limit != null && limit > 0) ? limit : 10;
|
||||
String searchName = name.trim();
|
||||
LambdaQueryWrapper<SdFishDictoryB> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SdFishDictoryB::getIsDeleted, 0);
|
||||
List<SdFishDictoryB> allFish = list(wrapper);
|
||||
String rawSearchName = name.trim();
|
||||
String searchName = normalizeText(rawSearchName);
|
||||
|
||||
SdFishDictoryB exactMatch = allFish.stream()
|
||||
.filter(f -> f.getName() != null && f.getName().equals(searchName))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (exactMatch != null) {
|
||||
Map<SdFishDictoryB, Integer> similarityMap = new HashMap<>();
|
||||
for (SdFishDictoryB fish : allFish) {
|
||||
if (fish.getId().equals(exactMatch.getId())) {
|
||||
continue;
|
||||
}
|
||||
int score = calculateSimilarity(exactMatch, fish);
|
||||
if (score > 0) {
|
||||
similarityMap.put(fish, score);
|
||||
}
|
||||
}
|
||||
|
||||
List<SdFishDictoryB> result = similarityMap.entrySet().stream()
|
||||
.sorted(Map.Entry.<SdFishDictoryB, Integer>comparingByValue().reversed())
|
||||
.limit(resultLimit)
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
result.add(0, exactMatch);
|
||||
return result;
|
||||
List<SdFishDictoryB> candidateFish = querySimilarFishCandidates(rawSearchName, searchName, resultLimit);
|
||||
if (candidateFish.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<SdFishDictoryB> containsMatches = allFish.stream()
|
||||
.filter(f -> f.getName() != null && f.getName().contains(searchName))
|
||||
Map<String, Integer> searchScoreMap = new HashMap<>();
|
||||
for (SdFishDictoryB fish : candidateFish) {
|
||||
if (fish == null || !StringUtils.hasText(fish.getId())) {
|
||||
continue;
|
||||
}
|
||||
int score = calculateSearchScore(searchName, fish);
|
||||
if (score > 0) {
|
||||
searchScoreMap.put(fish.getId(), score);
|
||||
}
|
||||
}
|
||||
|
||||
if (searchScoreMap.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
SdFishDictoryB referenceFish = candidateFish.stream()
|
||||
.filter(fish -> fish != null && searchScoreMap.containsKey(fish.getId()))
|
||||
.max((f1, f2) -> {
|
||||
int scoreCompare = Integer.compare(searchScoreMap.get(f1.getId()), searchScoreMap.get(f2.getId()));
|
||||
if (scoreCompare != 0) {
|
||||
return scoreCompare;
|
||||
}
|
||||
Integer order1 = f1.getOrderIndex() == null ? 0 : f1.getOrderIndex();
|
||||
Integer order2 = f2.getOrderIndex() == null ? 0 : f2.getOrderIndex();
|
||||
int orderCompare = Integer.compare(order1, order2);
|
||||
if (orderCompare != 0) {
|
||||
return orderCompare;
|
||||
}
|
||||
return safeText(f2.getName()).compareTo(safeText(f1.getName()));
|
||||
})
|
||||
.orElse(null);
|
||||
|
||||
if (referenceFish == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return candidateFish.stream()
|
||||
.filter(fish -> fish != null && StringUtils.hasText(fish.getId()))
|
||||
.map(fish -> new AbstractMap.SimpleEntry<>(fish, buildFinalScore(referenceFish, fish, searchScoreMap.getOrDefault(fish.getId(), 0))))
|
||||
.filter(entry -> entry.getValue() > 0)
|
||||
.sorted((e1, e2) -> {
|
||||
int scoreCompare = Integer.compare(e2.getValue(), e1.getValue());
|
||||
if (scoreCompare != 0) {
|
||||
return scoreCompare;
|
||||
}
|
||||
Integer order1 = e1.getKey().getOrderIndex() == null ? 0 : e1.getKey().getOrderIndex();
|
||||
Integer order2 = e2.getKey().getOrderIndex() == null ? 0 : e2.getKey().getOrderIndex();
|
||||
int orderCompare = Integer.compare(order2, order1);
|
||||
if (orderCompare != 0) {
|
||||
return orderCompare;
|
||||
}
|
||||
return safeText(e1.getKey().getName()).compareTo(safeText(e2.getKey().getName()));
|
||||
})
|
||||
.limit(resultLimit)
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (!containsMatches.isEmpty()) {
|
||||
List<SdFishDictoryB> result = containsMatches.stream()
|
||||
.sorted((f1, f2) -> {
|
||||
int len1 = f1.getName().length();
|
||||
int len2 = f2.getName().length();
|
||||
if (len1 != len2) {
|
||||
return Integer.compare(len1, len2);
|
||||
}
|
||||
return f1.getName().compareTo(f2.getName());
|
||||
})
|
||||
.limit(resultLimit)
|
||||
.collect(Collectors.toList());
|
||||
return result;
|
||||
private List<SdFishDictoryB> querySimilarFishCandidates(String rawSearchName, String normalizedSearchName, int resultLimit) {
|
||||
int candidateLimit = Math.min(Math.max(resultLimit * 30, 120), 500);
|
||||
LinkedHashMap<String, SdFishDictoryB> candidateMap = new LinkedHashMap<>();
|
||||
|
||||
mergeCandidates(candidateMap, queryExactCandidates(rawSearchName, normalizedSearchName, candidateLimit));
|
||||
if (candidateMap.size() < candidateLimit) {
|
||||
mergeCandidates(candidateMap, queryContainsCandidates(rawSearchName, normalizedSearchName, candidateLimit));
|
||||
}
|
||||
if (candidateMap.size() < candidateLimit) {
|
||||
mergeCandidates(candidateMap, queryTokenCandidates(normalizedSearchName, candidateLimit));
|
||||
}
|
||||
|
||||
SdFishDictoryB partialMatch = allFish.stream()
|
||||
.filter(f -> f.getName() != null && (searchName.contains(f.getName()) || f.getName().contains(searchName)))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
return new ArrayList<>(candidateMap.values());
|
||||
}
|
||||
|
||||
if (partialMatch != null) {
|
||||
Map<SdFishDictoryB, Integer> similarityMap = new HashMap<>();
|
||||
for (SdFishDictoryB fish : allFish) {
|
||||
if (fish.getId().equals(partialMatch.getId())) {
|
||||
continue;
|
||||
}
|
||||
int score = calculateSimilarity(partialMatch, fish);
|
||||
if (score > 0) {
|
||||
similarityMap.put(fish, score);
|
||||
}
|
||||
private void mergeCandidates(Map<String, SdFishDictoryB> candidateMap, List<SdFishDictoryB> candidates) {
|
||||
if (candidates == null || candidates.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (SdFishDictoryB fish : candidates) {
|
||||
if (fish == null || !StringUtils.hasText(fish.getId())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<SdFishDictoryB> result = similarityMap.entrySet().stream()
|
||||
.sorted(Map.Entry.<SdFishDictoryB, Integer>comparingByValue().reversed())
|
||||
.limit(resultLimit)
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
result.add(0, partialMatch);
|
||||
return result;
|
||||
candidateMap.putIfAbsent(fish.getId(), fish);
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
private List<SdFishDictoryB> queryExactCandidates(String rawSearchName, String normalizedSearchName, int limit) {
|
||||
if (!StringUtils.hasText(rawSearchName)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
LambdaQueryWrapper<SdFishDictoryB> wrapper = buildCandidateSelectWrapper();
|
||||
wrapper.and(w -> w
|
||||
.eq(SdFishDictoryB::getName, rawSearchName)
|
||||
.or().eq(SdFishDictoryB::getAlias, rawSearchName)
|
||||
.or().eq(SdFishDictoryB::getNameEn, rawSearchName)
|
||||
.or().eq(SdFishDictoryB::getSpecies, rawSearchName)
|
||||
.or().eq(SdFishDictoryB::getGenus, rawSearchName)
|
||||
.or().eq(SdFishDictoryB::getFamily, rawSearchName)
|
||||
.or().eq(SdFishDictoryB::getOrders, rawSearchName)
|
||||
.or().apply("LOWER(NAME) = {0}", normalizedSearchName)
|
||||
.or().apply("LOWER(ALIAS) = {0}", normalizedSearchName)
|
||||
.or().apply("LOWER(NAME_EN) = {0}", normalizedSearchName)
|
||||
.or().apply("LOWER(SPECIES) = {0}", normalizedSearchName)
|
||||
.or().apply("LOWER(GENUS) = {0}", normalizedSearchName)
|
||||
.or().apply("LOWER(FAMILY) = {0}", normalizedSearchName)
|
||||
.or().apply("LOWER(ORDERS) = {0}", normalizedSearchName));
|
||||
wrapper.orderByDesc(SdFishDictoryB::getOrderIndex);
|
||||
return page(new Page<>(1, limit), wrapper).getRecords();
|
||||
}
|
||||
|
||||
private List<SdFishDictoryB> queryContainsCandidates(String rawSearchName, String normalizedSearchName, int limit) {
|
||||
if (!StringUtils.hasText(rawSearchName)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String likeValue = "%" + rawSearchName + "%";
|
||||
String normalizedLikeValue = "%" + normalizedSearchName + "%";
|
||||
LambdaQueryWrapper<SdFishDictoryB> wrapper = buildCandidateSelectWrapper();
|
||||
wrapper.and(w -> w
|
||||
.like(SdFishDictoryB::getName, rawSearchName)
|
||||
.or().like(SdFishDictoryB::getAlias, rawSearchName)
|
||||
.or().like(SdFishDictoryB::getNameEn, rawSearchName)
|
||||
.or().like(SdFishDictoryB::getSpecies, rawSearchName)
|
||||
.or().like(SdFishDictoryB::getGenus, rawSearchName)
|
||||
.or().like(SdFishDictoryB::getFamily, rawSearchName)
|
||||
.or().like(SdFishDictoryB::getOrders, rawSearchName)
|
||||
.or().like(SdFishDictoryB::getIntroduce, rawSearchName)
|
||||
.or().apply("LOWER(NAME) LIKE {0}", normalizedLikeValue)
|
||||
.or().apply("LOWER(ALIAS) LIKE {0}", normalizedLikeValue)
|
||||
.or().apply("LOWER(NAME_EN) LIKE {0}", normalizedLikeValue)
|
||||
.or().apply("LOWER(SPECIES) LIKE {0}", normalizedLikeValue)
|
||||
.or().apply("LOWER(GENUS) LIKE {0}", normalizedLikeValue)
|
||||
.or().apply("LOWER(FAMILY) LIKE {0}", normalizedLikeValue)
|
||||
.or().apply("LOWER(ORDERS) LIKE {0}", normalizedLikeValue)
|
||||
.or().apply("LOWER(INTRODUCE) LIKE {0}", normalizedLikeValue));
|
||||
wrapper.orderByDesc(SdFishDictoryB::getOrderIndex);
|
||||
return page(new Page<>(1, limit), wrapper).getRecords();
|
||||
}
|
||||
|
||||
private List<SdFishDictoryB> queryTokenCandidates(String normalizedSearchName, int limit) {
|
||||
Set<String> tokens = splitTokens(normalizedSearchName).stream()
|
||||
.filter(StringUtils::hasText)
|
||||
.filter(token -> token.length() >= 2)
|
||||
.limit(5)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (tokens.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
LambdaQueryWrapper<SdFishDictoryB> wrapper = buildCandidateSelectWrapper();
|
||||
wrapper.and(w -> {
|
||||
boolean first = true;
|
||||
for (String token : tokens) {
|
||||
String likeValue = "%" + token + "%";
|
||||
if (!first) {
|
||||
w.or();
|
||||
}
|
||||
w.apply("LOWER(NAME) LIKE {0}", likeValue)
|
||||
.or().apply("LOWER(ALIAS) LIKE {0}", likeValue)
|
||||
.or().apply("LOWER(NAME_EN) LIKE {0}", likeValue)
|
||||
.or().apply("LOWER(SPECIES) LIKE {0}", likeValue)
|
||||
.or().apply("LOWER(GENUS) LIKE {0}", likeValue)
|
||||
.or().apply("LOWER(FAMILY) LIKE {0}", likeValue)
|
||||
.or().apply("LOWER(ORDERS) LIKE {0}", likeValue)
|
||||
.or().apply("LOWER(INTRODUCE) LIKE {0}", likeValue);
|
||||
first = false;
|
||||
}
|
||||
});
|
||||
wrapper.orderByDesc(SdFishDictoryB::getOrderIndex);
|
||||
return page(new Page<>(1, limit), wrapper).getRecords();
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<SdFishDictoryB> buildCandidateSelectWrapper() {
|
||||
LambdaQueryWrapper<SdFishDictoryB> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.select(
|
||||
SdFishDictoryB::getId,
|
||||
SdFishDictoryB::getCode,
|
||||
SdFishDictoryB::getName,
|
||||
SdFishDictoryB::getNameEn,
|
||||
SdFishDictoryB::getAlias,
|
||||
SdFishDictoryB::getIntroduce,
|
||||
SdFishDictoryB::getOrders,
|
||||
SdFishDictoryB::getFamily,
|
||||
SdFishDictoryB::getGenus,
|
||||
SdFishDictoryB::getSpecies,
|
||||
SdFishDictoryB::getType,
|
||||
SdFishDictoryB::getHabitat,
|
||||
SdFishDictoryB::getHabitMigrat,
|
||||
SdFishDictoryB::getFeedingHabit,
|
||||
SdFishDictoryB::getSpawnCharact,
|
||||
SdFishDictoryB::getPtype,
|
||||
SdFishDictoryB::getResourceType,
|
||||
SdFishDictoryB::getRare,
|
||||
SdFishDictoryB::getOrderIndex,
|
||||
SdFishDictoryB::getIsDeleted
|
||||
);
|
||||
wrapper.eq(SdFishDictoryB::getIsDeleted, 0);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private int calculateSimilarity(SdFishDictoryB ref, SdFishDictoryB target) {
|
||||
int score = 0;
|
||||
|
||||
if (ref.getGenus() != null && ref.getGenus().equals(target.getGenus()) && !ref.getGenus().isEmpty()) {
|
||||
score += 25;
|
||||
if (ref == null || target == null) {
|
||||
return 0;
|
||||
}
|
||||
if (StringUtils.hasText(ref.getId()) && ref.getId().equals(target.getId())) {
|
||||
return 5000;
|
||||
}
|
||||
|
||||
if (ref.getSpecies() != null && ref.getSpecies().equals(target.getSpecies()) && !ref.getSpecies().isEmpty()) {
|
||||
score += 20;
|
||||
}
|
||||
score += calculateFieldSimilarity(ref.getName(), target.getName(), 45);
|
||||
score += calculateFieldSimilarity(ref.getNameEn(), target.getNameEn(), 22);
|
||||
score += calculateFieldSimilarity(ref.getAlias(), target.getAlias(), 26);
|
||||
score += calculateFieldSimilarity(ref.getSpecies(), target.getSpecies(), 30);
|
||||
score += calculateFieldSimilarity(ref.getGenus(), target.getGenus(), 24);
|
||||
score += calculateFieldSimilarity(ref.getFamily(), target.getFamily(), 18);
|
||||
score += calculateFieldSimilarity(ref.getOrders(), target.getOrders(), 14);
|
||||
score += calculateFieldSimilarity(ref.getIntroduce(), target.getIntroduce(), 8);
|
||||
|
||||
if (ref.getFamily() != null && ref.getFamily().equals(target.getFamily()) && !ref.getFamily().isEmpty()) {
|
||||
score += 15;
|
||||
}
|
||||
|
||||
if (ref.getOrders() != null && ref.getOrders().equals(target.getOrders()) && !ref.getOrders().isEmpty()) {
|
||||
score += 10;
|
||||
}
|
||||
|
||||
if (ref.getType() != null && ref.getType().equals(target.getType()) && !ObjectUtil.isEmpty(ref.getType()) ) {
|
||||
if (ref.getType() != null && ref.getType().equals(target.getType()) && !ObjectUtil.isEmpty(ref.getType())) {
|
||||
score += 5;
|
||||
}
|
||||
|
||||
@ -208,4 +332,113 @@ public class SdFishDictoryBServiceImpl extends ServiceImpl<SdFishDictoryBMapper,
|
||||
|
||||
return score;
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateSearchScore(String searchName, SdFishDictoryB fish) {
|
||||
int score = 0;
|
||||
score += calculateFieldSearchScore(searchName, fish.getName(), 220, 140, 90, 40);
|
||||
score += calculateFieldSearchScore(searchName, fish.getAlias(), 180, 120, 80, 30);
|
||||
score += calculateFieldSearchScore(searchName, fish.getNameEn(), 160, 100, 70, 25);
|
||||
score += calculateFieldSearchScore(searchName, fish.getSpecies(), 140, 90, 60, 25);
|
||||
score += calculateFieldSearchScore(searchName, fish.getGenus(), 120, 80, 55, 20);
|
||||
score += calculateFieldSearchScore(searchName, fish.getFamily(), 100, 70, 45, 18);
|
||||
score += calculateFieldSearchScore(searchName, fish.getOrders(), 90, 60, 40, 15);
|
||||
score += calculateFieldSearchScore(searchName, fish.getIntroduce(), 45, 30, 0, 12);
|
||||
return score;
|
||||
}
|
||||
|
||||
private int buildFinalScore(SdFishDictoryB referenceFish, SdFishDictoryB currentFish, int searchScore) {
|
||||
int totalScore = searchScore;
|
||||
if (referenceFish != null) {
|
||||
totalScore += calculateSimilarity(referenceFish, currentFish);
|
||||
}
|
||||
return totalScore;
|
||||
}
|
||||
|
||||
private int calculateFieldSearchScore(String searchText, String candidate, int exactScore, int containsScore,
|
||||
int reverseContainsScore, int overlapMaxScore) {
|
||||
String normalizedSearch = normalizeText(searchText);
|
||||
String normalizedCandidate = normalizeText(candidate);
|
||||
if (!StringUtils.hasText(normalizedSearch) || !StringUtils.hasText(normalizedCandidate)) {
|
||||
return 0;
|
||||
}
|
||||
if (normalizedCandidate.equals(normalizedSearch)) {
|
||||
return exactScore;
|
||||
}
|
||||
if (normalizedCandidate.contains(normalizedSearch)) {
|
||||
return containsScore;
|
||||
}
|
||||
if (reverseContainsScore > 0 && normalizedSearch.contains(normalizedCandidate) && normalizedCandidate.length() >= 2) {
|
||||
return reverseContainsScore;
|
||||
}
|
||||
return calculateTextOverlapScore(normalizedSearch, normalizedCandidate, overlapMaxScore);
|
||||
}
|
||||
|
||||
private int calculateFieldSimilarity(String left, String right, int exactScore) {
|
||||
String normalizedLeft = normalizeText(left);
|
||||
String normalizedRight = normalizeText(right);
|
||||
if (!StringUtils.hasText(normalizedLeft) || !StringUtils.hasText(normalizedRight)) {
|
||||
return 0;
|
||||
}
|
||||
if (normalizedLeft.equals(normalizedRight)) {
|
||||
return exactScore;
|
||||
}
|
||||
if (normalizedLeft.contains(normalizedRight) || normalizedRight.contains(normalizedLeft)) {
|
||||
return Math.max(1, exactScore / 2);
|
||||
}
|
||||
return calculateTextOverlapScore(normalizedLeft, normalizedRight, Math.max(1, exactScore / 3));
|
||||
}
|
||||
|
||||
private int calculateTextOverlapScore(String left, String right, int maxScore) {
|
||||
if (!StringUtils.hasText(left) || !StringUtils.hasText(right) || maxScore <= 0) {
|
||||
return 0;
|
||||
}
|
||||
Set<String> leftTokens = splitTokens(left);
|
||||
Set<String> rightTokens = splitTokens(right);
|
||||
if (leftTokens.isEmpty() || rightTokens.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
long commonCount = leftTokens.stream().filter(rightTokens::contains).count();
|
||||
if (commonCount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
int base = Math.min(leftTokens.size(), rightTokens.size());
|
||||
if (base <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int) Math.min(maxScore, Math.round((double) commonCount * maxScore / base));
|
||||
}
|
||||
|
||||
private Set<String> splitTokens(String text) {
|
||||
String normalized = normalizeText(text);
|
||||
if (!StringUtils.hasText(normalized)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<String> tokens = new LinkedHashSet<>();
|
||||
String[] parts = normalized.split("[\\s,,;;、/\\\\()()]+");
|
||||
for (String part : parts) {
|
||||
if (StringUtils.hasText(part)) {
|
||||
tokens.add(part);
|
||||
}
|
||||
}
|
||||
if (tokens.isEmpty()) {
|
||||
for (int i = 0; i < normalized.length(); i++) {
|
||||
char ch = normalized.charAt(i);
|
||||
if (!Character.isWhitespace(ch)) {
|
||||
tokens.add(String.valueOf(ch));
|
||||
}
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private String normalizeText(String text) {
|
||||
if (!StringUtils.hasText(text)) {
|
||||
return "";
|
||||
}
|
||||
return text.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String safeText(String text) {
|
||||
return text == null ? "" : text;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user