Documents¶
A Document describes one OpenSearch index and the Django model that fills it. Everything this
library does — mapping generation, population, signal-driven updates — follows from that pairing.
from django_opensearch_models import Document
from django_opensearch_models.registries import registry
from .models import Car
@registry.register_document
class CarDocument(Document):
class Index:
name = "cars"
settings = {"number_of_shards": 1, "number_of_replicas": 0}
class Django:
model = Car
fields = ["name", "color"]
Documents must live in a documents.py module inside an installed app. The app config calls
autodiscover() on startup and imports exactly that name; a document declared elsewhere is never
registered, never mapped and never updated, with no error to tell you so.
The Index inner class¶
Describes the OpenSearch side of the pairing.
nameThe index name. Required.
settingsA dict of index settings, passed to OpenSearch as-is —
number_of_shards,number_of_replicas, custom analyzers and so on. See the OpenSearch index settings reference.Warning
These are applied before the project-wide
OPENSEARCH_INDEX_SETTINGS, and the two are merged with the later call winning. A key present in both takes the project-wide value, not the one written here — settingnumber_of_shardsglobally silently overrides every document that also sets it.
For anything more involved than a name and some settings — sharing one index between documents, or rebuilding behind an alias — use an index object instead.
The Django inner class¶
Describes which model feeds the index, and how.
modelThe Django model class. Required; registration fails with
ImproperlyConfiguredwithout it.fieldsA list of model field names to index. Their OpenSearch types are derived from the Django field types — see the mapping table. Naming a field here that you have also declared explicitly on the document raises
RedeclaredFieldError, so the two can never silently disagree.related_modelsModels whose changes should trigger a re-index of this document. Required whenever you embed data from another model with an
ObjectFieldorNestedField, otherwise that embedded copy goes stale as soon as the related row changes. Pair it withget_instances_from_related(), below.ignore_signalsFalseby default. Set it toTrueto stop this document being updated automatically onsave()/delete(), leaving population entirely to the management command. Useful for indices that are rebuilt on a schedule rather than maintained live.auto_refreshWhether to ask OpenSearch to refresh the index after each write, making the change immediately visible to searches. Defaults to
OPENSEARCH_AUTO_REFRESH; setting it here overrides that for this document only.queryset_paginationChunk size used when iterating the model during population. Without it the database driver’s default applies, which for large tables can mean loading far more rows into memory at once than you want. The same value sizes the bulk requests sent to OpenSearch, on both the serial and the
--parallelpath.
Computed and derived values¶
A different attribute¶
Index the string form of a field rather than its raw value by pointing a document field at
another attribute or method with attr:
# models.py
class Car(models.Model):
...
def type_to_string(self):
return dict(self._meta.get_field("type").choices).get(self.type, "")
# documents.py
from django_opensearch_models import Document, fields
@registry.register_document
class CarDocument(Document):
type = fields.TextField(attr="type_to_string")
class Django:
model = Car
fields = ["name", "color"] # `type` is declared on the document instead
attr is a dotted path resolved with Django template semantics — dictionary lookup, then attribute
lookup, then list-index lookup — so attr="manufacturer.country.name" works, and a callable found
along the way is called.
A prepare_ method¶
When a value needs real computation, define prepare_<field>(self, instance). It is called
whenever that field is indexed and takes precedence over attr:
class CarDocument(Document):
summary = fields.TextField()
def prepare_summary(self, instance):
return f"{instance.name} ({instance.color})"
Hooks¶
Every one of these is a method on your Document subclass.
get_queryset(self)The queryset used to populate the index. Defaults to
model._default_manager.all(). Override it to narrow what gets indexed, or — more often — to avoid the N+1 that embedding related data otherwise causes:def get_queryset(self): return super().get_queryset().select_related("manufacturer")
get_indexing_queryset(self)The iterator of instances used to populate the index, built from
get_queryset()and chunked byqueryset_pagination. The rows are drawn inside a transaction, because outside one PostgreSQL serves the underlying server-side cursor asWITH HOLDand materialises the whole result set to temporary storage before returning the first row. Override it only if you need a different traversal; if you do, keep the iteration itself inside the transaction, since a queryset is lazy and wrapping only its construction has no effect. Close the iterator when you are finished with it: the transaction lasts as long as it does. Under--parallelthe rows are drawn on a pool worker, so the transaction belongs to that thread’s connection rather than the one the command started on.should_index_object(self, obj)Called per object during indexing; return
Falseto skip it. ReturnsTrueby default. This is the right place for “only index published articles” logic, because unlike filtering inget_queryset()it also applies to signal-driven single-object updates.get_instances_from_related(self, related_instance)Required when you use
related_models. Given an instance of a related model, return the object or queryset of this document’s model that needs re-indexing: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
Warning
Use
related_modelsdeliberately. One write to a widely-referenced model can fan out into re-indexing a very large number of documents, synchronously, inside the request that saved it.generate_id(cls, object_instance)A classmethod returning the OpenSearch
_idfor an object. Defaults to the model’s primary key. Override it to key documents by something else:@classmethod def generate_id(cls, article): return article.slug
Updating documents directly¶
Document.update() writes one instance, an iterable of them, or a queryset:
CarDocument().update(car)
CarDocument().update(Car.objects.filter(color="red"))
CarDocument().update(car, action="delete")
CarDocument().update(qs, parallel=True, refresh=True)
action is "index" by default and accepts any OpenSearch bulk operation. parallel=True uses
parallel_bulk(); note that the post_index signal is not sent
in that mode.