重写 StarBlog 的搜索功能和页面,支持权重设置和结果高亮

前言

最近在整理本地的一些笔记

有些日期不太对的,我的博客上有记录发布和更新时间,所以我去搜索了一下

这时候发现 StarBlog 的搜索功能太简陋了

虽然上次更新增加了一大波功能,也优化了一下搜索功能,之前只能搜索标题,现在可以搜索正文内容了。详见: StarBlog v1.3.0 新版本,一大波更新以及迁移服务器部署

不过有个问题是没有权重,标题的权重应该比正文更高的

按理说这些应该得加入全文检索引擎,Elasticsearch、MeiliSearch 之类的来实现。但这些需要额外的服务,太重了。

再不济也要用 Lucene.NET 这种,这是 Elasticsearch 的基础,但不需要额外服务,纯本地嵌入式,支持权重控制、高亮、分词等功能。

但为了快速实现,这些我都不想用,先用最简单的方式来改进。

同时我也重写了搜索结果页面,之前的页面太业余了。

极简实现

最终我的方案是:在内存里手动算权重 + Regex 实现结果高亮

成本非常低,效果也不错

实现效果

来看看效果吧

这套 StarBlog 的前端是 Bootstrap,样式都得靠 CSS,相对于我现在用的 Tailwind CSS、Shadcn/ui、Magic UI 之类的,太原始了,重写这个界面已经尽力了hhh😄

代码

OK,接下来是大家不感兴趣的代码环节

模型

先定义搜索结果模型

cs 复制代码
public class SearchPost {
    public Post Post { get; set; }
    public int TitleScore { get; set; }

    public int ContentScore { get; set; }

    // 标题每命中一次+100分
    // 内容命中+1分
    public int Score => TitleScore * 100 + ContentScore;
    public string HighlightedTitle { get; set; }
    public string HighlightedSnippet { get; set; }
}

搜索逻辑

搜索功能逻辑都在 src/StarBlog.Web/Controllers/SearchController.cs 文件里

在内村里计算权重,从数据库查询出来后,用 Linq 计算权重,关键词出现一次为一分;总分是在 SearchPost 里计算的,标题每命中一次+100分,内容命中+1分

cs 复制代码
var searchPosts = _postRepo
    .Where(a => a.IsPublish)
    .Where(a =>
           a.Title!.Contains(keyword) ||
           a.Content.Contains(keyword)
          )
    .Include(a => a.Category)
    .ToList()
    .Select(p => new SearchPost {
        Post = p,
        TitleScore = p.Title.ToLower().Split(keyword).Length - 1,
        ContentScore = p.Content?.ToLower().Split(keyword).Length - 1 ?? 0,
    })
    .OrderByDescending(x => x.Score)
    .ToList();

搜索结果高亮

使用正则表达式来实现结果高亮

cs 复制代码
var regex = new Regex(Regex.Escape(keyword), RegexOptions.IgnoreCase);
foreach (var item in searchPosts) {
    item.HighlightedTitle = regex.Replace(item.Post.Title, m => $"<mark>{m.Value}</mark>");
    item.HighlightedSnippet = GetHighlightedSnippet(item.Post.Content, keyword);
}

生成高亮片段摘要

思路很简单:

  • 找到第一个命中的位置
  • 截取前后一定长度的内容(比如前后各 50 个字符)
  • 再用 Regex 替换加 <mark> 高亮
  • 最后拼上 ... 作为省略号
cs 复制代码
public static string GetHighlightedSnippet(string content, string keyword, int snippetLength = 100) {
    if (string.IsNullOrEmpty(content) || string.IsNullOrEmpty(keyword))
        return string.Empty;

    var regex = new Regex(Regex.Escape(keyword), RegexOptions.IgnoreCase);
    var match = regex.Match(content);

    if (!match.Success) {
        // 没匹配到,直接取前 snippetLength*2 个字符作为摘要
        return content.Length > snippetLength * 2
            ? content.Substring(0, snippetLength * 2) + "..."
            : content;
    }

    // 计算截取范围(匹配位置前后各 snippetLength)
    int start = Math.Max(0, match.Index - snippetLength);
    int length = Math.Min(content.Length - start, match.Length + snippetLength * 2);

    string snippet = content.Substring(start, length);

    // 高亮处理
    snippet = regex.Replace(snippet, m => $"<mark>{m.Value}</mark>");

    // 前后补省略号(如果不是全文开头或结尾)
    if (start > 0) snippet = "..." + snippet;
    if (start + length < content.Length) snippet += "...";

    return snippet;
}

小结

重构之后体验更上一层

不过在老架构上修修补补终究不是长久之计

等有空就得赶紧开始 v2 新版的开发💪

PS:接下来也许会拓展一下这个搜索功能,加入多个关键词搜索的支持,再进一步搭配 Lucene.NET 也不无可能。

相关推荐
hhh3u3u3u1 小时前
Visual C++ 6.0中文版安装包下载教程及win11安装教程
java·c语言·开发语言·c++·python·c#·vc-1
加号31 小时前
【C#】实现沃德普线光控制器通信控制(附完整源码)
开发语言·c#
lzhdim3 小时前
SharpCompress:跨平台的 C# 压缩与解压库
开发语言·c#
~plus~4 小时前
.NET 8 C# 委托与事件实战教程
网络·c#·.net·.net 8·委托与事件·c#进阶
beyond谚语5 小时前
接口&抽象类
c#·接口隔离原则·抽象类
新手小新6 小时前
C#学习笔记1-在VS CODE部署C#开发环境
笔记·学习·c#
rockey6279 小时前
AScript动态脚本多语言环境支持
sql·c#·.net·script·eval·function·动态脚本
ou.cs9 小时前
c# SemaphoreSlim保姆级教程
开发语言·网络·c#
龙侠九重天9 小时前
ML.NET 实战:快速构建分类模型
分类·数据挖掘·c#·.net
fengyehongWorld11 小时前
C# 创建Worker,杀死指定程序的线程
c#