在ZomboDB的示例上自定义Django ORM

通常在使用DjangoPostgreSQL时,需要数据库的其他扩展。 而且,例如,如果使用hstore或PostGIS(由于使用GeoDjango),一切都非常方便,那么使用罕见的扩展(如pgRouting,ZomboDB等),则必须用RawSQL编写或自定义Django ORM。 我在本文中提出的建议是以ZomboDB为例,并为其入门教程 。 同时,让我们考虑如何将ZomboDB连接到Django项目。


PostgreSQL有自己的全文本搜索,并且根据最新的基准测试,它可以快速运行。 但是其搜索功能仍然有很多不足之处。 结果,如果没有基于Lucene的解决方案-例如ElasticSearch-将会很严格。 内部的ElasticSearch有其自己的数据库,该数据库进行搜索。 目前的主要解决方案是使用信号或手动回调函数手动控制PostgreSQL和ElasticSearch之间的数据一致性。


ZomboDB是实现其自己的索引类型的扩展,将表值转换为指向ElasticSearch的指针,该指针允许使用ElasticSearch DSL作为SQL语法的一部分进行全文本表搜索。


在撰写本文时,网络搜索未产生任何结果。 在Habré上有关ZomboDB的文章中只有一篇 。 没有关于集成ZomboDB和Django的文章。


ZomboDB描述说,对Elasticsearch的调用通过RESTful API进行,因此性能尚不确定,但是现在我们不再赘述。 还存在正确删除ZomboDB而不会丢失数据的问题。


接下来,我们将在Docker中进行所有测试,因此我们将收集一个小的docker-compose文件


docker-compose.yaml
version: '3' services: postgres: build: docker/postgres environment: - POSTGRES_USER=django - POSTGRES_PASSWORD=123456 - POSTGRES_DB=zombodb - PGDATA=/home/postgresql/data ports: - 5432:5432 # sudo sysctl -w vm.max_map_count=262144 elasticsearch: image: elasticsearch:6.5.4 environment: - cluster.name=zombodb - bootstrap.memory_lock=true - ES_JAVA_OPTS=-Xms512m -Xmx512m ulimits: memlock: soft: -1 hard: -1 ports: - 9200:9200 django: build: docker/python command: python3 manage.py runserver 0.0.0.0:8000 volumes: - ./:/home/ ports: - 8000:8000 depends_on: - postgres - elasticsearch 

ZomboDB的最新版本最多可与Postgres的第10版配合使用,并且需要依赖项进行卷曲(我想在ElasticSearch中进行查询)。


 FROM postgres:10 WORKDIR /home/ RUN apt-get -y update && apt-get -y install curl ADD https://www.zombodb.com/releases/v10-1.0.3/zombodb_jessie_pg10-10-1.0.3_amd64.deb ./ RUN dpkg -i zombodb_jessie_pg10-10-1.0.3_amd64.deb RUN rm zombodb_jessie_pg10-10-1.0.3_amd64.deb RUN apt-get -y clean 

Django的容器很典型。 我们只会在其中放入Django和psycopg2的最新版本。


 FROM python:stretch WORKDIR /home/ RUN pip3 install --no-cache-dir django psycopg2-binary 

Linux上的ElasticSearch并不是从vm.max_map_count的基本设置开始的,因此我们将不得不稍微增加一些设置(他们知道如何通过docker自动执行此操作-在注释中写)。


 sudo sysctl -w vm.max_map_count=262144 

因此,测试环境已准备就绪。 您可以转到Django上的项目。 我不会整体介绍它;希望的人可以在GitLab存储库中看到它。 我将只关注关键点。


我们需要做的第一件事是将ZomboDB作为PostgreSQL的扩展插入。 当然,您可以连接到数据库并通过SQL CREATE EXTENSION zombodb;启用扩展CREATE EXTENSION zombodb; 。 您甚至可以在官方Postgres容器中使用docker-entrypoint-initdb.d挂钩。 但是由于我们拥有Django,因此我们将按照他的方式行事。
创建项目并创建第一个迁移后,向其添加扩展连接。


 from django.db import migrations, models from django.contrib.postgres.operations import CreateExtension class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ CreateExtension('zombodb'), ] 

其次,我们需要一个描述测试模式的模型。 为此,我们需要一个与zdb.fulltext数据类型一起使用的字段。 好吧, 让我们自己编写 。 由于django的此数据类型的行为与本机的postgresql文本相同,因此在创建字段时,我们将从models.TextField继承我们的类。 此外,还需要做两件重要的事情:关闭在此字段上使用Btree索引的功能,并限制数据库的后端。 最终结果如下:


 class ZomboField(models.TextField): description = "Alias for Zombodb field" def __init__(self, *args, **kwargs): kwargs['db_index'] = False super().__init__(*args, **kwargs) def db_type(self, connection): databases = [ 'django.db.backends.postgresql_psycopg2', 'django.db.backends.postgis' ] if connection.settings_dict['ENGINE'] in databases: return 'zdb.fulltext' else: raise TypeError('This database not support') 

第三,向ZomboDB解释在哪里寻找我们的ElasticSearch。 在数据库本身中,为此目的使用了来自ZomboDB的自定义索引。 因此,如果地址更改,则必须更改索引。
Django根据app_model模式命名表:在我们的例子中,应用程序称为main,模型为article。 elasticsearch是docker通过容器名称分配的dns名称。
在SQL中,它看起来像这样:


 CREATE INDEX idx_main_article ON main_article USING zombodb ((main_article.*)) WITH (url='elasticsearch:9200/'); 

在Django中,我们还需要创建一个自定义索引。 那里的索引还不是很灵活:特别是zombodb索引不是指示特定的列,而是整个 。 在Django中,索引需要必填字段引用。 因此,我用((main_article.*))替换了statement.parts['columns'] ,但是构造和解构方法仍然需要您在创建字段时指定fields属性。 我们还需要将其他参数传递给params。 为什么要覆盖__init__get_with_paramsget_with_params
总的来说,设计是可行的。 应用迁移并取消迁移没有问题。


 class ZomboIndex(models.Index): def __init__(self, *, url=None, **kwargs): self.url = url super().__init__(**kwargs) def create_sql(self, model, schema_editor, using=''): statement = super().create_sql(model, schema_editor, using=' USING zombodb') statement.parts['columns'] = '(%s.*)' % model._meta.db_table with_params = self.get_with_params() if with_params: statement.parts['extra'] = " WITH (%s) %s" % ( ', '.join(with_params), statement.parts['extra'], ) print(statement) return statement def deconstruct(self): path, args, kwargs = super().deconstruct() if self.url is not None: kwargs['url'] = self.url return path, args, kwargs def get_with_params(self): with_params = [] if self.url: with_params.append("url='%s'" % self.url) return with_params 

那些不喜欢这种方法的人可以通过直接添加索引来使用从RunSQL进行的迁移。 只需跟踪表的名称并自己编制索引即可。


 migrations.RunSQL( sql = ( "CREATE INDEX idx_main_article " "ON main_article " "USING zombodb ((main_article.*)) " "WITH (url='elasticsearch:9200/');" ), reverse_sql='DROP INDEX idx_main_article' ) 

结果就是这样一个模型。 ZomboField接受与TextField相同的参数,但有一个例外-index_db不会产生任何影响,就像ZomboIndex中的fields属性一样。


 class Article(models.Model): text = ZomboField() class Meta: indexes = [ ZomboIndex(url='elasticsearch:9200/', name='zombo_idx', fields=['text']) ] 

最终,迁移文件应如下所示:


 from django.db import migrations, models from django.contrib.postgres.operations import CreateExtension import main.models class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ CreateExtension('zombodb'), migrations.CreateModel( name='Article', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('text', main.models.ZomboField()), ], ), migrations.AddIndex( model_name='article', index=main.models.ZomboIndex(fields=['text'], name='zombo_idx', url='elasticsearch:9200/'), ) ] 

对于那些感兴趣的人,我附上了产生Django ORM的SQL(您可以仔细检查sqlmigrate ,或者考虑到sqlmigratesudo docker-compose exec django python3 manage.py sqlmigrate main 0001


 BEGIN; -- -- Creates extension zombodb -- CREATE EXTENSION IF NOT EXISTS "zombodb"; -- -- Create model Article -- CREATE TABLE "main_article" ("id" serial NOT NULL PRIMARY KEY, "text" zdb.fulltext NOT NULL); -- -- Create index zombo_idx on field(s) text of model article -- CREATE INDEX "zombo_idx" ON "main_article" USING zombodb ((main_article.*)) WITH (url='elasticsearch:9200/') ; COMMIT; 

因此,我们有一个模型。 现在仍然需要通过过滤器进行搜索。 为此,请描述您的查找并进行注册。


 @ZomboField.register_lookup class ZomboSearch(models.Lookup): lookup_name = 'zombo_search' def as_sql(self, compiler, connection): lhs, lhs_params = self.process_lhs(compiler, connection) rhs, rhs_params = self.process_rhs(compiler, connection) params = lhs_params + rhs_params return "%s ==> %s" % (lhs.split('.')[0], rhs), params 

在这种情况下,搜索将如下所示:


 Article.objects.filter(text__zombo_search='(call OR box)') 

但是通常一次搜索是不够的。 它还要求对结果进行排名并突出显示找到的单词。
好吧,排名非常简单。 我们编写自己的函数


 from django.db.models import FloatField, Func class ZomboScore(Func): lookup_name = 'score' function = 'zdb.score' template = "%(function)s(ctid)" arity = 0 @property def output_field(self): return FloatField() 

现在,您可以构建非常复杂的查询,而不会出现任何问题。


 scores = (Article.objects .filter(text__zombo_search='delete') .annotate(score=ZomboScore()) .values_list(F('score')) .order_by('-score')) 

突出显示结果(突出显示)要复杂得多,效果并不理想。 Django psycopg2后端在任何情况下都会将_转换为_ 。 如果有text ,则将存在"main_article"."text" ,ZomboDB绝对不接受。 列名称必须仅是该列的文本名称。 但是在这里,RawSQL可以提供帮助。


 from django.db.models.expressions import RawSQL highlighted = (Article.objects .filter(text__zombo_search='delete') .values(highlight_text=RawSQL("zdb.highlight(ctid, %s)", ('text',)))) 

可以在存储库中查看带有测试的项目的完整版本。 本文中的所有示例均以测试形式写在此处。 我希望这篇文章对某人有用,并会鼓励您不要写信号单车,而是能够证明自己的所有一致性,并使用现成的解决方案而不会失去ORM的所有积极方面。 也欢迎添加和更正。


UPD: django-zombodb库已经出现

Source: https://habr.com/ru/post/zh-CN442512/


All Articles