Kustomisasi Django ORM pada contoh ZomboDB

Seringkali ketika bekerja dengan Django dan PostgreSQL, ada kebutuhan untuk ekstensi tambahan untuk database. Dan jika misalnya dengan hstore atau PostGIS (terima kasih kepada GeoDjango) semuanya cukup nyaman, kemudian dengan ekstensi yang lebih jarang - seperti pgRouting, ZomboDB, dll. - Anda harus menulis di RawSQL atau menyesuaikan Django ORM. Apa yang saya usulkan dalam artikel ini adalah melakukannya menggunakan ZomboDB sebagai contoh dan tutorial untuk memulai . Dan pada saat yang sama, mari kita pertimbangkan bagaimana Anda dapat menghubungkan ZomboDB ke proyek Django.


PostgreSQL memiliki pencarian teks lengkapnya sendiri dan berfungsi, dilihat dari tolok ukur terbaru, cukup cepat. Tetapi kemampuannya dalam pencarian masih menyisakan banyak yang diinginkan. Akibatnya, tanpa solusi berbasis Lucene - ElasticSearch, misalnya - ketat. ElasticSearch di dalamnya memiliki database sendiri, yang melakukan pencarian. Solusi utama saat ini adalah kontrol manual konsistensi data antara PostgreSQL dan ElasticSearch menggunakan sinyal atau fungsi panggilan balik manual.


ZomboDB adalah ekstensi yang mengimplementasikan jenis indeksnya sendiri, mengubah nilai tabel menjadi pointer ke ElasticSearch , yang memungkinkan pencarian tabel teks lengkap menggunakan ElasticSearch DSL sebagai bagian dari sintaks SQL.


Pada saat penulisan, pencarian jaringan tidak membuahkan hasil. Artikel tentang Habré tentang ZomboDB hanya satu . Tidak ada artikel tentang pengintegrasian ZomboDB dan Django.


Deskripsi ZomboDB mengatakan bahwa panggilan ke Elasticsearch melalui RESTful API, sehingga kinerjanya diragukan, tetapi sekarang kami tidak akan menyentuhnya. Juga masalah penghapusan ZomboDB yang benar tanpa kehilangan data.


Selanjutnya, kami akan melakukan semua tes di Docker , jadi kami akan mengumpulkan file penulisan docker kecil


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 

Versi terbaru ZomboDB berfungsi dengan Postgres versi ke-10 maksimum dan membutuhkan curl dari dependensi (saya kira membuat pertanyaan di 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 

Wadah untuk Django adalah khas. Di dalamnya kita hanya akan memasukkan versi terbaru Django dan psycopg2.


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

ElasticSearch di Linux tidak dimulai dengan pengaturan dasar vm.max_map_count, jadi kita harus menambahkannya sedikit (siapa yang tahu bagaimana mengotomatisasi ini melalui buruh pelabuhan - tulis di komentar).


 sudo sysctl -w vm.max_map_count=262144 

Jadi, lingkungan pengujian siap. Anda dapat pergi ke proyek di Django. Saya tidak akan memberikannya secara keseluruhan, mereka yang ingin dapat melihatnya di repositori di GitLab . Saya hanya akan membahas poin-poin kritis.


Hal pertama yang perlu kita lakukan adalah memasukkan ZomboDB sebagai ekstensi di PostgreSQL. Anda tentu saja dapat terhubung ke database dan mengaktifkan ekstensi melalui SQL CREATE EXTENSION zombodb; . Anda bahkan dapat menggunakan kait docker-entrypoint-initdb.d dalam wadah Postgres resmi untuk ini. Tetapi karena kita memiliki Django, maka kita akan pergi jalannya.
Setelah membuat proyek dan membuat migrasi pertama, tambahkan koneksi ekstensi ke sana.


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

Kedua, kita membutuhkan model yang akan menggambarkan pola tes. Untuk melakukan ini, kita membutuhkan bidang yang berfungsi dengan tipe data zdb.fulltext. Baiklah, mari kita tulis sendiri . Karena tipe data ini untuk Django berperilaku sama dengan teks postgresql asli, ketika kita membuat bidang kita, kita akan mewarisi kelas kita dari models.TextField. Selain itu, dua hal penting yang perlu dilakukan: matikan kemampuan untuk menggunakan indeks Btree pada bidang ini dan batasi backend untuk database. Hasil akhirnya adalah sebagai berikut:


 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') 

Ketiga, jelaskan kepada ZomboDB di mana mencari ElasticSearch kami. Dalam database itu sendiri, indeks khusus dari ZomboDB digunakan untuk tujuan ini. Karena itu, jika alamatnya berubah, maka indeksnya harus diubah.
Django menamai tabel sesuai dengan pola app_model: dalam kasus kami, aplikasi disebut main, dan modelnya adalah artikel. elasticsearch adalah nama dns yang diberikan oleh buruh pelabuhan berdasarkan nama wadah.
Dalam SQL, tampilannya seperti ini:


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

Di Django, kita juga perlu membuat indeks khusus. Indeks di sana belum terlalu fleksibel: khususnya, indeks zombodb tidak menunjukkan kolom tertentu, tetapi seluruh tabel . Di Django, indeks membutuhkan referensi bidang wajib. Jadi saya mengganti statement.parts['columns'] dengan ((main_article.*)) , Tetapi metode membangun dan mendekonstruksi masih mengharuskan Anda untuk menentukan atribut bidang saat membuat bidang. Kita juga perlu memberikan parameter tambahan ke params. Mengapa mengganti metode __init__ , deconstruct , dan get_with_params .
Secara umum, desainnya ternyata bisa berfungsi. Migrasi diterapkan dan dibatalkan tanpa masalah.


 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 

Mereka yang tidak menyukai pendekatan ini dapat menggunakan migrasi dari RunSQL dengan langsung menambahkan indeks. Hanya perlu melacak nama tabel dan indeks sendiri.


 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' ) 

Hasilnya adalah model seperti itu. ZomboField menerima argumen yang sama dengan TextField, dengan satu pengecualian - index_db tidak memengaruhi apa pun, seperti halnya atribut fields di ZomboIndex.


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

Pada akhirnya, file migrasi akan terlihat seperti ini:


 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/'), ) ] 

Bagi yang berminat, saya lampirkan SQL yang menghasilkan Django ORM (Anda dapat melihat melalui sqlmigrate , baik, atau dengan mempertimbangkan docker akun: sudo 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; 

Jadi, kami punya model. Sekarang tinggal melakukan pencarian melalui filter. Untuk melakukan ini, jelaskan pencarian Anda dan daftarkan.


 @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 

Pencarian dalam hal ini akan terlihat seperti ini:


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

Tetapi biasanya satu pencarian saja tidak cukup. Itu juga membutuhkan pemeringkatan hasil dan menyoroti kata-kata yang ditemukan.
Nah, peringkatnya cukup mudah. Kami menulis fungsi kami sendiri:


 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() 

Sekarang Anda dapat membangun kueri yang sangat kompleks tanpa masalah.


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

Menyorot hasil (highlight) ternyata agak lebih rumit, itu tidak berhasil dengan indah. Backend psycopg2 Django dalam situasi apa pun mengubah _ menjadi _ . Jika ada text , maka akan ada "main_article"."text" , yang ZomboDB tidak terima. Penunjukan kolom harus berupa nama kolom tekstual. Tapi di sini RawSQL datang untuk menyelamatkan.


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

Versi lengkap proyek dengan tes dapat dilihat di repositori . Semua contoh dari artikel ditulis di sana dalam bentuk tes. Saya harap seseorang artikel ini akan berguna dan akan mendorong Anda untuk tidak menulis sepeda pada sinyal, dengan kemampuan untuk menembak diri sendiri semua konsistensi, dan menggunakan solusi siap pakai tanpa kehilangan semua aspek positif dari ORM. Penambahan dan koreksi juga diterima.


UPD: Perpustakaan django-zombodb telah muncul

Source: https://habr.com/ru/post/id442512/


All Articles