Step 4: Add Filtering & Pagination¶
In this final step, you'll learn how to implement advanced filtering, searching, and pagination for your API endpoints.
What You'll Learn¶
- Query parameter filtering
- Full-text search
- Ordering and sorting
- Custom pagination
- Filter combinations
- Performance optimization
Prerequisites¶
Make sure you've completed:
Basic Filtering¶
Simple Field Filters¶
Python
# views.py
from ninja_aio.views import APIViewSet
from .models import Article
class ArticleViewSet(APIViewSet):
model = Article
api = api
query_params = {
"is_published": (bool, None),
"author": (int, None),
"category": (int, None),
}
async def query_params_handler(self, queryset, filters):
# Filter by published status
if filters.get("is_published") is not None:
queryset = queryset.filter(is_published=filters["is_published"])
# Filter by author ID
if filters.get("author"):
queryset = queryset.filter(author_id=filters["author"])
# Filter by category ID
if filters.get("category"):
queryset = queryset.filter(category_id=filters["category"])
return queryset
ArticleViewSet().add_views_to_route()
Usage:
Bash
# Get published articles
GET /api/article/?is_published=true
# Get articles by author
GET /api/article/?author=5
# Get articles in category
GET /api/article/?category=3
# Combine filters
GET /api/article/?is_published=true&author=5&category=3
Date Range Filters¶
Python
from datetime import datetime
class ArticleViewSet(APIViewSet):
model = Article
api = api
query_params = {
"created_after": (str, None), # ISO date string
"created_before": (str, None),
"published_after": (str, None),
"published_before": (str, None),
}
async def query_params_handler(self, queryset, filters):
# Filter by creation date
if filters.get("created_after"):
date = datetime.fromisoformat(filters["created_after"])
queryset = queryset.filter(created_at__gte=date)
if filters.get("created_before"):
date = datetime.fromisoformat(filters["created_before"])
queryset = queryset.filter(created_at__lte=date)
# Filter by publication date
if filters.get("published_after"):
date = datetime.fromisoformat(filters["published_after"])
queryset = queryset.filter(published_at__gte=date)
if filters.get("published_before"):
date = datetime.fromisoformat(filters["published_before"])
queryset = queryset.filter(published_at__lte=date)
return queryset
Usage:
Bash
# Articles created after a date
GET /api/article/?created_after=2024-01-01
# Articles published in a date range
GET /api/article/?published_after=2024-01-01&published_before=2024-01-31
# Articles from last 7 days
GET /api/article/?created_after=2024-01-15
Numeric Range Filters¶
Python
class ArticleViewSet(APIViewSet):
model = Article
api = api
query_params = {
"min_views": (int, None),
"max_views": (int, None),
"min_rating": (float, None),
"max_rating": (float, None),
}
async def query_params_handler(self, queryset, filters):
# Filter by views
if filters.get("min_views"):
queryset = queryset.filter(views__gte=filters["min_views"])
if filters.get("max_views"):
queryset = queryset.filter(views__lte=filters["max_views"])
# Filter by rating
if filters.get("min_rating"):
queryset = queryset.filter(rating__gte=filters["min_rating"])
if filters.get("max_rating"):
queryset = queryset.filter(rating__lte=filters["max_rating"])
return queryset
Usage:
Bash
# Popular articles (1000+ views)
GET /api/article/?min_views=1000
# Highly rated articles (4.5+)
GET /api/article/?min_rating=4.5
# Articles with 100-1000 views
GET /api/article/?min_views=100&max_views=1000
Search Functionality¶
Simple Text Search¶
Python
from django.db.models import Q
class ArticleViewSet(APIViewSet):
model = Article
api = api
query_params = {
"search": (str, None),
}
async def query_params_handler(self, queryset, filters):
if filters.get("search"):
search_term = filters["search"]
queryset = queryset.filter(
Q(title__icontains=search_term) |
Q(content__icontains=search_term) |
Q(excerpt__icontains=search_term)
)
return queryset
Usage:
Bash
# Search in title and content
GET /api/article/?search=django
# Search with other filters
GET /api/article/?search=tutorial&is_published=true
Full-Text Search (PostgreSQL)¶
For better performance with large datasets:
Python
from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank
class ArticleViewSet(APIViewSet):
model = Article
api = api
query_params = {
"search": (str, None),
}
async def query_params_handler(self, queryset, filters):
if filters.get("search"):
search_term = filters["search"]
# Create search vector
vector = SearchVector('title', weight='A') + \
SearchVector('content', weight='B')
query = SearchQuery(search_term)
# Filter and rank by relevance
queryset = queryset.annotate(
rank=SearchRank(vector, query)
).filter(
rank__gte=0.1
).order_by('-rank')
return queryset
Search with Highlights¶
Python
from django.contrib.postgres.search import SearchVector, SearchQuery, SearchHeadline
class ArticleViewSet(APIViewSet):
model = Article
api = api
query_params = {
"search": (str, None),
}
async def query_params_handler(self, queryset, filters):
if filters.get("search"):
search_term = filters["search"]
query = SearchQuery(search_term)
# Add highlighted excerpts
queryset = queryset.annotate(
headline=SearchHeadline(
'content',
query,
start_sel='<mark>',
stop_sel='</mark>',
max_words=50,
)
)
return queryset
Ordering¶
Basic Ordering¶
Python
class ArticleViewSet(APIViewSet):
model = Article
api = api
query_params = {
"ordering": (str, "-created_at"), # Default: newest first
}
async def query_params_handler(self, queryset, filters):
ordering = filters.get("ordering", "-created_at")
# Whitelist allowed ordering fields
valid_orderings = [
"created_at", "-created_at",
"updated_at", "-updated_at",
"title", "-title",
"views", "-views",
"rating", "-rating",
"published_at", "-published_at",
]
if ordering in valid_orderings:
queryset = queryset.order_by(ordering)
return queryset
Usage:
Bash
# Newest first (default)
GET /api/article/?ordering=-created_at
# Oldest first
GET /api/article/?ordering=created_at
# By title A-Z
GET /api/article/?ordering=title
# Most viewed
GET /api/article/?ordering=-views
# Highest rated
GET /api/article/?ordering=-rating
Multiple Field Ordering¶
Python
class ArticleViewSet(APIViewSet):
model = Article
api = api
query_params = {
"ordering": (str, "-created_at,title"), # Multiple fields
}
async def query_params_handler(self, queryset, filters):
ordering = filters.get("ordering", "-created_at,title")
# Parse ordering string
order_fields = ordering.split(',')
# Validate each field
valid_fields = {
"created_at", "-created_at",
"title", "-title",
"views", "-views",
}
validated_fields = [
field for field in order_fields
if field in valid_fields
]
if validated_fields:
queryset = queryset.order_by(*validated_fields)
return queryset
Usage:
Bash
# Order by date, then title
GET /api/article/?ordering=-created_at,title
# Order by views, then rating
GET /api/article/?ordering=-views,-rating
Advanced Filtering¶
Related Field Filters¶
Python
class ArticleViewSet(APIViewSet):
model = Article
api = api
query_params = {
"author_username": (str, None),
"category_slug": (str, None),
"tag_name": (str, None),
}
async def query_params_handler(self, queryset, filters):
# Filter by author username
if filters.get("author_username"):
queryset = queryset.filter(
author__username__iexact=filters["author_username"]
)
# Filter by category slug
if filters.get("category_slug"):
queryset = queryset.filter(
category__slug=filters["category_slug"]
)
# Filter by tag name
if filters.get("tag_name"):
queryset = queryset.filter(
tags__name__iexact=filters["tag_name"]
)
return queryset
Usage:
Bash
# By author username
GET /api/article/?author_username=johndoe
# By category slug
GET /api/article/?category_slug=tutorials
# By tag name
GET /api/article/?tag_name=python
Multiple Tags Filter¶
Python
class ArticleViewSet(APIViewSet):
model = Article
api = api
query_params = {
"tags": (str, None), # Comma-separated tag IDs or names
"tags_mode": (str, "any"), # "any" or "all"
}
async def query_params_handler(self, queryset, filters):
if filters.get("tags"):
tag_list = filters["tags"].split(',')
mode = filters.get("tags_mode", "any")
# Check if tags are IDs or names
if tag_list[0].isdigit():
# Filter by tag IDs
tag_ids = [int(t) for t in tag_list]
if mode == "all":
# Must have ALL tags
for tag_id in tag_ids:
queryset = queryset.filter(tags__id=tag_id)
else:
# Must have ANY tag
queryset = queryset.filter(tags__id__in=tag_ids).distinct()
else:
# Filter by tag names
if mode == "all":
for tag_name in tag_list:
queryset = queryset.filter(tags__name__iexact=tag_name)
else:
queryset = queryset.filter(
tags__name__in=tag_list
).distinct()
return queryset
Usage:
Bash
# Articles with ANY of these tags
GET /api/article/?tags=1,2,3
# Articles with ALL of these tags
GET /api/article/?tags=python,django,tutorial&tags_mode=all
# Using tag IDs
GET /api/article/?tags=1,2,3&tags_mode=all
Exclude Filters¶
Python
class ArticleViewSet(APIViewSet):
model = Article
api = api
query_params = {
"exclude_author": (int, None),
"exclude_category": (int, None),
"exclude_ids": (str, None), # Comma-separated IDs
}
async def query_params_handler(self, queryset, filters):
# Exclude specific author
if filters.get("exclude_author"):
queryset = queryset.exclude(author_id=filters["exclude_author"])
# Exclude specific category
if filters.get("exclude_category"):
queryset = queryset.exclude(category_id=filters["exclude_category"])
# Exclude specific article IDs
if filters.get("exclude_ids"):
ids = [int(id) for id in filters["exclude_ids"].split(',')]
queryset = queryset.exclude(id__in=ids)
return queryset
Usage:
Bash
# Exclude articles by specific author
GET /api/article/?exclude_author=5
# Exclude specific articles
GET /api/article/?exclude_ids=1,2,3
Pagination¶
Default Pagination¶
Django Ninja Aio CRUD uses page-number pagination by default:
Python
class ArticleViewSet(APIViewSet):
model = Article
api = api
# Uses default PageNumberPagination
Usage:
Bash
# First page (10 items)
GET /api/article/?page=1
# Custom page size
GET /api/article/?page=1&page_size=20
# Second page
GET /api/article/?page=2&page_size=20
Response:
Custom Pagination¶
Python
from ninja.pagination import PageNumberPagination
class CustomPagination(PageNumberPagination):
page_size = 25 # Default items per page
max_page_size = 100 # Maximum allowed
class ArticleViewSet(APIViewSet):
model = Article
api = api
pagination_class = CustomPagination
Disable Pagination¶
Python
class ArticleViewSet(APIViewSet):
model = Article
api = api
pagination_class = None # Return all results
# Or conditionally
class ConditionalPagination(PageNumberPagination):
async def apaginate_queryset(self, queryset, pagination, request=None, **params):
# Disable if 'all' parameter present
if params.get('all'):
items = []
async for item in queryset:
items.append(item)
return {"results": items}
return await super().apaginate_queryset(queryset, pagination, request, **params)
Filter Presets¶
Create reusable filter combinations:
Python
class ArticleViewSet(APIViewSet):
model = Article
api = api
query_params = {
"preset": (str, None),
# ... other filters
}
async def query_params_handler(self, queryset, filters):
preset = filters.get("preset")
# Apply preset filters
if preset == "trending":
# Popular recent articles
from django.utils import timezone
from datetime import timedelta
last_week = timezone.now() - timedelta(days=7)
queryset = queryset.filter(
created_at__gte=last_week,
is_published=True
).order_by('-views', '-rating')
elif preset == "featured":
# Featured articles
queryset = queryset.filter(
is_published=True,
is_featured=True
).order_by('-featured_at')
elif preset == "recent":
# Recently published
queryset = queryset.filter(
is_published=True
).order_by('-published_at')[:20]
elif preset == "popular":
# All-time most viewed
queryset = queryset.filter(
is_published=True
).order_by('-views')[:50]
# Apply other filters
# ...
return queryset
Usage:
Bash
# Get trending articles
GET /api/article/?preset=trending
# Get featured articles
GET /api/article/?preset=featured
# Combine with other filters
GET /api/article/?preset=recent&category=1
Performance Optimization¶
Select Related¶
Optimize queries with foreign keys:
Python
class Article(ModelSerializer):
# ... fields ...
@classmethod
async def queryset_request(cls, request):
# Always include related objects
return cls.objects.select_related(
'author',
'category'
).prefetch_related(
'tags',
'comments'
)
class ArticleViewSet(APIViewSet):
model = Article
api = api
# Queries are now optimized automatically
Index Database Fields¶
Python
# models.py
class Article(ModelSerializer):
title = models.CharField(max_length=200, db_index=True)
slug = models.SlugField(unique=True, db_index=True)
is_published = models.BooleanField(default=False, db_index=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
indexes = [
models.Index(fields=['-created_at']),
models.Index(fields=['is_published', '-created_at']),
models.Index(fields=['author', '-created_at']),
models.Index(fields=['category', '-created_at']),
]
Limit Query Results¶
Python
class ArticleViewSet(APIViewSet):
model = Article
api = api
async def query_params_handler(self, queryset, filters):
# Apply filters
# ...
# Limit results for expensive queries
if filters.get("search"):
queryset = queryset[:1000] # Max 1000 results for search
return queryset
Complete Example¶
Here's a comprehensive filtering implementation:
Python
# views.py
from ninja_aio import NinjaAIO
from ninja_aio.views import APIViewSet
from ninja.pagination import PageNumberPagination
from django.db.models import Q
from datetime import datetime
from .models import Article
api = NinjaAIO(title="Blog API", version="1.0.0")
class ArticlePagination(PageNumberPagination):
page_size = 20
max_page_size = 100
class ArticleViewSet(APIViewSet):
model = Article
api = api
pagination_class = ArticlePagination
query_params = {
# Basic filters
"is_published": (bool, None),
"author": (int, None),
"category": (int, None),
# Text search
"search": (str, None),
# Date range
"created_after": (str, None),
"created_before": (str, None),
# Numeric range
"min_views": (int, None),
"max_views": (int, None),
# Related filters
"author_username": (str, None),
"category_slug": (str, None),
"tags": (str, None),
"tags_mode": (str, "any"),
# Ordering
"ordering": (str, "-created_at"),
# Presets
"preset": (str, None),
}
async def query_params_handler(self, queryset, filters):
# Apply preset first
preset = filters.get("preset")
if preset == "trending":
from django.utils import timezone
from datetime import timedelta
last_week = timezone.now() - timedelta(days=7)
queryset = queryset.filter(
created_at__gte=last_week,
is_published=True
)
elif preset == "popular":
queryset = queryset.filter(is_published=True)
# Published status
if filters.get("is_published") is not None:
queryset = queryset.filter(is_published=filters["is_published"])
# Author filter
if filters.get("author"):
queryset = queryset.filter(author_id=filters["author"])
elif filters.get("author_username"):
queryset = queryset.filter(
author__username__iexact=filters["author_username"]
)
# Category filter
if filters.get("category"):
queryset = queryset.filter(category_id=filters["category"])
elif filters.get("category_slug"):
queryset = queryset.filter(category__slug=filters["category_slug"])
# Tags filter
if filters.get("tags"):
tag_list = filters["tags"].split(',')
mode = filters.get("tags_mode", "any")
if mode == "all":
for tag in tag_list:
if tag.isdigit():
queryset = queryset.filter(tags__id=int(tag))
else:
queryset = queryset.filter(tags__name__iexact=tag)
else:
if tag_list[0].isdigit():
tag_ids = [int(t) for t in tag_list]
queryset = queryset.filter(tags__id__in=tag_ids).distinct()
else:
queryset = queryset.filter(tags__name__in=tag_list).distinct()
# Search
if filters.get("search"):
search_term = filters["search"]
queryset = queryset.filter(
Q(title__icontains=search_term) |
Q(content__icontains=search_term)
)
# Date range
if filters.get("created_after"):
date = datetime.fromisoformat(filters["created_after"])
queryset = queryset.filter(created_at__gte=date)
if filters.get("created_before"):
date = datetime.fromisoformat(filters["created_before"])
queryset = queryset.filter(created_at__lte=date)
# Views range
if filters.get("min_views"):
queryset = queryset.filter(views__gte=filters["min_views"])
if filters.get("max_views"):
queryset = queryset.filter(views__lte=filters["max_views"])
# Ordering
ordering = filters.get("ordering", "-created_at")
valid_orderings = [
"created_at", "-created_at",
"title", "-title",
"views", "-views",
"published_at", "-published_at",
]
if ordering in valid_orderings:
queryset = queryset.order_by(ordering)
elif preset == "trending":
queryset = queryset.order_by('-views', '-rating')
elif preset == "popular":
queryset = queryset.order_by('-views')
return queryset
ArticleViewSet().add_views_to_route()
Testing Filters¶
Bash
# Basic filtering
curl "http://localhost:8000/api/article/?is_published=true"
# Search
curl "http://localhost:8000/api/article/?search=django"
# Date range
curl "http://localhost:8000/api/article/?created_after=2024-01-01&created_before=2024-01-31"
# Multiple filters
curl "http://localhost:8000/api/article/?is_published=true&category=1&min_views=100&ordering=-views"
# Tags
curl "http://localhost:8000/api/article/?tags=python,django&tags_mode=all"
# Presets
curl "http://localhost:8000/api/article/?preset=trending"
# Pagination
curl "http://localhost:8000/api/article/?page=2&page_size=50"
# Combined
curl "http://localhost:8000/api/article/?search=tutorial&category=1&is_published=true&min_views=1000&ordering=-rating&page=1&page_size=20"
Congratulations! 🎉¶
You've completed all tutorial steps and built a complete, production-ready API with:
- ✅ Models with automatic schema generation
- ✅ Full CRUD operations
- ✅ JWT authentication
- ✅ Custom schemas and validation
- ✅ Advanced filtering and search
- ✅ Pagination
- ✅ Performance optimization
Next Steps¶
Explore advanced topics:
- API Reference - Complete API documentation
- Authentication - Advanced auth patterns
- Pagination - Custom pagination strategies
See Also¶
- Pagination API Reference - Pagination classes
- ModelUtil - Query optimization