Fields¶
Document fields are thin subclasses of the opensearch-py field types. They add one thing: knowing
how to pull a value off a Django model instance. Everything a plain opensearch-py field accepts —
analyzers, multi-fields, index options — works here too.
from django_opensearch_models import Document, fields
@registry.register_document
class CarDocument(Document):
description = fields.TextField(
analyzer=html_strip,
fields={"raw": fields.KeywordField()},
)
Available fields¶
Every field takes attr=None as its first argument and forwards any remaining keyword arguments to
the underlying OpenSearch field.
Simple fields
BooleanField, ByteField, CompletionField, DateField, DoubleField, FileField,
FloatField, GeoPointField, GeoShapeField, IntegerField, IpField, KeywordField,
LongField, ScaledFloatField, SearchAsYouTypeField, ShortField, TextField, TimeField
Two behave specially:
FileFieldindexes aFileField/ImageFieldas its.url, or""when no file is set — never theFieldFileobject.TimeFieldis aKeywordFieldthat serialisesdatetime.timevalues with.isoformat(). OpenSearch has no native time-of-day type.
Complex fields
ObjectField(properties, attr=None, **kwargs) and NestedField(properties, attr=None, **kwargs),
where properties maps field names to field instances. See
Relationships below.
ListField(field)
A wrapper, not a field class. It makes the wrapped field iterate its value, for indexing a to-many relationship as a flat list:
tags = fields.ListField(fields.KeywordField(attr="tag_names"))
How Django fields are mapped¶
When you name a field in Django.fields, its OpenSearch type comes from this table:
Django field |
Indexed as |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
A model field with no entry here raises ModelFieldNotMappedError at registration.
Note
DateField and DateTimeField both become an OpenSearch date, which has no date-only variant. A
Django DateField therefore round-trips back out of the index as a naive datetime at midnight,
not a date.
Custom mappings¶
To map a field type the table does not cover, override get_model_field_class_to_field_class().
Custom field classes must inherit OSField:
from django_opensearch_models.fields import OSField
from opensearchpy import Keyword
class MyCustomField(OSField, Keyword):
pass
class MyDocument(Document):
@classmethod
def get_model_field_class_to_field_class(cls):
mapping = super().get_model_field_class_to_field_class()
mapping[MyCustomDjangoField] = MyCustomField
return mapping
Relationships¶
Given a Car with a ForeignKey to Manufacturer and a reverse relation from Ad:
# models.py
class Manufacturer(models.Model):
name = models.CharField(max_length=255)
country_code = models.CharField(max_length=2)
class Car(models.Model):
name = models.CharField(max_length=255)
manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
class Ad(models.Model):
title = models.CharField(max_length=255)
car = models.ForeignKey(Car, related_name="ads", on_delete=models.CASCADE)
embed them with ObjectField for a single related object and NestedField for a collection:
# documents.py
@registry.register_document
class CarDocument(Document):
manufacturer = fields.ObjectField(
properties={
"name": fields.TextField(),
"country_code": fields.TextField(),
}
)
ads = fields.NestedField(
properties={
"title": fields.TextField(),
"pk": fields.IntegerField(),
}
)
class Index:
name = "cars"
class Django:
model = Car
fields = ["name"]
related_models = [Manufacturer, Ad]
def get_queryset(self):
# One query instead of one per car.
return super().get_queryset().select_related("manufacturer")
def get_instances_from_related(self, related_instance):
if isinstance(related_instance, Manufacturer):
return related_instance.car_set.all()
if isinstance(related_instance, Ad):
return related_instance.car
return None
Important
related_models and get_instances_from_related() are what keep embedded data fresh. Without them
the copy of a manufacturer’s name inside every car document is written once at index time and never
updated again — renaming the manufacturer leaves every car wrong until the next full rebuild.
ObjectField or NestedField?¶
That choice is OpenSearch’s, not this library’s. object flattens sub-fields, so a query cannot
require that two of them match within the same sub-document; nested keeps them as separate
indexed documents and can. Nested comes at a cost in index size and query complexity. The
OpenSearch field type documentation covers the
trade-off.
Analyzers¶
Analyzers come from opensearch-py and are used exactly as they are there:
from opensearchpy import analyzer
html_strip = analyzer(
"html_strip",
tokenizer="standard",
filter=["lowercase", "stop", "snowball"],
char_filter=["html_strip"],
)
@registry.register_document
class CarDocument(Document):
description = fields.TextField(
analyzer=html_strip,
fields={"raw": fields.KeywordField()},
)
Changing an analyzer changes the mapping, and OpenSearch will not alter the mapping of an existing index. Rebuild after any such change:
$ ./manage.py search_index --rebuild --models myapp.Car