How to join two Document objects in elasticsearch-dsl? - python

I am attempting to create a "relationship" between two indexed Documents using elasticsearch-dsl. When using the Object(EsPerson) as a field of EsComment. When I update EsPerson, the field in EsComment does not update.
I have tried using InnerDoc, but that is not indexed and also does not update
class EsPersonAttr(InnerDoc):
id = Long(required=True)
name = Text(fields={'keyword': Keyword()}, required=True)
def __repr__(self):
return '<EsPersonAttr: {}>'.format(
self.name,
)
class EsPersonIndex(Document):
"""
Elastic Search Person model.
"""
class Index:
name = 'es-person'
class meta:
doc_type = 'es-person'
id = Long(required=True)
name = Text(fields={'keyword': Keyword()}, required=True)
def save(self, **kwargs):
return super(EsPersonIndex, self).save(**kwargs)
def __repr__(self):
return '<EsPersonIndex: {}>'.format(
self.name,
)
class EsPerson(object):
def __init__(self, id, name):
self._id = id
self._name = name
self.index_doc = EsPersonIndex(
id=id,
name=name
)
self.attr_doc = EsPersonAttr(
id=id,
name=name
)
def __repr__(self):
return '<EsPerson: {}>'.format(
self._name,
)
#property
def id(self):
return self._id
#id.setter
# Set both Document & InnerDoc at the same time
def id(self, value):
self._id = value
# self.index_doc.id = value
self.index_doc.update()
self.attr_doc.id = value
#property
def name(self):
return self._id
#name.setter
# Set both Document & InnerDoc at the same time
def name(self, value):
self._name = value
self.index_doc.name = value
self.index_doc.save()
self.attr_doc.name = value
class EsComment(Document):
"""
Elastic Search Comment model.
"""
id = Long(required=True)
title = Text(fields={'keyword': Keyword()}, required=True)
text = Text(fields={'keyword': Keyword()})
author = Object(EsPersonAttr, required=True)
class Index:
name = 'es-comment'
class meta:
doc_type = 'es-comment'
def save(self, **kwargs):
# if there is no date, use now
return super(EsComment, self).save(**kwargs)
def __repr__(self):
return '<EsComment: {}>'.format(
self.title,
)
I expected that when I updated the name field for a EsPerson, it updates author.name in EsComment

You might want to have a look at the example using parent/child and nested which ar the two methods of joining objects in Elasticsearch: https://github.com/elastic/elasticsearch-dsl-py/blob/master/examples/parent_child.py

Related

Flask Admin and SQLAlchemy relationship advice, how do I get columns from a grandparent table?

Using Flask Admin, I'm having difficulties understanding how to retrieve columns from parent and grandparent tables to use for filtering / create dropdowns
class Project(db.Model):
project_id=db.Column(db.Integer, primary_key=True)
title=db.Column(db.String(100), nullable=False)
def __repr__(self):
return self.title
def __str__(self):
return self.title
def get_id(self):
return self.project_id
class Story(db.Model):
story_id=db.Column(db.Integer, primary_key=True)
description=db.Column(db.String(), nullable=False)
project_id=db.Column(db.Integer, db.ForeignKey(Project.project_id))
project=db.relationship(Project)
def __repr__(self):
return self.description
def __str__(self):
return self.description
def get_id(self):
return self.story_id
class Task(db.Model):
task_id=db.Column(db.Integer, primary_key=True)
description=db.Column(db.String(), nullable=False)
story_id=db.Column(db.Integer, db.ForeignKey(Story.story_id))
story=db.relationship(Story)
def __repr__(self):
return self.description
def __str__(self):
return self.description
def get_id(self):
return self.task_id
class TaskModelView(ModelView):
create_modal = False
edit_modal = False
can_set_page_size = True
page_size = 20
column_display_pk = True
column_display_all_relations = True
admin.add_view(TaskModelView(Task, db.session))
When dealing with the Tasks list, I see the Story description with no problems and can use that to filter the list but how would I obtain the Project title and be able to filter on that column?
Have been searching the docs but obviously missing something ..
Maybe this can help.
class TaskModelView(ModelView):
...
column_list = [ 'description', 'story', ]
column_filters = ['description', 'story.description', ]
column_labels = {'story.description': 'Story Desc.'}
References here.

Data in JSON Column as normal column

I have a lot of unstructured data in Column(JSONB).
I would like to "standardize" this data and access it like normal Columns.
This actually does work:
class Order(Base):
__tablename__ = "orders"
id_= Column(Integer, primary_key=True, nullable=False)
type = Column(String)
data = Column(JSONB)
#hybrid_property
def email(self):
return self.data['email']
#email.setter
def email(self, value):
self.data['email'] = value
#email.expression
def email(cls):
return cls.data['email'].astext
However data can have different keys, eg: email, e_mail, email_address, etc..
The solution I'm thinking about is to "extend" main class with another class depending
on value in "type":
class Parser1:
#hybrid_property
def email(self):
return self.data['email']
#email.setter
def email(self, value):
self.data['email'] = value
#email.expression
def email(cls):
return cls.data['email'].astext
class Parser2:
#hybrid_property
def email(self):
return self.data['email_address']
#email.setter
def email(self, value):
self.data['email_address'] = value
#email.expression
def email(cls):
return cls.data['email_address'].astext
class Order(Base):
__tablename__ = "orders"
id_= Column(Integer, primary_key=True, nullable=False)
type = Column(String)
data = Column(JSONB)
if type == 'one':
extend with class "Parser1"
elif type == 'two':
extend with class "Parser2"
Would this work or is it stupid idea?
Do you have any better solution?
Any hints very welcome.

Return different data for list

I'm writing Flask API for Angular Front end and my recipe Class returns JSON with all information about the recipes but I need return just some information for a list of recipes to make it load quicker and all information for a recipe detail.
So
#classmethod
def find_all(cls):
return cls.query.order_by(cls.time_added.desc()).all()
should return just JSON with id, name, description
and
def find_by_id(cls, _id):
return cls.query.filter_by(id=_id).first()
should return JSON with all information of a recipe.
What is the best solution for it? Two different Classes or is there any nice way to do it?
Many thanks for any help.
my recipe Model:
class RecipeModel(db.Model):
__tablename__ = 'recipes'
# create recipe table
id = db.Column(db.Integer, primary_key=True, )
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
cuisine_id = db.Column(db.Integer, db.ForeignKey('cuisine.id'))
name = db.Column(db.String(150))
description = db.Column(db.Text)
image_path = db.Column(db.String(100))
total_time = db.Column(db.Integer)
prep_time = db.Column(db.Integer)
cook_time = db.Column(db.Integer)
level = db.Column(db.String(45))
source = db.Column(db.String(45))
rating = db.Column(db.DECIMAL(3, 2))
time_added = db.Column(db.Date)
def __init__(self, user_id, cuisine_id, name, description, image_path, total_time, prep_time, cook_time, level,
source, ):
self.user_id = user_id
self.cuisine_id = cuisine_id
self.name = name
self.description = description
self.image_path = image_path
self.total_time = total_time
self.prep_time = prep_time
self.cook_time = cook_time
self.level = level
self.source = source
# Return recipe as JSON object
def json(self):
return {
'id': self.id,
'name': self.name,
'cuisine_id': self.cuisine_id,
'description': self.description,
'image_path': self.image_path,
'total_time': self.total_time,
'prep_time': self.prep_time,
'cook_time': self.cook_time,
'level': self.level,
'source': self.source,
}
# Find recipe by ID
#classmethod
def find_by_id(cls, _id):
return cls.query.filter_by(id=_id).first()
# Find all recipes
#classmethod
def find_all(cls):
return cls.query.order_by(cls.time_added.desc()).all()
My resources:
class RecipesList(Resource):
#classmethod
def get(cls):
return {'recipes': [recipes.json() for recipes in RecipeModel.find_all()]}
Instead of having .json() on the model, separate serializer into two (or more, as necessary) functions that do the two different kinds of serialization (or you can parameterize a single serialization function with mode='detail' or something).
def serialize_recipe_list(recipe):
return {"id": recipe.id}
def serialize_recipe_detail(recipe):
return {"id": recipe.id, "more": here}
class RecipesList(Resource):
#classmethod
def get(cls):
return {
"recipes": [
serialize_recipe_list(recipe)
for recipe in RecipeModel.find_all()
]
}
class RecipesDetail(Resource):
#classmethod
def get(cls, id):
recipe = RecipeModel.find_by_id(id)
return serialize_recipe_detail(recipe)

How to create form which will show fields from two models?

I have two models:
class Product(models.Model):
name = models.CharField(verbose_name="name", max_length=40)
cost = models.FloatField(verbose_name="price")
def __unicode__(self):
return self.name
class Shop(models.Model):
product = models.ManyToManyField(Product)
name = models.CharField(verbose_name="Nazwa", max_length=40)
budget = models.FloatField(verbose_name="kwota")
def __unicode__(self):
return self.name
I created a form
code:
class ShopForm(forms.ModelForm):
product = forms.ModelMultipleChoiceField(queryset = Product.objects.all(), widget=forms.CheckboxSelectMultiple(),required=True)
name = forms.CharField(max_length=15, label='Name')
budget = forms.FloatField()
class Meta:
model = Shop
fields = ('product','name', 'budget')
It looks like on this picture, i don't know how can i show next to name of products their price? For example:
hehe - 12$
Now i have only name, i want get cost from model "Product", anybody know how can i do it?
You can solve this in different ways:
make changes in __unicode__ method in your model
class Product(models.Model):
...
def __unicode__(self):
return u"{}({})".format(self.name, self.cost)
override init method in form
class ShopForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ShopForm, self).__init__(*args, **kwargs)
self.fields['product'].label_from_instance = lambda obj: u"%s | %s" % (obj.name, obj.cost)
create custom form field:
from django.forms.models import ModelMultipleChoiceField
class ProductModelChoiceField(ModelMultipleChoiceField):
def label_from_instance(self, obj):
return u"%s | %s" % (obj.name, obj.cost)

Does Django ORM have an equivalent to SQLAlchemy's Hybrid Attribute?

In SQLAlchemy, a hybrid attribute is either a property or method applied to an ORM-mapped class,
class Interval(Base):
__tablename__ = 'interval'
id = Column(Integer, primary_key=True)
start = Column(Integer, nullable=False)
end = Column(Integer, nullable=False)
def __init__(self, start, end):
self.start = start
self.end = end
#hybrid_property
def length(self):
return self.end - self.start
#hybrid_method
def contains(self,point):
return (self.start <= point) & (point < self.end)
#hybrid_method
def intersects(self, other):
return self.contains(other.start) | self.contains(other.end)
This allows for distinct behaviors at the class and instance levels, thus making it simpler to evaluate SQL statements using the same code,
>>> i1 = Interval(5, 10)
>>> i1.length
5
>>> print Session().query(Interval).filter(Interval.length > 10)
SELECT interval.id AS interval_id, interval.start AS interval_start,
interval."end" AS interval_end
FROM interval
WHERE interval."end" - interval.start > :param_1
Now in Django, if I have a property on a model,
class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
def _get_full_name(self):
"Returns the person's full name."
return '%s %s' % (self.first_name, self.last_name)
full_name = property(_get_full_name)
It is my understanding that I can not do the following,
Person.objects.filter(full_name="John Cadengo")
Is there an equivalent of SQLAlchemy's hybrid attribute in Django? If not, is there perhaps a common workaround?
You're right that you cannot apply django queryset filter based on python properties, because filter operates on a database level. It seems that there's no equivalent of SQLAlchemy's hybrid attributes in Django.
Please, take a look here and here, may be it'll help you to find a workaround. But, I think there is no generic solution.
One of the quick possible workarounds is to implement some descriptor, that would apply expressions via the annotation.
Something like this:
from django.db import models
from django.db.models import functions
class hybrid_property:
def __init__(self, func):
self.func = func
self.name = func.__name__
self.exp = None
def __get__(self, instance, owner):
if instance is None:
return self
return self.func(instance)
def __set__(self, instance, value):
pass
def expression(self, exp):
self.exp = exp
return self
class HybridManager(models.Manager):
def get_queryset(self):
qs = super().get_queryset()
for name, value in vars(qs.model).items():
if isinstance(value, hybrid_property) and value.exp is not None:
qs = qs.annotate(**{name: value.exp(qs.model)})
return qs
class TestAttribute(models.Model):
val1 = models.CharField(max_length=256)
val2 = models.CharField(max_length=256)
objects = HybridManager()
#hybrid_property
def vals(self):
return f"{self.val1} {self.val2}"
#vals.expression
def vals(cls):
return functions.Concat(models.F("val1"), models.Value(" "), models.F("val2"))
class HybridTests(TestCase):
def setUp(self) -> None:
self.test_attr = TestAttribute.objects.create(val1="val1", val2="val2")
def test_access(self):
self.assertTrue(TestAttribute.objects.exists())
self.assertEqual(self.test_attr.vals, f"{self.test_attr.val1} {self.test_attr.val2}")
self.assertTrue(TestAttribute.objects.filter(vals=f"{self.test_attr.val1} {self.test_attr.val2}").exists())
self.assertTrue(TestAttribute.objects.filter(vals__iexact=f"{self.test_attr.val1} {self.test_attr.val2}").exists())

Categories