From 9154d216a28abb87b1e99a8a94e2e2c778fc09a5 Mon Sep 17 00:00:00 2001 From: Edin Dazdarevic Date: Sun, 22 Mar 2015 16:16:52 +0100 Subject: [PATCH] basic version of search implemented --- front-api/controllers/search.rb | 48 +++++++++- front-api/db/schema.rb | 12 +-- front-ui/app/actions/navigationActions.js | 9 +- front-ui/app/actions/searchActions.js | 20 +++++ front-ui/app/components/rootApp.js | 8 +- .../components/search/searchResultsPage.js | 52 +++++++++++ front-ui/app/components/shared/searchBox.js | 46 ++++++++++ front-ui/app/constants/searchConstants.js | 7 ++ front-ui/app/models/itemSearchCollection.js | 24 +++++ front-ui/app/router.js | 2 + front-ui/app/stores/searchStore.js | 88 +++++++++++++++++++ 11 files changed, 305 insertions(+), 11 deletions(-) create mode 100644 front-ui/app/actions/searchActions.js create mode 100644 front-ui/app/components/search/searchResultsPage.js create mode 100644 front-ui/app/components/shared/searchBox.js create mode 100644 front-ui/app/constants/searchConstants.js create mode 100644 front-ui/app/models/itemSearchCollection.js create mode 100644 front-ui/app/stores/searchStore.js diff --git a/front-api/controllers/search.rb b/front-api/controllers/search.rb index 617501c..20dd909 100644 --- a/front-api/controllers/search.rb +++ b/front-api/controllers/search.rb @@ -1,7 +1,17 @@ +# TODO: make this private, not-public facing. +# for now we keep this here for simplicity reasons get '/search/index' do es_client = Elasticsearch::Client.new log: true - all_items = Item.all.to_a + # first delete the index + begin + es_client.indices.delete index: 'ribica' + rescue + logger.warn "Ribica index could not be deleted. Continuing with indexing operation..." + end + + # now index items + all_items = Item.includes(sub_category: { category: :section }).all.to_a all_items.each do |item| es_client.index index: 'ribica', type: 'items', id: item.id, body: { title: 'Test', @@ -16,3 +26,39 @@ get '/search/index' do "ok".to_json end + +get '/search' do + es_client = Elasticsearch::Client.new log: true + q = params[:q] + + # for now we do the basic query + results = es_client.search index: 'ribica', type: 'items', body: { query: { match: { _all: q } } } + ids = results["hits"]["hits"].map do |r| + r["_id"] + end + + ids_with_score = {} + results["hits"]["hits"].each do |r| + ids_with_score[r["_id"].to_i] = {:score => r["_score"], :item => nil} + end + + if ids.length > 0 + res = Item.where(:id => ids).to_a + # make sure we have correct relevance order, since `where in` does not guarantee order + res.each do |ii| + ids_with_score[ii.id][:item] = ii + end + final = [] + ids_with_score.each do |k,v| + final << v + end + final.sort_by! {|v| -v[:score]} + + final = final.map do |f| + f[:item] + end + prepare_items_for_mass_display(final) + else + [].to_json + end +end diff --git a/front-api/db/schema.rb b/front-api/db/schema.rb index fc2c17e..e0eac76 100644 --- a/front-api/db/schema.rb +++ b/front-api/db/schema.rb @@ -23,11 +23,11 @@ ActiveRecord::Schema.define(version: 20150321052740) do t.datetime "updated_at", null: false t.string "anonymous_id_string" t.integer "delivery_destination_id" - t.boolean "confirmed" - t.boolean "packed" - t.boolean "canceled_on_check" - t.boolean "canceled_on_delivery" - t.boolean "delivered" + t.boolean "confirmed", default: false + t.boolean "packed", default: false + t.boolean "canceled_on_check", default: false + t.boolean "canceled_on_delivery", default: false + t.boolean "delivered", default: false t.text "internal_note" end @@ -109,8 +109,8 @@ ActiveRecord::Schema.define(version: 20150321052740) do t.datetime "updated_at", null: false t.string "tags" t.json "traits" - t.integer "supplier_id" t.decimal "weight", precision: 5, scale: 3 + t.integer "supplier_id" t.integer "delivery_time_estimation_id" end diff --git a/front-ui/app/actions/navigationActions.js b/front-ui/app/actions/navigationActions.js index 87f923e..17a7d4a 100644 --- a/front-ui/app/actions/navigationActions.js +++ b/front-ui/app/actions/navigationActions.js @@ -85,10 +85,13 @@ var NavigationActions = { actionType: NavigationConstants.CHANGE_URL, url: '/hvala' }); + }, + goToSearchResults : function(q) { + AppDispatcher.handleAction({ + actionType: NavigationConstants.CHANGE_URL, + url: '/pretraga?q=' + q + }); } - - - }; module.exports = NavigationActions; diff --git a/front-ui/app/actions/searchActions.js b/front-ui/app/actions/searchActions.js new file mode 100644 index 0000000..2694862 --- /dev/null +++ b/front-ui/app/actions/searchActions.js @@ -0,0 +1,20 @@ +var AppDispatcher = require('../dispatcher/appDispatcher'); +var SearchContants = require('../constants/searchConstants'); + +// Define action methods +var SearchActions = { + searchBoxChange: function(q) { + AppDispatcher.handleAction({ + actionType: SearchContants.SEARCH_BOX_CHANGE, + q : q + }); + }, + getSearchResults: function(q) { + AppDispatcher.handleAction({ + actionType: SearchContants.GET_SEARCH_RESULTS, + q : q + }); + } +}; + +module.exports = SearchActions; diff --git a/front-ui/app/components/rootApp.js b/front-ui/app/components/rootApp.js index 36f41ce..ad2dde0 100644 --- a/front-ui/app/components/rootApp.js +++ b/front-ui/app/components/rootApp.js @@ -8,6 +8,7 @@ var React = require('react'), InitializationActions = require('../actions/initializationActions'); var CartIcon = require('./cart/cartIcon'); +var SearchBox = require('./shared/searchBox'); var RootApp = React.createClass({ @@ -55,10 +56,15 @@ var RootApp = React.createClass({
- +
diff --git a/front-ui/app/components/search/searchResultsPage.js b/front-ui/app/components/search/searchResultsPage.js new file mode 100644 index 0000000..f86538a --- /dev/null +++ b/front-ui/app/components/search/searchResultsPage.js @@ -0,0 +1,52 @@ +var React = require('react'), + NavigationActions = require('../../actions/navigationActions'), + Globals = require('../../globals') + Router = require("react-router"), + Link = Router.Link; + +var SearchStore = require('../../stores/searchStore'); +var SearchActions = require('../../actions/searchActions'); + +var ItemList = require('../items/itemList'); +var SearchResultsPage = React.createClass({ + mixins: [Router.State], + getInitialState: function() { + return SearchStore.getSearchResultsState(); + }, + render: function() { + return ( +
+

Resultati pretrage za {this.state.q}

+ + + +
+ + ); + + }, + componentWillReceiveProps: function() { + this.update(); + }, + update: function(){ + var query = this.getQuery(); + SearchActions.getSearchResults(query.q); + }, + componentDidMount: function() { + SearchStore.addChangeListener(this._onChange); + this.update(); + + //CartActions.load(); + }, + componentWillUnmount: function () { + SearchStore.removeChangeListener(this._onChange); + }, + _onChange: function() { + if(this.isMounted()) { + this.setState(SearchStore.getSearchResultsState()); + } + } +}); + + +module.exports = SearchResultsPage; diff --git a/front-ui/app/components/shared/searchBox.js b/front-ui/app/components/shared/searchBox.js new file mode 100644 index 0000000..bf2ef07 --- /dev/null +++ b/front-ui/app/components/shared/searchBox.js @@ -0,0 +1,46 @@ +var React = require('react'), + Router = require('react-router'); + +var NavigationActions = require('../../actions/navigationActions'); +var SearchActions = require('../../actions/searchActions'); +var SearchStore = require('../../stores/searchStore'); + +var SearchBox = React.createClass({ + getInitialState: function() { + return SearchStore.getSearchBoxState(); + }, + onSearchClick: function(e) { + NavigationActions.goToSearchResults(this.state.q); + e.preventDefault(); + }, + componentDidMount: function() { + SearchStore.addChangeListener(this.onSearchStoreChange); + }, + componentWillUnmount: function() { + SearchStore.removeChangeListener(this.onSearchStoreChange); + }, + onSearchStoreChange: function() { + if(this.isMounted()) { + this.setState(SearchStore.getSearchBoxState()); + } + }, + onSearchBoxChange: function(e) { + SearchActions.searchBoxChange(e.currentTarget.value); + }, + onKeyPress: function(e) { + var enterKeyCode = 13; + if(e.which == enterKeyCode) { + NavigationActions.goToSearchResults(this.state.q); + } + }, + render: function() { + return (
+ + + + +
) + } +}); + +module.exports = SearchBox; diff --git a/front-ui/app/constants/searchConstants.js b/front-ui/app/constants/searchConstants.js new file mode 100644 index 0000000..9166a75 --- /dev/null +++ b/front-ui/app/constants/searchConstants.js @@ -0,0 +1,7 @@ +var keyMirror = require('react/lib/keyMirror'); + +// Define action constants +module.exports = keyMirror({ + SEARCH_BOX_CHANGE: null, + GET_SEARCH_RESULTS: null +}); diff --git a/front-ui/app/models/itemSearchCollection.js b/front-ui/app/models/itemSearchCollection.js new file mode 100644 index 0000000..816cdbd --- /dev/null +++ b/front-ui/app/models/itemSearchCollection.js @@ -0,0 +1,24 @@ +var Backbone = require('backbone'), + Item = require('./item'), + Globals = require('../globals'); + +var ItemSearchCollection = Backbone.Collection.extend({ + initialize: function() { + $.ajaxPrefilter( + function(options, originalOptions, jqXHR) { + options.xhrFields = { + withCredentials: true + } + } + ); + }, + setQuery: function(q) { + this.q = q; + }, + model: Item, + url: function() { + return Globals.ApiUrl + "/search?q=" + this.q; + } +}); + +module.exports = ItemSearchCollection; diff --git a/front-ui/app/router.js b/front-ui/app/router.js index e5755c9..452b9cc 100644 --- a/front-ui/app/router.js +++ b/front-ui/app/router.js @@ -18,6 +18,7 @@ var ThankYouPage = require('./components/thankyou/thankYouPage'); var Register = require('./components/account/register'); var Login = require('./components/account/login'); +var SearchResultsPage = require('./components/search/searchResultsPage'); var routes = ( @@ -30,6 +31,7 @@ var routes = ( + ); diff --git a/front-ui/app/stores/searchStore.js b/front-ui/app/stores/searchStore.js new file mode 100644 index 0000000..5a58ba1 --- /dev/null +++ b/front-ui/app/stores/searchStore.js @@ -0,0 +1,88 @@ +var AppDispatcher = require('../dispatcher/appDispatcher'); +var EventEmitter = require('events').EventEmitter; + +var SearchConstants = require('../constants/searchConstants'); +var SearchActions = require('../actions/searchActions'); +var NavigationActions = require('../actions/navigationActions'); + +var ItemSearchCollection = require('../models/itemSearchCollection'); +var globals = require('../globals'); +var _ = require('underscore'); + +var _searchBoxState = { + q: '' +}; + +var _searchResultsState = { + q: '', + items: (new ItemSearchCollection()) +}; + +var handleSearchBoxChange = function(q) { + _searchBoxState.q = q; +}; + + +var handleGetSearchResults = function(q) { + _searchResultsState.q = q; + _searchBoxState.q = ''; + + var searchResults = new ItemSearchCollection(); + searchResults.setQuery(q); + searchResults.fetch({success: function() { + _searchResultsState.items = searchResults; + SearchStore.emit('change'); + }}); +}; + +// Extend ItemStore with EventEmitter to add eventing capabilities +var SearchStore = _.extend({}, EventEmitter.prototype, { + + getSearchBoxState: function() { + return _searchBoxState; + }, + getSearchResultsState: function() { + return _searchResultsState; + }, + // Emit Change event + emitChange: function() { + console.log("SearchStore change!"); + this.emit('change'); + }, + + // Add change listener + addChangeListener: function(callback) { + this.on('change', callback); + }, + + // Remove change listener + removeChangeListener: function(callback) { + this.removeListener('change', callback); + } + +}); + + +// Register callback with AppDispatcher +SearchStore.dispatchToken = AppDispatcher.register(function(payload) { + var action = payload.action; + + switch(action.actionType) { + case SearchConstants.SEARCH_BOX_CHANGE: + handleSearchBoxChange(action.q); + break; + + case SearchConstants.GET_SEARCH_RESULTS: + handleGetSearchResults(action.q); + break; + default: + return true; + } + + // If action was responded to, emit change event + SearchStore.emitChange(); + return true; + +}); + +module.exports = SearchStore;