Django 和 PostgreSQL, 从 SQL 的 LIKE 到全文搜索(Full-Text-Search) (2)

在我们的博客中, 记录了我们在开发过程中所使用的技术和遇到的问题, 希望作为其他开发和设计者的一个学习交流平台.

Django 和 PostgreSQL, 从 SQL 的 LIKE 到全文搜索(Full-Text-Search) (2)


在上一篇中, 我们解决了明确搜索的问题, 这一篇中我们说说口音或相近语的问题.

在使用全文搜索是我们会发现, 使用多种语言搜索document是常有的事情. 我们可以不设置语言而是用to_tsquery, 但是在运行的过程中, 全文搜素总是会自动使用至少一个.

默认的语言设置时英语, 但你必须根据你document的语言使用正确的stemmer, 否则就无法找到匹配.

例如, 我们在西班牙语的document中搜索física, 能得到精确地匹配:

    => SELECT text FROM terms WHERE to_tsvector(text) @@ to_tsquery('física');
                                      text
    -------------------------------------------------------------------------
     física (aparatos e instrumentos de —)
     física (educación —)
     física (investigación en —)
     rehabilitación física (aparatos de —) para uso médico
     educación física
     conversión de datos y programas informáticos, excepto conversión física
     investigación en física
     terapia física
    (8 rows)

但如果搜索fisica, 不带口音设置, 则无法得到任何结果:

    => SELECT text FROM terms WHERE to_tsvector(text) @@ to_tsquery('fisica');
     text
    ------
    (0 rows)

为了能在结果中显示física和其变体(físicas, físico, físicamente, 等), 我们必须使用正确的stemmer. 如果stemmer中没有这个词, 那么我们也无法获得正确的结果:

    => SELECT ts_lexize('english_stem', 'programming');
     ts_lexize
    -----------
     {program}
    (1 row)

    => SELECT ts_lexize('spanish_stem', 'programming');
       ts_lexize
    ---------------
     {programming}
    (1 row)

但当使用正确的语言设置时:

    => SELECT text FROM terms WHERE to_tsvector('spanish', text) @@ to_tsquery('spanish', 'física');
                                          text
    --------------------------------------------------------------------------------
     física (aparatos e instrumentos de —)
     ejercicios físicos (aparatos para —)
     entrenamiento físico (aparatos de —)
     físicos (aparatos para ejercicios —)
     física (educación —)
     preparador físico personal [mantenimiento físico] (servicios de —)
     física (investigación en —)
     ejercicio físico (aparatos de —) para uso médico
     rehabilitación física (aparatos de —) para uso médico
     aparatos para ejercicios físicos
     almacenamiento de soportes físicos de datos o documentos electrónicos
     clases de mantenimiento físico
     clubes deportivos [entrenamiento y mantenimiento físico]
     educación física
     conversión de datos o documentos de un soporte físico a un soporte electrónico
     conversión de datos y programas informáticos, excepto conversión física
     investigación en física
     terapia física
    (18 rows)

当然, 我们不需要每次都手动设置语言. 最直接的解决方法就是将语言存在数据库中. 我们可以使用语言字符来设置to_tsvector的语言, 也可以选择数据表中的一栏来设置:

    => SELECT text FROM term WHERE to_tsvector(language, text) @@ to_tsquery(language, 'entrena');
                               text
    ----------------------------------------------------------
     entrenamiento físico (aparatos de —)
     clubes deportivos [entrenamiento y mantenimiento físico]
    (2 rows)

唯一需要注意的是, 这一栏必须是regconfig. 如果你使用South或Django migrations来管理数据库, 那么一定要修改语言栏的类型:

    => ALTER TABLE terms ALTER COLUMN language TYPE regconfig USING language::regconfig;

这样一来, 我们便可以使用记录自身的语言来完成查询了.

那么如何设置口音类似搜索呢? 例如我们还是搜索fisica, 即使使用了以上提到的正确的语言设置, 我们还是无法搜索到任何东西:

    => SELECT text FROM terms WHERE to_tsvector(text) @@ to_tsquery('fisica');
     text
    ------
    (0 rows)

我们发现有时能搜到结果, 有时却不能, 这应该和选用的dictionary完整性有关. 但我们不能完全依赖dictionary的完整性来解决这一问题. 不用担心, 因为我们可以使用PostgreSQL的一个扩展来完成这一功能:

    CREATE EXTENSION unaccent;

unaccent扩展可以被用做扩展默认语言设置, 删选出口音相似词:

    CREATE TEXT SEARCH CONFIGURATION sp ( COPY = spanish );
    ALTER TEXT SEARCH CONFIGURATION sp ALTER MAPPING FOR hword, hword_part, word WITH unaccent, spanish_stem;

然后:

    => SELECT text FROM terms WHERE to_tsvector('sp', text) @@ to_tsquery('sp', 'fisica');
                                          text
    --------------------------------------------------------------------------------
     física (aparatos e instrumentos de —)
     ejercicios físicos (aparatos para —)
     entrenamiento físico (aparatos de —)
     físicos (aparatos para ejercicios —)
     física (educación —)
     preparador físico personal [mantenimiento físico] (servicios de —)
     física (investigación en —)
     ejercicio físico (aparatos de —) para uso médico
     rehabilitación física (aparatos de —) para uso médico
     aparatos para ejercicios físicos
     almacenamiento de soportes físicos de datos o documentos electrónicos
     clases de mantenimiento físico
     clubes deportivos [entrenamiento y mantenimiento físico]
     educación física
     conversión de datos o documentos de un soporte físico a un soporte electrónico
     conversión de datos y programas informáticos, excepto conversión física
     investigación en física
     terapia física
    (18 rows)

注意, 你必须是superuser才能安装这一扩展, 如果PostgreSQL提示无法找到这一扩展的话, 需要在系统中安装postgresql-contrib包.

为了使全文搜索在大数据库中运行流畅, 我们可以考虑检录GIN index:

    CREATE INDEX terms_idx ON terms USING gin(to_tsvector(language, text));

GIN index可能比GiST index的建立需要花更多的时间, 但对于之后的搜索会更快.

最后, 我们想要在django中使用的话, 需要写一个自定义的queryset并使用django-model-utils将其置于model 的manager中:

    from django.db import models
    from model_utils.managers import PassThroughManager


    class TermSearchQuerySet(models.query.QuerySet):
        def search(self, query, raw=False):
            function = "to_tsquery" if raw else "plainto_tsquery"
            search_vector = "language, text"
            ts_query = "%s(language, '%s')" % (function, query)
            where = "to_tsvector(%s) @@ %s" % (search_vector, ts_query)
            return self.extra(where=[where])


    class Term(models.Model):
        text = models.CharField(_('text'), max_length=255)
        language = models.CharField(max_length=20, default='sp', editable=False)

        objects = PassThroughManager.for_queryset_class(TermSearchQuerySet)()

还记得第一篇中的例子吗:

    >>> Entry.objects.search('man is biting a tail')
    [<Entry: Man Bites Dogs Tails>]

原文链接: http://weiguda.com/blog/64/