Usage

Define lookups in a lookups.py module in your app, which izaya imports on startup:

from izaya import ModelLookup, register


@register
class ArcLookup(ModelLookup):
    model = Arc
    search_fields = ["name__unaccent__icontains"]

search_fields holds complete field lookups on text fields, and a record matches if any of them do. Each lookup receives the search text as is, so a numeric lookup such as pk__exact fails on anything but a number. Override search to handle those. Use the lookup in a form, with multiple=True for a multi-select:

from izaya import LookupWidget


class IssueForm(forms.ModelForm):
    class Meta:
        model = Issue
        fields = ["arcs"]
        widgets = {"arcs": LookupWidget(ArcLookup, multiple=True)}

Customizing a lookup

Attribute or method

Purpose

min_length, max_results, max_query_length, placeholder

Search behaviour, defaulting to 3, 100, 100 and ""

no_results_text, more_results_text, min_length_text, access_denied_text, login_required_text

Messages shown in the results

clean_query(query)

Turn the raw search text into the text passed to search, by default stripping surrounding whitespace. min_length and max_query_length apply to its result, and whitespace alone is never searched

has_access(request)

Return False to answer searches with a 403, which shows login_required_text to anonymous users and access_denied_text to everyone else

get_queryset(request)

The records this request may search

get_base_queryset()

The records used for labels, for example with select_related

label(record)

The text shown for a record

rank_fields

Text fields that put the closest matches first, defaulting to ()

rank(queryset, query)

Apply rank_fields to a queryset, for use in a custom search

search(query, request)

Replace the search entirely; must return something sliceable

get_selected(keys)

Load the records for a form’s value; keys are strings

key_field

The unique field to submit, defaulting to "pk"

key_field must match the form field’s to_field_name, which is set automatically for a ForeignKey with to_field. izaya raises ImproperlyConfigured if they differ.

Results are cut off at max_results, so when a common word matches more records than that, the best match can be lost. rank_fields puts the closest matches first:

class CharacterLookup(ModelLookup):
    model = Character
    search_fields = ["name__unaccent__icontains", "alias__icontains"]
    rank_fields = ["name__unaccent"]

Unlike search_fields, these are field names without a lookup, and only change the order. A record ranks by its best match in any of them: the whole value, then the start of the value, then the start of a word, then everything else. Ties keep the order of get_queryset, or the model’s Meta.ordering, then fall back to the primary key. rank_fields can’t cross a to-many relation, which the system check izaya.E001 reports. Every match has to be ranked before the results are cut off, so measure this on large tables. Ranking is tested on SQLite, PostgreSQL, MySQL and MariaDB. On MySQL and MariaDB the default _ai_ci collations ignore accents when matching the whole value or its start, but not the start of a word, so for cafe “Café” ranks first while “Le Café” ranks with everything else.

A search override keeps its own order unless it calls rank, which raises ImproperlyConfigured for a sliced or combined queryset, or one using distinct() with field names.

For data that isn’t a model, subclass Lookup and implement search and get_selected, returning (key, label) pairs. On a field with choices, the widget only renders labels for keys among them; see Private data for fields without choices.