第三章 扩展博客功能

在上一章里,使用了基本的表单提交数据与处理表单,进行复杂的分组查询,还学习了集成第三方应用。

这一章涵盖如下的内容:

1自定义模板标签和过滤器

Django提供很多内置的模板标签和过滤器,例如{% if %}{% block %},在之前已经在模板中使用过它们。关于完整的内置模板标签和过滤器可以看https://docs.djangoproject.com/en/2.0/ref/templates/builtins/

Django也允许你创建自已的模板标签,用来在页面中进行各种操作。当你想在模板中实现Django没有提供的功能时,自定义模板标签是一个好的选择。

1.1自定义模板标签

Django提供下边的两个函数可以简单快速地创建自定义模板标签:

所有的自定义标签,只能够在模板中使用。

blog应用目录里新建一个目录templatetags,然后在其中创建一个空白的__init__.py,再创建一个文件blog_tags.py,文件结构如下:

blog/
    __init__.py
    models.py
    ...
    templatetags/
        __init__.py
        blog_tags.py

注意这里的命名很关键,一会在模板内载入自定义标签的时候就需要使用这个包的名称(templatetags)。

先创建一个简单的标签,在刚刚创建的blog_tags.py里写如下代码:

from django import template
from ..models import Post

register = template.Library()

@register.simple_tag
def total_posts():
    return Post.published.count()

我们创建了一个标签返回已经发布的文章总数。每个模板标签的模块内需要一个register变量,是template.Library的实例,用于注册自定义的标签。然后创建了一个Python函数total_posts,用@register.simple_tag装饰器将其注册为一个简单标签。Django会使用这个函数的名称作为标签名称,如果想使用其他的名称,可以通过name属性指定,例如@register.simple_tag(name='my_tag')

在添加了新的自定义模板标签或过滤器之后,必须重新启动django服务才能在模板中生效。

在模板内使用自定义标签之前,需要使用{% load %}在模板中引入自定义的标签,像之前提到的那样,使用创建的包的名字作为load的参数。

打开blog/templates/base.html模板,在最上边添加{% load blog_tags %},然后使用自定义标签{% total_posts %},这个模板最后看起来像这样:

{% load blog_tags %}
{% load static %}
<!DOCTYPE html>
<html>
    <head>
        <title>{% block title %}{% endblock %}</title>
        <link href="{% static "css/blog.css" %}" rel="stylesheet">
    </head>
    <body>
        <div id="content">
            {% block content %}
            {% endblock %}
        </div>
        <div id="sidebar">
            <h2>My blog</h2>
            <p>This is my blog. I've written {% total_posts %} posts so far.</p>
        </div>
    </body>
</html>

启动站点然后到http://127.0.0.1:8000/blog/,应该可以看到总文章数被显示在了侧边栏:

自定义标签威力强大之处在于不通过视图就可以处理数据和添加到模板中。

现在再来创建一个用于在侧边栏显示最新发布的文章的自定义标签。这次通过inclusion_tag渲染一段HTML代码。编辑blog_tags.py文件,添加如下内容:

@register.inclusion_tag('blog/post/latest_posts.html')
def show_latest_posts(count=5):
    latest_posts = Post.published.order_by('-publish')[:count]
    return {'latest_posts': latest_posts}

在上边的代码里,使用@register.inclusion_tag装饰器装饰了自定义函数show_latest_posts,同时指定了要渲染的模板为blog/post/latest_posts.html。我们的模板标签还接受一个参数count,通过Post.published.order_by('-publish')[:count]切片得到指定数量的最新发布的文章。注意这个自定义函数返回的是一个字典对象而不是一个具体的值。inclusion_tag必须返回一个类似给模板传入变量的字典,用于在blog/post/latest_posts.html中取得数据并渲染模板。刚刚创建的这一切在模板中以类似{% show_latest_posts 3 %}的形式来使用。

那么在模板里如何使用呢,这是一个带参数的tag,就像之前使用内置的那样,在标签后边加参数: {% show_latest_posts 3 %}

blog/post/目录下创建latest_posts.html文件,添加下列代码:

<ul>
    {% for post in latest_posts %}
    <li>
        <a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
    </li>
    {% endfor %}
</ul>

在上边的代码里,使用lastest_posts显示出了一个未排序的最新文章列表。然后编辑blog/base.html,加入新标签以显示最新的三篇文章:

<div id="sidebar">
    <h2>My blog</h2>
    <p>This is my blog. I've written {% total_posts %} posts so far.</p>
    <h3>Latest posts</h3>
    {% show_latest_posts 3 %}
</div>

模板中调用了自定义标签,然后传入了一个参数3,之后这个标签的位置会被替换成被渲染的模板。

现在返回浏览器,刷新页面,可以看到侧边栏显示如下:

最后再来创建一个simple_tag,将数据存放在这个标签内,而不是像我们创建的第一个标签一样直接展示出来。我们使用这种方法来显示评论最多的文章。编辑blog_tags.py,添加下列代码:

from django.db.models import Count

@register.simple_tag
def get_most_commented_posts(count=5):
    return Post.published.annotate(total_comments=Count('comments')).order_by('-total_comments')[:count]

在上边的代码里,使用annotate,对每篇文章的评论进行计数然后按照total_comments字段降序排列,之后使用[:count]切片得到评论数量最多的特定篇文章,

除了Count之外,Django提供了其他聚合函数AvgMaxMinSum,聚合函数的详情可以查看https://docs.djangoproject.com/en/2.0/topics/db/aggregation/

编辑blog/base.html把以下代码追加到侧边栏<div>元素内部:

<h3>Most commented posts</h3>
{% get_most_commented_posts as most_commented_posts %}
<ul>
    {% for post in most_commented_posts %}
        <li>
            <a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
        </li>
    {% endfor %}
</ul>

在这里使用了as将我们的模板标签保存在一个叫做most_commented_posts变量中,然后展示其中的内容。

现在打开浏览器刷新页面,可以看到新的页面如下:

关于自定义模板标签的详情可以查看https://docs.djangoproject.com/en/2.0/howto/custom-template-tags/

1.2自定义模板过滤器

Djangon内置很多模板过滤器用于在模板内修改变量。模板过滤器实际上是Ptyhon函数,接受1或2个参数-其中第一个参数是变量,第二个参数是一个可选的变量,然后返回一个可供其他模板过滤器操作的值。一个模板过滤器类似这样:{{ variable|my_filter }},带参数的模板过滤器类似:{{ variable|my_filter:"foo" }},可以连用过滤器,例如:{{ variable|filter1|filter2 }}

我们来通过自定义过滤器使我们的博客文章可以支持Markdown语法,然后将其转换成对应的HTML格式。Markdown是一种易于使用的轻型标记语言而且可以方便的转为HTML。可以在这里查看Markdown语法的详情:https://daringfireball.net/projects/markdown/basics

先通过pip安装Python的Markdown模块:

pip install Markdown==2.6.11

然后编辑blog_tags.py,添加如下内容:

from django.utils.safestring import mark_safe
import markdown

@register.filter(name='markdown')
def markdown_format(text):
    return mark_safe(markdown.markdown(text))

我们使用和模板标签类似的方式注册了模板过滤器,为了不使我们的函数和markdown模块重名,将我们的函数命名为markdown_format,但是指定了模板中的标签名称为markdown,这样就可以通过{{ variable|markdown }}来使用标签了。mark_safe用来告诉Django该段HTML代码是安全的,可以将其渲染到最终页面中。默认情况下,Django对于生成的HTML代码都会进行转义而不会当成HTML代码解析,只有对mark_safe标记的内容才会正常解析,这是为了避免在页面中出现危险代码(如添加外部JavaScript文件的代码)。

然后在blog/post/list.htmlblog/post/detail.html中的{% extends %}之后引入自定义模板的模块:

{% load blog_tags %}

post/detail.html中,找到下边这行:

{{ post.body|linebreaks }}

将其替换成:

{{ post.body|markdown }}

然后在post/list.html中,找到下边这行:

{{ post.body|truncatewords:30|linebreaks }}

将其替换成:

{{ post.body|markdown|truncatewords_html:30 }}

truncatewords_html过滤器不会截断未闭合的HTML标签。

浏览器中打开http://127.0.0.1:8000/admin/blog/post/add/然后写一段使用Markdown语法的正文:

This is a post formatted with markdown
--------------------------------------
*This is emphasized* and **this is more emphasized**.
Here is a list:

* One
* Two
* Three

And a [link to the Django website](https://www.djangoproject.com/)

然后在浏览器中查看刚添加的文章,可以看到如下的结果:

可以看到,自定义模板过滤器在需要自定义格式的时候非常好用。可在https://docs.djangoproject.com/en/2.0/howto/custom-template-tags/#writing-custom-template-filters找到更多关于自定义过滤器的信息。

2创建站点地图

Django带有站点地图功能框架,可以根据网站内容动态的生成站点地图。站点地图是一个XML文件,用于给搜索引擎提供信息,可以帮助搜索引擎爬虫索引站点的内容。

Django的站点地图框架是django.contrib.sites。如果使用同一个Django项目运行多个站点,站点地图功能允许为每个站点创建单独的站点地图。为了使用站点地图功能,需要启用django.contrib.sitesjango.contrib.sitemaps,将这两个应用添加到settings.pyINSTALLED_APPS设置中:

SITE_ID = 1
# Application definition
INSTALLED_APPS = [
    # ...
    'django.contrib.sites',
    'django.contrib.sitemaps',
]

由于添加了新应用,需要执行数据迁移。迁移完成之后,在blog应用目录内创建sitemaps.py文件并添加如下代码:

from django.contrib.sitemaps import Sitemap
from .models import Post


class PostSitemap(Sitemap):
    changefreq = 'weekly'
    priority = 0.9

    def items(self):
        return Post.published.all()

    def lastmod(self, obj):
        return obj.updated

我们通过继承django.contrib.sitemapsSitemap类创建了一个站点地图对象。changefreqpriority属性表示文章页面更新的频率和这些文章与站点的相关性(最大相关性为1)。使用items()方法返回这个站点地图所需的QuerySet,Django默认会调用数据对象的get_absolute_url()获取对应的URL,如果想手工指定具体的URL,可以为PostSitemap添加一个location方法。lastmod方法接收items()返回的每一个数据对象然后返回其更新时间。changefreqpriority可以通过定义方法也可以作为属性名进行设置。站点地图的详细使用可以看官方文档:https://docs.djangoproject.com/en/2.0/ref/contrib/sitemaps/

最后就是配置站点地图对应的URL,打开项目的根urls.py,添加如下代码:

from django.urls import path, include
from django.contrib import admin
from django.contrib.sitemaps.views import sitemap
from blog.sitemaps import PostSitemap

sitemaps = {'posts': PostSitemap,}

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls', namespace='blog')),
    path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap')
]

在上述代码中,导入了需要的库并且定义了一个站点地图的字典。将sitemap.xml的路径匹配到sitemap视图。sitemaps字典会被传递给sitemap视图。现在启动站点然后在浏览器中打开http://127.0.0.1:8000/sitemap.xml,可以看到如下的输出:

<?xml version="1.0" encoding="utf-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <url>
        <loc>http://example.com/blog/2017/12/15/markdown-post/</loc>
        <lastmod>2017-12-15</lastmod>
        <changefreq>weekly</changefreq>
        <priority>0.9</priority>
    </url>
    <url>
        <loc>
            http://example.com/blog/2017/12/14/who-was-django-reinhardt/
        </loc>
        <lastmod>2017-12-14</lastmod>
        <changefreq>weekly</changefreq>
        <priority>0.9</priority>
    </url>
</urlset>

其中的每个文章的URL都是由get_absolute_url()方法生成的。lastmod标签的内容是最后更新的时间,和在类中定义的一样。changefreqpriority标签也包含对应的值。可以看到站点名为example.com,这个名称来自于数据库存储的Site对象,这是我们在为站点地图应用进行数据迁移的时候默认生成的一个对象。打开http://127.0.0.1:8000/admin/sites/site/,可以看到类似下边的界面:

上边的截图里包含刚才使用的的主机名,可以修改成自己想要的主机名。可以将其修改成localhost:8000以使用本地地址生成URL。如下图所示:

设置之后,URL就会使用本地地址。在生产环境中,需要在此处设置正常的主机和站点名。

3创建订阅功能

Django内置一些功能,采用和创建站点地图类似的方法为站点增加RSS或者Atom订阅信息。订阅信息是一个特定的数据格式,通常是XML文件,用于向用户提供这个网站的更新数据,用户通过一个订阅代理程序,订阅这个网站的feed,就可以接收到新的内容通知。

blog应用目录下新建feeds.py文件并添加如下代码:

from django.contrib.syndication.views import Feed
from django.template.defaultfilters import truncatewords
from .models import Post

class LastestPostFeed(Feed):
    title = 'My blog'
    link = '/blog/'
    description = 'New posts of my blog.'

    def items(self):
        return Post.published.all()[:5]

    def item_title(self, item):
        return item.title

    def item_description(self, item):
        return truncatewords(item.body, 30)

译者注:item_description(self, item)这个函数并没有对post.body进行处理,所以会返回未经处理的markdown代码,在不支持markdown的Feed阅读器里会出现问题,读者可以修改该函数,调用markdown库输出转换后的字符串。

这段代码首先继承了内置的Feed类,titlelinkdescription属性分别对应XML文件的<title><link><description>标签。

items()方法用于获得订阅信息要使用的数据对象,这里只取了最新发布的5篇文章。item_title()item_description()方法接收每一个数据对象并且分别返回标题和正文前30个字符。

现在为配置订阅路径,编辑blog/urls.py,导入LatestPostsFeed然后配置一条新路由:

from .feeds import LatestPostsFeed

urlpatterns = [
    # ...
    path('feed/', LatestPostsFeed(), name='post_feed'),
]

打开地址http://127.0.0.1:8000/blog/feed/即可看到feed内容,包含最新的5篇文章:

<?xml version="1.0" encoding="utf-8"?>
    <rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <title>My blog</title>
        <link>http://localhost:8000/blog/</link>
        <description>New posts of my blog.</description>
        <atom:link href="http://localhost:8000/blog/feed/" rel="self"/>
        <language>en-us</language>
        <lastBuildDate>Fri, 15 Dec 2017 09:56:40 +0000</lastBuildDate>
        <item>
            <title>Who was Django Reinhardt?</title>
            <link>http://localhost:8000/blog/2017/12/14/who-was-djangoreinhardt/</
        link>
        <description>Who was Django Reinhardt.</description>
        <guid>http://localhost:8000/blog/2017/12/14/who-was-djangoreinhardt/</
        guid>
        </item>
        ...
    </channel>
</rss>

如果用一个RSS阅读器打开这个链接,就可以在其界面里看到对应信息。

最后一步是在侧边栏添加订阅本博客的链接,在blog/base.html里的侧边栏<div>里追加:

<p><a href='{% url "blog:post_feed" %}'>Subscribe to my RSS feed</a></p>

之后就可以在http://127.0.0.1:8000/blog/看到订阅链接,类似下图:

4增加全文搜索功能

现在可以为博客添加搜索功能。Django ORM可以使用contains或类似的icontains过滤器执行简单的匹配任务。比如:

from blog.models import Post
Post.objects.filter(body__contains='framework')

然而,如果要执行更加复杂的搜索,比如通过权重或者相似性,就必须使用一个全文搜索引擎(full-text search engine)

Django的全文检索功能基于PostgreSQL数据库的全文搜索特性,所以这个全文检索功能不能用于Django ORM支持的其他种类的数据库。PostgreSQL的全文搜索介绍在https://www.postgresql.org/docs/10/static/textsearch.html

虽然Django ORM通过面向对象抽象,可以不依赖于具体的数据库,但是用于PostgreSQL的一部分功能无法用于其他数据库。

4.1自定义模板过滤器

现在blog项目使用的是Python自带的SQLlite数据库,对于开发而言已经足够。在生产环境中,需要使用诸如MySQL,PostgreSQL和Oracle等更强力的数据库。为了实现全文搜索功能,我们将转而使用PostgreSQL。

在Linux环境下,需要先安装PostgreSQL和Python的相关依赖:

sudo apt-get install libpq-dev python-dev

之后使用下列命令安装PostgreSQL:

sudo apt-get install postgresql postgresql-contrib

如果使用MacOS X或者Windows,到https://www.postgresql.org/download/查看安装说明。

在安装完之后,还需要为python安装psycopg2模块:

pip install psycopg2==2.7.4

译者注:使用 pip install psycopg2-binary 命令安装 psycopg2 最新版模块。

在PostgreSQL中创建一个名叫blog的用户,供项目使用。在系统命令行中输入下列命令:

su postgres
createuser -dP blog

会被提示输入密码。创建用户成功之后,创建一个名叫blog的数据库并将所有权设置给blog用户:

createdb -E utf8 -U blog blog

译者注:PostgreSQL在Linux安装后会创建一个postgres用户,使用该用户身份可以登陆PostgreSQL数据库进行操作。

之后编辑settings.py文件中的DATABASES设置:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'blog',
        'USER': 'blog',
        'PASSWORD': '*****',
    }
}

这里我们将默认的数据库修改成了PostgreSQL,之后执行数据迁移和创建超级用户。然后可以在http://127.0.0.1:8000/admin/登录管理后台。由于更换了数据库,博客应用内没有任何文章数据,录入一些数据,为之后使用全文搜索做准备。

4.2执行简单搜索

编辑settings.py文件,将django.contrib.postgres加入到INSTALLED_APPS中:

INSTALLED_APPS = [
    # ...
    'django.contrib.postgres',
]

激活还应用后,现在可以通过search参数进行搜索:

from blog.models import Post
Post.objects.filter(body__search='django')

这个QuerySet会使用PostgreSQL为body字段创建内容是'django'字符串的搜索向量和一个查询,通过匹配查询和结果向量,返回最后的结果。

4.3执行简单搜索

可能想在多个字段中进行检索。在这种情况下,需要定义一个SearchVector搜索向量对象,来创建一个针对Post模型的titlebody进行搜索的向量:

from django.contrib.postgres.search import SearchVector
from blog.models import Post

Post.objects.annotate(search=SearchVector('title','body'),).filter(search='poem')

使用分组函数然后定义了两个字段的向量,之后使用查询,就可以得到最终的结果。

全文搜索是一个密集计算过程,如果要检索的数据多于几百行,最好创建一个匹配搜索向量的索引,Django提供了一个SearchVectorField字段在模型中定义搜索向量。具体可以参考https://docs.djangoproject.com/en/2.0/ref/contrib/postgres/search/#performance

4.4创建搜索视图

现在我们可以创建一个视图用于让用户执行搜索。首先需要一个表单让用户输入要查询的数据,编辑blog应用的forms.py增加下面的表单:

class SearchForm(forms.Form):
    query = forms.CharField()

query字段用于输入查询内容,然后编辑blog应用的views.py文件,然后添加如下代码:

from django.contrib.postgres.search import SearchVector
from .forms import EmailPostForm, CommentForm, SearchForm

def post_search(request):
    form = SearchForm()
    query = None
    results = []
    if 'query' in request.GET:
        form = SearchForm(request.GET)
        if form.is_valid():
            query = form.cleaned_data['query']
            results = Post.objects.annotate(search=SearchVector('title', 'slug', 'body'), ).filter(search=query)
    return render(request, 'blog/post/search.html', {'query': query, "form": form, 'results': results})

这个视图先初始化空白SearchForm表单,通过GET请求附加URL参数的方式提交表单。如果request.GET字典中存在query参数且通过了表单验证,就执行搜索并返回结果。

视图编写完毕,需要编写对应的模板,在/blog/post/目录下创建search.html文件,添加如下代码:

{% extends 'blog/base.html' %}

{% block title %}
Search
{% endblock %}

{% block content %}
{% if query %}
    <h1>Post containing {{ query }}</h1>
    <h3>
    {% with results.count as total_results %}
        Found {{ total_results }} result{{ total_results|pluralize }}
    {% endwith %}
    </h3>
    {% for post in results %}
        <h4>
            <a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
        </h4>
        {{ post.body|truncatewords:5 }}
    {% empty %}
        <p>There are no results for your query.</p>
    {% endfor %}
    <p><a href="{% url 'blog:post_search' %}">Search again</a></p>
{% else %}
    <h1>Search for posts</h1>
    <form action="." method="get">
    {{ form.as_p }}
        <input type="submit" value="Search">
    </form>
{% endif %}
{% endblock %}

就像在视图中的逻辑一样,我们通过query参数存在与否判断表单是否提交。默认不提交表单的页面,显示表单和一个搜索按钮,进行搜索后则显示结果总数和搜索到的文章列表。

由于表单里配置了反向解析,所以编辑blog应用的urls.py

path('search/', views.post_search, name='post_search'),

现在启动站点,到http://127.0.0.1:8000/blog/search/可以看到表单页面如下:

输入查询内容然后点击搜索按钮,可以看到查询结果,如下所示:

现在我们就创建了全文搜索功能了。

4.5词干提取与搜索排名

Django提供了一个SearchQuery类将一个查询词语转换成一个查询对象,默认会通过词干提取算法(stemming algorithms)转换成查询对象,用于更好的进行匹配。在查询时候还可能会依据相关性进行排名。PostgreSQL提供了一个排名功能,按照被搜索内容在一条数据里出现的次数和频率进行排名。

编辑blog应用的views.py文件:

from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank

然后找到下边这行:

results = Post.objects.annotate(search=SearchVector('title', 'body'),).filter(search=query)

替换成如下内容:

search_vector = SearchVector('title', 'body')
search_query = SearchQuery(query)
results = Post.objects.annotate(search=search_vector,
                rank=SearchRank(search_vector, search_query)
          ).filter(search=search_query).order_by('-rank')

在上边的代码中,先创建了一个SearchQuery对象,用其过滤结果。之后使用SearchRank方法将结果按照相关性排序。打开http://127.0.0.1:8000/blog/search/并且用不同的词语来测试搜索。下边是使用'django'搜索的示例:

4.6搜索权重

当按照相关性进行搜索时,可以给不同的向量赋予不同的权重,从而影响搜索结果。例如,可以对在标题中搜索到的结果给予比正文中搜索到的结果更大的权重。编辑blog应用的views.py文件:

search_vector = SearchVector('title', weight='A') + SearchVector('body', weight='B')
results = Post.objects.annotate(
                search=search_vector,
                rank=SearchRank(search_vector, search_query)
            ).filter(rank__gte=0.3).order_by('-rank')

在上边的代码中,给title和body字段的搜索向量赋予了不同的权重。默认的权重DCBA分别对应 0.1, 0.2, 0.41。我们给title字段的搜索向量赋予权重1.0,给body字段的搜索向量的权重是0.4,说明文章标题的重要性要比正文更重要,最后设置了只显示综合权重大于0.3的搜索结果。

4.7三元相似性搜索

还有一种搜索方式是三元相似性搜索。三元指的是三个连续的字符。通过比较两个字符串里,有多少个三个连续的字符相同,可以检测这两个字符串的相似性。这种搜索方式对于不同语言中的相近单词很高效。

如果要在PostgreSQL中使用三元检索,必须安装一个pg_trgm扩展。

在系统命令行执行下列命令连接到数据库:

psql blog

然后输入下列数据库指令:

CREATE EXTENSION pg_trgm

然后编辑视图来增加三元相似搜索功能,编辑blog应用的views.py,这一次需要导入的新组件:

from django.contrib.postgres.search import TrigramSimilarity

然后将Post搜索查询对象替换成如下这样:

results = Post.objects.annotate(
              similarity=TrigramSimilarity('title',query),
              ).filter(similarity__gte=0.1).order_by('-similarity')

打开http://127.0.0.1:8000/blog/search/,然后试验不同的三元相似锁边,下边的例子显示了使用yango想搜索django的结果:

现在就为我们的博客创建了一个强力的搜索引擎。关于在Django中使用PostgreSQL的全文搜索,可以参考https://docs.djangoproject.com/en/2.0/ref/contrib/postgres/search/

4.8使用其他全文搜索引擎

除了常用的PostgreSQL之外,还有Solr和Elasticsearch等常用的全文搜索引擎,可以使用Haystack来将其集成到Django中。Haystack是一个Django应用,作为一个搜索引擎的抽象层工作。提供了与Django的QuerySet非常类似的API供执行搜索操作。关于Haystack的详情可以查看:http://haystacksearch.org/

总结

在这一章学习了创建自定义的模板标签和过滤器,用于提供自定义的功能。还创建了站点地图用于搜索引擎优化和RSS feed为用户提供订阅功能。之后将站点数据库改用PostgreSQL从而实现了全文搜索功能。

在下一章中,将使用Django内置验证模块创建一个社交网站,创建用户信息和进行第三方认证登录。