Поиск по связанным таблицам через Elasticsearch

Всем привет, вопрос состоит в следующем, у меня есть три основных таблицы в postgre, я реализовал поиск по основной таблице Person, но я так же хочу искать и по связанной с этой таблицей Experience, прочел много в документации и много чего попробовал, но так и не получилось даже вывести объект Experience в объекте Person(А вот наоборот получилось, но так мне не подходит). Кто хорошо понимает, как работает Elasticsearch, подскажите, пожалуйста, как я бы мог это реализовать. То есть я хочу искать по полям keywords и area таблицы Experience, но не понимаю как.

Мой models.py

class Person(models.Model):
    """
    Our general model
    """
    person_hash_index = models.CharField(max_length=100, primary_key=True, null=False)
    first_name = models.CharField(max_length=150)
    surname = models.CharField(max_length=150)
    url = models.CharField(max_length=500)
    location = models.CharField(max_length=150, blank=True, db_index=True)  # country
    city = models.CharField(max_length=150, blank=True, null=True, db_index=True)
    last_position = models.CharField(max_length=150, blank=True, db_index=True)
    current_company = models.CharField(max_length=1500, blank=True, null=True, db_index=True)
    event = models.CharField(max_length=100, blank=True, db_index=True)
    event_date = models.CharField(max_length=100, blank=True)
    total_duration = models.FloatField(blank=True, default=0, null=True, db_index=True)
    updated = models.CharField(max_length=100, blank=True, null=True)

    def get_absolute_url(self):
        """
        Absolute url for our template
        :return: redirect to person_view page
        """
        return reverse('person_view', kwargs={'pk': self.pk})

    def __str__(self):
        return self.first_name


class Education(models.Model):
    """
    Person education table
    This table are inherited from Person table
    """
    person = models.ForeignKey('Person', on_delete=models.CASCADE, related_name='person_education')
    edu_id = models.CharField(max_length=100, primary_key=True, null=False)
    university = models.CharField(max_length=500)
    degree = models.CharField(max_length=500)
    info = models.CharField(max_length=65000, blank=True)
    admission_date = models.CharField(max_length=500)
    graduation_date = models.CharField(max_length=500)

    def __str__(self):
        return '%s, %s, %s, %s, %s' % (
            self.university, self.degree, self.info, self.admission_date, self.graduation_date
        )


class Experience(models.Model):
    """
    Person Experience table
    This table are inherited from Person table
    """
    exp_id = models.CharField(max_length=100, primary_key=True, null=False)
    person = models.ManyToManyField('Person', through='KeyRelationExp', related_name='person_experience')
    person_relation = models.ForeignKey('Person',
                                        on_delete=models.CASCADE,
                                        blank=True, null=True,
                                        related_name='person_relation_experience')
    company = models.CharField(max_length=150)
    position = models.CharField(max_length=150)
    hiring_date = models.CharField(max_length=150)
    fired_date = models.CharField(max_length=150)
    job_location = models.CharField(max_length=150)
    description = models.CharField(max_length=65535, blank=True)
    duration = models.FloatField()  # mb datetime
    exp_index = models.CharField(max_length=50, blank=True)
    keywords = models.CharField(max_length=2000, blank=True, null=True)
    area = models.CharField(max_length=2000, blank=True, null=True)

    def __str__(self):
        return '%s, %s, %s, %s, %s, %s' % (
            self.company, self.position, self.hiring_date, self.fired_date, self.job_location, self.description
        )

    class Meta:
        ordering = ['exp_id']


class Comments(models.Model):
    """
    User comment
    This table are inherited from Person table
    """
    person = models.ForeignKey('Person', on_delete=models.CASCADE, blank=True, null=True,
                               related_name='person_comments')
    parent = models.ForeignKey('self', on_delete=models.SET_NULL, blank=True, null=True)
    name = models.CharField(max_length=50, blank=True, null=True)
    content = models.TextField()
    publish = models.DateTimeField(auto_now_add=True)
    status = models.BooleanField(default=True)

    class Meta:
        ordering = ['publish']

    def __str__(self):
        return f'Comment by {self.name}'


class Keyword(models.Model):
    keyword_hash = models.CharField(max_length=100, primary_key=True)
    person = models.ManyToManyField('Person',
                                    through='KeyRelationPerson',
                                    related_name='person_keyword')
    experience = models.ManyToManyField('Experience', through='KeyRelationExp', related_name='person_keyword_experience')
    keyword = models.CharField(max_length=2000, blank=True, null=True, db_index=True)

    def __str__(self):
        return self.keyword


class Area(models.Model):
    keyword_hash = models.CharField(max_length=100, primary_key=True)
    person = models.ManyToManyField('Person',
                                    through='KeyRelationPerson',
                                    related_name='person_area')
    keyword = models.CharField(max_length=2000, blank=True, null=True, db_index=True)

    def __str__(self):
        return self.keyword


class KeyRelationPerson(models.Model):
    person = models.ForeignKey('Person', on_delete=models.CASCADE, blank=True, null=True, related_name='person_key_area')
    keyword = models.ForeignKey('Keyword', on_delete=models.CASCADE, blank=True, null=True)
    area = models.ForeignKey('Area', on_delete=models.CASCADE, blank=True, null=True)
    tag_order = models.IntegerField(blank=True, null=True)


class KeyRelationExp(models.Model):
    person = models.ForeignKey('Person', on_delete=models.CASCADE, blank=True, null=True)
    exp = models.ForeignKey('Experience', on_delete=models.CASCADE, blank=True, null=True)
    keyword = models.ForeignKey('Keyword', on_delete=models.CASCADE, blank=True, null=True)
    tag_order = models.IntegerField(blank=True, null=True)

Serializers.py

from .models import Person, Experience, Education, KeyRelationPerson, Area, Keyword
from django_elasticsearch_dsl_drf.serializers import DocumentSerializer
from rest_framework import serializers
from .documents import PersonDocument


class KeywordSerializer(serializers.ModelSerializer):
    class Meta:
        model = Keyword
        fields = '__all__'


class AreaSerializer(serializers.ModelSerializer):
    class Meta:
        model = Area
        fields = '__all__'


class ExperienceSerializer(serializers.ModelSerializer):
    class Meta:
        model = Experience
        fields = (
            'exp_id',
            'company',
            'position',
            'hiring_date',
            'fired_date',
            'job_location',
            'description',
            'duration',
            'keywords',
            'area')


class EducationSerializer(serializers.ModelSerializer):
    class Meta:
        model = Education
        fields = '__all__'


class PersonSerializer(serializers.ModelSerializer):
    experiences_list = ExperienceSerializer(source='person_relation_experience', many=True, read_only=True)
    education_list = EducationSerializer(source='person_education', many=True, read_only=True)

    class Meta:
        model = Person
        fields = '__all__'


class PersonDocumentSerializer(DocumentSerializer):
    class Meta:

        document = PersonDocument
        fields = '__all__'

Documents.py

from django_elasticsearch_dsl.registries import registry

from django_elasticsearch_dsl import Document, Index, fields
from .models import Person, Education, Experience

# python manage.py search_index --rebuild


@registry.register_document
class PersonDocument(Document):
    first_name = fields.TextField(attr='first_name', fields={'suggest': fields.Completion()})
    experience = fields.NestedField(properties={
        'company': fields.TextField(),
        'position': fields.TextField(),
        'hiring_date': fields.TextField(),
        'fired_date': fields.TextField(),
        'job_location': fields.TextField(),
        'description': fields.TextField(),
        'duration': fields.FloatField(),
        'exp_index': fields.TextField(),
        'keywords': fields.TextField(),
        'area': fields.TextField()
    })
    education = fields.NestedField(properties={
        'university': fields.TextField(),
        'degree': fields.TextField(),
        'info': fields.TextField(),
        'admission_date': fields.TextField(),
        'graduation_date': fields.TextField()
    })

    class Index:
        name = 'persons'

    class Django:
        model = Person
        fields = [
            'person_hash_index',
            'surname',
            'url',
            'location',
            'city',
            'last_position',
            'current_company',
            'event',
            'event_date',
            'total_duration',
            'updated',
        ]
        

Views.py

from django.db.models import Q
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import *
from rest_framework import viewsets
from django_elasticsearch_dsl_drf.constants import (
    LOOKUP_QUERY_GTE,
    LOOKUP_QUERY_LTE,
    LOOKUP_QUERY_IN,
    LOOKUP_FILTER_RANGE,
    SUGGESTER_COMPLETION
)
from django_elasticsearch_dsl_drf.filter_backends import (
    FilteringFilterBackend,    
    SuggesterFilterBackend, 
    CompoundSearchFilterBackend
)
from django_elasticsearch_dsl_drf.viewsets import DocumentViewSet
from .models import *
from .documents import PersonDocument


class PersonViewSet(viewsets.ModelViewSet):
    serializer_class = PersonSerializer
    queryset = Person.objects.all()


class PersonSearch(APIView):
    serializer_class = PersonSerializer
    document_class = PersonDocument

    def generate_q_expression(self, query):
        return Q('match', name={'query': query})

    def get(self, request, query):
        q = self.generate_q_expression(query)
        search = self.document_class.search().query(q)
        return Response(self.serializer_class(search.to_queryset(), many=True).data)


class PersonDocumentViewSet(DocumentViewSet):
    document = PersonDocument
    serializer_class = PersonDocumentSerializer

    filter_backends = (FilteringFilterBackend,
                       CompoundSearchFilterBackend,
                       SuggesterFilterBackend)
   
    search_fields = ('first_name',)
    filter_fields = {
        'location': {
            'field': 'location',
            'lookups': [LOOKUP_QUERY_IN]  # ?location__in=russia, ?location__in=russia__germany
        },
        'city': {
            'field': 'city',
            'lookups': [LOOKUP_QUERY_IN]
        },
        'last_position': {
            'field': 'last_position',
            'lookups': [LOOKUP_QUERY_IN]
        },
        'current_company': {
            'field': 'current_company',
            'lookups': [LOOKUP_QUERY_IN]
        },
        'event': {
            'field': 'event',
            'lookups': [LOOKUP_QUERY_IN]
        },
        'total_duration': {
            'field': 'total_duration',
            'lookups': [LOOKUP_QUERY_GTE, LOOKUP_QUERY_LTE, LOOKUP_FILTER_RANGE]
            # total_duration_gte=5, total_duration_lte=5, total_duration_range=5__7
        },
        'hash': {
            'field': 'person_hash_index',
            'lookups': [LOOKUP_QUERY_IN]
        },
        'area': {
            'field': 'area',
            'lookups': [LOOKUP_QUERY_IN]
        },
        'keyword': {
            'field': 'keywords',
            'lookups': [LOOKUP_QUERY_IN]
        }

    }

    suggester_fields = {
        'first_name_suggest': {
            'field': 'first_name.suggest',
            'suggesters': [SUGGESTER_COMPLETION]
        }
    }

Вернуться на верх