Frequentemente, ao trabalhar com Django e PostgreSQL, são necessárias extensões adicionais para o banco de dados. E se, por exemplo, com hstore ou PostGIS (graças ao GeoDjango) tudo é bastante conveniente, com extensões mais raras - como pgRouting, ZomboDB, etc. - você precisa escrever no RawSQL ou personalizar o Django ORM. O que proponho neste artigo é fazê-lo usando o ZomboDB como exemplo e seu tutorial de introdução. E, ao mesmo tempo, vamos considerar como você pode conectar o ZomboDB a um projeto Django.
O PostgreSQL tem sua própria pesquisa em texto completo e funciona, a julgar pelos últimos benchmarks, muito rapidamente. Mas seus recursos em busca ainda deixam muito a desejar. Como resultado, sem as soluções baseadas em Lucene - ElasticSearch, por exemplo - é difícil. O ElasticSearch possui um banco de dados próprio, que realiza uma pesquisa. A principal solução no momento é o controle manual da consistência dos dados entre o PostgreSQL e o ElasticSearch usando sinais ou funções de retorno de chamada manual.
O ZomboDB é uma extensão que implementa seu próprio tipo de índice, transformando o valor da tabela em um ponteiro para o ElasticSearch , que permite pesquisas de tabela de texto completo usando o ElasticSearch DSL como parte da sintaxe SQL.
No momento da redação deste artigo, uma pesquisa na rede não produziu nenhum resultado. De artigos sobre Habré sobre o ZomboDB, apenas um . Não há artigos sobre a integração do ZomboDB e Django.
A descrição do ZomboDB diz que as chamadas para o Elasticsearch passam pela API RESTful; portanto, o desempenho está em dúvida, mas agora não vamos tocá-lo. Também problemas de remoção correta do ZomboDB sem perda de dados.
Em seguida, realizaremos todos os testes no Docker , para coletarmos um pequeno arquivo de composição do docker
docker-compose.yamlversion: '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
A versão mais recente do ZomboDB funciona com a 10ª versão máxima do Postgres e requer curl de dependências (suponho que você faça consultas no 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
O contêiner para o Django é típico. Nele, colocaremos apenas as versões mais recentes do Django e psycopg2.
FROM python:stretch WORKDIR /home/ RUN pip3 install --no-cache-dir django psycopg2-binary
O ElasticSearch no Linux não inicia com as configurações básicas de vm.max_map_count, portanto, teremos que aumentá-las um pouco (quem sabe como automatizar isso através do docker - escreva nos comentários).
sudo sysctl -w vm.max_map_count=262144
Portanto, o ambiente de teste está pronto. Você pode ir para o projeto no Django. Não vou dar como um todo; quem quiser pode vê-lo no repositório no GitLab . Vou me debruçar apenas sobre pontos críticos.
A primeira coisa que precisamos fazer é conectar o ZomboDB como uma extensão no PostgreSQL. Obviamente, você pode se conectar ao banco de dados e ativar a extensão através do SQL CREATE EXTENSION zombodb;
. Você pode até usar o docker-entrypoint-initdb.d hook no contêiner oficial do Postgres para isso. Mas como temos o Django, seguiremos o caminho dele.
Depois de criar o projeto e criar a primeira migração, adicione uma conexão de extensão a ele.
from django.db import migrations, models from django.contrib.postgres.operations import CreateExtension class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ CreateExtension('zombodb'), ]
Em segundo lugar, precisamos de um modelo que descreva o padrão de teste. Para fazer isso, precisamos de um campo que funcione com o tipo de dados zdb.fulltext. Bem, vamos escrever o seu . Como esse tipo de dado para django se comporta da mesma forma que o texto nativo do postgresql, quando criamos nosso campo, herdamos nossa classe de models.TextField. Além disso, duas coisas importantes precisam ser feitas: desativar a capacidade de usar o índice Btree nesse campo e restringir o back-end para o banco de dados. O resultado final é o seguinte:
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')
Terceiro, explique ao ZomboDB onde procurar nosso ElasticSearch. No próprio banco de dados, um índice personalizado do ZomboDB é usado para essa finalidade. Portanto, se o endereço for alterado, o índice deverá ser alterado.
O Django nomeia tabelas de acordo com o padrão app_model: no nosso caso, o aplicativo é chamado main e o modelo é article. elasticsearch é o nome do DNS que o docker atribui pelo nome do contêiner.
No SQL, fica assim:
CREATE INDEX idx_main_article ON main_article USING zombodb ((main_article.*)) WITH (url='elasticsearch:9200/');
No Django, também precisamos criar um índice personalizado. Os índices ainda não são muito flexíveis: em particular, o índice zombodb não indica uma coluna específica, mas a tabela inteira. No Django, um índice requer uma referência de campo obrigatória. Então substituí o statement.parts['columns']
por ((main_article.*))
, Mas os métodos de construção e desconstrução ainda exigem que você especifique o atributo de campos ao criar o campo. Também precisamos passar um parâmetro adicional para os parâmetros. Por que substituir o get_with_params
__init__
, deconstruct
e get_with_params
.
Em geral, o design acabou funcionando. As migrações são aplicadas e canceladas sem problemas.
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
Aqueles que não gostam dessa abordagem podem usar migrações do RunSQL adicionando diretamente um índice. Só precisa acompanhar o nome da tabela e indexar-se.
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' )
O resultado é esse modelo. O ZomboField aceita os mesmos argumentos que o TextField, com uma exceção - index_db não afeta nada, assim como o atributo de campos no ZomboIndex.
class Article(models.Model): text = ZomboField() class Meta: indexes = [ ZomboIndex(url='elasticsearch:9200/', name='zombo_idx', fields=['text']) ]
Por fim, o arquivo de migração deve ficar assim:
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/'), ) ]
Para os interessados, eu incluo o SQL que produz o Django ORM (você pode examinar o sqlmigrate
, bem, ou levando em consideração o docker: sudo docker-compose exec django python3 manage.py sqlmigrate main 0001
)
BEGIN;
Então, nós temos um modelo. Resta agora fazer uma pesquisa através do filtro. Para fazer isso, descreva sua pesquisa e registre-a.
@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
A pesquisa neste caso terá a seguinte aparência:
Article.objects.filter(text__zombo_search='(call OR box)')
Mas geralmente uma pesquisa não é suficiente. Também requer a classificação do resultado e o destaque das palavras encontradas.
Bem, o ranking é bem direto. Nós escrevemos nossa própria função :
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()
Agora você pode criar consultas bastante complexas sem problemas.
scores = (Article.objects .filter(text__zombo_search='delete') .annotate(score=ZomboScore()) .values_list(F('score')) .order_by('-score'))
Destacar o resultado (destaque) acabou sendo um pouco mais complicado, não deu certo. O back-end do Django psycopg2 em qualquer situação converte nome da _
em _
. Se houver text
, haverá "main_article"."text"
, que o ZomboDB categoricamente não aceita. A designação da coluna deve ser exclusivamente o nome textual da coluna. Mas aqui o RawSQL vem em socorro.
from django.db.models.expressions import RawSQL highlighted = (Article.objects .filter(text__zombo_search='delete') .values(highlight_text=RawSQL("zdb.highlight(ctid, %s)", ('text',))))
A versão completa do projeto com testes pode ser visualizada no repositório . Todos os exemplos do artigo são escritos lá na forma de testes. Espero que este artigo seja útil e incentive você a não escrever uma bicicleta em sinais, com a capacidade de capturar toda a consistência e usar uma solução pronta sem perder todos os aspectos positivos do ORM. Adições e correções também são bem-vindas.
UPD: A biblioteca django-zombodb apareceu