Skip to content

Instantly share code, notes, and snippets.

@costela
Created October 28, 2020 15:08
Show Gist options
  • Save costela/39def2e6511867eb8c8f1c12d6e86f92 to your computer and use it in GitHub Desktop.
Save costela/39def2e6511867eb8c8f1c12d6e86f92 to your computer and use it in GitHub Desktop.
Self-updating search vector field using postgres 12 "generated" column feature
from django.contrib.postgres.search import SearchVectorField
from django.db.models.fields import NOT_PROVIDED
class AutoSearchVectorField(SearchVectorField):
def __init__(self, language_field: str, source_field_weights: dict, *args, **kwargs) -> None:
self.language_field = language_field
self.source_field_weights = source_field_weights
kwargs["editable"] = False
kwargs["serialize"] = False
kwargs["default"] = NOT_PROVIDED
kwargs["null"] = True
super().__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
kwargs["language_field"] = self.language_field
kwargs["source_field_weights"] = self.source_field_weights
del kwargs["editable"]
del kwargs["serialize"]
# del kwargs["default"]
del kwargs["null"]
return name, path, args, kwargs
def db_type(self, connection):
# TODO: this naive syntax only works for new columns, so the migrations need to be manually adjusted to
# RemoveField+AddField
return f"tsvector GENERATED ALWAYS AS ({self._get_search_vector()}) STORED"
def _get_search_vector(self) -> str:
regconfig = self._get_lazy_lang_to_regconfig()
return "||".join(
[
f"setweight(to_tsvector({regconfig}, coalesce({field},'')), '{weight}')"
for field, weight in self.source_field_weights.items()
],
)
def _get_lazy_lang_to_regconfig(self) -> str:
lang_to_regconfig = {
"en": "english",
"de": "german",
}
return "CASE %s END" % " ".join(
[
f"WHEN {self.language_field} LIKE '{lang}%%' THEN '{regconfig}'::regconfig"
for lang, regconfig in lang_to_regconfig.items()
]
)
# We need this hacky mixin because there's currently no "right" way to make a field read-only on the DB level.
# See: https://code.djangoproject.com/ticket/21454
class AutoSearchModelMixin:
def save(self, **kwargs) -> None:
search_fields = []
for i, field in enumerate(self._meta.local_fields):
if isinstance(field, AutoSearchVectorField):
search_fields.append(self._meta.local_fields.pop(i))
self._meta._expire_cache(forward=True, reverse=True)
super().save(**kwargs)
for field in search_fields:
self._meta.add_field(field)
self._meta._expire_cache(forward=True, reverse=True)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment