Skip to content

Commit

Permalink
elasticsearch enabled.
Browse files Browse the repository at this point in the history
- elastic search configured
-search function added
- doc written for search
  • Loading branch information
nimaafshar79 committed May 29, 2020
1 parent fce5db1 commit 74a1933
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 7 deletions.
129 changes: 129 additions & 0 deletions api-documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -757,3 +757,132 @@ response code will be **200**
```

----------
----------

### `/foods/search`

method: `GET`

recipe and foods detailed information

*input*:
GET method parameters:

- query: text to search
- page:pagination page, default value is 1
- page:items per page, default value is 10

*sample input*:

```
/foods/search?query=pasta&page=1&per_page=5
```

*output*:

response code will be **200**

- total results count in the elasticsearch
- food sample view of foods found int the search ordered by relevance

```json
{
"results": [ "list views.."],
"total_results_count": "count..."
}
```

*sample input*
```json
{
"results": [
{
"category": "pasta",
"id": 384279,
"image": "https://images.eatthismuch.com/site_media/img/384279_erin_m_77a48297-f148-454d-aa02-fdd277e70edf.png",
"nutrition": {
"calories": 476,
"fat": 8.6,
"fiber": 1.6,
"protein": 17.7
},
"thumbnail": "https://images.eatthismuch.com/site_media/thmb/384279_erin_m_77a48297-f148-454d-aa02-fdd277e70edf.png",
"title": "Pasta, Corn & Artichoke Bowl"
},
{
"category": "pasta",
"id": 1493432,
"image": "https://images.eatthismuch.com/site_media/img/1093241_Billie7_1975_f6db1d3f-2bed-4c82-bf10-e343b9dc8314.jpeg",
"nutrition": {
"calories": 591,
"fat": 15.8,
"fiber": 4.7,
"protein": 16.7
},
"thumbnail": "https://images.eatthismuch.com/site_media/thmb/1093241_Billie7_1975_f6db1d3f-2bed-4c82-bf10-e343b9dc8314.jpeg",
"title": "Spaghetti with Mushrooms, Garlic and Oil"
},
{
"category": "other",
"id": 907167,
"image": "https://images.eatthismuch.com/site_media/img/907167_tabitharwheeler_915ad93b-213d-4b3d-bcc2-e0570b833af3.jpg",
"nutrition": {
"calories": 309,
"fat": 7.2,
"fiber": 8.6,
"protein": 16.1
},
"thumbnail": "https://images.eatthismuch.com/site_media/thmb/907167_tabitharwheeler_915ad93b-213d-4b3d-bcc2-e0570b833af3.jpg",
"title": "Pasta with Red Sauce and Mozzarella"
},
{
"category": "pasta",
"id": 905979,
"image": "https://images.eatthismuch.com/site_media/img/905979_tabitharwheeler_82334d46-99b8-428d-aa16-4bdd9c3008cd.jpg",
"nutrition": {
"calories": 423,
"fat": 12.3,
"fiber": 4.0,
"protein": 24.2
},
"thumbnail": "https://images.eatthismuch.com/site_media/thmb/905979_tabitharwheeler_82334d46-99b8-428d-aa16-4bdd9c3008cd.jpg",
"title": "Spaghetti with Meat Sauce"
},
{
"category": "pasta",
"id": 45500,
"image": "https://images.eatthismuch.com/site_media/img/45500_simmyras_43adc56f-d597-4778-a682-4ddfa9f394a3.png",
"nutrition": {
"calories": 285,
"fat": 18.0,
"fiber": 0.9,
"protein": 15.4
},
"thumbnail": "https://images.eatthismuch.com/site_media/thmb/45500_simmyras_43adc56f-d597-4778-a682-4ddfa9f394a3.png",
"title": "Rigatoni with Brie, Grape Tomatoes, Olives, and Basil"
}
],
"total_results_count": 1211
}
```

*in case of errors*:

1- if you don't pass query parameter in the url **422**

```json
{
"error": "query should exist in the request"
}
```

2- if per_page value is more than 50 **404**

```json
{
"error": "per_page should not be more than 50"
}
```



108 changes: 102 additions & 6 deletions foods/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,86 @@
import json
from flask_admin.contrib.sqla import ModelView
from flask import jsonify
from extentions import db
from extentions import db, elastic
from wtforms import SelectField


class Food(db.Model):
class SearchableMixin(object):

@classmethod
def add_to_index(cls, instance):
if elastic is None:
return
if not hasattr(instance, 'elastic_document'):
raise Exception("model doesn't have 'elastic_document' attribute")

payload = instance.elastic_document
if not hasattr(cls, '__indexname__'):
raise Exception("class doesn't have '__indexname__' attribute")

elastic.index(index=cls.__indexname__, body=payload, id=instance.id)

@classmethod
def remove_from_index(cls, instance):
if elastic is None:
return
if not hasattr(cls, '__indexname__'):
raise Exception("class doesn't have '__indexname__' attribute")

elastic.delete(index=cls.__indexname__, id=instance.id)

@classmethod
def query_index(cls, query, page=1, per_page=10):
if elastic is None:
return [], 0
search = elastic.search(
index=cls.__indexname__,
body={'query': {'multi_match': {'query': query, 'fields': ['*']}},
'from': (page - 1) * per_page, 'size': per_page})
ids = [int(hit['_id']) for hit in search['hits']['hits']]
return ids, search['hits']['total']['value']

@classmethod
def search(cls, expression, page=1, per_page=10):
ids, total = cls.query_index(expression, page, per_page)
if total == 0:
return cls.query.filter_by(id=0), 0 # just returning nothing
when = []
for i in range(len(ids)):
when.append((ids[i], i))
return cls.query.filter(cls.id.in_(ids)).order_by(
db.case(when, value=cls.id)), total

@classmethod
def before_commit(cls, session):
session._changes = {
'add': list(session.new),
'update': list(session.dirty),
'delete': list(session.deleted)
}

@classmethod
def after_commit(cls, session):
for obj in session._changes['add']:
if isinstance(obj, SearchableMixin):
cls.add_to_index(obj)
for obj in session._changes['update']:
if isinstance(obj, SearchableMixin):
cls.add_to_index(obj)
for obj in session._changes['delete']:
if isinstance(obj, SearchableMixin):
cls.remove_from_index(obj)
session._changes = None

@classmethod
def reindex(cls):
for obj in cls.query:
cls.add_to_index(obj)


class Food(db.Model,SearchableMixin):
__tablename__ = 'foods'
__indexname__ = 'foods'

id = Column('id', Integer(), primary_key=True)
Calories = Column('calories', Integer())
Expand All @@ -36,7 +110,7 @@ class Food(db.Model):
# self._Category = category

@property
def recipe(self):
def recipe(self) -> dict:
if self.Recipe is None or self.Recipe == '':
return None
else:
Expand All @@ -49,7 +123,7 @@ def __repr__(self):
return f"<Food '{self.Title}'>"

@property
def simple_view(self):
def simple_view(self) -> dict:
"""
a simple view of food model
"""
Expand All @@ -68,12 +142,30 @@ def simple_view(self):
def __str__(self):
return json.dumps(self.simple_view)

def get_calorie(self):
def get_calorie(self) -> int:
return self.Calories

def get_category(self):
def get_category(self) -> str:
return self.Category.strip().lower()

@property
def elastic_document(self):
"""
:return: elastic search index document
"""
recipe = self.recipe
payload = {
'author': self.author.FullName,
'name': recipe['food_name'],
'description': recipe['description'],
'category': recipe['category'],
'tag_cloud': recipe['tag_cloud'],
'ingredients': [ingredient['food']['food_name'] for ingredient in recipe['ingredients']],
'directions': [direction['text'] for direction in recipe['directions']]
}

return payload


# for admin integration
class FoodModelView(ModelView):
Expand All @@ -99,3 +191,7 @@ class FoodModelView(ModelView):
'pasta'
]]
}


db.event.listen(db.session, 'before_commit', SearchableMixin.before_commit)
db.event.listen(db.session, 'after_commit', SearchableMixin.after_commit)
35 changes: 34 additions & 1 deletion foods/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from foods import foods
from flask import jsonify
from flask import jsonify, request
from foods.utils import get_foods_with_categories
from foods.diet import sevade, yevade, dovade
from .models import Food
from flask_jwt_extended import (jwt_required)
from utils.decorators import confirmed_only


@foods.route('/yevade/<int:calorie>', methods=['GET'])
def get_yevade(calorie):
cat = ['breakfast, ''mostly_meat', 'pasta', 'main_dish', 'sandwich', 'appetizers', 'drink']
Expand Down Expand Up @@ -73,3 +74,35 @@ def get_food(id):
return jsonify({"error": "food not found."}), 404

return jsonify(food.simple_view)


@foods.route('/search', methods=['GET'])
def food_search():
"""
food full text search using elasticsearch
http parameters:
query: text to search
page: pagination page number
per_page: pagination per_page count
:return:
"""
query = request.args.get('query')
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 10))

if query == "" or query is None:
return jsonify({
'error': "query should exist in the request"
}), 422 # invalid input error

if per_page > 50:
return jsonify({
'error': 'per_page should not be more than 50'
}), 422

results, count = Food.search(query, page, per_page)

return jsonify({
'results': [result.simple_view for result in results.all()],
'total_results_count': count
})
3 changes: 3 additions & 0 deletions search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from extentions import elastic


0 comments on commit 74a1933

Please sign in to comment.