Búsqueda por palabras clave y paginación

En este capítulo aprenderás a implementar búsqueda combinada con SQLAlchemy y paginación con offset/limit.


Búsqueda ilike insensible a mayúsculas y minúsculas

Ejemplo

# Búsqueda de campo único
posts = db.query(Post).filter(Post.title.ilike(f'%{keyword}%')).all()

# Búsqueda combinada de múltiples campos (relación OR)
from sqlalchemy import or_
posts = db.query(Post).filter(
    or_(
        Post.title.ilike(f'%{keyword}%'),
        Post.summary.ilike(f'%{keyword}%')
    )
).all()

Paginación con offset/limit

Flask-SQLAlchemy proporciona un conveniente.paginate()método.

FastAPI utiliza SQLAlchemy nativo, por lo que se necesitaoffset + limitimplementar la paginación manualmente.

Método de paginaciónFlask-SQLAlchemyFastAPI (SQLAlchemy nativo)
Obtener datos de la página actual.paginate(page, per_page).items.offset(skip).limit(per_page).all()
Número total de registrospagination.totalquery.count()
Total de páginaspagination.pagesCalcular manualmente math.ceil(total / per_page)

Fórmula de cálculo de paginación

Ejemplo

import math
from fastapi import Query

def get_pagination(page: int = 1, size: int = 6):
    """
Calcular parámetros de paginación
page: número de página actual (desde 1)
size: número de elementos por página
    """

    skip = (page - 1) * size    # Número de registros a omitir
    return skip, size

# Ejemplo de uso
skip, limit = get_pagination(page=2, size=6)
# skip = 6, limit = 6 → omitir los primeros 6 registros, tomar los del 7 al 12

Vista completa de búsqueda + paginación

Ejemplo

# Ruta de archivo: routers/posts.py (extender la ruta index)
from sqlalchemy import or_
from math import ceil
from fastapi import APIRouter, Depends, Request, Query

@router.get("/", name="index")
def index(
    request: Request,
    category: str | None = Query(None),
    q: str | None = Query(None, description=Palabra clave de búsqueda),
    page: int = Query(1, ge=1, description=Número de página),
    size: int = Query(6, ge=1, le=50, description=Elementos por página),
    db: Session = Depends(get_db)
):
    """Página de inicio: filtro de categoría + búsqueda por palabra clave + paginación"""
    posts_query = db.query(Post).order_by(Post.created_at.desc())

    # Filtro de categoría
    if category:
        posts_query = posts_query.join(Category).filter(Category.slug == category)

    # Búsqueda por palabra clave
    if q:
        keyword = f'%{q.strip()}%'
        posts_query = posts_query.filter(
            or_(
                Post.title.ilike(keyword),
                Post.summary.ilike(keyword)
            )
        )

    # Paginación
    total = posts_query.count()
    total_pages = ceil(total / size)
    skip = (page - 1) * size
    posts = posts_query.offset(skip).limit(size).all()

    return templates.TemplateResponse("index.html", {
        "request": request,
        "posts": posts,
        "categories": db.query(Category).all(),
        "category_slug": category or "",
        "keyword": q or "",
        "page": page,
        "total_pages": total_pages,
        "total": total
    })

Query(1, ge=1)engees la abreviatura de greater than or equal. Los parámetros de consulta de FastAPI admiten restricciones numéricas completas: ge, le, gt, lt. Si el usuario pasa page=0, FastAPI devuelve automáticamente un error 422.


Navegación de paginación en la plantilla

Ejemplo

<!-- Agregar navegación de paginación debajo de la lista de artículos en index.html -->
{% if total_pages > 1 %}
<div class="pagination">
    {% if page > 1 %}
    <a href="/?page={{ page - 1 }}&category={{ category_slug }}&q={{ keyword }}">← Página anterior</a>
    {% endif %}

    <span class="page-info">Página {{ page }} / {{ total_pages }} páginas en total</span>

    {% if page < total_pages %}
   <a href="/?page={{ page + 1 }}&category={{ category_slug }}&q={{ keyword }}">Página siguiente →</a>
    {% endif %}
</div>
{% endif %}

Resumen del capítulo

En este capítulo implementaste las funciones prácticas de la página de listado: búsqueda ilike (insensible a mayúsculas y minúsculas), búsqueda combinada de múltiples campos con or_(), paginación manual con offset/limit (reemplazando el paginate de Flask), y parámetros de consulta limitados con Query().

La búsqueda + el filtro de categoría + la paginación se pueden combinar de manera arbitraria, las condiciones no se pierden.

Otras extensiones