diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdFishDictoryBServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdFishDictoryBServiceImpl.java index 5a635c3a..7544ea93 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdFishDictoryBServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdFishDictoryBServiceImpl.java @@ -74,107 +74,231 @@ public class SdFishDictoryBServiceImpl extends ServiceImpl 0) ? limit : 10; - String searchName = name.trim(); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(SdFishDictoryB::getIsDeleted, 0); - List 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 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 result = similarityMap.entrySet().stream() - .sorted(Map.Entry.comparingByValue().reversed()) - .limit(resultLimit) - .map(Map.Entry::getKey) - .collect(Collectors.toList()); - - result.add(0, exactMatch); - return result; + List candidateFish = querySimilarFishCandidates(rawSearchName, searchName, resultLimit); + if (candidateFish.isEmpty()) { + return Collections.emptyList(); } - List containsMatches = allFish.stream() - .filter(f -> f.getName() != null && f.getName().contains(searchName)) + Map 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 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 querySimilarFishCandidates(String rawSearchName, String normalizedSearchName, int resultLimit) { + int candidateLimit = Math.min(Math.max(resultLimit * 30, 120), 500); + LinkedHashMap 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 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 candidateMap, List candidates) { + if (candidates == null || candidates.isEmpty()) { + return; + } + for (SdFishDictoryB fish : candidates) { + if (fish == null || !StringUtils.hasText(fish.getId())) { + continue; } - - List result = similarityMap.entrySet().stream() - .sorted(Map.Entry.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 queryExactCandidates(String rawSearchName, String normalizedSearchName, int limit) { + if (!StringUtils.hasText(rawSearchName)) { + return Collections.emptyList(); + } + LambdaQueryWrapper 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 queryContainsCandidates(String rawSearchName, String normalizedSearchName, int limit) { + if (!StringUtils.hasText(rawSearchName)) { + return Collections.emptyList(); + } + String likeValue = "%" + rawSearchName + "%"; + String normalizedLikeValue = "%" + normalizedSearchName + "%"; + LambdaQueryWrapper 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 queryTokenCandidates(String normalizedSearchName, int limit) { + Set 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 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 buildCandidateSelectWrapper() { + LambdaQueryWrapper 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 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 leftTokens = splitTokens(left); + Set 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 splitTokens(String text) { + String normalized = normalizeText(text); + if (!StringUtils.hasText(normalized)) { + return Collections.emptySet(); + } + Set 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; + } +}