Introduction
APIs that return everything and leave filtering to the client waste bandwidth and processing power. Server-side filtering and search let clients request exactly the data they need, making your API efficient and user-friendly.
Allow clients to filter and search your API data efficiently.
Key Concepts
Filtering: Narrowing results based on field values (e.g., ?category=tech&author=john).
Search: Finding records that match a text query across multiple fields.
Q Objects: Django's tool for building complex queries with AND/OR logic.
Full-Text Search: PostgreSQL's built-in search with ranking, stemming, and relevance scoring.
Real World Context
Every data-driven API needs filtering:
- E-commerce: Filter products by category, price range, brand, rating
- Job boards: Filter by location, salary, experience level
- Analytics dashboards: Filter metrics by date range, region, segment
- Content platforms: Search articles by keyword, author, or tag
Basic Filtering
Filters are applied conditionally based on which query parameters are present, chaining .filter() calls to narrow the queryset:
pythondef api_articles(request): articles = Article.objects.all() # Filter by category category = request.GET.get('category') if category: articles = articles.filter(category__slug=category) # Filter by author author = request.GET.get('author') if author: articles = articles.filter(author__username=author) # Filter by published status is_published = request.GET.get('is_published') if is_published is not None: articles = articles.filter(is_published=is_published.lower() == 'true') # Filter by date range date_from = request.GET.get('date_from') date_to = request.GET.get('date_to') if date_from: articles = articles.filter(created_at__gte=date_from) if date_to: articles = articles.filter(created_at__lte=date_to) return JsonResponse({ 'results': list(articles.values('id', 'title', 'created_at')) })
Each filter is independent and optional. When no parameters are provided, the view returns all articles unfiltered.
Search Implementation
Django's Q objects let you combine conditions with OR logic, enabling search across multiple fields with a single query:
pythonfrom django.db.models import Q def api_articles(request): articles = Article.objects.all() # Search across multiple fields search = request.GET.get('search', '').strip() if search: articles = articles.filter( Q(title__icontains=search) | Q(body__icontains=search) | Q(author__username__icontains=search) | Q(tags__name__icontains=search) ).distinct() return JsonResponse({ 'results': list(articles.values('id', 'title')[:50]) })
The .distinct() call is important when searching across related fields (like tags) to prevent duplicate results from JOINs.
Full-Text Search (PostgreSQL)
PostgreSQL's built-in full-text search supports stemming, ranking by relevance, and weighted fields -- far more powerful than icontains for text-heavy applications:
pythonfrom django.contrib.postgres.search import ( SearchVector, SearchQuery, SearchRank ) def api_search_articles(request): query = request.GET.get('q', '') if not query: return JsonResponse({'results': []}) # Create search vector vector = SearchVector('title', weight='A') + \ SearchVector('body', weight='B') + \ SearchVector('author__username', weight='C') search_query = SearchQuery(query) articles = Article.objects.annotate( search=vector, rank=SearchRank(vector, search_query) ).filter( search=search_query ).order_by('-rank')[:20] return JsonResponse({ 'query': query, 'results': [{ 'id': a.id, 'title': a.title, 'relevance': a.rank } for a in articles] })
The weight parameter ('A', 'B', 'C') controls how much each field contributes to the relevance score. Title matches rank higher than body matches.
Filter Class
Encapsulating filter logic in a dedicated class keeps views clean and makes filters reusable across multiple endpoints:
pythonclass ArticleFilter: """Reusable filter class.""" def __init__(self, request): self.params = request.GET def apply(self, queryset): # Category filter category = self.params.get('category') if category: queryset = queryset.filter(category__slug=category) # Author filter author = self.params.get('author') if author: queryset = queryset.filter(author__username=author) # Published filter published = self.params.get('published') if published == 'true': queryset = queryset.filter(is_published=True) elif published == 'false': queryset = queryset.filter(is_published=False) # Date range created_after = self.params.get('created_after') created_before = self.params.get('created_before') if created_after: queryset = queryset.filter(created_at__gte=created_after) if created_before: queryset = queryset.filter(created_at__lte=created_before) # Search search = self.params.get('search', '').strip() if search: queryset = queryset.filter( Q(title__icontains=search) | Q(body__icontains=search) ) return queryset # Usage def api_articles(request): articles = Article.objects.all() # Apply filters filter = ArticleFilter(request) articles = filter.apply(articles) # Apply ordering ordering = request.GET.get('ordering', '-created_at') if ordering.lstrip('-') in ['created_at', 'title', 'views']: articles = articles.order_by(ordering) # Paginate and return paginator = Paginator(request) result = paginator.paginate(articles) return JsonResponse({ 'results': [serialize_article(a) for a in result['items']], 'pagination': result['meta'] })
The view composes filtering, ordering, and pagination into a clean pipeline: build the queryset, apply filters, sort, then paginate.
API Usage Examples
GET /api/articles/
GET /api/articles/?page=2&page_size=10
GET /api/articles/?category=tech&author=john
GET /api/articles/?search=django+tutorial
GET /api/articles/?created_after=2024-01-01&ordering=-views
GET /api/articles/?published=true&category=python&page=1
Common Pitfalls
-
SQL injection via filter fields: Never pass user input directly to
filter()kwargs. Whitelist allowed filter fields. -
Missing indexes: Filtering on non-indexed fields causes full table scans. Add database indexes for frequently filtered columns.
-
Case sensitivity:
filter(title=search)is case-sensitive in PostgreSQL. UseicontainsorLower()for case-insensitive filtering.
Best Practices
-
Whitelist filter fields: Only allow filtering on safe, indexed fields.
-
Combine with pagination: Always paginate filtered results.
-
Use Q objects for OR logic: Regular
filter()chains use AND. UseQ()with|for OR queries. -
Consider full-text search: For text-heavy applications, PostgreSQL's full-text search is faster and more relevant than
icontains.
Summary
Server-side filtering and search make APIs efficient by returning only relevant data. Use query parameters for filtering, Q objects for complex OR queries, and PostgreSQL's full-text search for relevance-ranked results. Always whitelist filter fields and add database indexes for performance.
Code Examples
from django.db.models import Q
from django.http import JsonResponse
def api_articles(request):
articles = Article.objects.all()
category = request.GET.get('category')
if category:
articles = articles.filter(category__slug=category)
search = request.GET.get('search', '').strip()
if search:
articles = articles.filter(
Q(title__icontains=search) |
Q(body__icontains=search)
).distinct()
return JsonResponse({'results': list(articles.values('id', 'title')[:50])})