Фильтрация и поиск в DRF
DRF предоставляет мощные инструменты для фильтрации и поиска по API. Правильная настройка фильтров значительно улучшает производительность и удобство использования API.
Основные компоненты фильтрации
- DjangoFilterBackend - точная фильтрация по полям
- SearchFilter - полнотекстовый поиск
- OrderingFilter - сортировка результатов
- Кастомные фильтры - сложная логика фильтрации
- Фильтры по диапазонам - числовые и временные интервалы
Базовая настройка фильтрации
1from rest_framework import viewsets, filters
2from django_filters.rest_framework import DjangoFilterBackend
3from .models import Book, Author, Category
4from .serializers import BookSerializer
5
6class BookViewSet(viewsets.ModelViewSet):
7 queryset = Book.objects.all()
8 serializer_class = BookSerializer
9
10 # Настройка фильтров
11 filter_backends = [
12 DjangoFilterBackend, # Точная фильтрация
13 filters.SearchFilter, # Поиск по тексту
14 filters.OrderingFilter # Сортировка
15 ]
16
17 # Поля для точной фильтрации
18 filterset_fields = [
19 'author', # Фильтр по автору
20 'category', # Фильтр по категории
21 'is_published', # Фильтр по статусу публикации
22 'language' # Фильтр по языку
23 ]
24
25 # Поля для поиска
26 search_fields = [
27 'title', # Поиск по названию
28 'description', # Поиск по описанию
29 'author__name', # Поиск по имени автора
30 'category__name' # Поиск по названию категории
31 ]
32
33 # Поля для сортировки
34 ordering_fields = [
35 'title', # Сортировка по названию
36 'published_date', # Сортировка по дате публикации
37 'price', # Сортировка по цене
38 'rating' # Сортировка по рейтингу
39 ]
40
41 # Сортировка по умолчанию
42 ordering = ['-published_date', 'title']
Расширенные фильтры с django-filter
1import django_filters
2from django_filters import rest_framework as filters
3from .models import Book, Product, Order
4
5class BookFilter(filters.FilterSet):
6 """Кастомный фильтр для книг"""
7
8 # Фильтр по диапазону цен
9 price_min = filters.NumberFilter(
10 field_name='price',
11 lookup_expr='gte',
12 label='Минимальная цена'
13 )
14 price_max = filters.NumberFilter(
15 field_name='price',
16 lookup_expr='lte',
17 label='Максимальная цена'
18 )
19
20 # Фильтр по диапазону дат
21 published_after = filters.DateFilter(
22 field_name='published_date',
23 lookup_expr='gte',
24 label='Опубликовано после'
25 )
26 published_before = filters.DateFilter(
27 field_name='published_date',
28 lookup_expr='lte',
29 label='Опубликовано до'
30 )
31
32 # Фильтр по рейтингу
33 rating_min = filters.NumberFilter(
34 field_name='rating',
35 lookup_expr='gte',
36 label='Минимальный рейтинг'
37 )
38
39 # Фильтр по наличию
40 in_stock = filters.BooleanFilter(
41 field_name='stock',
42 lookup_expr='gt',
43 label='В наличии'
44 )
45
46 # Фильтр по множественным значениям
47 categories = filters.ModelMultipleChoiceFilter(
48 queryset=Category.objects.all(),
49 field_name='category',
50 label='Категории'
51 )
52
53 # Фильтр по поиску в связанных моделях
54 author_name = filters.CharFilter(
55 field_name='author__name',
56 lookup_expr='icontains',
57 label='Имя автора содержит'
58 )
59
60 # Фильтр по сложным условиям
61 @filters.filter
62 def popular_books(self, queryset, name, value):
63 """Фильтр для популярных книг (рейтинг > 4.5 и отзывов > 10)"""
64 if value:
65 return queryset.filter(
66 rating__gte=4.5,
67 review_count__gte=10
68 )
69 return queryset
70
71 class Meta:
72 model = Book
73 fields = {
74 'title': ['exact', 'icontains', 'startswith'],
75 'author': ['exact'],
76 'category': ['exact'],
77 'is_published': ['exact'],
78 'language': ['exact'],
79 }
80
81class ProductFilter(filters.FilterSet):
82 """Фильтр для продуктов с геолокацией"""
83
84 # Фильтр по расстоянию от точки
85 latitude = filters.NumberFilter(method='filter_by_distance')
86 longitude = filters.NumberFilter(method='filter_by_distance')
87 radius = filters.NumberFilter(method='filter_by_distance')
88
89 def filter_by_distance(self, queryset, name, value):
90 """Фильтр по расстоянию от указанной точки"""
91 if all([
92 self.data.get('latitude'),
93 self.data.get('longitude'),
94 self.data.get('radius')
95 ]):
96 lat = float(self.data['latitude'])
97 lng = float(self.data['longitude'])
98 radius = float(self.data['radius'])
99
100 # Используем raw SQL для вычисления расстояния
101 return queryset.raw('''
102 SELECT *,
103 (6371 * acos(cos(radians(%s)) * cos(radians(latitude)) *
104 cos(radians(longitude) - radians(%s)) +
105 sin(radians(%s)) * sin(radians(latitude)))) AS distance
106 FROM products_product
107 HAVING distance < %s
108 ORDER BY distance
109 ''', [lat, lng, lat, radius])
110
111 return queryset
112
113 class Meta:
114 model = Product
115 fields = {
116 'name': ['exact', 'icontains'],
117 'category': ['exact'],
118 'price': ['gte', 'lte'],
119 'is_available': ['exact'],
120 }
121
122# Использование в ViewSet
123class BookViewSet(viewsets.ModelViewSet):
124 queryset = Book.objects.all()
125 serializer_class = BookSerializer
126 filter_backends = [filters.DjangoFilterBackend]
127 filterset_class = BookFilter
Поиск с различными стратегиями
1from rest_framework import filters
2from django.db.models import Q
3from .models import Article, Post, User
4
5class ArticleViewSet(viewsets.ModelViewSet):
6 queryset = Article.objects.all()
7 serializer_class = ArticleSerializer
8
9 # Настройка поиска
10 filter_backends = [filters.SearchFilter]
11 search_fields = [
12 'title', # Точный поиск по названию
13 'content', # Поиск по содержимому
14 'author__username', # Поиск по имени автора
15 'tags__name', # Поиск по тегам
16 'category__name' # Поиск по категории
17 ]
18
19 # Настройка поиска
20 search_fields = ['title', 'content']
21 search_type = 'icontains' # Регистронезависимый поиск
22
23class PostViewSet(viewsets.ModelViewSet):
24 queryset = Post.objects.all()
25 serializer_class = PostSerializer
26
27 def get_queryset(self):
28 """Кастомный queryset с расширенным поиском"""
29 queryset = Post.objects.all()
30 search_query = self.request.query_params.get('search', None)
31
32 if search_query:
33 # Создаем сложный поисковый запрос
34 search_terms = search_query.split()
35 q_objects = Q()
36
37 for term in search_terms:
38 q_objects |= (
39 Q(title__icontains=term) |
40 Q(content__icontains=term) |
41 Q(author__username__icontains=term) |
42 Q(tags__name__icontains=term)
43 )
44
45 queryset = queryset.filter(q_objects).distinct()
46
47 return queryset
48
49class UserViewSet(viewsets.ModelViewSet):
50 queryset = User.objects.all()
51 serializer_class = UserSerializer
52
53 def get_queryset(self):
54 """Поиск пользователей с различными критериями"""
55 queryset = User.objects.all()
56 search_query = self.request.query_params.get('search', None)
57
58 if search_query:
59 # Поиск по имени, email, username
60 queryset = queryset.filter(
61 Q(username__icontains=search_query) |
62 Q(first_name__icontains=search_query) |
63 Q(last_name__icontains=search_query) |
64 Q(email__icontains=search_query)
65 )
66
67 # Дополнительные фильтры
68 is_active = self.request.query_params.get('is_active', None)
69 if is_active is not None:
70 queryset = queryset.filter(is_active=is_active.lower() == 'true')
71
72 date_joined = self.request.query_params.get('date_joined', None)
73 if date_joined:
74 queryset = queryset.filter(date_joined__date=date_joined)
75
76 return queryset
Сортировка и пагинация
1from rest_framework import viewsets, filters
2from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination
3from django.db.models import Count, Avg
4from .models import Product, Review, Order
5
6class ProductPagination(PageNumberPagination):
7 """Кастомная пагинация для продуктов"""
8 page_size = 20
9 page_size_query_param = 'page_size'
10 max_page_size = 100
11 page_query_param = 'page'
12
13class ProductViewSet(viewsets.ModelViewSet):
14 queryset = Product.objects.all()
15 serializer_class = ProductSerializer
16 pagination_class = ProductPagination
17
18 # Настройка сортировки
19 filter_backends = [filters.OrderingFilter]
20 ordering_fields = [
21 'name', # По названию
22 'price', # По цене
23 'created_at', # По дате создания
24 'rating', # По рейтингу
25 'sales_count', # По количеству продаж
26 'review_count' # По количеству отзывов
27 ]
28 ordering = ['-created_at'] # По умолчанию новые первыми
29
30 def get_queryset(self):
31 """Оптимизированный queryset с аннотациями"""
32 queryset = Product.objects.annotate(
33 review_count=Count('reviews'),
34 avg_rating=Avg('reviews__rating'),
35 sales_count=Count('order_items')
36 ).select_related('category').prefetch_related('tags')
37
38 return queryset
39
40class OrderViewSet(viewsets.ModelViewSet):
41 queryset = Order.objects.all()
42 serializer_class = OrderSerializer
43
44 # Фильтрация и сортировка
45 filter_backends = [filters.OrderingFilter, filters.DjangoFilterBackend]
46 filterset_fields = ['status', 'user', 'created_at']
47 ordering_fields = ['created_at', 'total_amount', 'status']
48 ordering = ['-created_at']
49
50 def get_queryset(self):
51 """Queryset с оптимизацией для заказов"""
52 return Order.objects.select_related(
53 'user', 'shipping_address'
54 ).prefetch_related(
55 'items__product'
56 ).annotate(
57 item_count=Count('items')
58 )
59
60# Использование LimitOffset пагинации
61class ReviewPagination(LimitOffsetPagination):
62 """Пагинация с лимитом и смещением"""
63 default_limit = 10
64 limit_query_param = 'limit'
65 offset_query_param = 'offset'
66 max_limit = 50
67
68class ReviewViewSet(viewsets.ModelViewSet):
69 queryset = Review.objects.all()
70 serializer_class = ReviewSerializer
71 pagination_class = ReviewPagination
72
73 # Сортировка отзывов
74 filter_backends = [filters.OrderingFilter]
75 ordering_fields = ['created_at', 'rating', 'helpful_votes']
76 ordering = ['-created_at']
Кастомные фильтры и валидация
1import django_filters
2from django_filters import rest_framework as filters
3from django.core.exceptions import ValidationError
4from .models import Event, Booking, Venue
5
6class EventFilter(filters.FilterSet):
7 """Фильтр для событий с кастомной логикой"""
8
9 # Фильтр по дате с валидацией
10 date_from = filters.DateFilter(
11 field_name='start_date',
12 lookup_expr='gte',
13 input_formats=['%Y-%m-%d', '%d.%m.%Y']
14 )
15 date_to = filters.DateFilter(
16 field_name='end_date',
17 lookup_expr='lte',
18 input_formats=['%Y-%m-%d', '%d.%m.%Y']
19 )
20
21 # Фильтр по цене с диапазоном
22 price_range = filters.RangeFilter(
23 field_name='ticket_price',
24 label='Диапазон цен'
25 )
26
27 # Фильтр по категориям
28 categories = filters.ModelMultipleChoiceFilter(
29 queryset=Category.objects.all(),
30 field_name='category',
31 conjoined=True # Все категории должны совпадать
32 )
33
34 # Фильтр по доступности
35 available_seats = filters.NumberFilter(
36 method='filter_available_seats',
37 label='Доступные места'
38 )
39
40 # Фильтр по геолокации
41 near_venue = filters.NumberFilter(
42 method='filter_near_venue',
43 label='Рядом с местом проведения'
44 )
45
46 def filter_available_seats(self, queryset, name, value):
47 """Фильтр по количеству доступных мест"""
48 if value:
49 return queryset.filter(
50 total_seats__gte=F('booked_seats') + value
51 )
52 return queryset
53
54 def filter_near_venue(self, queryset, name, value):
55 """Фильтр по расстоянию до места проведения"""
56 if value and hasattr(self, 'venue_coordinates'):
57 lat, lng = self.venue_coordinates
58 # Логика фильтрации по расстоянию
59 return queryset.filter(
60 venue__latitude__range=(lat - 0.01, lat + 0.01),
61 venue__longitude__range=(lng - 0.01, lng + 0.01)
62 )
63 return queryset
64
65 def clean(self):
66 """Валидация фильтров"""
67 cleaned_data = super().clean()
68
69 # Проверяем, что дата начала раньше даты окончания
70 date_from = cleaned_data.get('date_from')
71 date_to = cleaned_data.get('date_to')
72
73 if date_from and date_to and date_from > date_to:
74 raise ValidationError(
75 "Дата начала должна быть раньше даты окончания"
76 )
77
78 return cleaned_data
79
80 class Meta:
81 model = Event
82 fields = {
83 'title': ['exact', 'icontains'],
84 'venue': ['exact'],
85 'organizer': ['exact'],
86 'is_free': ['exact'],
87 }
88
89class BookingFilter(filters.FilterSet):
90 """Фильтр для бронирований"""
91
92 # Фильтр по статусу
93 status = filters.ChoiceFilter(
94 choices=Booking.STATUS_CHOICES,
95 field_name='status'
96 )
97
98 # Фильтр по дате бронирования
99 booked_after = filters.DateTimeFilter(
100 field_name='created_at',
101 lookup_expr='gte'
102 )
103
104 # Фильтр по пользователю
105 user = filters.ModelChoiceFilter(
106 queryset=User.objects.all(),
107 field_name='user'
108 )
109
110 class Meta:
111 model = Booking
112 fields = ['event', 'status', 'user']
Оптимизация производительности
1from rest_framework import viewsets
2from django.db.models import Prefetch, Q
3from django.core.cache import cache
4from .models import Product, Category, Review
5
6class OptimizedProductViewSet(viewsets.ModelViewSet):
7 """Оптимизированный ViewSet для продуктов"""
8 serializer_class = ProductSerializer
9 pagination_class = ProductPagination
10
11 def get_queryset(self):
12 """Оптимизированный queryset с кешированием"""
13 # Пытаемся получить из кеша
14 cache_key = self.get_cache_key()
15 queryset = cache.get(cache_key)
16
17 if queryset is None:
18 # Создаем оптимизированный queryset
19 queryset = Product.objects.select_related(
20 'category', 'brand'
21 ).prefetch_related(
22 Prefetch(
23 'reviews',
24 queryset=Review.objects.filter(
25 is_approved=True
26 ).select_related('user')[:5]
27 ),
28 'tags',
29 'images'
30 ).annotate(
31 review_count=Count('reviews'),
32 avg_rating=Avg('reviews__rating'),
33 sales_count=Count('order_items')
34 )
35
36 # Кешируем результат на 5 минут
37 cache.set(cache_key, queryset, 300)
38
39 return queryset
40
41 def get_cache_key(self):
42 """Генерация ключа кеша на основе параметров запроса"""
43 params = self.request.query_params.copy()
44 # Убираем параметры, которые не влияют на результат
45 params.pop('page', None)
46 params.pop('page_size', None)
47
48 # Сортируем параметры для стабильного ключа
49 sorted_params = sorted(params.items())
50 return f"products_filter_{hash(str(sorted_params))}"
51
52 def filter_queryset(self, queryset):
53 """Применение фильтров с оптимизацией"""
54 # Применяем базовые фильтры
55 queryset = super().filter_queryset(queryset)
56
57 # Дополнительная оптимизация для больших наборов данных
58 if queryset.count() > 1000:
59 # Используем только необходимые поля
60 queryset = queryset.only(
61 'id', 'name', 'price', 'category_id', 'brand_id'
62 )
63
64 return queryset
65
66class CategoryViewSet(viewsets.ModelViewSet):
67 """ViewSet для категорий с оптимизацией"""
68 serializer_class = CategorySerializer
69
70 def get_queryset(self):
71 """Оптимизированный queryset для категорий"""
72 return Category.objects.prefetch_related(
73 Prefetch(
74 'products',
75 queryset=Product.objects.filter(
76 is_active=True
77 ).only('id', 'name', 'price')[:10]
78 )
79 ).annotate(
80 product_count=Count('products'),
81 avg_price=Avg('products__price')
82 )
Тестирование фильтров
1from django.test import TestCase
2from django.urls import reverse
3from rest_framework.test import APITestCase
4from rest_framework import status
5from .models import Book, Author, Category
6from .serializers import BookSerializer
7
8class BookFilterTestCase(APITestCase):
9 def setUp(self):
10 """Подготовка тестовых данных"""
11 self.author = Author.objects.create(
12 name="Тестовый Автор"
13 )
14 self.category = Category.objects.create(
15 name="Тестовая Категория"
16 )
17
18 self.book1 = Book.objects.create(
19 title="Тестовая Книга 1",
20 author=self.author,
21 category=self.category,
22 price=100,
23 rating=4.5
24 )
25
26 self.book2 = Book.objects.create(
27 title="Тестовая Книга 2",
28 author=self.author,
29 category=self.category,
30 price=200,
31 rating=3.8
32 )
33
34 self.url = reverse('book-list')
35
36 def test_filter_by_author(self):
37 """Тест фильтрации по автору"""
38 response = self.client.get(
39 self.url,
40 {'author': self.author.id}
41 )
42
43 self.assertEqual(response.status_code, status.HTTP_200_OK)
44 self.assertEqual(len(response.data['results']), 2)
45
46 def test_filter_by_price_range(self):
47 """Тест фильтрации по диапазону цен"""
48 response = self.client.get(
49 self.url,
50 {'price_min': 150, 'price_max': 250}
51 )
52
53 self.assertEqual(response.status_code, status.HTTP_200_OK)
54 self.assertEqual(len(response.data['results']), 1)
55 self.assertEqual(
56 response.data['results'][0]['title'],
57 "Тестовая Книга 2"
58 )
59
60 def test_search_functionality(self):
61 """Тест поиска"""
62 response = self.client.get(
63 self.url,
64 {'search': 'Книга 1'}
65 )
66
67 self.assertEqual(response.status_code, status.HTTP_200_OK)
68 self.assertEqual(len(response.data['results']), 1)
69 self.assertEqual(
70 response.data['results'][0]['title'],
71 "Тестовая Книга 1"
72 )
73
74 def test_ordering(self):
75 """Тест сортировки"""
76 response = self.client.get(
77 self.url,
78 {'ordering': '-price'}
79 )
80
81 self.assertEqual(response.status_code, status.HTTP_200_OK)
82 results = response.data['results']
83 self.assertEqual(results[0]['price'], 200)
84 self.assertEqual(results[1]['price'], 100)
85
86class FilterPerformanceTestCase(TestCase):
87 """Тесты производительности фильтров"""
88
89 def setUp(self):
90 """Создание большого количества тестовых данных"""
91 self.author = Author.objects.create(name="Автор")
92 self.category = Category.objects.create(name="Категория")
93
94 # Создаем 1000 книг для тестирования производительности
95 books = []
96 for i in range(1000):
97 books.append(Book(
98 title=f"Книга {i}",
99 author=self.author,
100 category=self.category,
101 price=i * 10,
102 rating=i % 5 + 1
103 ))
104
105 Book.objects.bulk_create(books)
106
107 def test_filter_performance(self):
108 """Тест производительности фильтрации"""
109 import time
110
111 start_time = time.time()
112 queryset = Book.objects.filter(
113 price__gte=100,
114 price__lte=500
115 )
116 count = queryset.count()
117 end_time = time.time()
118
119 # Проверяем, что фильтрация выполняется быстро
120 self.assertLess(end_time - start_time, 0.1)
121 self.assertGreater(count, 0)
Лучшие практики
- Используй select_related и prefetch_related - избегай N+1 запросов
- Кешируй часто используемые фильтры - используй Redis или Memcached
- Ограничивай размер страницы - предотвращай загрузку больших объемов данных
- Используй индексы в БД - ускоряй фильтрацию и поиск
- Валидируй входные параметры - проверяй корректность фильтров
- Тестируй производительность - измеряй время выполнения запросов
- Документируй API - объясняй параметры фильтрации и поиска
- Используй пагинацию - разбивай большие результаты на страницы
- Оптимизируй queryset - используй только необходимые поля
- Мониторь использование - отслеживай популярные фильтры и поисковые запросы
FAQ
Q: Как создать кастомные фильтры?
A: Создай класс FilterSet с нужными полями и методами фильтрации, используй django-filter для сложной логики.
Q: В чем разница между DjangoFilterBackend и SearchFilter?
A: DjangoFilterBackend для точной фильтрации по полям, SearchFilter для полнотекстового поиска по нескольким полям.
Q: Как оптимизировать производительность фильтров?
A: Используй select_related, prefetch_related, индексы в БД, кеширование и ограничение размера страниц.
Q: Можно ли комбинировать несколько фильтров?
A: Да, можно использовать несколько filter_backends одновременно для различных типов фильтрации.
Q: Как создать фильтр по диапазону дат?
A: Используй DateFilter с lookup_expr='gte' и 'lte' для начала и конца диапазона.
Q: Как добавить поиск по связанным моделям?
A: Используй двойное подчеркивание в search_fields: 'author__name', 'category__title'.
Q: Как тестировать фильтры в API?
A: Создавай тестовые данные и проверяй различные комбинации параметров фильтрации.