fix: 鱼类字典相似度查询优化

This commit is contained in:
tangwei 2026-06-11 08:47:53 +08:00
parent 687ec712e0
commit f0dcf99d28

View File

@ -74,107 +74,231 @@ public class SdFishDictoryBServiceImpl extends ServiceImpl<SdFishDictoryBMapper,
} }
int resultLimit = (limit != null && limit > 0) ? limit : 10; int resultLimit = (limit != null && limit > 0) ? limit : 10;
String searchName = name.trim(); String rawSearchName = name.trim();
LambdaQueryWrapper<SdFishDictoryB> wrapper = new LambdaQueryWrapper<>(); String searchName = normalizeText(rawSearchName);
wrapper.eq(SdFishDictoryB::getIsDeleted, 0);
List<SdFishDictoryB> allFish = list(wrapper);
SdFishDictoryB exactMatch = allFish.stream() List<SdFishDictoryB> candidateFish = querySimilarFishCandidates(rawSearchName, searchName, resultLimit);
.filter(f -> f.getName() != null && f.getName().equals(searchName)) if (candidateFish.isEmpty()) {
.findFirst() return Collections.emptyList();
.orElse(null); }
if (exactMatch != null) { Map<String, Integer> searchScoreMap = new HashMap<>();
Map<SdFishDictoryB, Integer> similarityMap = new HashMap<>(); for (SdFishDictoryB fish : candidateFish) {
for (SdFishDictoryB fish : allFish) { if (fish == null || !StringUtils.hasText(fish.getId())) {
if (fish.getId().equals(exactMatch.getId())) {
continue; continue;
} }
int score = calculateSimilarity(exactMatch, fish); int score = calculateSearchScore(searchName, fish);
if (score > 0) { if (score > 0) {
similarityMap.put(fish, score); searchScoreMap.put(fish.getId(), score);
} }
} }
List<SdFishDictoryB> result = similarityMap.entrySet().stream() if (searchScoreMap.isEmpty()) {
.sorted(Map.Entry.<SdFishDictoryB, Integer>comparingByValue().reversed()) return Collections.emptyList();
.limit(resultLimit)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
result.add(0, exactMatch);
return result;
} }
List<SdFishDictoryB> containsMatches = allFish.stream() SdFishDictoryB referenceFish = candidateFish.stream()
.filter(f -> f.getName() != null && f.getName().contains(searchName)) .filter(fish -> fish != null && searchScoreMap.containsKey(fish.getId()))
.collect(Collectors.toList()); .max((f1, f2) -> {
int scoreCompare = Integer.compare(searchScoreMap.get(f1.getId()), searchScoreMap.get(f2.getId()));
if (!containsMatches.isEmpty()) { if (scoreCompare != 0) {
List<SdFishDictoryB> result = containsMatches.stream() return scoreCompare;
.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()); 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) .limit(resultLimit)
.collect(Collectors.toList());
return result;
}
SdFishDictoryB partialMatch = allFish.stream()
.filter(f -> f.getName() != null && (searchName.contains(f.getName()) || f.getName().contains(searchName)))
.findFirst()
.orElse(null);
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);
}
}
List<SdFishDictoryB> result = similarityMap.entrySet().stream()
.sorted(Map.Entry.<SdFishDictoryB, Integer>comparingByValue().reversed())
.limit(resultLimit)
.map(Map.Entry::getKey) .map(Map.Entry::getKey)
.collect(Collectors.toList()); .collect(Collectors.toList());
result.add(0, partialMatch);
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));
}
return new ArrayList<>(candidateMap.values());
}
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;
}
candidateMap.putIfAbsent(fish.getId(), fish);
}
}
private List<SdFishDictoryB> queryExactCandidates(String rawSearchName, String normalizedSearchName, int limit) {
if (!StringUtils.hasText(rawSearchName)) {
return Collections.emptyList(); 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) { private int calculateSimilarity(SdFishDictoryB ref, SdFishDictoryB target) {
int score = 0; int score = 0;
if (ref.getGenus() != null && ref.getGenus().equals(target.getGenus()) && !ref.getGenus().isEmpty()) { if (ref == null || target == null) {
score += 25; 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 += calculateFieldSimilarity(ref.getName(), target.getName(), 45);
score += 20; 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()) { if (ref.getType() != null && ref.getType().equals(target.getType()) && !ObjectUtil.isEmpty(ref.getType())) {
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()) ) {
score += 5; score += 5;
} }
@ -208,4 +332,113 @@ public class SdFishDictoryBServiceImpl extends ServiceImpl<SdFishDictoryBMapper,
return score; 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;
}
} }