namespace PCL.Core.Utils; using System; using System.Collections.Generic; using System.Linq; /// /// 提供文本相似度搜索功能。 /// public static class SimilaritySearch { //region: SearchSimilarity Constants // 这些常量来自原始算法,用于调整评分权重。 /// /// 匹配长度权重计算的指数底数。越长匹配的得分呈指数增长。 /// private const double LengthPowerBase = 1.4; /// /// 匹配长度权重计算的偏移量。 /// private const double LengthWeightOffset = 3.6; /// /// 匹配位置邻近度的奖励因子。 /// private const double PositionBonusFactor = 0.3; /// /// 计算位置奖励时,允许的最大位置差异。 /// private const int MaxPositionBonusDistance = 3; /// /// 源文本长度对最终得分的影响因子。 /// private const double SourceLengthImpactFactor = 3.0; /// /// 源文本长度惩罚的平滑参数。 /// private const double SourceLengthSmoothing = 15.0; /// /// 对长度为1的查询的得分奖励。 /// private const double ShortQueryBonusFactor = 2.0; //endregion /// /// 获取搜索文本的相似度。(已优化) /// /// 被搜索的长内容。 /// 用户输入的搜索文本。 /// 一个表示相似度的 double 值。 private static double _SearchSimilarity(string source, string query) { if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(query)) { return 0.0; } // 预处理:转为小写并移除空格,然后转换为 ReadOnlySpan 以提高性能。 var sourceSpan = source.ToLower().Replace(" ", "").AsSpan(); var querySpan = query.ToLower().Replace(" ", "").AsSpan(); if (sourceSpan.IsEmpty || querySpan.IsEmpty) { return 0.0; } // 使用布尔数组来跟踪源文本中已被匹配的字符,避免重复分配字符串。 var usedSourceIndices = new bool[sourceSpan.Length]; double weightedLengthSum = 0; var queryIndex = 0; while (queryIndex < querySpan.Length) { var longestMatchLength = 0; var bestMatchSourceStartIndex = -1; // 寻找以当前 queryIndex 为起点的最长匹配子串 for (var sourceIndex = 0; sourceIndex < sourceSpan.Length; sourceIndex++) { var currentMatchLength = 0; // 计算从当前 sourceIndex 和 queryIndex 开始的匹配长度 // 同时确保源字符未被使用 while ((queryIndex + currentMatchLength) < querySpan.Length && (sourceIndex + currentMatchLength) < sourceSpan.Length && !usedSourceIndices[sourceIndex + currentMatchLength] && sourceSpan[sourceIndex + currentMatchLength] == querySpan[queryIndex + currentMatchLength]) { currentMatchLength++; } // 卫语句:如果当前匹配不比最长匹配更长,则直接继续下一次循环 if (currentMatchLength <= longestMatchLength) { continue; } // 如果满足条件,更新最长匹配信息 longestMatchLength = currentMatchLength; bestMatchSourceStartIndex = sourceIndex; } if (longestMatchLength > 0) { // 标记源中对应的字符为已使用 for (var i = 0; i < longestMatchLength; i++) { usedSourceIndices[bestMatchSourceStartIndex + i] = true; } // 根据长度加成 var incrementWeight = Math.Pow(LengthPowerBase, 3 + longestMatchLength) - LengthWeightOffset; // 根据位置加成 var positionDifference = Math.Abs(queryIndex - bestMatchSourceStartIndex); var positionBonus = 1.0 + PositionBonusFactor * Math.Max(0, MaxPositionBonusDistance - positionDifference); incrementWeight *= positionBonus; weightedLengthSum += incrementWeight; } // 推进查询指针 queryIndex += Math.Max(1, longestMatchLength); } // 计算最终结果:(加权匹配总和 / 查询长度) * 源长度影响比例 * 短查询奖励 var normalizedScore = weightedLengthSum / querySpan.Length; var sourceLengthPenalty = SourceLengthImpactFactor / Math.Sqrt(sourceSpan.Length + SourceLengthSmoothing); var shortQueryBonus = query.Length == 1 ? ShortQueryBonusFactor : 1.0; return normalizedScore * sourceLengthPenalty * shortQueryBonus; } /// /// 获取多段文本加权后的相似度。 /// private static double _SearchSimilarityWeighted(List> source, string query) { if (source.Count == 0) return 0.0; var totalWeight = source.Sum(pair => pair.Value); if (totalWeight == 0) return 0.0; var weightedSum = source.Sum(pair => _SearchSimilarity(pair.Key, query) * pair.Value); return weightedSum / totalWeight; } /// /// 检查一个条目的所有搜索源是否完全匹配查询的所有部分。 /// private static bool _IsAbsoluteMatch(IEnumerable> searchSources, string[] queryParts) { // 预处理搜索源:转小写并移除空格,避免在循环中重复操作 var processedSources = searchSources .Select(s => s.Key.Replace(" ", "").ToLower()) .ToList(); // 必须所有查询词都在至少一个源中找到 return queryParts.All(queryPart => processedSources.Any(source => source.Contains(queryPart))); } /// /// 进行多段文本加权搜索,获取相似度较高的数项结果。 /// /// 搜索条目的泛型类型。 /// 要搜索的条目列表。 /// 用户输入的查询字符串。 /// 返回的最大模糊结果数。 /// 返回结果要求的最低相似度。 /// 排序和过滤后的搜索结果列表。 public static List> Search( List> entries, string query, int maxBlurCount = 5, double minBlurSimilarity = 0.1) { if (entries.Count == 0) { return []; } if (string.IsNullOrWhiteSpace(query)) { return entries; // 或者返回空列表,取决于业务需求 } var queryParts = query.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(q => q.ToLower()) .ToArray(); if (queryParts.Length == 0) { return entries; } // 1. 计算每个条目的相似度和是否完全匹配 foreach (var entry in entries) { entry.Similarity = _SearchSimilarityWeighted(entry.SearchSource, query); entry.AbsoluteRight = _IsAbsoluteMatch(entry.SearchSource, queryParts); } // 2. 排序:完全匹配的优先,其次按相似度降序 var sortedEntries = entries .OrderByDescending(e => e.AbsoluteRight) .ThenByDescending(e => e.Similarity); // 3. 构建最终结果列表 var sortedEntriesList = sortedEntries.ToList(); var absoluteMatches = sortedEntriesList.Where(e => e.AbsoluteRight); var blurMatches = sortedEntriesList .Where(e => !e.AbsoluteRight && e.Similarity >= minBlurSimilarity) .Take(maxBlurCount); return absoluteMatches.Concat(blurMatches).ToList(); } } /// /// 用于搜索的项目。使用主构造函数 (C# 12+)。 /// /// 该项目对应的源数据类型。 public class SearchEntry(T item, List> searchSource) { /// /// 该项目对应的源数据。 /// public T Item { get; set; } = item; /// /// 该项目用于搜索的源文本及其权重。 /// public List> SearchSource { get; set; } = searchSource; /// /// 计算出的相似度。 /// public double Similarity { get; set; } /// /// 是否完全匹配。 /// 如果查询的所有部分都能在搜索源中找到,则为 true。 /// public bool AbsoluteRight { get; set; } }