CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
236 lines
8.9 KiB
C#
236 lines
8.9 KiB
C#
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; }
|
|
}
|