Wagtail 搜索功能实战:从项目模板内置搜索应用到索引扩展与 update_index 索引重建
【免费下载链接】wagtailA Django content management system focused on flexibility and user experience项目地址: https://gitcode.com/GitHub_Trending/wa/wagtail
在 Wagtail 的 portfolio 站点教程中,“为站点添加搜索”是上线前的最后一块拼图:wagtail start创建的项目自带一个可直接使用的搜索应用,但默认行为只覆盖“能搜什么”和“怎么展示”的最低要求。本篇基于教程文档 add_search.md 展开,完整走一遍搜索模板定制(结果计数、有序列表、分页)、全局导航入口接入,以及通过search_fields将intro、body等自定义字段加入搜索索引并用update_index命令重建索引的全过程。读完本篇,你将掌握 Wagtail 内置搜索应用的视图、模板与索引三层结构,并能按自己的站点需求扩展可搜索字段。
项目自带的搜索应用:视图与路由
用 Wagtail 的start命令(wagtail start mysite一类的项目创建流程)启动项目时,会生成一个内置搜索应用。项目模板 project_template/search/views.py 中的search视图是它的核心,可以逐行理解其工作机制:
def search(request): search_query = request.GET.get("query", None) page = request.GET.get("page", 1) # Search if search_query: search_results = Page.objects.live().search(search_query) else: search_results = Page.objects.none() # Pagination paginator = Paginator(search_results, 10) try: search_results = paginator.page(page) except PageNotAnInteger: search_results = paginator.page(1) except EmptyPage: search_results = paginator.page(paginator.num_pages)这段源码揭示了几个对模板定制至关重要的事实(见 search 视图):
- 查询词来自 GET 参数
query,所以搜索表单必须用method="get",输入框name="query";未提供查询词时视图返回空查询集,模板中search_results为假值,走“未搜索”分支。 - 只搜已发布页面:
Page.objects.live().search(search_query)意味着草稿页、未发布页不会出现在结果中。 - 分页基于 Django 的
Paginator,每页 10 条:模板里用到的search_results.paginator.count(总条数)、search_results.paginator.num_pages(总页数)、search_results.number(当前页)、has_previous/has_next、previous_page_number/next_page_number全部来自 Django 分页对象的属性,视图层已把异常页码兜底为第 1 页或末页。 - 视图还支持接入 Wagtail 的“搜索推广结果”(Promoted search results)模块:源码中留有注释掉的
Query.get(search_query)/query.add_hit()调用(views.py 注释),取消注释并安装wagtail.contrib.search_promotions即可记录查询日志。
路由层面,项目模板的根 URL 配置把该视图挂在/search/并命名为search(见 urls.py):
path("search/", search_views.search, name="search"),这正是后续模板中{% url 'search' %}能解析的原因。默认搜索页模板位于 project_template/search/templates/search/search.html:一个 GET 表单加一个<ul>结果列表,外加上一页/下一页链接,但没有结果计数和完整的分页说明。下一节就在这个默认模板基础上做定制。
定制搜索模板:结果计数、有序列表与完整分页
教程的做法是修改你项目中的search/templates/search/search.html(注意:教程中的站点mysite是把模板放到自己项目目录里的,base.html继承自该项目的页面模板)。定制后的完整模板如下:
{% extends "base.html" %} {% load static wagtailcore_tags %} {% block body_class %}template-searchresults{% endblock %} {% block title %}Search{% endblock %} {% block content %} <h1>Search</h1> <form action="{% url 'search' %}" method="get"> <input type="text" name="query"{% if search_query %} value="{{ search_query }}"{% endif %}> <input type="submit" value="Search" class="button"> </form> {% if search_results %} {# Add this paragraph to display the details of results found: #} <p>You searched{% if search_query %} for "{{ search_query }}"{% endif %}, {{ search_results.paginator.count }} result{{ search_results.paginator.count|pluralize }} found.</p> {# Replace the <ul> HTML element with the <ol> html element: #} <ol> {% for result in search_results %} <li> <h4><a href="{% pageurl result %}">{{ result }}</a></h4> {% if result.search_description %} {{ result.search_description }} {% endif %} </li> {% endfor %} </ol> {# Improve pagination by adding: #} {% if search_results.paginator.num_pages > 1 %} <p>Page {{ search_results.number }} of {{ search_results.paginator.num_pages }}, showing {{ search_results|length }} result{{ search_results|pluralize }} out of {{ search_results.paginator.count }}</p> {% endif %} {% if search_results.has_previous %} <a href="{% url 'search' %}?query={{ search_query|urlencode }}&page={{ search_results.previous_page_number }}">Previous</a> {% endif %} {% if search_results.has_next %} <a href="{% url 'search' %}?query={{ search_query|urlencode }}&page={{ search_results.next_page_number }}">Next</a> {% endif %} {% elif search_query %} No results found {% endif %} {% endblock %}对照默认模板,这版模板做了三处定制,逐一说明其原理与依赖:
1. 结果统计段落
<p>You searched{% if search_query %} for "{{ search_query }}"{% endif %}, {{ search_results.paginator.count }} result{{ search_results.paginator.count|pluralize }} found.</p>search_query是视图通过模板上下文传入的用户查询词(request.GET.get("query"));search_results.paginator.count是 DjangoPaginator的总结果数(不是当前页条数,当前页条数用search_results|length获取);pluralize过滤器处理单复数:result与results自动切换。
2. 用有序列表<ol>替代无序列表<ul>
<ol> {% for result in search_results %} <li> <h4><a href="{% pageurl result %}">{{ result }}</a></h4> {% if result.search_description %} {{ result.search_description }} {% endif %} </li> {% endfor %} </ol>循环遍历当前页的搜索结果,用<ol>渲染后结果自动带序号。这里有两个关键点:
{% pageurl result %}来自wagtailcore_tags,它根据 Wagtail 的多站点模型解析出该页面在当前站点下的真实 URL——比直接{{ result.url }}更贴合 Wagtail 的站点路由机制;result.search_description是搜索结果对象上的描述属性,当搜索后端能基于命中的字段生成摘要时会显示,为空时整段省略。
3. 完整的分页呈现
{% if search_results.paginator.num_pages > 1 %} <p>Page {{ search_results.number }} of {{ search_results.paginator.num_pages }}, showing {{ search_results|length }} result{{ search_results|pluralize }} out of {{ search_results.paginator.count }}</p> {% endif %} {% if search_results.has_previous %} <a href="{% url 'search' %}?query={{ search_query|urlencode }}&page={{ search_results.previous_page_number }}">Previous</a> {% endif %} {% if search_results.has_next %} <a href="{% url 'search' %}?query={{ search_query|urlencode }}&page={{ search_results.next_page_number }}">Next</a> {% endif %}分页逻辑分三层:
search_results.paginator.num_pages > 1才显示“第 X 页 / 共 Y 页,本页 Z 条 / 共 N 条”的说明,避免单页时分页信息冗余;has_previous/has_next分别控制 Previous / Next 链接的显隐,链接 URL 由{% url 'search' %}加上query与page两个查询参数拼成——注意{{ search_query|urlencode }}保证查询词中的空格、引号等字符在 URL 中安全;- 最后
elif search_query分支处理“搜了但没有结果”的情况,输出No results found;若用户根本没输入查询词(search_query为空、search_results为空),则什么都不显示,只保留空表单。
模板结构{% if search_results %} ... {% elif search_query %} ... {% endif %}与视图行为严格对应:有查询且命中 → 走第一个分支;有查询但零命中 →search_results是空查询集(假值)但search_query为真 → 走elif分支。
在站点头部暴露搜索入口
搜索页做好后,需要在整个站点可触达。教程选择在mysite/templates/includes/header.html的导航末尾追加一个搜索链接:
{% load wagtailcore_tags navigation_tags wagtailuserbar %} <header> <a href="#main" class="skip-link">Skip to content</a> {% get_site_root as site_root %} <nav> <p> <a href="{% pageurl site_root %}">{{ site_root.title }}</a> | {% for menuitem in site_root.get_children.live.in_menu %} <a href="{% pageurl menuitem %}">{{ menuitem.title }}</a>{% if not forloop.last %} | {% endif %} {% endfor %} {# Display your search by adding this: #} | <a href="/search/">Search</a> </p> </nav> {% wagtailuserbar "top-right" %} </header>头部通过{% get_site_root as site_root %}取得当前站点根页面,遍历其get_children.live.in_menu渲染站点主导航(这是教程前文“设置站点菜单”一节的成果);搜索入口以| <a href="/search/">Search</a>的形式硬编码在导航尾部,直接指向项目模板中定义的/search/路径。到这一步,用户已经可以发起搜索并浏览结果了——但默认情况下,只有出现在页面标题里的词才能被搜到,这就引出了最后一部分:索引扩展。
让 intro 与 body 字段可被搜索:search_fields 与 update_index
Wagtail 的搜索基于“索引”机制:页面模型需要声明哪些字段进入搜索索引,搜索后端才会对它们建立倒排数据。默认的Page.search_fields只覆盖标题相关字段,所以教程要求把BlogPage自定义的intro和body字段显式加入索引。在blog/models.py中做如下修改:
# Add to the existing imports: from wagtail.search import index class BlogPage(Page): # Keep the existing parent_page_types, fields, methods and content_panels definitions, and add: search_fields = Page.search_fields + [ index.SearchField("intro"), index.SearchField("body"), ]这里有三个值得展开的细节:
Page.search_fields + [...]而非覆盖:search_fields是类属性,写成search_fields = [...]会丢掉父类Page已有的标题索引;用+拼接可继承父模型的全部可搜索字段,再追加intro、body。这是 Wagtail(以及其底层的 modelsearch 库)索引约定中最重要的惯用法。index.SearchField的声明方式:from wagtail.search import index引入后,index.SearchField("字段名")即声明“该模型的这个字段应被索引”。字段名是字符串,对应模型上的属性(如 StreamField、RichText 等复杂字段同样适用)。在wagtail/search/index.py中可以看到该模块对上游modelsearch库的再导出(见 index.py),Wagtail 的搜索索引 API 建立在modelsearch包之上,SearchField、SearchRelationField等声明器均源于此。body是 StreamField 也能索引:SearchField("body")声明后,索引时会自动从该 StreamField 的块内容中提取可检索文本,因此正文里的文字都可以被搜到,这正是教程最后“Searching will now return results for words found within the body text”一句的实现基础。
重建索引:update_index 管理命令
声明了search_fields之后,已存在的页面并不会自动获得新字段的索引数据,需要手动重建。教程给出的命令是:
python manage.py update_index在当前仓库中,该命令由 Wagtail 的管理命令模块再导出实现(见 update_index.py,一行from modelsearch.management.commands.rebuild_modelsearch_index import *即可看出其委托给了modelsearch的索引重建命令),另有别名命令 wagtail_update_index.py。执行update_index会遍历所有已注册的搜索引擎模型并重建索引,之后intro、body中的词就会被搜索命中。
需要注意适用前提:
- 该命令面向默认数据库搜索后端与基于
modelsearch的索引管线;若项目切换到其他后端(Elasticsearch/OpenSearch 等,见 backends.md),索引方式与命令行为以对应文档为准; - 修改了
search_fields声明后,每次调整都要重新执行update_index才能让存量页面生效(新增/修改页面时会由信号机制自动增量更新,见 searching.md 与 indexing.md 的相关说明)。
小结与后续
至此,教程站点的搜索功能形成完整闭环:项目模板的search视图提供查询与分页 → 定制的search.html呈现计数、有序结果与分页导航 → 头部导航暴露入口 →search_fields声明 +update_index命令把intro、body纳入索引。按教程原文的收尾语:“Well done! You now have a fully deployable portfolio site.”——搜索是部署前的最后一环,接下来的 deployment.md 会讲解如何把站点真正部署上线。
延伸阅读(均为仓库内文档):
- docs/topics/search/indexing.md:搜索索引的完整机制与
search_fields深入说明; - docs/topics/search/searching.md:从代码中执行搜索查询的更多 API;
- docs/topics/search/backends.md:搜索后端(含
update_index在后端间的行为差异)。
【免费下载链接】wagtailA Django content management system focused on flexibility and user experience项目地址: https://gitcode.com/GitHub_Trending/wa/wagtail
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考