📢 新文章推送 · 每周更新优质内容 · 订阅更新 →
向下滚动
技术笔记

Lumin Blog 全站搜索功能文档

AI 智能总结

Lumin Blog 全站搜索功能文档

📌 概述

Lumin Blog 的全站搜索功能采用纯前端实现方案,无需后端服务。Hugo 构建时生成 index.json 搜索索引,前端通过 fetch 加载索引后,在浏览器端完成搜索匹配、权重排序和结果渲染。搜索入口位于导航栏 Logo 右侧,点击或使用 Ctrl+K 快捷键即可打开搜索面板。

🎯 一、功能说明

属性说明
搜索入口导航栏 Logo 右侧搜索按钮(🔍 搜索 Ctrl+K)
快捷键Ctrl+K 打开搜索 / ESC 关闭搜索
搜索方式多关键词 AND 搜索,空格分隔关键词
排序算法权重排序:标题(10) > 标签(6) > 分类(4) > 摘要(2) > 内容(1)
结果数量最多显示 12 条结果
关键词高亮搜索结果中匹配的关键词以渐变背景高亮
索引加载懒加载模式,首次打开搜索框时才加载索引
移动端适配移动端隐藏文字和快捷键,仅显示搜索图标

搜索流程

1用户点击搜索按钮 / Ctrl+K
2  → 打开搜索遮罩层,自动聚焦输入框
3  → 首次打开时懒加载 index.json 索引
4  → 用户输入关键词(200ms 防抖)
5  → 拆分为多个关键词,AND 匹配
6  → 计算每条结果的权重分数
7  → 按分数降序排列,取前 12 条
8  → 渲染结果:标题高亮 + 摘要截取 + 分类 + 标签

🏗️ 二、代码结构

2.1 搜索入口按钮

定义在 layouts/partials/header.html 中,位于 Logo 右侧:

 1{{ if .Site.Params.search.enable }}
 2<button class="search-bar-toggle" id="search-toggle"
 3        aria-label="搜索" title="搜索 (Ctrl+K)">
 4  <svg width="16" height="16" viewBox="0 0 24 24" ...>
 5    <circle cx="11" cy="11" r="8"></circle>
 6    <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
 7  </svg>
 8  <span class="search-bar-text">搜索</span>
 9  <kbd class="search-bar-kbd">Ctrl+K</kbd>
10</button>
11{{ end }}

布局位置header-brandsearch-bar-toggleheader-navheader-actions

搜索按钮紧跟 Logo,header-navmargin-left: auto 将导航菜单推到右侧,形成 Logo + 搜索 | 导航 | 操作按钮 的三段式布局。

2.2 搜索遮罩层

定义在 layouts/partials/search-overlay.html 中:

 1{{ if .Site.Params.search.enable }}
 2<div class="search-overlay" id="search-overlay">
 3  <div class="search-container">
 4    <div class="search-header">
 5      <svg class="search-icon" ...>...</svg>
 6      <input type="text" class="search-input" id="search-input"
 7             placeholder="{{ .Site.Params.search.placeholder | default '搜索文章...' }}"
 8             autocomplete="off">
 9      <kbd class="search-kbd">ESC</kbd>
10      <button class="search-close" id="search-close" ...></button>
11    </div>
12    <div class="search-hint" id="search-hint">
13      <div class="search-hint-item"><kbd>↑↓</kbd> 导航</div>
14      <div class="search-hint-item"><kbd></kbd> 打开</div>
15      <div class="search-hint-item"><kbd>ESC</kbd> 关闭</div>
16    </div>
17    <div class="search-results" id="search-results"></div>
18  </div>
19</div>
20{{ end }}

结构说明

  • search-header:搜索图标 + 输入框 + ESC 提示 + 关闭按钮
  • search-hint:快捷键引导提示,输入关键词后自动隐藏
  • search-results:搜索结果容器,最大高度 420px 可滚动

2.3 搜索索引

定义在 layouts/_default/index.json 中,Hugo 构建时自动生成:

 1[{{- $pages := .Site.RegularPages }}
 2{{- $total := len $pages }}
 3{{- range $index, $page := $pages }}
 4{
 5  "title": {{ $page.Title | jsonify }},
 6  "permalink": {{ $page.Permalink | jsonify }},
 7  "date": {{ $page.Date.Format "2006-01-02" | jsonify }},
 8  "summary": {{ $page.Summary | plainify | truncate 200 | jsonify }},
 9  "content": {{ $page.Plain | truncate 1200 | jsonify }},
10  "tags": {{ with $page.Params.tags }}{{ . | jsonify }}{{ else }}[]{{ end }},
11  "categories": {{ with $page.Params.categories }}{{ . | jsonify }}{{ else }}[]{{ end }},
12  "section": {{ $page.Section | jsonify }}
13}{{ if ne (add $index 1) $total }},{{ end }}
14{{- end }}
15]

索引字段说明

字段来源截取用途
title.Title不截取搜索匹配(权重 10)+ 结果标题
permalink.Permalink不截取结果链接跳转
date.Date格式化结果日期显示
summary.Summary200 字搜索匹配(权重 2)+ 摘要回退
content.Plain1200 字搜索匹配(权重 1)+ 摘要截取
tags.Params.tags不截取搜索匹配(权重 6)+ 结果标签展示
categories.Params.categories不截取搜索匹配(权重 4)+ 结果分类展示
section.Section不截取内容分区标识

2.4 JavaScript 实现

搜索逻辑集成在 assets/js/main.jsSearch IIFE 模块中。

索引懒加载

 1var indexLoaded = false;
 2var indexLoading = false;
 3
 4function loadIndex() {
 5  indexLoading = true;
 6  fetch('/index.json').then(function(r) { return r.json(); }).then(function(d) {
 7    indexData = d || [];
 8    indexLoaded = true;
 9    indexLoading = false;
10  }).catch(function(e) {
11    indexLoading = false;
12    if (results) {
13      results.innerHTML = '<div class="search-empty">⚠️ 搜索索引加载失败,请刷新页面重试</div>';
14    }
15  });
16}
17
18function open() {
19  // ...
20  if (!indexLoaded && !indexLoading) loadIndex();
21}

懒加载策略:不在页面加载时立即请求索引,而是在用户首次打开搜索框时才加载。indexLoading 标志防止重复请求。

多关键词 AND 匹配

 1function matchItem(item, keywords) {
 2  var t = (item.title || '').toLowerCase();
 3  var s = (item.summary || '').toLowerCase();
 4  var c = (item.content || '').toLowerCase();
 5  var tags = (item.tags || []).join(' ').toLowerCase();
 6  var cats = (item.categories || []).join(' ').toLowerCase();
 7  var all = t + ' ' + tags + ' ' + cats + ' ' + s + ' ' + c;
 8
 9  for (var i = 0; i < keywords.length; i++) {
10    if (keywords[i] && all.indexOf(keywords[i]) === -1) return false;
11  }
12  return true;
13}

输入 “Hugo 搜索” 会被 split(/\s+/) 拆分为 ["hugo", "搜索"],两个关键词必须在文章的任意字段中都能匹配才算命中。

权重评分排序

 1var WEIGHT = { title: 10, tags: 6, categories: 4, summary: 2, content: 1 };
 2
 3function scoreItem(item, keywords) {
 4  var score = 0;
 5  // ... 对每个关键词在每个字段中检查匹配
 6  for (var i = 0; i < keywords.length; i++) {
 7    var kw = keywords[i];
 8    if (t.indexOf(kw) > -1) score += WEIGHT.title;      // 标题匹配 +10
 9    if (tags.indexOf(kw) > -1) score += WEIGHT.tags;     // 标签匹配 +6
10    if (cats.indexOf(kw) > -1) score += WEIGHT.categories; // 分类匹配 +4
11    if (s.indexOf(kw) > -1) score += WEIGHT.summary;     // 摘要匹配 +2
12    if (c.indexOf(kw) > -1) score += WEIGHT.content;     // 内容匹配 +1
13  }
14  return score;
15}

评分示例:搜索 “Hugo”

文章标题匹配标签匹配分类匹配摘要匹配内容匹配总分
Hugo 搭建指南+10+6+4+2+123
静态博客搜索优化0+60+2+19
我的博客历程0000+11

结果按分数降序排列,标题包含关键词的文章排在最前面。

关键词高亮

1function highlightMulti(text, keywords) {
2  var escaped = keywords.map(function(k) {
3    return k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4  }).join('|');
5  var re = new RegExp('(' + escaped + ')', 'gi');
6  return text.replace(re, '<mark class="search-highlight">$1</mark>');
7}

多个关键词通过 | 组合成正则表达式,一次替换完成所有关键词的高亮,性能优于逐个关键词多次替换。

摘要截取

 1function extractSnippetMulti(text, keywords, maxLen) {
 2  var lower = text.toLowerCase();
 3  var bestIdx = -1;
 4  for (var i = 0; i < keywords.length; i++) {
 5    var idx = lower.indexOf(keywords[i]);
 6    if (idx > -1) { bestIdx = idx; break; }
 7  }
 8  if (bestIdx === -1) return text.substring(0, maxLen).trim() + '...';
 9  var start = Math.max(0, bestIdx - Math.floor(maxLen / 3));
10  var end = Math.min(text.length, start + maxLen);
11  var snippet = text.substring(start, end).trim();
12  if (start > 0) snippet = '…' + snippet;
13  if (end < text.length) snippet = snippet + '…';
14  return snippet;
15}

截取策略:以第一个匹配关键词的位置为基准,向前取 maxLen/3,向后取剩余长度,确保关键词出现在摘要前部。

结果渲染

 1results.innerHTML = matched.map(function(entry) {
 2  var item = entry.item;
 3  var titleHl = highlightMulti(item.title, keywords);
 4  var snippet = highlightMulti(extractSnippetMulti(
 5    item.content || item.summary || '', keywords, 80), keywords);
 6  var dateStr = item.date || '';
 7  var catHtml = item.categories.length
 8    ? '<span class="search-result-cat">' + item.categories.join(' / ') + '</span>' : '';
 9  var tagHtml = item.tags.slice(0, 3).map(function(tag) {
10    return '<span class="search-result-tag">#' + tag + '</span>';
11  }).join('');
12
13  return '<a href="' + item.permalink + '" class="search-result-item">' +
14    '<div class="search-result-header">' +
15      '<h4 class="search-result-title">' + titleHl + '</h4>' +
16      (dateStr ? '<span class="search-result-date">' + dateStr + '</span>' : '') +
17    '</div>' +
18    '<p class="search-result-snippet">' + snippet + '</p>' +
19    '<div class="search-result-meta">' + catHtml + tagHtml + '</div>' +
20  '</a>';
21}).join('');

每条结果包含:标题(高亮)+ 日期 + 摘要(高亮)+ 分类标签 + 文章标签(最多3个)。

🎨 三、样式设计

3.1 搜索按钮

 1.search-bar-toggle {
 2  display: inline-flex;
 3  align-items: center;
 4  gap: 8px;
 5  padding: 6px 14px;
 6  margin-left: 16px;
 7  background: var(--bg-card);
 8  border: 1px solid var(--border-color);
 9  border-radius: 8px;
10  color: var(--text-secondary);
11  font-size: 0.84rem;
12  transition: all 0.2s ease;
13}
14
15.search-bar-toggle:hover {
16  border-color: rgba(59,130,246,0.5);
17  color: var(--text-primary);
18  background: var(--bg-secondary);
19  box-shadow: 0 2px 8px rgba(59,130,246,0.1);
20}

暗黑主题

 1[data-theme="dark"] .search-bar-toggle {
 2  background: var(--bg-tertiary);
 3  border-color: var(--border-color);
 4  color: var(--text-secondary);
 5}
 6
 7[data-theme="dark"] .search-bar-toggle:hover {
 8  border-color: rgba(96,165,250,0.5);
 9  color: var(--text-primary);
10  background: var(--bg-secondary);
11  box-shadow: 0 2px 8px rgba(96,165,250,0.08);
12}

3.2 搜索遮罩层

 1.search-overlay {
 2  display: none;
 3  position: fixed;
 4  inset: 0;
 5  background: rgba(0, 0, 0, 0.5);
 6  backdrop-filter: blur(4px);
 7  z-index: 10000;
 8  align-items: flex-start;
 9  justify-content: center;
10  padding-top: calc(var(--header-offset) + 20px);
11}
12
13.search-overlay.active {
14  display: flex;
15}

遮罩层使用 backdrop-filter: blur(4px) 毛玻璃效果增强视觉层次。

3.3 关键词高亮

 1.search-highlight {
 2  background: linear-gradient(120deg, rgba(59,130,246,0.28), rgba(99,102,241,0.28));
 3  color: var(--text-primary);
 4  padding: 1px 4px;
 5  border-radius: 3px;
 6  font-weight: 600;
 7}
 8
 9[data-theme="dark"] .search-highlight {
10  background: linear-gradient(120deg, rgba(96,165,250,0.38), rgba(167,139,250,0.38));
11}

3.4 结果标签

 1.search-result-tag {
 2  font-size: 0.7rem;
 3  color: rgba(59,130,246,0.8);
 4  padding: 1px 6px;
 5  background: rgba(59,130,246,0.08);
 6  border-radius: 4px;
 7}
 8
 9[data-theme="dark"] .search-result-tag {
10  color: rgba(96,165,250,0.9);
11  background: rgba(96,165,250,0.1);
12}

3.5 移动端适配

 1@media (max-width: 768px) {
 2  .search-bar-toggle {
 3    padding: 5px 10px;
 4    margin-left: 8px;
 5  }
 6  .search-bar-text { display: none; }
 7  .search-bar-kbd { display: none; }
 8  .search-input { font-size: 16px; }
 9  .search-kbd { display: none; }
10  .search-hint { display: none; }
11  .search-results { max-height: 55vh; }
12}

移动端关键处理:font-size: 16px 防止 iOS Safari 自动缩放页面。

⚙️ 四、配置方法

4.1 启用搜索

hugo.toml 中配置:

1[params.search]
2  enable = true
3  placeholder = "搜索文章..."

placeholder 控制搜索框的占位文字,不设置时默认为"搜索文章…"。

4.2 调整权重

main.jsSearch 模块中修改:

1var WEIGHT = { title: 10, tags: 6, categories: 4, summary: 2, content: 1 };
字段默认权重说明
title10标题匹配权重最高,确保标题含关键词的文章排前面
tags6标签匹配权重次高,标签是人工标注的关键词
categories4分类匹配
summary2摘要匹配
content1正文匹配权重最低,避免长文章仅因字数多而排名靠前

4.3 调整搜索结果数

1var matched = scored.slice(0, 12);  // 修改 12 调整最大结果数

4.4 调整防抖时间

1timer = setTimeout(search, 200);  // 修改 200 调整防抖毫秒数

4.5 调整索引内容长度

layouts/_default/index.json 中修改:

1"content": {{ $page.Plain | truncate 1200 | jsonify }},

truncate 1200 控制正文截取字数。增大可提高长文章的搜索覆盖率,但会增加索引文件体积。

🔍 五、搜索算法详解

5.1 与旧版算法的对比

特性旧版新版
搜索方式完整字符串匹配多关键词 AND 匹配
排序方式原始顺序(无排序)权重评分降序
搜索范围标题 + 内容 + 摘要标题 + 标签 + 分类 + 摘要 + 内容
结果数10 条12 条
索引加载页面加载时立即加载首次打开搜索框时懒加载
加载失败静默失败显示错误提示
标签展示仅分类分类 + 标签(最多3个)

5.2 多关键词搜索示例

输入 “Hugo 搜索”:

  1. 拆分为 ["hugo", "搜索"]
  2. 遍历索引,每篇文章必须同时包含 “hugo” 和 “搜索” 才算匹配
  3. 计算每篇匹配文章的权重分数
  4. 按分数降序排列

输入 “评论气泡”:

  1. 拆分为 ["评论", "气泡"]
  2. 文章必须同时包含 “评论” 和 “气泡”
  3. 标题含 “评论气泡” 的文章分数最高(标题匹配 +10 × 2 = 20)

🐛 六、常见问题排查

Q1: 搜索按钮不显示

  1. 检查 hugo.toml[params.search] enable = true 是否已设置
  2. 打开浏览器开发者工具,搜索 #search-toggle,确认 DOM 是否渲染
  3. 检查 CSS 中 .search-bar-toggle 是否被其他样式覆盖

Q2: 搜索无结果

  1. 打开浏览器控制台,检查是否有 [Search] ✓ 索引已加载 日志
  2. 如果没有 → 索引未加载,检查 /index.json 是否可访问
  3. 在控制台执行 fetch('/index.json').then(r=>r.json()).then(d=>console.log(d.length)) 确认索引内容
  4. 尝试输入单个简单关键词(如文章标题中的词)

Q3: Ctrl+K 快捷键无效

  1. 检查是否有其他扩展或页面脚本占用了 Ctrl+K
  2. 在控制台执行 document.addEventListener('keydown', e => console.log(e.key, e.ctrlKey)) 确认按键事件
  3. macOS 上同时支持 Cmd+Ke.metaKey

Q4: 移动端搜索框导致页面缩放

已通过 font-size: 16px 解决。iOS Safari 在 input 字号小于 16px 时会自动缩放页面。

Q5: 搜索索引文件过大

  1. index.json 模板中减小 truncate 值(如从 1200 改为 800)
  2. 检查是否有大量短页面被索引(可通过 .Site.RegularPages 过滤)

📊 七、性能指标

指标数值
索引文件大小约 50-200KB(取决于文章数量和内容长度)
索引加载时机首次打开搜索框时(懒加载)
搜索防抖200ms
单次搜索耗时< 5ms(100 篇文章以内)
DOM 节点数搜索按钮 + 遮罩层(初始隐藏)
内存占用索引数据 + 闭包变量,约 100-500KB

📁 八、相关文件索引

文件作用
layouts/partials/header.html搜索入口按钮(Logo 右侧)
layouts/partials/search-overlay.html搜索遮罩层模板
layouts/_default/index.json搜索索引生成模板
assets/js/main.js Search 模块搜索算法 + 显隐控制 + 事件绑定
assets/css/main.css搜索按钮 + 遮罩层 + 结果项样式
hugo.toml [params.search]搜索功能配置开关
9 / 14
版权声明

本文作者 Lumin

本文链接 https://www.zhengquan.xyz/tech/search-guide/

许可协议 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

请作者喝杯咖啡 ☕

  • 微信打赏
    微信支付
  • 支付宝打赏
    支付宝
点击按钮查看打赏二维码
🎁 推荐工具
试试这些实用在线工具,提升工作效率
前往工具集 →

留言评论

期待你的想法

评论加载中