Files
MRCC/launcher/PCL-CE/PCL.Core/Utils/SimilaritySearch.cs
T

236 lines
8.9 KiB
C#
Raw Normal View History

namespace PCL.Core.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// 提供文本相似度搜索功能。
/// </summary>
public static class SimilaritySearch {
//region: SearchSimilarity Constants
// 这些常量来自原始算法,用于调整评分权重。
/// <summary>
/// 匹配长度权重计算的指数底数。越长匹配的得分呈指数增长。
/// </summary>
private const double LengthPowerBase = 1.4;
/// <summary>
/// 匹配长度权重计算的偏移量。
/// </summary>
private const double LengthWeightOffset = 3.6;
/// <summary>
/// 匹配位置邻近度的奖励因子。
/// </summary>
private const double PositionBonusFactor = 0.3;
/// <summary>
/// 计算位置奖励时,允许的最大位置差异。
/// </summary>
private const int MaxPositionBonusDistance = 3;
/// <summary>
/// 源文本长度对最终得分的影响因子。
/// </summary>
private const double SourceLengthImpactFactor = 3.0;
/// <summary>
/// 源文本长度惩罚的平滑参数。
/// </summary>
private const double SourceLengthSmoothing = 15.0;
/// <summary>
/// 对长度为1的查询的得分奖励。
/// </summary>
private const double ShortQueryBonusFactor = 2.0;
//endregion
/// <summary>
/// 获取搜索文本的相似度。(已优化)
/// </summary>
/// <param name="source">被搜索的长内容。</param>
/// <param name="query">用户输入的搜索文本。</param>
/// <returns>一个表示相似度的 double 值。</returns>
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;
}
/// <summary>
/// 获取多段文本加权后的相似度。
/// </summary>
private static double _SearchSimilarityWeighted(List<KeyValuePair<string, double>> 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;
}
/// <summary>
/// 检查一个条目的所有搜索源是否完全匹配查询的所有部分。
/// </summary>
private static bool _IsAbsoluteMatch(IEnumerable<KeyValuePair<string, double>> searchSources, string[] queryParts) {
// 预处理搜索源:转小写并移除空格,避免在循环中重复操作
var processedSources = searchSources
.Select(s => s.Key.Replace(" ", "").ToLower())
.ToList();
// 必须所有查询词都在至少一个源中找到
return queryParts.All(queryPart => processedSources.Any(source => source.Contains(queryPart)));
}
/// <summary>
/// 进行多段文本加权搜索,获取相似度较高的数项结果。
/// </summary>
/// <typeparam name="T">搜索条目的泛型类型。</typeparam>
/// <param name="entries">要搜索的条目列表。</param>
/// <param name="query">用户输入的查询字符串。</param>
/// <param name="maxBlurCount">返回的最大模糊结果数。</param>
/// <param name="minBlurSimilarity">返回结果要求的最低相似度。</param>
/// <returns>排序和过滤后的搜索结果列表。</returns>
public static List<SearchEntry<T>> Search<T>(
List<SearchEntry<T>> 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();
}
}
/// <summary>
/// 用于搜索的项目。使用主构造函数 (C# 12+)。
/// </summary>
/// <typeparam name="T">该项目对应的源数据类型。</typeparam>
public class SearchEntry<T>(T item, List<KeyValuePair<string, double>> searchSource) {
/// <summary>
/// 该项目对应的源数据。
/// </summary>
public T Item { get; set; } = item;
/// <summary>
/// 该项目用于搜索的源文本及其权重。
/// </summary>
public List<KeyValuePair<string, double>> SearchSource { get; set; } = searchSource;
/// <summary>
/// 计算出的相似度。
/// </summary>
public double Similarity { get; set; }
/// <summary>
/// 是否完全匹配。
/// 如果查询的所有部分都能在搜索源中找到,则为 true。
/// </summary>
public bool AbsoluteRight { get; set; }
}