A menudo, cuando se trabaja con Django y PostgreSQL, existe la necesidad de extensiones adicionales para la base de datos. Y si, por ejemplo, con hstore o PostGIS (gracias a GeoDjango) todo es bastante conveniente, entonces con extensiones más raras, como pgRouting, ZomboDB, etc., debe escribir en RawSQL o personalizar Django ORM. Lo que propongo en este artículo es hacerlo usando ZomboDB como ejemplo y su tutorial de inicio . Y al mismo tiempo, consideremos cómo puede conectar ZomboDB a un proyecto de Django.
PostgreSQL tiene su propia búsqueda de texto completo y funciona, a juzgar por los últimos puntos de referencia, bastante rápido. Pero sus capacidades de búsqueda aún dejan mucho que desear. Como resultado, sin soluciones basadas en Lucene, ElasticSearch, por ejemplo, es estricto. ElasticSearch tiene su propia base de datos, que realiza una búsqueda. La solución principal en este momento es el control manual de la consistencia de datos entre PostgreSQL y ElasticSearch mediante señales o funciones de devolución de llamada manual.
ZomboDB es una extensión que implementa su propio tipo de índice, convirtiendo el valor de la tabla en un puntero a ElasticSearch , que permite búsquedas de tablas de texto completo utilizando ElasticSearch DSL como parte de la sintaxis SQL.
Al momento de escribir, una búsqueda en la red no produjo ningún resultado. De los artículos sobre Habré sobre ZomboDB, solo uno . No hay artículos sobre la integración de ZomboDB y Django.
La descripción de ZomboDB dice que las llamadas a Elasticsearch pasan por la API RESTful, por lo que el rendimiento está en duda, pero ahora no lo abordaremos. También problemas de eliminación correcta de ZomboDB sin pérdida de datos.
A continuación, realizaremos todas las pruebas en Docker , por lo que recopilaremos un pequeño archivo docker-compose
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
La última versión de ZomboDB funciona con un máximo de la décima versión de Postgres y requiere curl de dependencias (supongo que para hacer consultas en 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
El contenedor para Django es típico. En él pondremos solo las últimas versiones de Django y psycopg2.
FROM python:stretch WORKDIR /home/ RUN pip3 install --no-cache-dir django psycopg2-binary
ElasticSearch en Linux no comienza con la configuración básica de vm.max_map_count, por lo que tendremos que aumentarlos un poco (quién sabe cómo automatizar esto a través de la ventana acoplable, escriba los comentarios).
sudo sysctl -w vm.max_map_count=262144
Entonces, el entorno de prueba está listo. Puedes ir al proyecto en Django. No lo daré como un todo; aquellos que lo deseen pueden verlo en el repositorio de GitLab . Me detendré solo en puntos críticos.
Lo primero que debemos hacer es conectar ZomboDB como una extensión en PostgreSQL. Por supuesto, puede conectarse a la base de datos y habilitar la extensión a través de SQL CREATE EXTENSION zombodb;
. Incluso puede usar el gancho docker-entrypoint-initdb.d en el contenedor oficial de Postgres para esto. Pero como tenemos Django, seguiremos su camino.
Después de crear el proyecto y crear la primera migración, agregue una conexión de extensión.
from django.db import migrations, models from django.contrib.postgres.operations import CreateExtension class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ CreateExtension('zombodb'), ]
En segundo lugar, necesitamos un modelo que describa el patrón de prueba. Para hacer esto, necesitamos un campo que funcione con el tipo de datos zdb.fulltext. Bueno, escribamos el tuyo . Dado que este tipo de datos para django se comporta igual que el texto postgresql nativo, cuando creamos nuestro campo, heredaremos nuestra clase de models.TextField. Además, se deben hacer dos cosas importantes: desactivar la capacidad de usar el índice Btree en este campo y restringir el back-end de la base de datos. El resultado final es el siguiente:
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')
Tercero, explique a ZomboDB dónde buscar nuestro ElasticSearch. En la base de datos, se utiliza un índice personalizado de ZomboDB para este propósito. Por lo tanto, si la dirección cambia, entonces el índice debe cambiarse.
Django nombra las tablas de acuerdo con el patrón app_model: en nuestro caso, la aplicación se llama main y el modelo es el artículo. elasticsearch es el nombre DNS que Docker asigna por nombre de contenedor.
En SQL, se ve así:
CREATE INDEX idx_main_article ON main_article USING zombodb ((main_article.*)) WITH (url='elasticsearch:9200/');
En Django, también necesitamos crear un índice personalizado. Los índices aún no son muy flexibles: en particular, el índice zombodb no indica una columna específica, sino toda la tabla . En Django, un índice requiere una referencia de campo obligatoria. Así que reemplacé la statement.parts['columns']
con ((main_article.*))
, Pero los métodos de construcción y deconstrucción aún requieren que especifique el atributo de campo al crear el campo. También necesitamos pasar un parámetro adicional a los params. ¿Por qué anular el __init__
, deconstruct
y get_with_params
?
En general, el diseño resultó estar funcionando. Las migraciones se aplican y cancelan sin 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
Aquellos a quienes no les gusta este enfoque pueden usar las migraciones desde RunSQL agregando directamente un índice. Solo tiene que hacer un seguimiento del nombre de la tabla e indexarse.
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' )
El resultado es tal modelo. ZomboField acepta los mismos argumentos que TextField, con una excepción: index_db no afecta nada, al igual que el atributo de campos en ZomboIndex.
class Article(models.Model): text = ZomboField() class Meta: indexes = [ ZomboIndex(url='elasticsearch:9200/', name='zombo_idx', fields=['text']) ]
En definitiva, el archivo de migración debería verse así:
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 los interesados, adjunto SQL que produce Django ORM (puede mirar sqlmigrate
, bien, o teniendo en cuenta docker: sudo docker-compose exec django python3 manage.py sqlmigrate main 0001
)
BEGIN;
Entonces, tenemos un modelo. Ahora queda hacer una búsqueda a través del filtro. Para hacer esto, describa su búsqueda y regístrela.
@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
La búsqueda en este caso se verá así:
Article.objects.filter(text__zombo_search='(call OR box)')
Pero generalmente una búsqueda no es suficiente. También requiere clasificar el resultado y resaltar las palabras encontradas.
Bueno, el ranking es bastante sencillo. Escribimos nuestra propia función :
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()
Ahora puede crear consultas bastante complejas sin ningún problema.
scores = (Article.objects .filter(text__zombo_search='delete') .annotate(score=ZomboScore()) .values_list(F('score')) .order_by('-score'))
Resaltar el resultado (resaltado) resultó ser algo más complicado, no funcionó a la perfección. El backend de Django psycopg2 en cualquier situación convierte _
a _
. _
. Si hubo text
, habrá "main_article"."text"
, que ZomboDB categóricamente no acepta. La indicación de la columna debe ser exclusivamente el nombre textual de la columna. Pero aquí RawSQL viene al rescate.
from django.db.models.expressions import RawSQL highlighted = (Article.objects .filter(text__zombo_search='delete') .values(highlight_text=RawSQL("zdb.highlight(ctid, %s)", ('text',))))
La versión completa del proyecto con pruebas se puede ver en el repositorio . Todos los ejemplos del artículo están escritos allí en forma de pruebas. Espero para alguien que este artículo sea útil y lo aliente a no escribir una bicicleta con señales, con la capacidad de dispararse a sí mismo con toda la consistencia, y usar una solución preparada sin perder todos los aspectos positivos de ORM. Adiciones y correcciones también son bienvenidas.
UPD: ha aparecido la biblioteca django-zombodb