Search¶
Added in: Milestone 2 (Phase 3)
Django-admin-deux provides built-in search functionality with automatic PostgreSQL full-text search detection and fallback to generic icontains-based search.
Overview¶
Search in django-admin-deux: - Database-agnostic: Works with any Django-supported database - Smart PostgreSQL detection: Uses full-text search when available - Django admin pattern: Splits query into words with AND/OR logic - Related field support: Search across related models - Case-insensitive: Partial matching for user-friendly search
Configuration¶
Enable search by setting search_fields on your ModelAdmin:
from djadmin import ModelAdmin, register
@register(Product)
class ProductAdmin(ModelAdmin):
# Basic search across local fields
search_fields = ['name', 'sku', 'description']
Related Field Search¶
Use Django's double-underscore notation to search related fields:
@register(Product)
class ProductAdmin(ModelAdmin):
search_fields = [
'name',
'sku',
'description',
'category__name', # Search category name
'tags__name', # Search tag names
'manufacturer__company_name', # Nested relation
]
Search Behavior¶
Word Splitting¶
Search queries are split into words following Django admin's pattern:
# User searches: "laptop computer"
# Splits into: ["laptop", "computer"]
# Each word must match at least one field (OR)
# All words must be found (AND)
Example:
search_fields = ['name', 'description', 'category__name']
query = "laptop computer"
# Finds products where:
# ("laptop" in name OR "laptop" in description OR "laptop" in category.name)
# AND
# ("computer" in name OR "computer" in description OR "computer" in category.name)
Case-Insensitive Matching¶
All searches are case-insensitive using icontains (generic) or SearchQuery (PostgreSQL):
Partial Matching¶
Searches use substring matching (contains, not startswith):
# search_fields = ['name']
# Product: name="MacBook Pro 16-inch"
"MacBook" # ✅ Matches
"Pro" # ✅ Matches
"16" # ✅ Matches
"book Pro" # ✅ Matches
"mac pro" # ✅ Matches (two words, both found)
"windows" # ❌ No match
PostgreSQL Full-Text Search¶
When using PostgreSQL, django-admin-deux automatically uses full-text search for better performance and relevance.
Automatic Detection¶
Database detection happens at query time:
# djadmin/plugins/core/mixins.py (simplified)
from django.db import connection
def apply_search(self, queryset):
if connection.vendor == 'postgresql':
return self._apply_search_postgresql(queryset)
else:
return self._apply_search_generic(queryset)
PostgreSQL Implementation¶
Uses SearchVector and SearchQuery for full-text search:
from django.contrib.postgres.search import SearchVector, SearchQuery
from django.db.models import Q
# Combine all search_fields into a SearchVector
search_vector = SearchVector('name') + SearchVector('description')
# Create SearchQuery from user input
search_query = SearchQuery(query)
# Filter queryset
queryset = queryset.annotate(
search=search_vector
).filter(search=search_query)
Benefits:
- Faster than icontains on large datasets
- Better relevance ranking
- Stemming support (finds "running" when searching "run")
- Stop word handling
Fallback to Generic Search¶
For non-PostgreSQL databases (MySQL, SQLite, etc.), the system uses Q objects:
from django.db.models import Q
words = query.split()
search_q = Q()
for word in words:
word_q = Q()
for field in search_fields:
word_q |= Q(**{f'{field}__icontains': word})
search_q &= word_q
queryset = queryset.filter(search_q)
UI Integration¶
Search Widget¶
When search_fields is configured, a search widget appears at the top of the sidebar:
{# djadmin/templates/djadmin/includes/search_widget.html #}
<h3>Search</h3>
<form method="get" action="">
{% load djadmin_tags %}
{% query_params_as_hidden_inputs 'search' 'page' %}
<div class="search-input-wrapper">
<input type="search"
name="search"
value="{{ request.GET.search|default:'' }}"
placeholder="Search..."
class="search-input">
<button type="submit" class="search-button" title="Search">
{% include "djadmin/icons/magnifying-glass.html" %}
<span class="sr-only">Search</span>
</button>
</div>
<div class="search-actions">
{% if request.GET.search %}
<a href="{% querystring search=None page=None %}" class="clear-button">
Clear
</a>
{% endif %}
</div>
</form>
Query Parameter Preservation¶
Search preserves other query parameters (filters, ordering):
# Before search
/djadmin/webshop/product/?category=1&ordering=-price
# After searching for "laptop"
/djadmin/webshop/product/?category=1&ordering=-price&search=laptop
This is handled by the query_params_as_hidden_inputs template tag.
URL Parameter¶
Search uses the search query parameter:
# No search
/djadmin/webshop/product/
# Single word
/djadmin/webshop/product/?search=laptop
# Multiple words (URL encoded)
/djadmin/webshop/product/?search=laptop+computer
# With other parameters
/djadmin/webshop/product/?search=laptop&category=1&ordering=price
Advanced Examples¶
Search with Annotations¶
Search across computed fields using annotations:
from django.db.models import Value
from django.db.models.functions import Concat
@register(Person)
class PersonAdmin(ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
# Annotate full_name for searching
return qs.annotate(
full_name=Concat('first_name', Value(' '), 'last_name')
)
search_fields = [
'full_name', # Search the annotated field
'email',
'phone',
]
Multi-Database Search¶
For models with custom database routing:
@register(Product)
class ProductAdmin(ModelAdmin):
search_fields = ['name', 'sku']
def get_queryset(self, request):
# SearchMixin will detect the database vendor
# from the queryset's connection
return Product.objects.using('analytics_db').all()
Search with Permissions¶
Combine search with permission filtering:
@register(Document)
class DocumentAdmin(ModelAdmin):
search_fields = ['title', 'content', 'author__username']
def get_queryset(self, request):
qs = super().get_queryset(request)
# Filter by user permissions
if not request.user.is_superuser:
qs = qs.filter(
Q(owner=request.user) | Q(is_public=True)
)
return qs
Performance Considerations¶
PostgreSQL Tips¶
- Add GIN indexes for better full-text search performance:
from django.db import models
from django.contrib.postgres.indexes import GinIndex
class Product(models.Model):
name = models.CharField(max_length=200)
description = models.TextField()
class Meta:
indexes = [
GinIndex(fields=['name', 'description']),
]
- Use SearchVectorField for pre-computed search vectors:
from django.contrib.postgres.search import SearchVectorField
class Product(models.Model):
# ... other fields ...
search_vector = SearchVectorField(null=True)
class Meta:
indexes = [
GinIndex(fields=['search_vector']),
]
Generic Database Tips¶
- Add indexes on frequently searched fields:
class Product(models.Model):
name = models.CharField(max_length=200, db_index=True)
sku = models.CharField(max_length=50, db_index=True)
- Limit search_fields to essential fields only:
# Good - Only essential fields
search_fields = ['name', 'sku']
# Avoid - Too many fields, including large text
search_fields = ['name', 'sku', 'description', 'notes', 'meta_data']
- Use select_related/prefetch_related for related field searches:
@register(Product)
class ProductAdmin(ModelAdmin):
search_fields = ['name', 'category__name']
def get_queryset(self, request):
return super().get_queryset(request).select_related('category')
Testing¶
Test search functionality using the DynamicURLConf pattern:
from django.test import TestCase, override_settings
from django.urls import clear_url_caches
from djadmin import ModelAdmin, site
from myapp.models import Product
class DynamicURLConf:
@property
def urlpatterns(self):
from django.urls import path, include
return [path('djadmin/', include(site.urls))]
@override_settings(ROOT_URLCONF=DynamicURLConf())
class TestProductSearch(TestCase):
def setUp(self):
if Product in site._registry:
site.unregister(Product)
self.laptop = Product.objects.create(name='MacBook Pro')
self.phone = Product.objects.create(name='iPhone 15')
def tearDown(self):
if Product in site._registry:
site.unregister(Product)
clear_url_caches()
def test_search(self):
class ProductAdmin(ModelAdmin):
search_fields = ['name']
site.register(Product, ProductAdmin, override=True)
url = site.reverse('myapp_product_list')
response = self.client.get(url, {'search': 'macbook'})
self.assertEqual(len(response.context['object_list']), 1)
self.assertEqual(response.context['object_list'][0], self.laptop)
See Also¶
- Filtering (djadmin-filters plugin) - Filter results
- Ordering (djadmin-filters plugin) - Sort results
- Sidebar Widgets - Customize sidebar
- Django PostgreSQL Search - Full-text search docs