文章多起来以后,导航只能帮人缩小范围,搜索才能直接找答案。我给首页加一个搜索框,检索标题、摘要和正文,并让搜索词跟着分页链接继续传递。

搜索词进入视图后先清理空格,再构造 OR 条件。只要标题、摘要或正文命中,文章就进入结果集。
搜索框放在每个页面都可见的位置
公共导航加入表单。
1
2
3
4
| <form method="get" action="{% url 'blog:list' %}" class="search-form">
<input type="search" name="q" value="{{ query|default:'' }}" placeholder="搜索文章" aria-label="搜索文章">
<button type="submit">搜索</button>
</form>
|
用 GET 而不是 POST。搜索没有修改数据,结果地址应该可以复制、收藏、后退和刷新。name="q" 很短,但语义固定,模板和视图一起使用。
输入框回填 query。搜完以后用户能看到自己刚才输入的词,也方便改一两个字继续搜。
视图里组合 OR 条件
列表视图先取已发布文章,再按关键词过滤。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| from django.db.models import Q
def post_list(request):
posts = Post.published.select_related("category").prefetch_related("tags")
query = request.GET.get("q", "").strip()
if query:
posts = posts.filter(
Q(title__icontains=query)
| Q(summary__icontains=query)
| Q(body__icontains=query)
)
return render(request, "blog/post_list.html", {
**sidebar_context(),
"page_obj": paginate_posts(request, posts),
"query": query,
})
|
Post.published 仍然排在最前面。搜索结果绝不能把草稿带出来。
icontains 生成不区分大小写的包含查询。SQLite 和 PostgreSQL 都能执行,但对大量文本的效率不同。当前规模够用;等文章数量成为瓶颈,再换数据库全文索引或专门搜索服务。
空格用 strip() 清掉。用户输入的全是空格时,query 变成空字符串,页面回到普通列表,不会拿空词去查全表。
分页链接必须带上搜索词
搜索结果可能超过一页。分页链接只带 page 会让第二页丢失关键词。
1
2
3
4
5
6
7
| {% if page_obj.has_previous %}
<a href="?q={{ query|urlencode }}&page={{ page_obj.previous_page_number }}">上一页</a>
{% endif %}
<span>第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页</span>
{% if page_obj.has_next %}
<a href="?q={{ query|urlencode }}&page={{ page_obj.next_page_number }}">下一页</a>
{% endif %}
|
urlencode 处理中文和特殊字符。没有它,含空格或标点的搜索词会在跳页时变形。
标题也根据有没有关键词切换。
1
2
3
4
5
| {% if query %}
<h1>搜索:{{ query }}</h1>
{% else %}
<h1>{{ page_title|default:"文章列表" }}</h1>
{% endif %}
|
这样读者能确认当前确实在搜索结果里。
测试标题、摘要和正文
在视图测试里加一条。
1
2
3
4
5
| def test_search_matches_title_summary_and_body(self):
response = self.client.get(reverse("blog:list"), {"q": "详情页正文"})
self.assertContains(response, "博客的第一页")
self.assertEqual(response.context["query"], "详情页正文")
|
测试文章的标题没有「详情页正文」,摘要也没有,只有正文里有。它能证明搜索确实覆盖了正文。
再补一条草稿不参与搜索的断言更稳妥。
1
2
3
4
5
6
7
8
9
| Post.objects.create(
title="草稿里的秘密关键词",
slug="draft-search",
body="搜索测试不该看到它。",
category=self.category,
author=self.user,
)
response = self.client.get(reverse("blog:list"), {"q": "秘密关键词"})
self.assertNotContains(response, "草稿里的秘密关键词")
|
运行完整测试。
当前共 23 条测试。
1
2
3
| Ran 23 tests in 3.608s
OK
|
搜索结果页显示命中文章、搜索词和分页。

这个实现仍然基于 icontains,适合中小型博客。它没有分词,也不会按相关度打分。等到文章量、查询频率或搜索质量成为问题,再迁移到数据库全文索引或外部搜索服务。到那时,视图可以先继续接受 q 参数,只替换内部查询实现。
源码
GitHub 地址:https://github.com/yidao620c/simpleblog