using System.Text.RegularExpressions;

namespace Dpz.Core.Service.Mediator.Features.Search;

/// <summary>
/// 统一分析搜索关键字,兼容中英文输入并生成查询词与高亮词。
/// </summary>
public static partial class SearchKeywordAnalyzer
{
    private const int MaxQueryTerms = 8;
    private const int MaxHighlightTerms = 24;

    /// <summary>
    /// 分析用户输入,生成规范化关键字、查询词和高亮词。
    /// </summary>
    public static SearchKeywordAnalysis Analyze(string? keyword)
    {
        var normalizedKeyword = Normalize(keyword);
        if (string.IsNullOrWhiteSpace(normalizedKeyword))
        {
            return SearchKeywordAnalysis.Empty;
        }

        var queryTerms = new List<string>();
        var highlightTerms = new List<string>();

        var rawTokens = TokenRegex()
            .Matches(normalizedKeyword)
            .Select(x => x.Value)
            .Where(x => !string.IsNullOrWhiteSpace(x))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToList();

        foreach (var token in rawTokens)
        {
            if (ContainsCjk(token))
            {
                AddTerm(queryTerms, token);
                AddTerm(highlightTerms, token);

                foreach (var gram in BuildNGrams(token, 2))
                {
                    AddTerm(queryTerms, gram);
                    AddTerm(highlightTerms, gram);
                }
                continue;
            }

            AddTerm(queryTerms, token.ToLowerInvariant());
            AddTerm(highlightTerms, token);
        }

        if (queryTerms.Count == 0)
        {
            AddTerm(queryTerms, normalizedKeyword);
            AddTerm(highlightTerms, normalizedKeyword);
        }

        var sortedQueryTerms = queryTerms
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .OrderByDescending(x => x.Length)
            .ThenBy(x => x, StringComparer.OrdinalIgnoreCase)
            .Take(MaxQueryTerms)
            .ToList();

        var sortedHighlightTerms = highlightTerms
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .OrderByDescending(x => x.Length)
            .ThenBy(x => x, StringComparer.OrdinalIgnoreCase)
            .Take(MaxHighlightTerms)
            .ToList();

        return new SearchKeywordAnalysis(normalizedKeyword, sortedQueryTerms, sortedHighlightTerms);
    }

    /// <summary>
    /// 将关键词转成正则并按长度降序,避免短词吞噬长词。
    /// </summary>
    public static string BuildRegexPattern(IEnumerable<string> terms)
    {
        var escapedTerms = terms
            .Where(x => !string.IsNullOrWhiteSpace(x))
            .Select(Regex.Escape)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .OrderByDescending(x => x.Length)
            .ToList();
        return escapedTerms.Count == 0 ? string.Empty : string.Join('|', escapedTerms);
    }

    private static string Normalize(string? keyword)
    {
        if (string.IsNullOrWhiteSpace(keyword))
        {
            return string.Empty;
        }

        return string.Join(
            ' ',
            keyword.Split([' ', '\t', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
        );
    }

    private static bool ContainsCjk(string token)
    {
        return CjkRegex().IsMatch(token);
    }

    private static IEnumerable<string> BuildNGrams(string token, int n)
    {
        if (token.Length < n)
        {
            yield break;
        }

        for (var i = 0; i <= token.Length - n; i++)
        {
            yield return token.Substring(i, n);
        }
    }

    private static void AddTerm(List<string> terms, string candidate)
    {
        if (string.IsNullOrWhiteSpace(candidate) || candidate.Length < 2)
        {
            return;
        }

        terms.Add(candidate);
    }

    [GeneratedRegex(@"[\p{L}\p{N}]+", RegexOptions.Compiled | RegexOptions.CultureInvariant)]
    private static partial Regex TokenRegex();
    [GeneratedRegex(@"[\u4E00-\u9FFF]", RegexOptions.Compiled | RegexOptions.CultureInvariant)]
    private static partial Regex CjkRegex();
}

/// <summary>
/// 搜索关键词分析结果。
/// </summary>
public sealed record SearchKeywordAnalysis(
    string NormalizedKeyword,
    IReadOnlyList<string> QueryTerms,
    IReadOnlyList<string> HighlightTerms
)
{
    public static readonly SearchKeywordAnalysis Empty = new(string.Empty, [], []);
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个用于搜索关键词分析的工具类,主要用于处理用户输入的搜索关键词,并生成用于查询和高亮显示的词条。以下是详细解释:

主要功能

1. 关键词分析 (Analyze 方法)

  • 输入处理:接收用户输入的关键词字符串
  • 标准化:通过 Normalize 方法清理和格式化输入
  • 词条提取:使用正则表达式提取有效的词条(字母和数字组合)
  • 差异化处理
    • 中文处理:检测包含中日韩(CJK)字符的词条,生成2-gram分词
    • 英文处理:将英文词条转换为小写用于查询,保持原大小写用于高亮

2. 词条管理

  • 去重:使用 StringComparer.OrdinalIgnoreCase 进行大小写不敏感的去重
  • 排序:按词条长度降序排列,然后按字母顺序排列
  • 限制
    • 查询词条最多8个 (MaxQueryTerms)
    • 高亮词条最多24个 (MaxHighlightTerms)

3. 辅助功能

BuildRegexPattern 方法

  • 将词条列表转换为正则表达式模式
  • 对特殊字符进行转义处理
  • 按长度降序排列,防止短词匹配覆盖长词

私有辅助方法

  • Normalize:清理输入字符串,移除多余空白字符
  • ContainsCjk:检测是否包含中日韩字符
  • BuildNGrams:为中文文本生成N-gram分词
  • AddTerm:添加有效词条(长度至少2个字符)

4. 正则表达式(使用源生成器)

  • TokenRegex:匹配字母和数字组合的词条
  • CjkRegex:匹配中日韩汉字字符范围

设计特点

  1. 多语言支持:兼容中英文混合搜索
  2. 性能优化:使用编译的正则表达式和源生成器
  3. 灵活性:分离查询词条和高亮词条,满足不同场景需求
  4. 健壮性:完善的空值检查和边界条件处理

使用场景

适用于搜索引擎、内容管理系统等需要处理多语言搜索关键词的场景,特别是需要同时支持精确查询和关键词高亮显示的应用。

评论加载中...