├── .gitignore ├── client ├── logo.png ├── styles.css ├── index.html └── app.js ├── screenshot-demo.png ├── requirements.txt ├── server_evolution ├── server_2_mongodb.py ├── server_1_statics.py ├── server_3_restaurants.py └── server_4_facets.py ├── README.md └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea -------------------------------------------------------------------------------- /client/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubnvp/mongo-facets/HEAD/client/logo.png -------------------------------------------------------------------------------- /screenshot-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubnvp/mongo-facets/HEAD/screenshot-demo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==6.7 2 | Flask==0.12.2 3 | itsdangerous==0.24 4 | Jinja2==2.10 5 | MarkupSafe==1.0 6 | pymongo==3.6.0 7 | six==1.11.0 8 | Werkzeug==0.14.1 9 | -------------------------------------------------------------------------------- /server_evolution/server_2_mongodb.py: -------------------------------------------------------------------------------- 1 | from pymongo import MongoClient 2 | 3 | # connect to database test in localhost 4 | client = MongoClient('localhost:27017') 5 | db = client.test 6 | 7 | # read from db 8 | print list(db.restaurants.find({'name': 'Morris Park Bake Shop'})) -------------------------------------------------------------------------------- /server_evolution/server_1_statics.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__, static_folder='../client') 4 | 5 | # Statics 6 | @app.route('/') 7 | def root(): 8 | return app.send_static_file('index.html') 9 | 10 | @app.route('/') 11 | def static_proxy(path): 12 | # send_static_file will guess the correct MIME type 13 | return app.send_static_file(path) 14 | 15 | # run the application without flask-cli 16 | if __name__ == "__main__": 17 | app.run(debug=True) -------------------------------------------------------------------------------- /server_evolution/server_3_restaurants.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | from pymongo import MongoClient 3 | 4 | app = Flask(__name__, static_folder='../client') 5 | client = MongoClient('localhost:27017') 6 | db = client.test 7 | API_ENDPOINT = '/api/v1' 8 | 9 | def _get_array_param(param): 10 | return filter(None, param.split(",")) 11 | 12 | # API 13 | @app.route(API_ENDPOINT + "/restaurants") 14 | def restaurants(): 15 | # pagination 16 | page = int(request.args.get('page', '0')) 17 | page_size = int(request.args.get('page-size', '50')) 18 | skip = page * page_size 19 | limit = min(page_size, 50) 20 | 21 | # filters 22 | search = request.args.get('search', '') 23 | boroughs = _get_array_param(request.args.get('boroughs', '')) 24 | cuisines = _get_array_param(request.args.get('cuisines', '')) 25 | zipcode = _get_array_param(request.args.get('zipcodes', '')) 26 | 27 | find = {} 28 | if search: 29 | find['$text'] = {'$search': search} 30 | if boroughs: 31 | find['borough'] = {'$in': boroughs} 32 | if cuisines: 33 | find['cuisine'] = {'$in': cuisines} 34 | if zipcode: 35 | find['address.zipcode'] = {'$in': zipcode} 36 | 37 | response = { 38 | 'restaurants': list(db.restaurants.find(find).skip(skip).limit(limit)), 39 | 'count': db.restaurants.find(find).count() 40 | } 41 | 42 | for restaurant in response['restaurants']: # remove _id, is an ObjectId and is not serializable 43 | del restaurant['_id'] 44 | return jsonify(response) 45 | 46 | # Statics 47 | @app.route('/') 48 | def root(): 49 | return app.send_static_file('index.html') 50 | 51 | @app.route('/') 52 | def static_proxy(path): 53 | # send_static_file will guess the correct MIME type 54 | return app.send_static_file(path) 55 | 56 | # run the application without flask-cli 57 | if __name__ == "__main__": 58 | app.run(debug=True) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mongo facets 2 | 3 | These are the specific steps to build this demo of faceted search navigation implemented with the stack: VueJS, Flask, MongoDB. 4 | 5 | ![screensot demo](screenshot-demo.png "Screenshot Demo") 6 | 7 | ## Installing stuff 8 | Everything assumes a MacOSX environment with [brew](https://brew.sh/), python, pip and [virtualenvwrapper](http://virtualenvwrapper.readthedocs.io/en/latest/index.html) 9 | 10 | ### Install mongo 11 | ``` 12 | $ brew install mongo 13 | ``` 14 | 15 | check if it works 16 | ``` 17 | $ mongo 18 | MongoDB shell version v3.4.4 19 | connecting to: mongodb://127.0.0.1:27017 20 | MongoDB server version: 3.4.4 21 | ... 22 | > show databases 23 | > exit 24 | ``` 25 | 26 | ### Load sample data (restaurants.json) 27 | 28 | This is a sample collection of 25K restaurants of NYC from [mongo documentation](https://docs.mongodb.com/getting-started/shell/import-data/). 29 | 30 | 1. Download the dataset: 31 | 32 | ``` 33 | $ curl -O https://raw.githubusercontent.com/mongodb/docs-assets/primer-dataset/primer-dataset.json 34 | ``` 35 | 36 | 2. Import the dataset in 'test' database: 37 | 38 | ``` 39 | $ mongoimport --db test --collection restaurants --drop --file primer-dataset.json 40 | ``` 41 | 42 | 3. Check if it was imported well: 43 | 44 | ``` 45 | $ mongo 46 | MongoDB shell version v3.4.4 47 | connecting to: mongodb://127.0.0.1:27017 48 | MongoDB server version: 3.4.4 49 | ... 50 | > use test 51 | > db.restaurants.findOne() 52 | { 53 | "_id" : ObjectId("596286ff0b13d7ec5826380f"), 54 | "address" : { 55 | "building" : "1007", 56 | ... 57 | > db.restaurants.find().count() 58 | 25359 59 | > db.restaurants.find({borough: 'Bronx'}).count() 60 | 2338 61 | ``` 62 | 63 | 4. Create index: 64 | 65 | ``` 66 | > db.restaurants.createIndex( { cuisine: -1, borough: -1, zipcode: -1 } ) 67 | > db.restaurants.createIndex( { name: "text" } ) 68 | > exit 69 | ``` 70 | 71 | 5. Remove downloaded dataset: 72 | 73 | ``` 74 | $ rm primer-dataset.json 75 | ``` 76 | 77 | ### Setup Python project 78 | 79 | Clone this repository and inside the folder: 80 | 81 | 1. Make a virtualenv: 82 | 83 | ``` 84 | $ mkvirtualenv mongo-facets 85 | ``` 86 | 87 | 2. Install Python dependencies (Flask and PyMongo): 88 | 89 | ``` 90 | $ pip install -r requirements.txt 91 | ``` 92 | 93 | ## Run the Flask server 94 | ``` 95 | $ python server.py 96 | ``` 97 | 98 | And enjoy playing in the frontend at [http://127.0.0.1:5000/](http://127.0.0.1:5000/) ! :D -------------------------------------------------------------------------------- /client/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Avenir', Helvetica, Arial, sans-serif; 3 | color: #2c3e50; 4 | } 5 | 6 | .text-clickable { 7 | cursor: pointer; 8 | } 9 | 10 | .text-clickable:hover { 11 | background-color: #eee; 12 | } 13 | 14 | .page-title { 15 | display: flex; 16 | align-items: center; 17 | } 18 | 19 | .page-title .logo { 20 | height: 26px; 21 | margin-right: 10px; 22 | } 23 | 24 | .selected-factes { 25 | display: flex; 26 | align-items: baseline; 27 | } 28 | 29 | .selected-facets .facet-chip { 30 | min-height: 32px; 31 | padding: 3px 12px; 32 | font-size: 14px; 33 | border: 1px solid #e0e0e0; 34 | border-radius: 2rem; 35 | cursor: pointer; 36 | margin-left: 5px; 37 | } 38 | 39 | .clear-all { 40 | cursor: pointer; 41 | font-size: 10px; 42 | margin-left: 5px; 43 | color: dodgerblue; 44 | text-decoration: underline; 45 | } 46 | 47 | .restaurants-navigation { 48 | display: flex; 49 | align-items: baseline; 50 | justify-content: space-between; 51 | } 52 | 53 | .restaurants-navigation .page-button { 54 | border: none; 55 | display: inline-block; 56 | outline: 0; 57 | padding: 4px 13px; 58 | font-size: 20px; 59 | overflow: hidden; 60 | background-color: #f1f1f1; 61 | cursor: pointer; 62 | border-radius: 50%; 63 | border: 1px solid #ccc; 64 | } 65 | 66 | .searchbox { 67 | border: 0; 68 | border-bottom: 2px solid #ccc; 69 | font-size: 18px; 70 | text-align: center; 71 | padding: 5px; 72 | } 73 | 74 | .content { 75 | display: flex; 76 | } 77 | 78 | .restaurant-facets { 79 | display: flex; 80 | flex-direction: column; 81 | min-width: 200px; 82 | max-width: 200px; 83 | margin-right: 10px; 84 | } 85 | 86 | .list-facet { 87 | margin-bottom: 10px; 88 | min-height: 200px; 89 | } 90 | 91 | .facet-item { 92 | padding: 5px; 93 | } 94 | 95 | .facet-item.selected { 96 | font-weight: bold; 97 | } 98 | 99 | .restaurants-list { 100 | display: flex; 101 | flex-wrap: wrap; 102 | flex-grow: 1; 103 | } 104 | 105 | .restaurant-card { 106 | display: flex; 107 | flex-direction: column; 108 | min-width: 250px; 109 | max-width: 250px; 110 | max-height: 250px; 111 | padding: 10px; 112 | margin: 5px; 113 | border: 1px solid #ccc; 114 | border-radius: 2px; 115 | box-shadow: 0 1px 5px rgba(0,0,0,.2), 0 2px 2px rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.12); 116 | } -------------------------------------------------------------------------------- /server_evolution/server_4_facets.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | from pymongo import MongoClient 3 | 4 | app = Flask(__name__, static_folder='../client') 5 | client = MongoClient('localhost:27017') 6 | db = client.test 7 | API_ENDPOINT = '/api/v1' 8 | 9 | def _get_array_param(param): 10 | return filter(None, param.split(",")) 11 | 12 | # API 13 | @app.route(API_ENDPOINT + "/restaurants") 14 | def restaurants(): 15 | # pagination 16 | page = int(request.args.get('page', '0')) 17 | page_size = int(request.args.get('page-size', '50')) 18 | skip = page * page_size 19 | limit = min(page_size, 50) 20 | 21 | # filters 22 | search = request.args.get('search', '') 23 | boroughs = _get_array_param(request.args.get('boroughs', '')) 24 | cuisines = _get_array_param(request.args.get('cuisines', '')) 25 | zipcode = _get_array_param(request.args.get('zipcodes', '')) 26 | 27 | find = {} 28 | if search: 29 | find['$text'] = {'$search': search} 30 | if boroughs: 31 | find['borough'] = {'$in': boroughs} 32 | if cuisines: 33 | find['cuisine'] = {'$in': cuisines} 34 | if zipcode: 35 | find['address.zipcode'] = {'$in': zipcode} 36 | 37 | response = { 38 | 'restaurants': list(db.restaurants.find(find).skip(skip).limit(limit)), 39 | 'count': db.restaurants.find(find).count() 40 | } 41 | 42 | for restaurant in response['restaurants']: # remove _id, is an ObjectId and is not serializable 43 | del restaurant['_id'] 44 | return jsonify(response) 45 | 46 | @app.route(API_ENDPOINT + "/restaurants/facets") 47 | def restaurant_facets(): 48 | # filters 49 | search = request.args.get('search', '') 50 | boroughs = _get_array_param(request.args.get('boroughs', '')) 51 | cuisines = _get_array_param(request.args.get('cuisines', '')) 52 | zipcodes = _get_array_param(request.args.get('zipcodes', '')) 53 | 54 | pipeline = [{ 55 | '$match': {'$text': {'$search': search}} 56 | }] if search else [] 57 | 58 | pipeline += [{ 59 | '$facet': { 60 | 'borough': _get_facet_borough_pipeline(cuisines, zipcodes), 61 | 'cuisine': _get_facet_cuisine_pipeline(boroughs, zipcodes), 62 | 'zipcode': _get_facet_zipcode_pipeline(boroughs, cuisines), 63 | } 64 | }] 65 | 66 | restaurant_facets = list(db.restaurants.aggregate(pipeline))[0] 67 | 68 | return jsonify(restaurant_facets) 69 | 70 | def _get_facet_borough_pipeline(cuisines, zipcodes): 71 | match = {} 72 | 73 | if cuisines: 74 | match['cuisine'] = {'$in': cuisines} 75 | if zipcodes: 76 | match['address.zipcode'] = {'$in': zipcodes} 77 | 78 | pipeline = [ 79 | {'$match': match} 80 | ] if match else [] 81 | 82 | return pipeline + _get_group_pipeline('borough') 83 | 84 | def _get_facet_cuisine_pipeline(boroughs, zipcodes): 85 | match = {} 86 | 87 | if boroughs: 88 | match['borough'] = {'$in': boroughs} 89 | if zipcodes: 90 | match['address.zipcode'] = {'$in': zipcodes} 91 | 92 | pipeline = [ 93 | {'$match': match} 94 | ] if match else [] 95 | 96 | return pipeline + _get_group_pipeline('cuisine') 97 | 98 | def _get_facet_zipcode_pipeline(boroughs, cuisines): 99 | match = {} 100 | 101 | if boroughs: 102 | match['borough'] = {'$in': boroughs} 103 | if cuisines: 104 | match['cuisine'] = {'$in': cuisines} 105 | 106 | pipeline = [ 107 | {'$match': match}, 108 | ] if match else [] 109 | 110 | return pipeline + _get_group_pipeline('address.zipcode') 111 | 112 | def _get_group_pipeline(group_by): 113 | return [ 114 | { 115 | '$group': { 116 | '_id': '$' + group_by, 117 | 'count': {'$sum': 1}, 118 | } 119 | }, 120 | { 121 | '$project': { 122 | '_id': 0, 123 | 'value': '$_id', 124 | 'count': 1, 125 | } 126 | }, 127 | { 128 | '$sort': {'count': -1} 129 | }, 130 | { 131 | '$limit': 6, 132 | } 133 | ] 134 | 135 | # Statics 136 | @app.route('/') 137 | def root(): 138 | return app.send_static_file('index.html') 139 | 140 | @app.route('/') 141 | def static_proxy(path): 142 | # send_static_file will guess the correct MIME type 143 | return app.send_static_file(path) 144 | 145 | # run the application without flask-cli 146 | if __name__ == "__main__": 147 | app.run(debug=True) -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | La cuchara 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | from pymongo import MongoClient 3 | 4 | 5 | app = Flask(__name__, static_folder='client') 6 | client = MongoClient('localhost:27017') 7 | db = client.test 8 | API_ENDPOINT = '/api/v1' 9 | 10 | 11 | def _get_array_param(param): 12 | return filter(None, param.split(",")) 13 | 14 | 15 | # API 16 | @app.route(API_ENDPOINT + "/restaurants") 17 | def restaurants(): 18 | # pagination 19 | page = int(request.args.get('page', '0')) 20 | page_size = int(request.args.get('page-size', '50')) 21 | skip = page * page_size 22 | limit = min(page_size, 50) 23 | 24 | # filters 25 | search = request.args.get('search', '') 26 | boroughs = _get_array_param(request.args.get('boroughs', '')) 27 | cuisines = _get_array_param(request.args.get('cuisines', '')) 28 | zipcode = _get_array_param(request.args.get('zipcodes', '')) 29 | 30 | match = {} 31 | if search: 32 | match['$text'] = {'$search': search} 33 | if boroughs: 34 | match['borough'] = {'$in': boroughs} 35 | if cuisines: 36 | match['cuisine'] = {'$in': cuisines} 37 | if zipcode: 38 | match['address.zipcode'] = {'$in': zipcode} 39 | 40 | pipeline = [{ 41 | '$match': match 42 | }] if match else [] 43 | 44 | pipeline += [{ 45 | '$facet': { 46 | 'restaurants': [ 47 | {'$skip': skip}, 48 | {'$limit': limit} 49 | ], 50 | 'count': [ 51 | {'$count': 'total'} 52 | ], 53 | } 54 | }] 55 | 56 | result = list(db.restaurants.aggregate(pipeline))[0] 57 | 58 | for restaurant in result['restaurants']: # remove _id, is an ObjectId and is not serializable 59 | del restaurant['_id'] 60 | result['count'] = result['count'][0]['total'] if result['count'] else 0 61 | return jsonify(result) 62 | 63 | 64 | @app.route(API_ENDPOINT + "/restaurants/facets") 65 | def restaurant_facets(): 66 | # filters 67 | search = request.args.get('search', '') 68 | boroughs = _get_array_param(request.args.get('boroughs', '')) 69 | cuisines = _get_array_param(request.args.get('cuisines', '')) 70 | zipcodes = _get_array_param(request.args.get('zipcodes', '')) 71 | 72 | pipeline = [{ 73 | '$match': {'$text': {'$search': search}} 74 | }] if search else [] 75 | 76 | pipeline += [{ 77 | '$facet': { 78 | 'borough': _get_facet_borough_pipeline(cuisines, zipcodes), 79 | 'cuisine': _get_facet_cuisine_pipeline(boroughs, zipcodes), 80 | 'zipcode': _get_facet_zipcode_pipeline(boroughs, cuisines), 81 | } 82 | }] 83 | 84 | restaurant_facets = list(db.restaurants.aggregate(pipeline))[0] 85 | 86 | return jsonify(restaurant_facets) 87 | 88 | 89 | def _get_facet_borough_pipeline(cuisines, zipcodes): 90 | match = {} 91 | 92 | if cuisines: 93 | match['cuisine'] = {'$in': cuisines} 94 | if zipcodes: 95 | match['address.zipcode'] = {'$in': zipcodes} 96 | 97 | pipeline = [ 98 | {'$match': match} 99 | ] if match else [] 100 | 101 | return pipeline + _get_group_pipeline('borough') 102 | 103 | 104 | def _get_facet_cuisine_pipeline(boroughs, zipcodes): 105 | match = {} 106 | 107 | if boroughs: 108 | match['borough'] = {'$in': boroughs} 109 | if zipcodes: 110 | match['address.zipcode'] = {'$in': zipcodes} 111 | 112 | pipeline = [ 113 | {'$match': match} 114 | ] if match else [] 115 | 116 | return pipeline + _get_group_pipeline('cuisine') 117 | 118 | 119 | def _get_facet_zipcode_pipeline(boroughs, cuisines): 120 | match = {} 121 | 122 | if boroughs: 123 | match['borough'] = {'$in': boroughs} 124 | if cuisines: 125 | match['cuisine'] = {'$in': cuisines} 126 | 127 | pipeline = [ 128 | {'$match': match}, 129 | ] if match else [] 130 | 131 | return pipeline + _get_group_pipeline('address.zipcode') 132 | 133 | 134 | def _get_group_pipeline(group_by): 135 | return [ 136 | { 137 | '$group': { 138 | '_id': '$' + group_by, 139 | 'count': {'$sum': 1}, 140 | } 141 | }, 142 | { 143 | '$project': { 144 | '_id': 0, 145 | 'value': '$_id', 146 | 'count': 1, 147 | } 148 | }, 149 | { 150 | '$sort': {'count': -1} 151 | }, 152 | { 153 | '$limit': 6, 154 | } 155 | ] 156 | 157 | 158 | # Statics 159 | @app.route('/') 160 | def root(): 161 | return app.send_static_file('index.html') 162 | 163 | @app.route('/') 164 | def static_proxy(path): 165 | # send_static_file will guess the correct MIME type 166 | return app.send_static_file(path) 167 | 168 | # run the application without flask-cli 169 | if __name__ == "__main__": 170 | app.run(debug=True) -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | var API_ENDPOINT = '/api/v1'; 5 | 6 | var app = new Vue({ 7 | el: '#main', 8 | template: '#app', 9 | data: function() { 10 | return { 11 | page: 0, 12 | pageSize: 50, 13 | search: '', 14 | selected: { 15 | cuisine: [], 16 | borough: [], 17 | zipcode: [], 18 | }, 19 | all: { 20 | cuisine: [], 21 | borough: [], 22 | zipcode: [], 23 | restaurants: [], 24 | }, 25 | restaurantsCount: '', 26 | }; 27 | }, 28 | computed: { 29 | pagesCount: function() { 30 | return Math.floor(this.restaurantsCount / this.pageSize) + 1; 31 | }, 32 | previousPageDisabled: function() { 33 | return this.page === 0; 34 | }, 35 | nextPageDisabled: function() { 36 | return this.page === this.pagesCount - 1; 37 | }, 38 | selectedFilters: function() { 39 | return [] 40 | .concat( 41 | this.selected.cuisine.map(function(value){return {value: value, type: 'cuisine', icon: 'cutlery'};}) 42 | ).concat( 43 | this.selected.borough.map(function(value){return {value: value, type: 'borough', icon: 'building'};}) 44 | ).concat( 45 | this.selected.zipcode.map(function(value){return {value: value, type: 'zipcode', icon: 'map-marker'};}) 46 | ); 47 | }, 48 | }, 49 | watch: { 50 | search: function() { 51 | this.page = 0; 52 | this.fetchRestaurants(); 53 | this.fetchFacets(); 54 | }, 55 | }, 56 | methods: { 57 | previousPage: function() { 58 | this.page--; 59 | this.fetchRestaurants(); 60 | }, 61 | nextPage: function() { 62 | this.page++; 63 | this.fetchRestaurants(); 64 | }, 65 | removeChip: function(chip) { 66 | this.removeFacet(chip.value, chip.type); 67 | }, 68 | facetClicked: function(value, type) { 69 | var facetList = this.selected[type]; 70 | if (!facetList) return; 71 | 72 | var facetIndex = facetList.indexOf(value); 73 | // add facet 74 | if (facetIndex === -1 ) { 75 | this.addFacet(value, type); 76 | } 77 | else { // remove facet 78 | this.removeFacet(value, type); 79 | } 80 | }, 81 | addFacet: function(value, type) { 82 | this.selected[type].push(value); 83 | this.page = 0; 84 | this.fetchRestaurants(); 85 | this.fetchFacets(); 86 | }, 87 | removeFacet: function(value, type) { 88 | var facetIndex = this.selected[type].indexOf(value); 89 | this.selected[type].splice(facetIndex, 1); 90 | this.page = 0; 91 | this.fetchRestaurants(); 92 | this.fetchFacets(); 93 | }, 94 | isFacetSelected: function(value, type) { 95 | var facetList = this.selected[type]; 96 | if (!facetList) return false; 97 | return facetList.indexOf(value) !== -1; 98 | }, 99 | clearAll: function() { 100 | this.selected.cuisine = []; 101 | this.selected.borough = []; 102 | this.selected.zipcode = []; 103 | this.page = 0; 104 | this.fetchRestaurants(); 105 | this.fetchFacets(); 106 | }, 107 | getQueryOptions: function() { 108 | var options = { 109 | params: { 110 | page: this.page, 111 | page_size: this.pageSize, 112 | search: this.search, 113 | boroughs: this.selected.borough.join(','), 114 | cuisines: this.selected.cuisine.join(','), 115 | zipcodes: this.selected.zipcode.join(','), 116 | } 117 | }; 118 | if (this.page <= 0) delete options.params.page; 119 | if (!this.search) delete options.params.search; 120 | if (!options.params.boroughs) delete options.params.boroughs; 121 | if (!options.params.cuisines) delete options.params.cuisines; 122 | if (!options.params.zipcodes) delete options.params.zipcodes; 123 | return options; 124 | }, 125 | fetchRestaurants: function() { 126 | var self = this; 127 | var options = this.getQueryOptions(); 128 | return axios.get(API_ENDPOINT + '/restaurants', options).then(function(response) { 129 | self.all.restaurants = response.data.restaurants; 130 | self.restaurantsCount = response.data.count; 131 | }); 132 | }, 133 | fetchFacets: function() { 134 | var self = this; 135 | var options = this.getQueryOptions(); 136 | delete options.params.page; 137 | delete options.params.page_size; 138 | return axios.get(API_ENDPOINT + '/restaurants/facets', options).then(function(response) { 139 | self.all.borough = _getOrderedFacets( 140 | self.selected.borough, 141 | response.data.borough 142 | ); 143 | self.all.cuisine = _getOrderedFacets( 144 | self.selected.cuisine, 145 | response.data.cuisine 146 | ); 147 | self.all.zipcode = _getOrderedFacets( 148 | self.selected.zipcode, 149 | response.data.zipcode 150 | ); 151 | }); 152 | }, 153 | }, 154 | mounted: function() { 155 | this.fetchRestaurants(); 156 | this.fetchFacets(); 157 | } 158 | }); 159 | 160 | function _getOrderedFacets(selectedValues, facets) { 161 | return selectedValues 162 | .map(function(value) { // get selected facets (and add count if exists) 163 | var facet = facets.find(function(facet) { 164 | return facet.value === value; 165 | }); 166 | return { 167 | value: value, 168 | count: (facet && facet.count) || 'x', 169 | }; 170 | }) 171 | .concat( // then add unselect facets (excluding the ones that are selected) 172 | facets.filter(function(facet){ 173 | return selectedValues.indexOf(facet.value) === -1; 174 | }) 175 | ); 176 | } 177 | 178 | })(); --------------------------------------------------------------------------------