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
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éthodes | Opérateurs SQL | Sensible à la casse |
|---|---|---|
| like() | LIKE | Distinguer |
| ilike() | LIKE (insensible à la casse) | Ne pas distinguer |
| contains() | LIKE '%x%' | Distinguer |
| startswith() | LIKE 'x%' | Distinguer |
| in_() | IN | Correspondance 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
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é | Type | Description |
|---|---|---|
| items | Liste | Données de la page actuelle |
| page | int | Numéro de la page actuelle |
| pages | int | Nombre total de pages |
| total | int | Total des enregistrements |
| has_prev | bool | Indique s'il existe une page précédente |
| has_next | bool | Indique s'il existe une page suivante |
| prev_num | int | Numéro de la page précédente |
| next_num | int | Numéro de la page suivante |
Vue complète de recherche + pagination
Exemple
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
{% 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