basic version of search implemented

This commit is contained in:
Edin Dazdarevic
2015-03-22 16:16:52 +01:00
parent 38548e3e33
commit 9154d216a2
11 changed files with 305 additions and 11 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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({
</div>
</div>
<div className='row'>
<div className='col-md-12' id='header'>
<div className='col-md-8' id='header'>
<SectionsListComponent />
</div>
<div className="col-md-4">
<SearchBox />
</div>
</div>
<div className='row'>
<RouteHandler />
</div>

View File

@@ -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 (
<div>
<h1>Resultati pretrage za {this.state.q}</h1>
<ItemList items={this.state.items} />
</div>
);
},
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;

View File

@@ -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 (<div className="input-group">
<input type="text" onKeyPress={this.onKeyPress} className="form-control" value={this.state.q} onChange={this.onSearchBoxChange} placeholder="Pretraga"> </input>
<span className="input-group-btn">
<button className="btn btn-default" type="button" onClick={this.onSearchClick}>Traži</button>
</span>
</div>)
}
});
module.exports = SearchBox;

View File

@@ -0,0 +1,7 @@
var keyMirror = require('react/lib/keyMirror');
// Define action constants
module.exports = keyMirror({
SEARCH_BOX_CHANGE: null,
GET_SEARCH_RESULTS: null
});

View File

@@ -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;

View File

@@ -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 = (
<Route name='login' path="/login" handler={Login} />
<Route name='byCat' path="sekcija/:sekcijaName/kategorija/:id/*" handler={ByCategory} />
<Route name='hvala' path="/hvala" handler={ThankYouPage} />
<Route name='pretraga' path="/pretraga" handler={SearchResultsPage} />
<DefaultRoute handler={StartPage}/>
</Route>
);

View File

@@ -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;