YouTip LogoYouTip

Flask Blog Search Pagination

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.

← Vue3 Blog Lifecycle FetchFlask Blog Views Blueprint β†’