Recherche par mot-clé et pagination

Dans ce chapitre, vous apprendrez à utiliser SQLAlchemy pour la recherche combinée et à optimiser la page de liste avec la pagination intégrée de Flask-SQLAlchemy.


ilike — recherche insensible à la casse

La méthodeilikede SQLAlchemy implémente une correspondance floue insensible à la casse.

Il correspond à SQLLIKEopérateur, mais ignore la casse.

Exemple

# Recherche sur un seul champ
posts = Post.query.filter(Post.title.ilike('%flask%')).all()

# Recherche combinée multi-champs : or_() désigne une relation OR
from sqlalchemy import or_
posts = Post.query.filter(
    or_(
        Post.title.ilike('%flask%'),
        Post.summary.ilike('%flask%')
    )
).all()
MéthodesOpérateurs SQLSensible à la casse
like()LIKEDistinguer
ilike()LIKE (insensible à la casse)Ne pas distinguer
contains()LIKE '%x%'Distinguer
startswith()LIKE 'x%'Distinguer
in_()INCorrespondance exacte

paginate — Pagination

Flask-SQLAlchemy fournitpaginate()La méthode renvoie les données de la page actuelle + les métadonnées de pagination en un seul appel.

Exemple

# Refactoriser la vue index de main.py
page = request.args.get('page', 1, type=int)  # Numéro de page actuel (page 1 par défaut)
per_page = 6                                    # Afficher 6 articles par page

# paginate(page, per_page, error_out)
# error_out=False : lorsque le numéro de page dépasse la plage, renvoie une liste vide au lieu d'une 404
pagination = posts_query.paginate(page=page, per_page=per_page, error_out=False)
posts = pagination.items     # Liste des articles pour la page actuelle

attributs de l'objet pagination

PropriétéTypeDescription
itemsListeDonnées de la page actuelle
pageintNuméro de la page actuelle
pagesintNombre total de pages
totalintTotal des enregistrements
has_prevboolIndique s'il existe une page précédente
has_nextboolIndique s'il existe une page suivante
prev_numintNuméro de la page précédente
next_numintNuméro de la page suivante

Vue complète de recherche + pagination

Exemple

# Chemin du fichier: 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

    # Requête de base
    posts_query = Post.query.order_by(Post.created_at.desc())

    # Filtre de catégorie
    if category_slug:
        posts_query = posts_query.join(Category).filter(Category.slug == category_slug)

    # Recherche par mot-clé (titre ou résumé)
    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)

Les paramètres de requête sont passés via l'URL (par exemple,/?q=flask&page=2), les liens de navigation de pagination doivent transporter les paramètres de recherche et de catégorie actuels, sinon les conditions seront perdues après la pagination.


Champ de recherche et navigation de pagination dans le modèle

Exemple

<!-- Chemin du fichier : app/templates/index.html -->
{% extends 'base.html' %}

{% block title %}EXAMPLE Blog - Accueil{% endblock %}

{% block content %}
<h2 class="section-title">Derniers articles</h2>

<!-- Boîte de recherche -->
<div class="search-bar">
    <form method="get" action="{{ url_for('main.index') }}">
        <input type="text" name="q" value="{{ keyword }}"
              placeholder=Rechercher dans les titres ou résumés d'articles... class="search-input">
        {% if keyword %}
        <a href="{{ url_for('main.index', category=category_slug) }}" class="clear-btn">✕</a>
        {% endif %}
    </forme>
</div>

<!-- Filtre par catégorie -->
<div class="category-bar">
    <a href="{{ url_for('main.index', q=keyword) }}"
      class="{% if not category_slug %}active{% endif %}">Toutes</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 %}, recherche de "{{ 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>

<!-- Navigation de pagination -->
{% 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) }}">← Page précédente</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) }}">Page suivante →</a>
    {% endif %}
</div>
{% endif %}

{% else %}
<p class="empty-tip">Aucun article correspondant n'a été trouvé.</p>
{% endif %}
{% endblock %}

Résumé du chapitre

Dans ce chapitre, vous avez maîtrisé les fonctionnalités pratiques de la page de liste : la recherche insensible à la casse avec ilike, la recherche combinée multi-champs avec or_(), la requête de pagination avec paginate(), et le rendu de la navigation de pagination dans les gabarits tout en préservant les paramètres de filtre.

La recherche + le filtrage par catégorie + la pagination peuvent être combinés de n'importe quelle manière, et les conditions ne seront pas perdues.

autres extensions