Фильтрация и поиск в 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: Создавай тестовые данные и проверяй различные комбинации параметров фильтрации.