Filtering derived fields with Wagtail search

Wagtail’s built in search functionality has this nifty feature where it will index callables on your model, e.g. your model has a start and end date, but you want to search on duration. Hypothetically we could add a FilterField here to index this for Elasticsearch ((Note the requirement for a type, otherwise this will be indexed as a string.)).

[python]
class Job(Model, index.Indexed):
"""A job you can apply for."""

start_date = models.DateField()
end_date = models.DateField()

search_fields = (
index.FilterField(‘duration’, type=’IntegerField’),
)

@property
def duration(self):
"""Duration of the assignment."""
return 12 * (self.end_date.year – self.start_date.year) + \
self.end_date.month – self.start_date.month

[/python]

Wagtail is quite clever in that it takes a Django QuerySet and decomposes the filters.

[python]
queryset = queryset\
.filter(duration__range=(data[‘duration’].lower or 0,
data[‘duration’].upper or 99999))
query = backend.search(keywords, queryset)
[/python]

Of course Django will get upset about this, since that’s not a field you can filter on. So we can annotate the value in.

[python highlight=”14-16″]
from django.db.models import Func, fields

class DurationInMonths(Func): # pylint:disable=abstract-method
"""
SQL Function to calculate the duration of assignments in months.
"""
template = \
‘EXTRACT(year FROM age(%(expressions)s)) * 12 + ‘ \
‘EXTRACT(month FROM age(%(expressions)s))’
output_field = fields.IntegerField()

queryset = queryset\
.annotate(duration=DurationInMonths(
F(‘end_date’), F(‘start_date’)
))\
.filter(duration__range=(data[‘duration’].lower or 0,
data[‘duration’].upper or 99999))
[/python]

Also Django will be upset it can’t annotate the duration, so you’ll need to add a setter to your model.

[python highlight=”6-9″]
class Job(Model, index.Indexed):
@property
def duration(self):

@duration.setter
def duration(self, value):
"""Ignored to make Django annotations happy."""
pass
[/python]

And you ideally would be done except now Wagtail gets upset that it can’t determine the attribute name for the field but you can kludge around this for now:

[python highlight=”9-11″]
class DurationInMonths(Func): # pylint:disable=abstract-method
"""
SQL Function to calculate the duration of assignments in months.
"""
template = \
‘EXTRACT(year FROM age(%(expressions)s)) * 12 + ‘ \
‘EXTRACT(month FROM age(%(expressions)s))’
output_field = fields.IntegerField()
target = type(‘IntegerFieldKludge’,
(fields.IntegerField,),
{‘attname’: ‘duration’})()
[/python]

Author: Danielle

Danielle is an Australian software engineer, computer scientist and feminist. She doesn't really work on GNOME any more (sadly). Opinions and writing are solely her own and so not represent her employer, the GNOME Foundation, or anyone else but herself.

Leave a Reply

Your email address will not be published. Required fields are marked *

Creative Commons Attribution-ShareAlike 2.5 Australia
This work by Danielle Madeley is licensed under a Creative Commons Attribution-ShareAlike 2.5 Australia.