Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recommendation engine #73

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
1 change: 0 additions & 1 deletion README.md

This file was deleted.

1 change: 1 addition & 0 deletions Recommendation System/Functional Flow
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
RE
7 changes: 7 additions & 0 deletions Recommendation System/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from re import app
import os

app.secret_key = os.urandom(24)
port = int(os.environ.get('PORT', 5000))
app.run(host='0.0.0.0', port=port)

2 changes: 2 additions & 0 deletions Recommendation System/re/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .views import app
from .models import graph
125 changes: 125 additions & 0 deletions Recommendation System/re/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
from py2neo import Graph, Node, Relationship
from passlib.hash import bcrypt
from datetime import datetime
import os
import uuid

url = os.environ.get('GRAPHENEDB_URL', 'http://localhost:7474')
username = os.environ.get('NEO4J_USERNAME')
password = os.environ.get('NEO4J_PASSWORD')

graph = Graph(url + '/db/data/', username=username, password=password)

class User:
def __init__(self, username):
self.username = username

def find(self):
user = graph.find_one('User', 'username', self.username)
return user

def register(self, password):
if not self.find():
user = Node('User', username=self.username, password=bcrypt.encrypt(password))
graph.create(user)
return True
else:
return False

def verify_password(self, password):
user = self.find()
if user:
return bcrypt.verify(password, user['password'])
else:
return False

def add_post(self, title, tags, text):
user = self.find()
post = Node(
'Post',
id=str(uuid.uuid4()),
title=title,
text=text,
timestamp=timestamp(),
date=date()
)
rel = Relationship(user, 'PUBLISHED', post)
graph.create(rel)

tags = [x.strip() for x in tags.lower().split(',')]
for name in set(tags):
tag = Node('Tag', name=name)
graph.merge(tag)

rel = Relationship(tag, 'TAGGED', post)
graph.create(rel)

def like_post(self, post_id):
user = self.find()
post = graph.find_one('Post', 'id', post_id)
graph.merge(Relationship(user, 'LIKED', post))

def get_recent_posts(self):
query = '''
MATCH (user:User)-[:PUBLISHED]->(post:Post)<-[:TAGGED]-(tag:Tag)
WHERE user.username = {username}
RETURN post, COLLECT(tag.name) AS tags
ORDER BY post.timestamp DESC LIMIT 5
'''

return graph.run(query, username=self.username)

def get_products(self):
query = '''
MATCH (user:User)-[:PUBLISHED]->(post:Post)<-[:TAGGED]-(tag:Tag)
WHERE user.username = {username}
RETURN post, COLLECT(tag.name) AS tags
ORDER BY post.timestamp DESC LIMIT 5
'''

return graph.run(query, username=self.username)

def get_similar_users(self):

query = '''
MATCH (you:User)-[:PUBLISHED]->(:Post)<-[:TAGGED]-(tag:Tag),
(they:User)-[:PUBLISHED]->(:Post)<-[:TAGGED]-(tag)
WHERE you.username = {username} AND you <> they
WITH they, COLLECT(DISTINCT tag.name) AS tags
ORDER BY SIZE(tags) DESC LIMIT 3
RETURN they.username AS similar_user, tags
'''

return graph.run(query, username=self.username)

def get_commonality_of_user(self, other):

query = '''
MATCH (they:User {username: {they} })
MATCH (you:User {username: {you} })
OPTIONAL MATCH (they)-[:PUBLISHED]->(:Post)<-[:TAGGED]-(tag:Tag),
(you)-[:PUBLISHED]->(:Post)<-[:TAGGED]-(tag)
RETURN SIZE((they)-[:LIKED]->(:Post)<-[:PUBLISHED]-(you)) AS likes,
COLLECT(DISTINCT tag.name) AS tags
'''

return graph.run(query, they=other.username, you=self.username).next

def get_todays_recent_posts():
query = '''
MATCH (user:User)-[:PUBLISHED]->(post:Post)<-[:TAGGED]-(tag:Tag)
WHERE post.date = {today}
RETURN user.username AS username, post, COLLECT(tag.name) AS tags
ORDER BY post.timestamp DESC LIMIT 5
'''

return graph.run(query, today=date())

def timestamp():
epoch = datetime.utcfromtimestamp(0)
now = datetime.now()
delta = now - epoch
return delta.total_seconds()

def date():
return datetime.now().strftime('%Y-%m-%d')
15 changes: 15 additions & 0 deletions Recommendation System/re/static/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
body { font-family: sans-serif; background: #eee; }
a, h1, h3 { color: #377ba8; }
h1, h2, h3 { font-family: 'Georgia', serif; margin: 0; }
h1, h2 { border-bottom: 2px solid #eee; padding: 3px; }
h3 { padding: 3px; }
dd { display: block; margin-left: 0px; }
dl { font-weight: bold; }
a:visited { color: #800080; }
.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; padding: 0.8em; background: white; }
.posts { list-style: none; margin: 0; padding: 0; }
.posts li { margin: 0.8em 1.2em; }
.posts li h2 { margin-left: -1em; }
.metanav { text-align: right; font-size: 0.8em; padding: 0.3em; margin-bottom: 1em; background: #fafafa; }
.flash { background: #cee5F5; padding: 0.5em; border: 1px solid #aacbe2; }
.error { background: #f0d6d6; padding: 0.5em; }
15 changes: 15 additions & 0 deletions Recommendation System/re/templates/display_host
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<ul class="posts">
{% for row in posts %}
<li>
<b>{{ row.post.title }}</b>
{% if request.path == "/" %}
by <a href="{{ url_for('profile', username=row.username) }}">{{ row.username }}</a>
{% endif %}
on {{ row.post.date }}
<a href="{{ url_for('like_post', post_id=row.post.id) }}">like</a><br>
<i>{{ ", ".join(row.tags) }}</i><br>
{{ row.post.text }}
{% else %}
<li>There aren't any purchases yet!
{% endfor %}
</ul>
25 changes: 25 additions & 0 deletions Recommendation System/re/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{% extends "layout.html" %}
{% block body %}

<h2>Home</h2>
{% if session.username %}
<h3>Purchase New Items</h3>
<form action="{{ url_for('add_product') }}" method="post">
<dl>
<dt>Product Name:</dt>
<dd><input type="text" size="30" name="title"></dd>
<dt>Categories (separated by commas):</dt>
<dd><input type="text" size="30" name="tags"></dd>
<dt>Description:</dt>
<dd><textarea name="text" rows="5" cols="40"></textarea></dd>
</dl>
<input type="submit" value="Share">
</form>
{% endif %}

<br>

<h3>Today's Recent Purchases</h3>
{% include "display_posts.html" %}

{% endblock %}
23 changes: 23 additions & 0 deletions Recommendation System/re/templates/layout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!doctype html>
<title>My Recommendation Engine</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
<div class="page">
<h1>My Recommendation Site</h1>
<div class="metanav">
{% if session.username %}
Logged in as {{ session.username }}
{% endif %}
<a href="{{ url_for('index') }}">Home</a>
{% if not session.username %}
<a href="{{ url_for('register') }}">Register</a>
<a href="{{ url_for('login') }}">Login</a>
{% else %}
<a href="{{ url_for('profile', username=session.username) }}">Profile</a>
<a href="{{ url_for('logout') }}">Logout</a>
{% endif %}
</div>
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% block body %}{% endblock %}
</div>
51 changes: 51 additions & 0 deletions Recommendation System/re/templates/profile.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{% extends "layout.html" %}
{% block body %}

<h2>{{ username }}'s profile</h2>

{% if session.username %}
{% if session.username == username %}
<h3>Users who brought items similar to you:</h3>

{% for user in similar %}
<p>
<a href="{{ url_for('profile', username=user.similar_user) }}">{{ user.similar_user }}</a>
also purchased <i>{{ ", ".join(user.tags) }}</i> {{ ", ".join(user.posts) }}
</p>
{% else %}
<p>There aren't any users who've purchased products the same category as you!</p>
{% endfor %}

<h3>Recommendations for you</h3>
Based on your previous purchases
{% for product in products %}
<p>
{{ product.post.title }}
</p>


{% else %}
<p>There aren't any users who've purchased products the same category as you!</p>
{% endfor %}

<h3>Your recent purchases:</h3>


{% else %}

<p>{{ username }} has liked {{ common.likes }} of your purchases and
{% if common.tags %}
also purchased <i>{{ ", ".join(common.tags) }}</i>
{% else %}
hasn't purchased any of the same category
{% endif %}
</p>

<h3>{{ username }}'s recent purchases:</h3>

{% endif %}
{% endif %}

{% include "display_posts.html" %}

{% endblock %}
109 changes: 109 additions & 0 deletions Recommendation System/re/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from .models import User, get_todays_recent_posts
from flask import Flask, request, session, redirect, url_for, render_template, flash

app = Flask(__name__)

@app.route('/')
def index():
posts = get_todays_recent_posts()
return render_template('index.html', posts=posts)

@app.route('/register', methods=['GET','POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']

if len(username) < 1:
flash('Your username must be at least one character.')
elif len(password) < 5:
flash('Your password must be at least 5 characters.')
elif not User(username).register(password):
flash('A user with that username already exists.')
else:
session['username'] = username
flash('Logged in.')
return redirect(url_for('index'))

return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']

if not User(username).verify_password(password):
flash('Invalid login.')
else:
session['username'] = username
flash('Logged in.')
return redirect(url_for('index'))

return render_template('login.html')

@app.route('/logout')
def logout():
session.pop('username', None)
flash('Logged out.')
return redirect(url_for('index'))

@app.route('/add_product', methods=['POST'])
def add_product():
title = request.form['title']
tags = request.form['tags']
text = request.form['text']

if not title:
flash('You must give your post a title.')
elif not tags:
flash('You must give your post at least one tag.')
elif not text:
flash('You must give your post a text body.')
else:
User(session['username']).add_post(title, tags, text)

return redirect(url_for('index'))

@app.route('/like_post/<post_id>')
def like_post(post_id):
username = session.get('username')

if not username:
flash('You must be logged in to like a post.')
return redirect(url_for('login'))

User(username).like_post(post_id)

flash('Liked post.')
return redirect(request.referrer)

@app.route('/profile/<username>')
def profile(username):
logged_in_username = session.get('username')
user_being_viewed_username = username

user_being_viewed = User(user_being_viewed_username)
posts = user_being_viewed.get_recent_posts()

similar = []
common = []
products = []

if logged_in_username:
logged_in_user = User(logged_in_username)

if logged_in_user.username == user_being_viewed.username:
similar = logged_in_user.get_similar_users()
products = logged_in_user.get_products()
else:
common = logged_in_user.get_commonality_of_user(user_being_viewed)

return render_template(
'profile.html',
username=username,
posts=posts,
similar=similar,
common=common,
products=products
)
13 changes: 13 additions & 0 deletions Recommendation System/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
bcrypt==3.1.0
cffi==1.7.0
click==6.6
Flask==0.11.1
itsdangerous==0.24
Jinja2==2.8
MarkupSafe==0.23
passlib==1.6.5
py2neo==3.1.1
pycparser==2.14
six==1.10.0
Werkzeug==0.11.10

Binary file added Recommendation_Engine_Architecture_Diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.