我给网站加了个全站搜索:3 个组件、4 个 API、5 个快捷键
文章多了找不到?工具混在一起不好找?全站搜索是个人网站的标配 —— 自己做一个比想象的简单。
为什么自己做
13 篇文章 + 9 个工具,手动找东西开始变慢。搜过的方案:
| 方案 | 问题 |
|---|---|
| Algolia / MeiliSearch | 免费额度小,超出要钱 |
| PostgreSQL 全文检索 | 小项目过度设计,要装插件 |
| Django + SQLite LIKE | ✅ 够用、零依赖、2 分钟搞定 |
作为数通工程师,我习惯:最简方案优先。SQLite 的 LIKE '%keyword%' 完全够个人博客用。
整体架构
┌─────────────────────────────────────────────────┐
│ 导航栏 🔍 按钮 / `/` 快捷键 │
└────────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 搜索面板(fixed 顶部下拉) │
│ ┌───────────────────────────────────────┐ │
│ │ [搜索框] 200ms 防抖 → API 请求 │ │
│ └───────────────────────────────────────┘ │
│ ┌───────────────────────────────────────┐ │
│ │ 实时结果(带高亮) │ │
│ │ • 工具 + 文章 │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
用户可:
- 点击结果 → 跳转
- ↑↓ Enter → 键盘选中
- Esc → 关闭面板
- Enter 无选中 → 进入完整搜索结果页 `/search/?q=...`
1. Django 后端:3 个视图
视图 1:完整搜索结果页 /search/
def search(request):
query = request.GET.get('q', '').strip()
articles = []
matched_tools = []
if query:
# 文章:标题、摘要、Markdown 内容、标签、分类
article_qs = Article.objects.filter(status='published')\
.select_related('category').prefetch_related('tags')
# 多关键词 AND 搜索(空格分隔)
keywords = [k for k in re.split(r'\s+', query) if k]
for kw in keywords:
article_qs = article_qs.filter(
Q(title__icontains=kw) |
Q(excerpt__icontains=kw) |
Q(markdown_content__icontains=kw) |
Q(tags__name__icontains=kw) |
Q(category__name__icontains=kw)
)
articles = article_qs.distinct()\
.order_by('-is_top', '-published_at')[:30]
# 工具搜索(内存里匹配)
all_tools = get_all_tools_full()
matched_tools = [
t for t in all_tools
if any(kw.lower() in t['name'].lower() or
kw.lower() in t['desc'].lower()
for kw in keywords)
]
视图 2 & 3:JSON API(导航栏实时下拉用)
def api_search(request):
"""文章 API:返回前 5 条"""
query = request.GET.get('q', '').strip()
if not query or len(query) < 2:
return JsonResponse({'results': [], 'count': 0})
article_qs = Article.objects.filter(status='published')\
.select_related('category')
keywords = [k for k in re.split(r'\s+', query) if k]
for kw in keywords:
article_qs = article_qs.filter(
Q(title__icontains=kw) | Q(excerpt__icontains=kw)
)
# ⚠️ 注意:先 filter 后 slice(不能反过来!)
articles = article_qs.distinct()[:5]
results = [{
'title': a.title,
'url': a.get_absolute_url(),
'excerpt': a.excerpt[:80] if a.excerpt else '',
'category': a.category.name if a.category else '',
'type': 'article',
} for a in articles]
return JsonResponse({'results': results, 'count': len(results)})
def api_search_tools(request):
"""工具 API:实时返回"""
query = request.GET.get('q', '').strip().lower()
if not query:
return JsonResponse({'results': []})
all_tools = get_all_tools_full()
matched = [{
'name': t['name'],
'slug': t['slug'],
'desc': t['desc'],
'icon': t['icon'],
'category': t['category'],
} for t in all_tools
if query in t['name'].lower() or
query in t['desc'].lower() or
query in t['slug']]
return JsonResponse({'results': matched})
🐛 我踩过的坑
调试时第一版报:TypeError: Cannot filter a query once a slice has been taken
# ❌ 错误写法
article_qs = Article.objects.filter(...).select_related(...)[:5]
article_qs = article_qs.filter(...) # ← 这里报错!
# ✅ 正确写法
article_qs = Article.objects.filter(...).select_related(...)
article_qs = article_qs.filter(...)
articles = article_qs.distinct()[:5] # 在最后 slice
Django ORM 不允许 slice 后再 filter —— 必须先 filter,再 slice。
2. 前端:实时下拉 + 键盘导航
search.js 核心逻辑:
// 防抖:输入停止 200ms 后才发请求
let debounceTimer;
input.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(doSearch, 200);
});
function doSearch() {
const q = input.value.trim();
if (q.length < 2) {
resultsBox.classList.remove('active');
return;
}
// 并行请求文章 + 工具
Promise.all([
fetch(`/api/search/?q=${encodeURIComponent(q)}`).then(r => r.json()),
fetch(`/api/search/tools/?q=${encodeURIComponent(q)}`).then(r => r.json()),
]).then(([articles, tools]) => {
currentResults = [];
// 工具在前(短小,结果明确)
tools.results.forEach(t => {
currentResults.push({
type: 'tool',
title: t.name,
meta: t.desc,
icon: t.icon,
url: `/tools/${t.slug}/`,
});
});
// 文章
articles.results.forEach(a => {
currentResults.push({
type: 'article',
title: a.title,
meta: a.category || '文章',
icon: 'bi-newspaper',
url: a.url,
});
});
renderResults();
});
}
键盘导航
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closePanel();
else if (e.key === 'ArrowDown') selectNext();
else if (e.key === 'ArrowUp') selectPrev();
else if (e.key === 'Enter') {
e.preventDefault();
openSelected();
}
});
// 全局快捷键:按 / 聚焦搜索框
document.addEventListener('keydown', (e) => {
if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(e.target.tagName)) {
e.preventDefault();
openPanel();
}
});
关键词高亮
function highlight(text, q) {
if (!q) return text;
const re = new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return text.replace(re, '<mark>$1</mark>');
}
replace 的参数需要转义正则特殊字符 —— 否则用户搜 (192.168) 会爆炸。
3. UI 设计:CSS 动画
搜索面板用 transform: translateY(-100%) 隐藏,.open 时 translateY(0):
.search-panel {
position: fixed;
top: 0;
left: 0;
right: 0;
background: var(--bg-card);
box-shadow: var(--shadow-hover);
transform: translateY(-100%);
transition: transform 0.3s ease;
}
.search-panel.open {
transform: translateY(0);
}
比 display:none 流畅 —— GPU 加速,60 fps 不掉帧。
4. 完整快捷键清单
| 快捷键 | 作用 | 范围 |
|---|---|---|
/ |
打开搜索 | 全局(输入框内除外) |
Esc |
关闭搜索 | 全局 |
↑ |
上一项 | 搜索面板内 |
↓ |
下一项 | 搜索面板内 |
Enter |
打开选中项 / 跳转结果页 | 搜索面板内 |
5 个快捷键 —— 数通工程师标配 ⌨️
5. 踩坑经验总结
坑 1:Django ORM slice + filter
见上文。
坑 2:搜索框 autofocus 在 firefox 不灵
<!-- ❌ Firefox 第一次访问不聚焦 -->
<input autofocus>
<!-- ✅ 兼容写法 -->
<input autofocus>
<!-- JS 加 setTimeout 兜底 -->
input.focus();
input.select();
坑 3:截图脚本截图渲染前 CSS 未生效
# ❌ 设置完主题立刻截图(CSS 还没应用)
set_theme(driver, 'dark')
driver.save_screenshot(...)
# ✅ 刷新页面让 JS 应用主题
driver.refresh()
time.sleep(2)
driver.save_screenshot(...)
坑 4:搜索性能
LIKE '%keyword%' 不走索引,全表扫描。
对个人博客(< 100 篇文章)完全够用。
如果以后文章破千: 1. 加 SQLite FTS5 全文索引(10 分钟搞定) 2. 或迁 PostgreSQL + GIN 索引
但现在不需要。
6. 测试效果
| 查询 | 命中 | 性能 |
|---|---|---|
VLAN |
1 篇文章 + 0 工具 | < 50ms |
MAC |
2 篇文章 + 2 工具 | < 50ms |
OSPF 配置 |
AND 搜索,2 篇文章 | < 80ms |
xxxnotexist |
空结果 + 提示 | < 30ms |
13 篇 + 9 个工具的规模,单次查询 < 100ms,完全够用。
在线使用
👉 试试全站搜索
按 / 或点导航栏 🔍 图标即可。
📚 后续优化方向
如果以后想做更"专业"的搜索:
- 🔍 SQLite FTS5 全文索引(替代 LIKE)
- 🌟 结果评分(标题命中权重 > 摘要 > 内容)
- 📝 搜索历史(localStorage)
- 🤖 AI 语义搜索(embedding + 向量数据库)
- 🔗 相关文章推荐(点击搜索结果后侧栏显示)
但对 13 篇文章 + 9 个工具,当前方案已足够。
📱 喜欢这类技术拆解?扫码右侧关注「网英的日常」,第一时间收到新功能上线通知!