Поиск по связанным таблицам через 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]
}
}