175 lines
5.9 KiB
Python
175 lines
5.9 KiB
Python
from django.apps import apps
|
|
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.db import connection, models
|
|
from django.db.models.fields import TextField
|
|
from django.db.models.fields.related import OneToOneField
|
|
from django.db.models.functions import Cast
|
|
from django.db.models.sql.where import WhereNode
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from .index import class_is_indexed
|
|
from .utils import get_descendants_content_types_pks
|
|
|
|
|
|
class TextIDGenericRelation(GenericRelation):
|
|
auto_created = True
|
|
|
|
def get_content_type_lookup(self, alias, remote_alias):
|
|
field = self.remote_field.model._meta.get_field(self.content_type_field_name)
|
|
return field.get_lookup("in")(
|
|
field.get_col(remote_alias), get_descendants_content_types_pks(self.model)
|
|
)
|
|
|
|
def get_object_id_lookup(self, alias, remote_alias):
|
|
from_field = self.remote_field.model._meta.get_field(self.object_id_field_name)
|
|
to_field = self.model._meta.pk
|
|
return from_field.get_lookup("exact")(
|
|
from_field.get_col(remote_alias), Cast(to_field.get_col(alias), from_field)
|
|
)
|
|
|
|
def get_extra_restriction(self, alias, remote_alias):
|
|
cond = WhereNode()
|
|
cond.add(self.get_content_type_lookup(alias, remote_alias), "AND")
|
|
cond.add(self.get_object_id_lookup(alias, remote_alias), "AND")
|
|
return cond
|
|
|
|
def resolve_related_fields(self):
|
|
return []
|
|
|
|
|
|
class BaseIndexEntry(models.Model):
|
|
"""
|
|
This is an abstract class that only contains fields common to all database vendors.
|
|
It should be extended by the models specific for each backend.
|
|
"""
|
|
|
|
content_type = models.ForeignKey(
|
|
ContentType, on_delete=models.CASCADE, related_name="+"
|
|
)
|
|
# We do not use an IntegerField since primary keys are not always integers.
|
|
object_id = models.CharField(max_length=50)
|
|
content_object = GenericForeignKey()
|
|
|
|
# TODO: Add per-object boosting.
|
|
# This field stores the "Title Normalisation Factor"
|
|
# This factor is multiplied onto the rank of the title field.
|
|
# This allows us to apply a boost to results with shorter titles
|
|
# elevating more specific matches to the top.
|
|
title_norm = models.FloatField(default=1.0)
|
|
|
|
wagtail_reference_index_ignore = True
|
|
|
|
class Meta:
|
|
unique_together = ("content_type", "object_id")
|
|
verbose_name = _("index entry")
|
|
verbose_name_plural = _("index entries")
|
|
abstract = True
|
|
|
|
def __str__(self):
|
|
return f"{self.content_type.name}: {self.content_object}"
|
|
|
|
@property
|
|
def model(self):
|
|
return self.content_type.model
|
|
|
|
@classmethod
|
|
def add_generic_relations(cls):
|
|
for model in apps.get_models():
|
|
if class_is_indexed(model):
|
|
TextIDGenericRelation(cls).contribute_to_class(model, "index_entries")
|
|
|
|
|
|
# AbstractIndexEntry will be defined depending on which database system we're using.
|
|
if connection.vendor == "postgresql":
|
|
from django.contrib.postgres.indexes import GinIndex
|
|
from django.contrib.postgres.search import SearchVectorField
|
|
|
|
class AbstractPostgresIndexEntry(BaseIndexEntry):
|
|
"""
|
|
This class is the specific IndexEntry model for PostgreSQL database systems.
|
|
It inherits the fields defined in BaseIndexEntry, and adds PostgreSQL-specific
|
|
fields (tsvectors), plus indexes for doing full-text search on those fields.
|
|
"""
|
|
|
|
# TODO: Add per-object boosting.
|
|
autocomplete = SearchVectorField()
|
|
title = SearchVectorField()
|
|
body = SearchVectorField()
|
|
|
|
class Meta(BaseIndexEntry.Meta):
|
|
abstract = True
|
|
# An additional computed GIN index on 'title || body' is created in a SQL migration
|
|
# covers the default case of PostgresSearchQueryCompiler.get_index_vectors.
|
|
indexes = [
|
|
GinIndex(fields=["autocomplete"]),
|
|
GinIndex(fields=["title"]),
|
|
GinIndex(fields=["body"]),
|
|
]
|
|
|
|
AbstractIndexEntry = AbstractPostgresIndexEntry
|
|
|
|
elif connection.vendor == "sqlite":
|
|
from wagtail.search.backends.database.sqlite.utils import fts5_available
|
|
|
|
class AbstractSQLiteIndexEntry(BaseIndexEntry):
|
|
"""
|
|
This class is the specific IndexEntry model for SQLite database systems. The autocomplete, title, and body fields store additional
|
|
"""
|
|
|
|
autocomplete = TextField(null=True)
|
|
title = TextField(null=False)
|
|
body = TextField(null=True)
|
|
|
|
class Meta(BaseIndexEntry.Meta):
|
|
abstract = True
|
|
|
|
AbstractIndexEntry = AbstractSQLiteIndexEntry
|
|
|
|
if fts5_available():
|
|
|
|
class SQLiteFTSIndexEntry(models.Model):
|
|
autocomplete = TextField(null=True)
|
|
title = TextField(null=False)
|
|
body = TextField(null=True)
|
|
index_entry = OneToOneField(
|
|
primary_key=True,
|
|
to="wagtailsearch.indexentry",
|
|
on_delete=models.CASCADE,
|
|
db_column="rowid",
|
|
)
|
|
|
|
class Meta:
|
|
db_table = "wagtailsearch_indexentry_fts"
|
|
|
|
elif connection.vendor == "mysql":
|
|
|
|
class AbstractMySQLIndexEntry(BaseIndexEntry):
|
|
"""
|
|
This class is the specific IndexEntry model for MySQL database systems.
|
|
"""
|
|
|
|
autocomplete = TextField(null=True)
|
|
title = TextField(null=False)
|
|
body = TextField(null=True)
|
|
|
|
class Meta(BaseIndexEntry.Meta):
|
|
abstract = True
|
|
|
|
AbstractIndexEntry = AbstractMySQLIndexEntry
|
|
|
|
else:
|
|
AbstractIndexEntry = BaseIndexEntry
|
|
|
|
|
|
class IndexEntry(AbstractIndexEntry):
|
|
"""
|
|
The IndexEntry model that will get created in the database.
|
|
"""
|
|
|
|
class Meta(AbstractIndexEntry.Meta):
|
|
"""
|
|
Contains everything in the AbstractIndexEntry Meta class, but makes this model concrete.
|
|
"""
|
|
|
|
abstract = False
|