关键词搜索与分页
本章你将学会用 SQLAlchemy 实现联合搜索,并用 Flask-SQLAlchemy 内置分页功能优化列表页。
ilike — 大小写不敏感搜索
SQLAlchemy 的 ilike 方法实现了大小写不敏感的模糊匹配。
它对应 SQL 的 LIKE 操作符,但忽略大小写。
实例
# 单字段搜索
posts = Post.query.filter(Post.title.ilike('%flask%')).all()
# 多字段联合搜索:or_() 表示 OR 关系
from sqlalchemy import or_
posts = Post.query.filter(
or_(
Post.title.ilike('%flask%'),
Post.summary.ilike('%flask%')
)
).all()
posts = Post.query.filter(Post.title.ilike('%flask%')).all()
# 多字段联合搜索:or_() 表示 OR 关系
from sqlalchemy import or_
posts = Post.query.filter(
or_(
Post.title.ilike('%flask%'),
Post.summary.ilike('%flask%')
)
).all()
| 方法 | SQL 操作符 | 区分大小写 |
|---|---|---|
| like() | LIKE | 区分 |
| ilike() | LIKE(忽略大小写) | 不区分 |
| contains() | LIKE '%x%' | 区分 |
| startswith() | LIKE 'x%' | 区分 |
| in_() | IN | 精确匹配 |
paginate — 分页
Flask-SQLAlchemy 提供了 paginate() 方法,一次调用返回当前页数据 + 分页元信息。
实例
# 从 main.py 改造 index 视图
page = request.args.get('page', 1, type=int) # 当前页码(默认第 1 页)
per_page = 6 # 每页显示 6 篇文章
# paginate(page, per_page, error_out)
# error_out=False:页码超出范围时不报 404,而是返回空列表
pagination = posts_query.paginate(page=page, per_page=per_page, error_out=False)
posts = pagination.items # 当前页的文章列表
page = request.args.get('page', 1, type=int) # 当前页码(默认第 1 页)
per_page = 6 # 每页显示 6 篇文章
# paginate(page, per_page, error_out)
# error_out=False:页码超出范围时不报 404,而是返回空列表
pagination = posts_query.paginate(page=page, per_page=per_page, error_out=False)
posts = pagination.items # 当前页的文章列表
pagination 对象属性
| 属性 | 类型 | 说明 |
|---|---|---|
| items | 列表 | 当前页的数据 |
| page | int | 当前页码 |
| pages | int | 总页数 |
| total | int | 总记录数 |
| has_prev | bool | 是否有上一页 |
| has_next | bool | 是否有下一页 |
| prev_num | int | 上一页页码 |
| next_num | int | 下一页页码 |
完整的搜索 + 分页视图
实例
# 文件路径:app/blueprints/main.py
from flask import Blueprint, render_template, request
from sqlalchemy import or_
from app.models import Post, Category
main_bp = Blueprint('main', __name__)
@main_bp.route("/")
def index():
category_slug = request.args.get('category', '')
keyword = request.args.get('q', '').strip()
page = request.args.get('page', 1, type=int)
per_page = 6
# 基础查询
posts_query = Post.query.order_by(Post.created_at.desc())
# 分类筛选
if category_slug:
posts_query = posts_query.join(Category).filter(Category.slug == category_slug)
# 关键词搜索(标题或摘要)
if keyword:
posts_query = posts_query.filter(
or_(
Post.title.ilike(f'%{keyword}%'),
Post.summary.ilike(f'%{keyword}%')
)
)
# 分页
pagination = posts_query.paginate(
page=page, per_page=per_page, error_out=False)
posts = pagination.items
return render_template('index.html',
posts=posts, pagination=pagination,
categories=Category.query.all(),
category_slug=category_slug, keyword=keyword)
from flask import Blueprint, render_template, request
from sqlalchemy import or_
from app.models import Post, Category
main_bp = Blueprint('main', __name__)
@main_bp.route("/")
def index():
category_slug = request.args.get('category', '')
keyword = request.args.get('q', '').strip()
page = request.args.get('page', 1, type=int)
per_page = 6
# 基础查询
posts_query = Post.query.order_by(Post.created_at.desc())
# 分类筛选
if category_slug:
posts_query = posts_query.join(Category).filter(Category.slug == category_slug)
# 关键词搜索(标题或摘要)
if keyword:
posts_query = posts_query.filter(
or_(
Post.title.ilike(f'%{keyword}%'),
Post.summary.ilike(f'%{keyword}%')
)
)
# 分页
pagination = posts_query.paginate(
page=page, per_page=per_page, error_out=False)
posts = pagination.items
return render_template('index.html',
posts=posts, pagination=pagination,
categories=Category.query.all(),
category_slug=category_slug, keyword=keyword)
查询参数通过 URL 传递(如
/?q=flask&page=2),分页导航的链接必须带上当前的搜索和分类参数,否则翻页后条件会丢失。
模板中的搜索框和分页导航
实例
<!-- 文件路径:app/templates/index.html -->
{% extends 'base.html' %}
{% block title %}RUNOOB 博客 - 首页{% endblock %}
{% block content %}
<h2 class="section-title">最新文章</h2>
<!-- 搜索框 -->
<div class="search-bar">
<form method="get" action="{{ url_for('main.index') }}">
<input type="text" name="q" value="{{ keyword }}"
placeholder="搜索文章标题或摘要..." class="search-input">
{% if keyword %}
<a href="{{ url_for('main.index', category=category_slug) }}" class="clear-btn">✕</a>
{% endif %}
</form>
</div>
<!-- 分类筛选 -->
<div class="category-bar">
<a href="{{ url_for('main.index', q=keyword) }}"
class="{% if not category_slug %}active{% endif %}">全部</a>
{% for cat in categories %}
<a href="{{ url_for('main.index', category=cat.slug, q=keyword) }}"
class="{% if category_slug == cat.slug %}active{% endif %}">
{{ cat.name }}
</a>
{% endfor %}
</div>
<p class="result-info">
共 {{ pagination.total }} 篇文章
{% if keyword %},搜索「{{ keyword }}」{% endif %}
</p>
{% if posts %}
<div class="article-grid">
{% for post in posts %}
<a href="{{ url_for('posts.post_detail', post_id=post.id) }}" class="card-link">
<div class="article-card">
<div class="card-content">
<span class="card-category">{{ post.category.name }}</span>
<h3>{{ post.title }}</h3>
<p>{{ post.summary|truncate(80) }}</p>
<span class="card-date">{{ post.created_at.strftime('%Y-%m-%d') }}</span>
</div>
</div>
</a>
{% endfor %}
</div>
<!-- 分页导航 -->
{% if pagination.pages > 1 %}
<div class="pagination">
{% if pagination.has_prev %}
<a href="{{ url_for('main.index', page=pagination.prev_num, category=category_slug, q=keyword) }}">← 上一页</a>
{% endif %}
{% for page_num in range(1, pagination.pages + 1) %}
{% if page_num == pagination.page %}
<span class="current">{{ page_num }}</span>
{% else %}
<a href="{{ url_for('main.index', page=page_num, category=category_slug, q=keyword) }}">{{ page_num }}</a>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<a href="{{ url_for('main.index', page=pagination.next_num, category=category_slug, q=keyword) }}">下一页 →</a>
{% endif %}
</div>
{% endif %}
{% else %}
<p class="empty-tip">没有找到匹配的文章。</p>
{% endif %}
{% endblock %}
{% extends 'base.html' %}
{% block title %}RUNOOB 博客 - 首页{% endblock %}
{% block content %}
<h2 class="section-title">最新文章</h2>
<!-- 搜索框 -->
<div class="search-bar">
<form method="get" action="{{ url_for('main.index') }}">
<input type="text" name="q" value="{{ keyword }}"
placeholder="搜索文章标题或摘要..." class="search-input">
{% if keyword %}
<a href="{{ url_for('main.index', category=category_slug) }}" class="clear-btn">✕</a>
{% endif %}
</form>
</div>
<!-- 分类筛选 -->
<div class="category-bar">
<a href="{{ url_for('main.index', q=keyword) }}"
class="{% if not category_slug %}active{% endif %}">全部</a>
{% for cat in categories %}
<a href="{{ url_for('main.index', category=cat.slug, q=keyword) }}"
class="{% if category_slug == cat.slug %}active{% endif %}">
{{ cat.name }}
</a>
{% endfor %}
</div>
<p class="result-info">
共 {{ pagination.total }} 篇文章
{% if keyword %},搜索「{{ keyword }}」{% endif %}
</p>
{% if posts %}
<div class="article-grid">
{% for post in posts %}
<a href="{{ url_for('posts.post_detail', post_id=post.id) }}" class="card-link">
<div class="article-card">
<div class="card-content">
<span class="card-category">{{ post.category.name }}</span>
<h3>{{ post.title }}</h3>
<p>{{ post.summary|truncate(80) }}</p>
<span class="card-date">{{ post.created_at.strftime('%Y-%m-%d') }}</span>
</div>
</div>
</a>
{% endfor %}
</div>
<!-- 分页导航 -->
{% if pagination.pages > 1 %}
<div class="pagination">
{% if pagination.has_prev %}
<a href="{{ url_for('main.index', page=pagination.prev_num, category=category_slug, q=keyword) }}">← 上一页</a>
{% endif %}
{% for page_num in range(1, pagination.pages + 1) %}
{% if page_num == pagination.page %}
<span class="current">{{ page_num }}</span>
{% else %}
<a href="{{ url_for('main.index', page=page_num, category=category_slug, q=keyword) }}">{{ page_num }}</a>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<a href="{{ url_for('main.index', page=pagination.next_num, category=category_slug, q=keyword) }}">下一页 →</a>
{% endif %}
</div>
{% endif %}
{% else %}
<p class="empty-tip">没有找到匹配的文章。</p>
{% endif %}
{% endblock %}
本章小结
本章你掌握了列表页的实用功能:ilike 大小写不敏感搜索、or_() 多字段联合搜索、paginate() 分页查询、模板中渲染分页导航并保留筛选参数。
搜索 + 分类筛选 + 分页,三者可以任意组合,条件不会丢失。
