Saltar al contenido

Creando un Índice de Gin con Trigram (gin_trgm_ops) en el modelo Django

Después de tanto luchar ya dimos con el arreglo de esta pregunta que muchos lectores de este sitio web tienen. Si tienes algún dato que aportar puedes dejar tu comentario.

Solución:

Tuve un problema similar al intentar usar el pg_tgrm extensión para apoyar eficiente contains y icontains Búsquedas de campo de Django.

Puede haber una forma más elegante, pero definir un nuevo tipo de índice como este funcionó para mí:

from django.contrib.postgres.indexes import GinIndex

class TrigramIndex(GinIndex):
    def get_sql_create_template_values(self, model, schema_editor, using):
        fields = [model._meta.get_field(field_name) for field_name, order in self.fields_orders]
        tablespace_sql = schema_editor._get_index_tablespace_sql(model, fields)
        quote_name = schema_editor.quote_name
        columns = [
            ('%s %s' % (quote_name(field.column), order)).strip() + ' gin_trgm_ops'
            for field, (field_name, order) in zip(fields, self.fields_orders)
        ]
        return 
            'table': quote_name(model._meta.db_table),
            'name': quote_name(self.name),
            'columns': ', '.join(columns),
            'using': using,
            'extra': tablespace_sql,
        

El método get_sql_create_template_values es copiado de Index.get_sql_create_template_values(), con una sola modificación: la adición de + ' gin_trgm_ops'.

Para su caso de uso, luego definiría el índice en name_txt usando esto TrigramIndex en lugar de un GinIndex. Entonces corre makemigrations, lo que producirá una migración que generará la necesaria CREATE INDEX SQL.

ACTUALIZAR:

Veo que también estás haciendo una consulta usando icontains:

result.exclude(name_txt__icontains = 'sp.')

El backend de Postgresql lo convertirá en algo como esto:

UPPER("NCBI_names"."name_txt"::text) LIKE UPPER('sp.')

y luego el índice de trigramas no se utilizará debido a la UPPER().

Tuve el mismo problema y terminé subclasificando el backend de la base de datos para solucionarlo:

from django.db.backends.postgresql import base, operations

class DatabaseFeatures(base.DatabaseFeatures):
    pass

class DatabaseOperations(operations.DatabaseOperations):
    def lookup_cast(self, lookup_type, internal_type=None):
        lookup = '%s'

        # Cast text lookups to text to allow things like filter(x__contains=4)
        if lookup_type in ('iexact', 'contains', 'icontains', 'startswith',
                           'istartswith', 'endswith', 'iendswith', 'regex', 'iregex'):
            if internal_type in ('IPAddressField', 'GenericIPAddressField'):
                lookup = "HOST(%s)"
            else:
                lookup = "%s::text"

        return lookup


class DatabaseWrapper(base.DatabaseWrapper):
    """
        Override the defaults where needed to allow use of trigram index
    """
    ops_class = DatabaseOperations

    def __init__(self, *args, **kwargs):
        self.operators.update(
            'icontains': 'ILIKE %s',
            'istartswith': 'ILIKE %s',
            'iendswith': 'ILIKE %s',
        )
        self.pattern_ops.update()
        super(DatabaseWrapper, self).__init__(*args, **kwargs)

Encontré un artículo de 12/2020 que usa la versión más reciente de Django ORM como tal:

class Author(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)

    class Meta:
        indexes = [
            GinIndex(
                name='review_author_ln_gin_idx', 
                fields=['last_name'], 
                opclasses=['gin_trgm_ops'],
            )
        ]

Inspirado en un artículo antiguo sobre este tema, llegué a uno actual que ofrece la siguiente solución para un GistIndex:

Actualización: desde Django-1.11 las cosas parecen ser más simples, ya que esta respuesta y los documentos de django sugieren:

from django.contrib.postgres.indexes import GinIndex

class MyModel(models.Model):
    the_field = models.CharField(max_length=512, db_index=True)

    class Meta:
        indexes = [GinIndex(fields=['the_field'])]

Desde Django-2.2, un attribute opclasses estará disponible en class Index(fields=(), name=None, db_tablespace=None, opclasses=()) para este propósito.


from django.contrib.postgres.indexes import GistIndex

class GistIndexTrgrmOps(GistIndex):
    def create_sql(self, model, schema_editor):
        # - this Statement is instantiated by the _create_index_sql()
        #   method of django.db.backends.base.schema.BaseDatabaseSchemaEditor.
        #   using sql_create_index template from
        #   django.db.backends.postgresql.schema.DatabaseSchemaEditor
        # - the template has original value:
        #   "CREATE INDEX %(name)s ON %(table)s%(using)s (%(columns)s)%(extra)s"
        statement = super().create_sql(model, schema_editor)
        # - however, we want to use a GIST index to accelerate trigram
        #   matching, so we want to add the gist_trgm_ops index operator
        #   class
        # - so we replace the template with:
        #   "CREATE INDEX %(name)s ON %(table)s%(using)s (%(columns)s gist_trgrm_ops)%(extra)s"
        statement.template =
            "CREATE INDEX %(name)s ON %(table)s%(using)s (%(columns)s gist_trgm_ops)%(extra)s"

        return statement

Que luego puede usar en su clase modelo de esta manera:

class YourModel(models.Model):
    some_field = models.TextField(...)

    class Meta:
        indexes = [
            GistIndexTrgrmOps(fields=['some_field'])
        ]

En caso de que alguien quiera tener un índice en varias columnas unidas (concatenadas) con espacio, puede usar mi modificación del índice incorporado.

Crea un índice como gin (("column1" || ' ' || "column2" || ' ' || ...) gin_trgm_ops)

class GinSpaceConcatIndex(GinIndex):

    def get_sql_create_template_values(self, model, schema_editor, using):

        fields = [model._meta.get_field(field_name) for field_name, order in self.fields_orders]
        tablespace_sql = schema_editor._get_index_tablespace_sql(model, fields)
        quote_name = schema_editor.quote_name
        columns = [
            ('%s %s' % (quote_name(field.column), order)).strip()
            for field, (field_name, order) in zip(fields, self.fields_orders)
        ]
        return 

Si conservas algún disgusto y capacidad de aclarar nuestro tutorial eres capaz de escribir un comentario y con mucho placer lo leeremos.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *