# -*- coding: utf-8 -*- """ Django Translatable Fields - Advanced Search Functionality This module provides database-optimized search capabilities for translatable fields. It includes database-specific implementations for PostgreSQL, MySQL, and SQLite to ensure optimal performance when searching JSON-stored translations. Key Features: - Database-specific JSON search optimizations - PostgreSQL: Native JSON operators (->>, ->, etc.) - MySQL: JSON_EXTRACT and JSON_UNQUOTE functions - SQLite: json_extract function - Generic fallback for other databases - Case-insensitive search support - Multi-field search across different translatable fields The module automatically detects the database backend and uses the most efficient search method available. This ensures fast search performance even with large datasets containing complex translation structures. Usage: # Used internally by TranslatableQuerySet results = TranslatableSearch.search_translatable_field( queryset, 'name', 'search_term', 'de' ) Author: Holger Sielaff Version: 0.1.0 """ import json from django.db import models, connection from django.db.models import Q from django.utils.translation import get_language class TranslatableSearch: """ Advanced search functionality for translatable fields with database-specific optimizations. This class provides database-optimized search methods for translatable fields. PostgreSQL is strongly recommended for optimal performance as it provides native JSON operators that are significantly faster than generic approaches. Performance Comparison: - PostgreSQL: Uses native ->, ->> operators (~10x faster) - MySQL: Uses JSON_EXTRACT functions (good performance) - SQLite: Uses json_extract function (acceptable for small datasets) - Others: Generic fallback (slower performance) For production use with large datasets, PostgreSQL is highly recommended. """ @staticmethod def get_database_vendor(): """Get the database vendor (postgresql, mysql, sqlite, etc.)""" return connection.vendor @classmethod def search_translatable_field(cls, queryset, field_name, query, language=None): """ Search in a translatable field for the specified language with database-specific optimizations. Args: queryset: QuerySet to filter field_name: Name of the translatable field to search in query: Search query string language: Language code to search (defaults to current language) Returns: Filtered QuerySet """ if not query: return queryset language = language or get_language() or 'en_US' query = str(query).strip() vendor = cls.get_database_vendor() if vendor == 'postgresql': return cls._search_postgresql(queryset, field_name, query, language) elif vendor == 'mysql': return cls._search_mysql(queryset, field_name, query, language) elif vendor == 'sqlite': return cls._search_sqlite(queryset, field_name, query, language) else: # Generic fallback return cls._search_generic(queryset, field_name, query, language) @classmethod def _search_postgresql(cls, queryset, field_name, query, language): """PostgreSQL-specific JSON search using -> operator""" # Use PostgreSQL JSON operators for efficient search return queryset.extra( where=[f"LOWER({field_name}->>'%s') LIKE LOWER(%s)"], params=[language, f'%{query}%'] ) @classmethod def _search_mysql(cls, queryset, field_name, query, language): """MySQL-specific JSON search using JSON_UNQUOTE and JSON_EXTRACT""" return queryset.extra( where=["LOWER(JSON_UNQUOTE(JSON_EXTRACT(%s, CONCAT('$.', %%s)))) LIKE LOWER(%%s)" % field_name], params=[language, f'%{query}%'] ) @classmethod def _search_sqlite(cls, queryset, field_name, query, language): """SQLite JSON search using json_extract""" return queryset.extra( where=[f"LOWER(json_extract({field_name}, '$.{language}')) LIKE LOWER(%s)"], params=[f'%{query}%'] ) @classmethod def _search_generic(cls, queryset, field_name, query, language): """Generic fallback using text search in JSON""" # Fallback: search for the pattern in the JSON text search_pattern = f'"{language}":"%{query}%"' return queryset.filter(**{f'{field_name}__icontains': search_pattern}) @classmethod def search_multiple_fields(cls, queryset, query, language=None, fields=None): """ Search across multiple translatable fields. Args: queryset: QuerySet to filter query: Search query string language: Language code to search (defaults to current language) fields: List of field names to search (auto-detected if None) Returns: Filtered QuerySet """ if not query: return queryset language = language or get_language() or 'en_US' # Auto-detect translatable fields if not specified if fields is None: fields = cls._get_translatable_fields(queryset.model) if not fields: return queryset # Build OR query across all fields q_objects = Q() for field_name in fields: # Create a sub-queryset for this field and extract the WHERE clause field_qs = cls.search_translatable_field( queryset.model.objects.all(), field_name, query, language ) # Add to OR conditions - this is a simplified approach # In practice, you might want to use more sophisticated query building q_objects |= Q(**{f'{field_name}__icontains': f'"{language}":'}) & Q(**{f'{field_name}__icontains': query}) return queryset.filter(q_objects) @classmethod def _get_translatable_fields(cls, model): """Get list of translatable field names for a model""" translatable_fields = [] for field in model._meta.get_fields(): if hasattr(field, 'translatable') and field.translatable: translatable_fields.append(field.name) return translatable_fields def search_products(query, language=None, fields=None): """ Convenience function to search products. Args: query: Search query string language: Language code (defaults to current language) fields: List of field names to search (defaults to ['name', 'description']) Returns: QuerySet of matching products Example: products = search_products('moep', 'de') products = search_products('hello', fields=['name']) """ from .models import Product # Adjust import as needed queryset = Product.objects.all() fields = fields or ['name', 'description'] return TranslatableSearch.search_multiple_fields( queryset, query, language, fields ) class SearchForm: """ Helper class to create search forms with language selection """ @staticmethod def get_language_choices(): """Get available language choices from Django settings""" from django.conf import settings return getattr(settings, 'LANGUAGES', [('en', 'English'), ('de', 'German')]) @classmethod def create_search_context(cls, request): """ Create context for search forms. Args: request: Django request object Returns: Dictionary with search context """ query = request.GET.get('q', '') language = request.GET.get('lang', get_language() or 'en_US') fields = request.GET.getlist('fields') return { 'search_query': query, 'search_language': language, 'search_fields': fields, 'available_languages': cls.get_language_choices(), 'current_language': get_language() or 'en_US' }