Initial
This commit is contained in:
235
django_translatable_fields/search.py
Normal file
235
django_translatable_fields/search.py
Normal file
@@ -0,0 +1,235 @@
|
||||
# -*- 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 <holger@backender.de>
|
||||
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'
|
||||
}
|
||||
Reference in New Issue
Block a user