【问题标题】:Creating a Gin Index with Trigram (gin_trgm_ops) in Django model在 Django 模型中使用 Trigram (gin_trgm_ops) 创建 Gin 索引
【发布时间】:2017-12-02 20:56:07
【问题描述】:

django.contrib.postgres 的新 TrigramSimilarity 功能非常适合我遇到的问题。我将它用于搜索栏以查找难以拼写的拉丁名称。问题是有超过 200 万个名字,搜索时间比我想要的要长。

我想为 postgres documentation 中描述的三元组创建一个索引。

但我不确定如何以 Django API 使用它的方式执行此操作。对于 postgres 文本搜索,有关于如何创建索引的描述,但对于 trigram similarity 则没有。

这就是我现在拥有的:

class NCBI_names(models.Model):
    tax_id          =   models.ForeignKey(NCBI_nodes, on_delete=models.CASCADE, default = 0)
    name_txt        =   models.CharField(max_length=255, default = '')
    name_class      =   models.CharField(max_length=32, db_index=True, default = '')

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

在视图的get_queryset方法中:

class TaxonSearchListView(ListView):    
    #form_class=TaxonSearchForm
    template_name='collectie/taxon_list.html'
    paginate_by=20
    model=NCBI_names
    context_object_name = 'taxon_list'

    def dispatch(self, request, *args, **kwargs):
        query = request.GET.get('q')
        if query:
            try:
                tax_id = self.model.objects.get(name_txt__iexact=query).tax_id.tax_id
                return redirect('collectie:taxon_detail', tax_id)
            except (self.model.DoesNotExist, self.model.MultipleObjectsReturned) as e:
                return super(TaxonSearchListView, self).dispatch(request, *args, **kwargs)
        else:
            return super(TaxonSearchListView, self).dispatch(request, *args, **kwargs)
    
    def get_queryset(self):
        result = super(TaxonSearchListView, self).get_queryset()
        #
        query = self.request.GET.get('q')
        if query:            
            result = result.exclude(name_txt__icontains = 'sp.')
            result = result.annotate(similarity=TrigramSimilarity('name_txt', query)).filter(similarity__gt=0.3).order_by('-similarity')
        return result

【问题讨论】:

  • 在 postgresql 前端添加了带有选项的索引,似乎没有改变任何东西。是否与查询的方式有关?

标签: python django postgresql indexing similarity


【解决方案1】:

我找到了一个12/2020 article,它使用了最新版本的 Django ORM:

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'],
            )
        ]

如果像原始海报一样,您希望创建一个与 icontains 一起使用的索引,则必须为列的 UPPER() 建立索引,这需要从 OpClass 进行特殊处理:

from django.db.models.functions import Upper
from django.contrib.postgres.indexes import GinIndex, OpClass

class Author(models.Model):
        indexes = [
            GinIndex(
                OpClass(Upper('last_name'), name='gin_trgm_ops'),
                name='review_author_ln_gin_idx',
            )
        ]

受old article 对此主题的启发,我找到了current one,它为GistIndex 提供了以下解决方案:

更新: 从 Django-1.11 开始,事情似乎更简单了,this answer 和 django docs 建议:

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'])]

从Django-2.2 开始,opclasses 将为此目的在class Index(fields=(), name=None, db_tablespace=None, opclasses=()) 中提供一个属性。


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

然后你可以像这样在你的模型类中使用它:

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

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

【讨论】:

    【解决方案2】:

    要让 Django 2.2 使用icontains 和类似搜索的索引:

    子类 GinIndex 以创建不区分大小写的索引(大写所有字段值):

    from django.contrib.postgres.indexes import GinIndex
    
    class UpperGinIndex(GinIndex):
    
        def create_sql(self, model, schema_editor, using=''):
            statement = super().create_sql(model, schema_editor, using=using)
            quote_name = statement.parts['columns'].quote_name
    
            def upper_quoted(column):
                return f'UPPER({quote_name(column)})'
            statement.parts['columns'].quote_name = upper_quoted
            return statement
    

    像这样将索引添加到您的模型中,包括使用 opclasses 时需要的 kwarg name:

    class MyModel(Model):
        name = TextField(...)
    
        class Meta:
            indexes = [
                UpperGinIndex(fields=['name'], name='mymodel_name_gintrgm', opclasses=['gin_trgm_ops'])
            ]
    

    生成迁移并编辑生成的文件:

    # Generated by Django 2.2.3 on 2019-07-15 10:46
    from django.contrib.postgres.operations import TrigramExtension  # <<< add this
    from django.db import migrations
    import myapp.models
    
    
    class Migration(migrations.Migration):
    
        operations = [
            TrigramExtension(),   # <<< add this
            migrations.AddIndex(
                model_name='mymodel',
                index=myapp.models.UpperGinIndex(fields=['name'], name='mymodel_name_gintrgm', opclasses=['gin_trgm_ops']),
            ),
        ]
    

    【讨论】:

      【解决方案3】:

      这已经有了答案,但是在 Django 2.2 中你可以更容易地做到这一点:

      class MyModel(models.Model):
         name = models.TextField()
         class Meta:
             indexes = [GistIndex(name="gist_trgm_idx", fields=("name",), opclasses=("gist_trgm_ops",))]
      
      

      您也可以使用GinIndex。

      【讨论】:

      【解决方案4】:

      如果有人想在多个列上使用空间连接(连接)索引,您可以使用我对内置索引的修改。

      创建像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 {
                  'table': quote_name(model._meta.db_table),
                  'name': quote_name(self.name),
                  'columns': "({}) gin_trgm_ops".format(" || ' ' || ".join(columns)),
                  'using': using,
                  'extra': tablespace_sql,
              }
      

      【讨论】:

      • get_sql_create_template_values 在 Django 3 中不起作用。
      【解决方案5】:

      我遇到了类似的问题,尝试使用pg_tgrm 扩展来支持高效的contains 和icontains Django 字段查找。

      可能有更优雅的方式,但像这样定义新的索引类型对我有用:

      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,
              }
      

      get_sql_create_template_values 方法是从Index.get_sql_create_template_values() 复制而来,只做了一处修改:添加了+ ' gin_trgm_ops'。

      对于您的用例,您将使用 TrigramIndex 而不是 GinIndex 在 name_txt 上定义索引。然后运行makemigrations,这将产生一个生成所需CREATE INDEX SQL 的迁移。

      更新:

      我看到您也在使用icontains 进行查询:

      result.exclude(name_txt__icontains = 'sp.')
      

      Postgresql 后端会把它变成这样的:

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

      然后由于UPPER(),将不会使用三元组索引。

      我遇到了同样的问题,最终将数据库后端子类化以解决它:

      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({
                  'icontains': "ILIKE '%%' || {} || '%%'",
                  'istartswith': "ILIKE {} || '%%'",
                  'iendswith': "ILIKE '%%' || {}",
              })
              super(DatabaseWrapper, self).__init__(*args, **kwargs)
      

      【讨论】:

      • 您可以通过创建这样的 GIN 索引来避免使用数据库包装器:# CREATE INDEX blog_article_title_trigram ON blog_article USING gin (UPPER(title) gin_trgm_ops); 参见 gs-9.com/tuning-django-orm-text-queries
      • get_sql_create_template_values 在 Django 3 中不起作用。
      猜你喜欢
      • 2018-07-21
      • 1970-01-01
      • 2020-05-27
      • 2017-10-03
      • 1970-01-01
      • 1970-01-01
      • 2023-03-19
      • 1970-01-01
      • 2015-09-21
      相关资源
      最近更新 更多