تخصيص Django ORM على سبيل المثال من ZomboDB

غالبًا عند العمل مع Django و PostgreSQL ، هناك حاجة إلى إضافات إضافية لقاعدة البيانات. وإذا كان على سبيل المثال مع hstore أو PostGIS (بفضل GeoDjango) ، فكل شيء مناسب تمامًا ، ثم مع امتدادات أكثر ندرة - مثل pgRouting أو ZomboDB ، وما إلى ذلك - عليك إما الكتابة باستخدام RawSQL أو تخصيص Django ORM. ما أقترحه في هذه المقالة هو أن تفعل ذلك باستخدام ZomboDB كمثال والبرنامج التعليمي للبدء . وفي الوقت نفسه ، دعونا نفكر في كيفية توصيل ZomboDB بمشروع Django.


لدى PostgreSQL البحث عن النص الكامل الخاص به ، وهو يعمل ، وفقًا لأحدث المعايير ، بسرعة كبيرة. لكن قدراتها في البحث لا تزال تترك الكثير مما هو مرغوب فيه. نتيجة لذلك ، بدون الحلول المستندة إلى لوسين - تطبيق البحث المرن ، على سبيل المثال - يكون ضيقًا. ElasticSearch الداخل لديه قاعدة بيانات خاصة به ، والتي تجري عملية بحث. الحل الرئيسي في الوقت الحالي هو التحكم اليدوي في تناسق البيانات بين PostgreSQL و ElasticSearch باستخدام الإشارات أو وظائف رد الاتصال اليدوي.


ZomboDB هو امتداد يطبق نوع الفهرس الخاص به ، حيث يحول قيمة الجدول إلى مؤشر لـ ElasticSearch ، والذي يسمح بالبحث في جدول النص الكامل باستخدام ElasticSearch DSL كجزء من بناء جملة SQL.


في وقت كتابة هذا التقرير ، لم ينتج عن شبكة البحث أي نتائج. من المقالات حول Habré حول ZomboDB واحد فقط. لا توجد مقالات عن دمج ZomboDB و Django.


يقول وصف ZomboDB أن المكالمات إلى Elasticsearch تمر عبر واجهة برمجة تطبيقات RESTful ، لذلك فإن الأداء موضع شك ، لكننا الآن لن نتطرق إليه. أيضا قضايا الإزالة الصحيحة لل ZomboDB دون فقدان البيانات.


بعد ذلك ، سنجري جميع الاختبارات في Docker ، لذلك سنقوم بجمع ملف صغير لرسو السفن


عامل ميناء يؤلف
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 ويتطلب حليقة من التبعيات (أفترض إجراء استعلامات في 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 بالإعدادات الأساسية لـ vm.max_map_count ، لذلك سيتعين علينا زيادتها قليلاً (من يعرف كيفية أتمتة ذلك من خلال عامل ميناء - اكتب في التعليقات).


 sudo sysctl -w vm.max_map_count=262144 

لذلك ، بيئة الاختبار جاهزة. يمكنك الذهاب إلى المشروع في جانغو. لن أعطيها ككل ؛ يمكن لأولئك الذين يرغبون في رؤيتها في مستودع GitLab . سأتناول فقط النقاط الحرجة.


أول ما نحتاج إلى القيام به هو سد ZomboDB كملحق في PostgreSQL. يمكنك بالطبع الاتصال بقاعدة البيانات وتمكين الامتداد من خلال SQL CREATE EXTENSION zombodb; . يمكنك أيضًا استخدام ربط docker-entrypoint-initdb.d في حاوية Postgres الرسمية لهذا الغرض. ولكن بما أن لدينا جانغو ، فسوف نمضي في طريقه.
بعد إنشاء المشروع وإنشاء الترحيل الأول ، أضف اتصال ملحق به.


 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 الأصلي ، فعندما ننشئ حقلنا ، سنرث فصلنا من الموديلات. 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 مكان البحث عن تطبيق البحث المرن. في قاعدة البيانات نفسها ، يتم استخدام فهرس مخصص من ZomboDB لهذا الغرض. لذلك ، إذا تغير العنوان ، فيجب تغيير الفهرس.
يسمي Django الجداول وفقًا لنمط app_model: في حالتنا ، يُطلق على التطبيق الرئيسي ، والنموذج مقال. elasticsearch هو اسم نظام أسماء النطاقات الذي يقوم عامل النقل بتعيينه بواسطة اسم الحاوية.
في SQL ، يبدو كما يلي:


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

في جانغو ، نحتاج أيضًا إلى إنشاء فهرس مخصص. فهارس هناك ليست مرنة للغاية حتى الآن: على وجه الخصوص ، مؤشر zombodb لا يشير إلى عمود معين ، ولكن الجدول بأكمله. في جانغو ، يحتاج الفهرس إلى مرجع ميداني إلزامي. لذا استبدلت statement.parts['columns'] بـ ((main_article.*)) ، ولكن أساليب الإنشاء والتفكيك لا تزال تتطلب منك تحديد سمة الحقول عند إنشاء الحقل. نحتاج أيضًا إلى تمرير معلمة إضافية إلى params. لماذا تتخطى طريقة __init__ ، deconstruct get_with_params و get_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.


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

للمهتمين ، أرفق SQL التي تنتج Django ORM (يمكنك البحث من خلال sqlmigrate ، جيدًا ، أو مع مراعاة عامل sudo docker-compose exec django python3 manage.py sqlmigrate main 0001 : 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; 

لذلك ، لدينا نموذج. يبقى الآن القيام بالبحث من خلال التصفية. للقيام بذلك ، صف البحث الخاص بك وقم بتسجيله.


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

تبين أن إبراز النتيجة (إبرازها) أكثر تعقيدًا إلى حد ما ، لم ينجح ذلك بشكل جيد. جانغو 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. الإضافات والتصحيحات هي أيضا موضع ترحيب.


محدث: ظهرت مكتبة django-zombodb

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


All Articles