Bei der Arbeit mit Django und PostgreSQL sind häufig zusätzliche Erweiterungen für die Datenbank erforderlich. Und wenn zum Beispiel mit hstore oder PostGIS (dank GeoDjango) alles recht praktisch ist, müssen Sie mit selteneren Erweiterungen - wie pgRouting, ZomboDB usw. - entweder in RawSQL schreiben oder Django ORM anpassen. Was ich in diesem Artikel vorschlage, ist, es am Beispiel von ZomboDB und dem Tutorial für den Einstieg zu tun. Lassen Sie uns gleichzeitig überlegen, wie Sie ZomboDB mit einem Django-Projekt verbinden können.
PostgreSQL verfügt über eine eigene Volltextsuche und funktioniert nach den neuesten Benchmarks recht schnell. Die Suchfunktionen lassen jedoch noch zu wünschen übrig. Ohne Lucene-basierte Lösungen - zum Beispiel ElasticSearch - ist dies daher eng. ElasticSearch verfügt über eine eigene Datenbank, die eine Suche durchführt. Die derzeitige Hauptlösung ist die manuelle Steuerung der Datenkonsistenz zwischen PostgreSQL und ElasticSearch mithilfe von Signalen oder manuellen Rückruffunktionen.
ZomboDB ist eine Erweiterung, die einen eigenen Indextyp implementiert und den Tabellenwert in einen Zeiger auf ElasticSearch verwandelt, der die Suche in Volltexttabellen mit ElasticSearch DSL als Teil der SQL-Syntax ermöglicht.
Zum Zeitpunkt des Schreibens ergab eine Netzwerksuche keine Ergebnisse. Von Artikeln über Habré über ZomboDB nur einer . Es gibt keine Artikel zur Integration von ZomboDB und Django.
Die ZomboDB-Beschreibung besagt, dass Aufrufe von Elasticsearch die RESTful-API durchlaufen, sodass die Leistung zweifelhaft ist, aber jetzt werden wir nicht darauf eingehen. Auch Probleme beim korrekten Entfernen von ZomboDB ohne Datenverlust.
Als nächstes werden wir alle Tests in Docker durchführen , um eine kleine Docker-Compose- Datei zu sammeln
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
Die neueste Version von ZomboDB funktioniert mit der maximal 10. Version von Postgres und erfordert Curl von Abhängigkeiten (ich nehme an, Abfragen in ElasticSearch durchzuführen).
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
Der Container für Django ist typisch. Darin werden nur die neuesten Versionen von Django und psycopg2 enthalten sein.
FROM python:stretch WORKDIR /home/ RUN pip3 install --no-cache-dir django psycopg2-binary
ElasticSearch unter Linux beginnt nicht mit den Grundeinstellungen von vm.max_map_count, daher müssen wir sie ein wenig erhöhen (wer weiß, wie man dies durch Docker automatisiert - schreiben Sie in die Kommentare).
sudo sysctl -w vm.max_map_count=262144
Die Testumgebung ist also bereit. Sie können auf Django zum Projekt gehen. Ich werde es nicht als Ganzes geben, diejenigen, die es wünschen, können es im Repository auf GitLab sehen . Ich werde nur auf kritische Punkte eingehen.
Als erstes müssen wir ZomboDB als Erweiterung in PostgreSQL einstecken. Sie können natürlich eine Verbindung zur Datenbank herstellen und die Erweiterung über SQL CREATE EXTENSION zombodb;
. Sie können hierfür sogar den Hook docker-entrypoint-initdb.d im offiziellen Postgres-Container verwenden. Aber da wir Django haben, werden wir seinen Weg gehen.
Fügen Sie nach dem Erstellen des Projekts und der ersten Migration eine Erweiterungsverbindung hinzu.
from django.db import migrations, models from django.contrib.postgres.operations import CreateExtension class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ CreateExtension('zombodb'), ]
Zweitens benötigen wir ein Modell, das das Testmuster beschreibt. Dazu benötigen wir ein Feld, das mit dem Datentyp zdb.fulltext funktioniert. Nun, lass uns deine eigenen schreiben . Da sich dieser Datentyp für django genauso verhält wie der native postgresql-Text, erben wir beim Erstellen unseres Felds unsere Klasse von models.TextField. Darüber hinaus müssen zwei wichtige Dinge getan werden: Deaktivieren Sie die Möglichkeit, den Btree-Index für dieses Feld zu verwenden, und beschränken Sie das Backend für die Datenbank. Das Endergebnis ist wie folgt:
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')
Drittens erklären Sie ZomboDB, wo Sie nach unserer ElasticSearch suchen müssen. In der Datenbank selbst wird zu diesem Zweck ein benutzerdefinierter Index aus ZomboDB verwendet. Wenn sich die Adresse ändert, muss daher der Index geändert werden.
Django benennt Tabellen nach dem Muster app_model: In unserem Fall heißt die Anwendung main und das Modell ist article. elasticsearch ist der DNS-Name, den Docker anhand des Containernamens zuweist.
In SQL sieht es so aus:
CREATE INDEX idx_main_article ON main_article USING zombodb ((main_article.*)) WITH (url='elasticsearch:9200/');
In Django müssen wir auch einen benutzerdefinierten Index erstellen. Die dortigen Indizes sind noch nicht sehr flexibel: Insbesondere zeigt der Zombodb-Index nicht eine bestimmte Spalte an, sondern die gesamte Tabelle . In Django erfordert ein Index eine obligatorische Feldreferenz. Daher habe ich statement.parts['columns']
durch ((main_article.*))
den Konstruktions- und Dekonstruktionsmethoden müssen Sie jedoch beim Erstellen des Felds das ((main_article.*))
angeben. Wir müssen auch einen zusätzlichen Parameter an params übergeben. Warum die get_with_params
__init__
, get_with_params
und get_with_params
überschreiben __init__
Im Allgemeinen erwies sich das Design als funktionierend. Migrationen werden problemlos angewendet und abgebrochen.
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
Diejenigen, die diesen Ansatz nicht mögen, können Migrationen von RunSQL verwenden, indem sie direkt einen Index hinzufügen. Sie müssen nur den Namen der Tabelle verfolgen und sich selbst indizieren.
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' )
Das Ergebnis ist ein solches Modell. ZomboField akzeptiert dieselben Argumente wie TextField, mit einer Ausnahme: index_db hat keine Auswirkungen, genau wie das Feldattribut in ZomboIndex.
class Article(models.Model): text = ZomboField() class Meta: indexes = [ ZomboIndex(url='elasticsearch:9200/', name='zombo_idx', fields=['text']) ]
Letztendlich sollte die Migrationsdatei folgendermaßen aussehen:
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/'), ) ]
Für Interessenten sqlmigrate
ich SQL bei, das Django ORM erzeugt (Sie können sqlmigrate
gut sqlmigrate
oder Docker berücksichtigen: sudo docker-compose exec django python3 manage.py sqlmigrate main 0001
)
BEGIN;
Wir haben also ein Modell. Es bleibt nun eine Suche durch Filter. Beschreiben Sie dazu Ihre Suche und registrieren Sie sie.
@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
Die Suche in diesem Fall sieht folgendermaßen aus:
Article.objects.filter(text__zombo_search='(call OR box)')
Aber normalerweise reicht eine Suche nicht aus. Außerdem muss das Ergebnis eingestuft und die gefundenen Wörter hervorgehoben werden.
Das Ranking ist ziemlich einfach. Wir schreiben unsere eigene Funktion :
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()
Jetzt können Sie problemlos recht komplexe Abfragen erstellen.
scores = (Article.objects .filter(text__zombo_search='delete') .annotate(score=ZomboScore()) .values_list(F('score')) .order_by('-score'))
Das Hervorheben des Ergebnisses (Hervorheben) erwies sich als etwas komplizierter, es funktionierte nicht besonders gut. Das Django psycopg2-Backend konvertiert in jeder Situation den _
in eine _
. _
. Wenn es text
, gibt es "main_article"."text"
, den ZomboDB kategorisch nicht akzeptiert. Die Angabe der Spalte sollte ausschließlich der Textname der Spalte sein. Aber hier kommt RawSQL zur Rettung.
from django.db.models.expressions import RawSQL highlighted = (Article.objects .filter(text__zombo_search='delete') .values(highlight_text=RawSQL("zdb.highlight(ctid, %s)", ('text',))))
Die Vollversion des Projekts mit Tests kann im Repository angezeigt werden. Alle Beispiele aus dem Artikel sind dort in Form von Tests geschrieben. Ich hoffe, dass dieser Artikel für jemanden nützlich ist und Sie dazu ermutigt, kein Fahrrad auf Signale zu schreiben, mit der Fähigkeit, sich selbst die ganze Konsistenz zu erschießen und eine fertige Lösung zu verwenden, ohne alle positiven Aspekte von ORM zu verlieren. Ergänzungen und Korrekturen sind ebenfalls willkommen.
UPD: Die Django-Zombodb- Bibliothek ist erschienen