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. 常量定义

private const int MaxQueryTerms = 8;      // 最大查询词数量
private const int MaxHighlightTerms = 24; // 最大高亮词数量

2. 核心方法 Analyze

这是主要的分析方法,处理流程如下:

  1. 标准化输入:调用 Normalize 方法清理输入
  2. 分词:使用正则表达式 TokenRegex 将输入分解为词汇单元
  3. 分类处理
    • 中文字符(CJK)
      • 直接添加原词
      • 生成 2-gram(两个字符的组合),如"搜索引擎" → "搜索"、"索引"、"引擎"
    • 英文/数字:转换为小写形式
  4. 排序和限制:按长度降序排列,限制数量

3. 辅助方法

  • Normalize:清理输入字符串,移除多余空格
  • ContainsCjk:检测是否包含中日韩字符
  • BuildNGrams:生成 N-gram 组合
  • BuildRegexPattern:将关键词转换为正则表达式模式
  • AddTerm:添加词汇到列表(过滤长度小于2的词)

4. 正则表达式(.NET 7+ Source Generators)

[GeneratedRegex(@"[\p{L}\p{N}]+")]  // 匹配字母和数字
private static partial Regex TokenRegex();

[GeneratedRegex(@"[\u4E00-\u9FFF]")] // 匹配中文字符
private static partial Regex CjkRegex();

使用场景示例

假设用户输入:"Hello 搜索引擎"

  1. 标准化:保持不变
  2. 分词:["Hello", "搜索引擎"]
  3. 处理
    • "Hello" → 查询词: ["hello"],高亮词: ["Hello"]
    • "搜索引擎" → 查询词: ["搜索引擎", "搜索", "索引", "引擎"],高亮词同样
  4. 排序:按长度降序排列
  5. 限制:取前8个查询词,前24个高亮词

设计亮点

  1. 中英文兼容:特别处理了CJK字符的分词需求
  2. 性能优化:使用 Source Generator 编译时生成正则表达式
  3. 灵活性:分离查询词和高亮词,满足不同使用场景
  4. 防止短词覆盖:优先处理长词,避免搜索结果中短词掩盖长词的问题

这个工具类非常适合用在搜索引擎、内容管理系统或任何需要关键词搜索和高亮显示的应用中。

评论加载中...