Keyword Search and Pagination
This chapter teaches you how to implement combined searches using SQLAlchemy and optimize the article list page using Flask-SQLAlchemyβs built-in pagination functionality.
ilike β Case-Insensitive Search
SQLAlchemyβs ilike method enables case-insensitive fuzzy matching.
It corresponds to SQLβs LIKE operator but ignores case.
Examples
# Single-field search
posts = Post.query.filter(Post.title.ilike('%flask%')).all()
# Multi-field combined search: or_() indicates OR relationship
from sqlalchemy import or_
posts = Post.query.filter(
or_(
Post.title.ilike('%flask%'),
Post.summary.ilike('%flask%')
)
).all()
| Method | SQL Operator | Case-Sensitive |
|---|---|---|
like() |
LIKE |
Yes |
ilike() |
LIKE (case-insensitive) |
No |
contains() |
LIKE '%x%' |
Yes |
startswith() |
LIKE 'x%' |
Yes |
in_() |
IN |
Exact match |
paginate β Pagination
Flask-SQLAlchemy provides the paginate() method, which returns both the current pageβs data and pagination metadata in a single call.
Example
# Refactored index view from main.py
page = request.args.get('page', 1, type=int) # Current page number (default: page 1)
per_page = 6 # Display 6 articles per page
# paginate(page, per_page, error_out)
# error_out=False: Do not raise 404 when page number exceeds range; return empty list instead
pagination = posts_query.paginate(page=page, per_page=per_page, error_out=False)
posts = pagination.items # Article list for the current page
Pagination Object Attributes
| Attribute | Type | Description |
|---|---|---|
items |
list | Data for the current page |
page |
int | Current page number |
pages |
int | Total number of pages |
total |
int | Total number of records |
has_prev |
bool | Whether there is a previous page |
has_next |
bool | Whether there is a next page |
prev_num |
int | Previous page number |
next_num |
int | Next page number |
Complete Search + Pagination View
Example
# File path: 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
# Base query
posts_query = Post.query.order_by(Post.created_at.desc())
# Category filter
if category_slug:
posts_query = posts_query.join(Category).filter(Category.slug == category_slug)
# Keyword search (title or summary)
if keyword:
posts_query = posts_query.filter(
or_(
Post.title.ilike(f'%{keyword}%'),
Post.summary.ilike(f'%{keyword}%')
)
)
# Pagination
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)
# Query parameters are passed via URL (e.g., `/?q=flask&page=2`). Pagination navigation links must retain current search and category parameters; otherwise, filters will be lost when navigating pages.
Search Box and Pagination Navigation in Templates
Example
{% extends 'base.html' %}
{% block title %}TUTORIAL Blog - Home{% endblock %}
{% block content %}
<h2 class="section-title">Latest Articles{% endblock %}
<!-- Search Box -->
<div class="search-bar">
<form method="get" action="{{ url_for('main.index') }}">
<input type="text" name="q" value="{{ keyword }}"
placeholder="Search article titles or summaries..." class="search-input">
{% if keyword %}
<a href="{{ url_for('main.index', category=category_slug) }}" class="clear-btn">β</a>
{% endif %}
</form>
</div>
<!-- Category Filter -->
<div class="category-bar">
<a href="{{ url_for('main.index', q=keyword) }}"
class="{% if not category_slug %}active{% endif %}">All</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">
Total {{ pagination.total }} articles
{% if keyword %}, searching for "{{ 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>
<!-- Pagination Navigation -->
{% 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) }}">β Previous Page</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) }}">Next Page β</a>
{% endif %}
</div>
{% endif %}
{% else %}
<p class="empty-tip">No matching articles found.</p>
{% endif %}
{% endblock %}
Chapter Summary
In this chapter, you mastered practical features for the article list page: ilike case-insensitive search, or_() multi-field combined search, paginate() pagination queries, and rendering pagination navigation in templates while preserving filter parameters.
Search, category filtering, and pagination can be combined arbitrarilyβfilters will not be lost.
YouTip