searchjoy 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +147 -0
- data/Rakefile +1 -0
- data/app/controllers/searchjoy/searches_controller.rb +65 -0
- data/app/views/layouts/searchjoy/application.html.erb +312 -0
- data/app/views/searchjoy/searches/index.html.erb +27 -0
- data/app/views/searchjoy/searches/overview.html.erb +48 -0
- data/app/views/searchjoy/searches/recent.html.erb +18 -0
- data/app/views/searchjoy/searches/stream.html.erb +13 -0
- data/config/routes.rb +7 -0
- data/lib/generators/searchjoy/install_generator.rb +29 -0
- data/lib/generators/searchjoy/templates/install.rb +19 -0
- data/lib/searchjoy.rb +37 -0
- data/lib/searchjoy/engine.rb +5 -0
- data/lib/searchjoy/search.rb +26 -0
- data/lib/searchjoy/track.rb +16 -0
- data/lib/searchjoy/version.rb +3 -0
- data/searchjoy.gemspec +26 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 24cd1d01229d4b88ff02a976eae9411999d7dce7
|
4
|
+
data.tar.gz: ba333cc0e1703fabb3fc1e8c21555807f494c699
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 92dc8de2bfbf711948729c6bef82a829a2262c1b0670e34b5e6280a5257efb12282edb35a5c9ce324ba70ef39dc4962ea5f2d6ef5aed5d8a3cbd1b8585e40d59
|
7
|
+
data.tar.gz: 539fc9cef9a06d3b9f87b57c5d611365a02ec4cdb5e9a0ba616d6a6bd66165f2444b8be029e9e97da4324c7894b7b2b97acdb0600d5c5f90548a6f94ee77f0c1
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Andrew Kane
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# Searchjoy
|
2
|
+
|
3
|
+
:monkey_face: Search analytics made easy
|
4
|
+
|
5
|
+
[See it in action](http://searchjoy-demo.herokuapp.com/)
|
6
|
+
|
7
|
+
- view searches in real-time
|
8
|
+
- track conversions week over week
|
9
|
+
- monitor the performance of top searches
|
10
|
+
|
11
|
+
:cupid: An amazing companion to [Searchkick](https://github.com/ankane/searchkick)
|
12
|
+
|
13
|
+
Works with Rails 3.1+ and any search engine, including Elasticsearch, Sphinx, and Solr
|
14
|
+
|
15
|
+
:tangerine: Battle-tested at [Instacart](https://www.instacart.com)
|
16
|
+
|
17
|
+
## Get Started
|
18
|
+
|
19
|
+
Add this line to your application’s Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem "searchjoy"
|
23
|
+
```
|
24
|
+
|
25
|
+
And run the generator. This creates a migration to store searches.
|
26
|
+
|
27
|
+
```sh
|
28
|
+
rails generate searchjoy:install
|
29
|
+
rake db:migrate
|
30
|
+
```
|
31
|
+
|
32
|
+
Next, add the dashboard to your `config/routes.rb`.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
mount Searchjoy::Engine, at: "admin/searchjoy"
|
36
|
+
```
|
37
|
+
|
38
|
+
Be sure to protect the endpoint in production - see the [Authentication](#authentication) section for ways to do this.
|
39
|
+
|
40
|
+
### Track Searches
|
41
|
+
|
42
|
+
Track searches by creating a record in the database.
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
Searchjoy::Search.create(
|
46
|
+
search_type: "Item", # typically the model name
|
47
|
+
query: "apple",
|
48
|
+
results_count: 12
|
49
|
+
)
|
50
|
+
```
|
51
|
+
|
52
|
+
With [Searchkick](https://github.com/ankane/searchkick), you can use the `track` option to do this automatically.
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
Item.search "apple", track: true
|
56
|
+
```
|
57
|
+
|
58
|
+
If you want to track more attributes, add them to the `searchjoy_searches` table. Then, pass the values to the `track` option.
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
Item.search "apple", track: {user_id: 1, source: "web"}
|
62
|
+
```
|
63
|
+
|
64
|
+
It’s that easy.
|
65
|
+
|
66
|
+
### Track Conversions
|
67
|
+
|
68
|
+
First, choose a conversion metric. At Instacart, an item added to the cart from the search results page counts as a conversion.
|
69
|
+
|
70
|
+
Next, when a user searches, keep track of the search id. With Searchkick, you can get the id with `@results.search.id`.
|
71
|
+
|
72
|
+
When a user converts, find the record and call `convert`.
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
search = Searchjoy::Search.find params[:id]
|
76
|
+
search.convert
|
77
|
+
```
|
78
|
+
|
79
|
+
Better yet, record the model that converted.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
item = Item.find params[:item_id]
|
83
|
+
search.convert(item)
|
84
|
+
```
|
85
|
+
|
86
|
+
### Authentication
|
87
|
+
|
88
|
+
Don’t forget to protect the dashboard in production.
|
89
|
+
|
90
|
+
#### Basic Authentication
|
91
|
+
|
92
|
+
Set the following variables in your environment or an initializer.
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
ENV["SEARCHJOY_USERNAME"] = "andrew"
|
96
|
+
ENV["SEARCHJOY_PASSWORD"] = "secret"
|
97
|
+
```
|
98
|
+
|
99
|
+
#### Devise
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
authenticate :user, lambda{|user| user.admin? } do
|
103
|
+
mount Searchjoy::Engine, at: "admin/searchjoy"
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
### Customize
|
108
|
+
|
109
|
+
#### Time Zone
|
110
|
+
|
111
|
+
To change the time zone, create an initializer `config/initializers/searchjoy.rb` with:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
Searchjoy.time_zone = "Pacific Time (US & Canada)" # defaults to Time.zone
|
115
|
+
```
|
116
|
+
|
117
|
+
#### Top Searches
|
118
|
+
|
119
|
+
Change the number of top searches shown with:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
Searchjoy.top_searches = 500 # defaults to 100
|
123
|
+
```
|
124
|
+
|
125
|
+
#### Live Conversions
|
126
|
+
|
127
|
+
Show the conversion name in the live stream.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
Searchjoy.conversion_name = proc{|model| model.name }
|
131
|
+
```
|
132
|
+
|
133
|
+
## TODO
|
134
|
+
|
135
|
+
- customize views
|
136
|
+
- analytics for individual queries
|
137
|
+
- group similar queries
|
138
|
+
- track pagination, facets, sorting, etc
|
139
|
+
|
140
|
+
## Contributing
|
141
|
+
|
142
|
+
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
143
|
+
|
144
|
+
- [Report bugs](https://github.com/ankane/searchjoy/issues)
|
145
|
+
- Fix bugs and [submit pull requests](https://github.com/ankane/searchjoy/pulls)
|
146
|
+
- Write, clarify, or fix documentation
|
147
|
+
- Suggest or add new feature
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Searchjoy
|
2
|
+
class SearchesController < ActionController::Base
|
3
|
+
layout "searchjoy/application"
|
4
|
+
|
5
|
+
http_basic_authenticate_with name: ENV["SEARCHJOY_USERNAME"], password: ENV["SEARCHJOY_PASSWORD"] if ENV["SEARCHJOY_PASSWORD"]
|
6
|
+
|
7
|
+
before_filter :set_time_zone
|
8
|
+
before_filter :set_search_types
|
9
|
+
before_filter :set_search_type, only: [:index, :overview]
|
10
|
+
before_filter :set_time_range, only: [:index, :overview]
|
11
|
+
before_filter :set_searches, only: [:index, :overview]
|
12
|
+
|
13
|
+
def index
|
14
|
+
if params[:sort] == "conversion_rate"
|
15
|
+
@searches.sort_by!{|s| [s["conversion_rate"].to_f, s["query"]] }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def overview
|
20
|
+
relation = Searchjoy::Search.where(search_type: params[:search_type])
|
21
|
+
@searches_by_week = relation.group_by_week(:created_at, Time.zone, @time_range).count
|
22
|
+
@conversions_by_week = relation.where("converted_at is not null").group_by_week(:created_at, Time.zone, @time_range).count
|
23
|
+
@top_searches = @searches.first(5)
|
24
|
+
@bad_conversion_rate = @searches.sort_by{|s| [s["conversion_rate"].to_f, s["query"]] }.first(5).select{|s| s["conversion_rate"] < 50 }
|
25
|
+
@conversion_rate_by_week = {}
|
26
|
+
@searches_by_week.each do |week, searches_count|
|
27
|
+
@conversion_rate_by_week[week] = searches_count > 0 ? (100.0 * @conversions_by_week[week] / searches_count).round : 0
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def stream
|
32
|
+
end
|
33
|
+
|
34
|
+
def recent
|
35
|
+
@searches = Searchjoy::Search.order("created_at desc").limit(50)
|
36
|
+
render layout: false
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def set_search_types
|
42
|
+
@search_types = Searchjoy::Search.uniq.pluck(:search_type).sort
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_search_type
|
46
|
+
@search_type = params[:search_type].to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_time_zone
|
50
|
+
@time_zone = Searchjoy.time_zone || Time.zone
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_time_range
|
54
|
+
@time_range = 8.weeks.ago.in_time_zone(@time_zone).beginning_of_week(:sunday)..Time.now
|
55
|
+
end
|
56
|
+
|
57
|
+
def set_searches
|
58
|
+
@limit = params[:limit] || Searchjoy.top_searches
|
59
|
+
@searches = Searchjoy::Search.connection.select_all(Searchjoy::Search.select("normalized_query, COUNT(*) as searches_count, COUNT(converted_at) as conversions_count, AVG(results_count) as avg_results_count").where(created_at: @time_range, search_type: @search_type).group("normalized_query").order("searches_count desc, normalized_query asc").limit(@limit).to_sql).to_a
|
60
|
+
@searches.each do |search|
|
61
|
+
search["conversion_rate"] = 100 * search["conversions_count"].to_i / search["searches_count"].to_f
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,312 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Searchjoy</title>
|
5
|
+
|
6
|
+
<meta charset="utf-8" />
|
7
|
+
|
8
|
+
<style>
|
9
|
+
body {
|
10
|
+
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
11
|
+
margin: 0;
|
12
|
+
padding: 20px;
|
13
|
+
font-size: 14px;
|
14
|
+
line-height: 1.4;
|
15
|
+
color: #333;
|
16
|
+
}
|
17
|
+
|
18
|
+
a, a:visited, a:active {
|
19
|
+
color: #428bca;
|
20
|
+
text-decoration: none;
|
21
|
+
}
|
22
|
+
|
23
|
+
a:hover {
|
24
|
+
text-decoration: underline;
|
25
|
+
}
|
26
|
+
|
27
|
+
table {
|
28
|
+
width: 100%;
|
29
|
+
border-collapse: collapse;
|
30
|
+
border-spacing: 0;
|
31
|
+
margin-bottom: 20px;
|
32
|
+
}
|
33
|
+
|
34
|
+
th {
|
35
|
+
text-align: left;
|
36
|
+
border-bottom: solid 2px #ddd;
|
37
|
+
}
|
38
|
+
|
39
|
+
h1 {
|
40
|
+
font-size: 20px;
|
41
|
+
}
|
42
|
+
|
43
|
+
h1 small {
|
44
|
+
color: #999;
|
45
|
+
font-weight: normal;
|
46
|
+
}
|
47
|
+
|
48
|
+
h2 {
|
49
|
+
font-size: 16px;
|
50
|
+
}
|
51
|
+
|
52
|
+
ul {
|
53
|
+
list-style-type: none;
|
54
|
+
padding: 0;
|
55
|
+
}
|
56
|
+
|
57
|
+
table td, table th {
|
58
|
+
padding: 8px;
|
59
|
+
}
|
60
|
+
|
61
|
+
td {
|
62
|
+
border-top: solid 1px #ddd;
|
63
|
+
}
|
64
|
+
|
65
|
+
.text-muted {
|
66
|
+
color: #999;
|
67
|
+
}
|
68
|
+
|
69
|
+
#brand {
|
70
|
+
font-size: 18px;
|
71
|
+
line-height: 20px;
|
72
|
+
font-weight: bold;
|
73
|
+
color: #999;
|
74
|
+
}
|
75
|
+
|
76
|
+
a.active {
|
77
|
+
color: #5cb85c;
|
78
|
+
}
|
79
|
+
|
80
|
+
a.type-link {
|
81
|
+
background-color: #f0ad4e;
|
82
|
+
color: #fff;
|
83
|
+
padding: 8px;
|
84
|
+
border-radius: 4px;
|
85
|
+
}
|
86
|
+
|
87
|
+
a.type-link:hover {
|
88
|
+
text-decoration: none;
|
89
|
+
}
|
90
|
+
|
91
|
+
/* suspiciously similar to bootstrap 3 */
|
92
|
+
a.type-link-0 {
|
93
|
+
background-color: #5bc0de;
|
94
|
+
}
|
95
|
+
|
96
|
+
a.type-link-1 {
|
97
|
+
background-color: #d9534f;
|
98
|
+
}
|
99
|
+
|
100
|
+
a.type-link-2 {
|
101
|
+
background-color: #5cb85c;
|
102
|
+
}
|
103
|
+
|
104
|
+
#header {
|
105
|
+
border-bottom: solid 1px #ddd;
|
106
|
+
padding-bottom: 20px;
|
107
|
+
}
|
108
|
+
|
109
|
+
.nav, .nav li {
|
110
|
+
display: inline;
|
111
|
+
}
|
112
|
+
|
113
|
+
.nav li {
|
114
|
+
margin-right: 20px;
|
115
|
+
}
|
116
|
+
|
117
|
+
#stream {
|
118
|
+
border-bottom: solid 1px #ddd;
|
119
|
+
}
|
120
|
+
|
121
|
+
.num {
|
122
|
+
text-align: right;
|
123
|
+
}
|
124
|
+
|
125
|
+
.container {
|
126
|
+
max-width: 800px;
|
127
|
+
margin-left: auto;
|
128
|
+
margin-right: auto;
|
129
|
+
}
|
130
|
+
|
131
|
+
/*
|
132
|
+
Simple Grid
|
133
|
+
Learn More - http://dallasbass.com/simple-grid-a-lightweight-responsive-css-grid/
|
134
|
+
Project Page - http://thisisdallas.github.com/Simple-Grid/
|
135
|
+
Author - Dallas Bass
|
136
|
+
Site - dallasbass.com
|
137
|
+
*/
|
138
|
+
|
139
|
+
*, *:after, *:before {
|
140
|
+
-webkit-box-sizing: border-box;
|
141
|
+
-moz-box-sizing: border-box;
|
142
|
+
box-sizing: border-box;
|
143
|
+
}
|
144
|
+
|
145
|
+
body {
|
146
|
+
margin: 0px;
|
147
|
+
}
|
148
|
+
|
149
|
+
[class*='col-'] {
|
150
|
+
float: left;
|
151
|
+
padding-right: 20px;
|
152
|
+
}
|
153
|
+
|
154
|
+
[class*='col-']:last-of-type {
|
155
|
+
padding-right: 0px;
|
156
|
+
}
|
157
|
+
|
158
|
+
.grid {
|
159
|
+
width: 100%;
|
160
|
+
|
161
|
+
margin: 0 auto;
|
162
|
+
overflow: hidden;
|
163
|
+
}
|
164
|
+
|
165
|
+
.grid:after {
|
166
|
+
content: "";
|
167
|
+
display: table;
|
168
|
+
clear: both;
|
169
|
+
}
|
170
|
+
|
171
|
+
.grid-pad {
|
172
|
+
padding: 20px 0 0px 20px;
|
173
|
+
}
|
174
|
+
|
175
|
+
.grid-pad > [class*='col-']:last-of-type {
|
176
|
+
padding-right: 20px;
|
177
|
+
}
|
178
|
+
|
179
|
+
.push-right {
|
180
|
+
float: right;
|
181
|
+
}
|
182
|
+
|
183
|
+
/* Content Columns */
|
184
|
+
|
185
|
+
.col-1-1 {
|
186
|
+
width: 100%;
|
187
|
+
}
|
188
|
+
.col-2-3, .col-8-12 {
|
189
|
+
width: 66.66%;
|
190
|
+
}
|
191
|
+
|
192
|
+
.col-1-2, .col-6-12 {
|
193
|
+
width: 50%;
|
194
|
+
}
|
195
|
+
|
196
|
+
.col-1-3, .col-4-12 {
|
197
|
+
width: 33.33%;
|
198
|
+
}
|
199
|
+
|
200
|
+
.col-1-4, .col-3-12 {
|
201
|
+
width: 25%;
|
202
|
+
}
|
203
|
+
|
204
|
+
.col-1-5 {
|
205
|
+
width: 20%;
|
206
|
+
}
|
207
|
+
|
208
|
+
.col-1-6, .col-2-12 {
|
209
|
+
width: 16.667%;
|
210
|
+
}
|
211
|
+
|
212
|
+
.col-1-7 {
|
213
|
+
width: 14.28%;
|
214
|
+
}
|
215
|
+
|
216
|
+
.col-1-8 {
|
217
|
+
width: 12.5%;
|
218
|
+
}
|
219
|
+
|
220
|
+
.col-1-9 {
|
221
|
+
width: 11.1%;
|
222
|
+
}
|
223
|
+
|
224
|
+
.col-1-10 {
|
225
|
+
width: 10%;
|
226
|
+
}
|
227
|
+
|
228
|
+
.col-1-11 {
|
229
|
+
width: 9.09%;
|
230
|
+
}
|
231
|
+
|
232
|
+
.col-1-12 {
|
233
|
+
width: 8.33%
|
234
|
+
}
|
235
|
+
|
236
|
+
/* Layout Columns */
|
237
|
+
|
238
|
+
.col-11-12 {
|
239
|
+
width: 91.66%
|
240
|
+
}
|
241
|
+
|
242
|
+
.col-10-12 {
|
243
|
+
width: 83.333%;
|
244
|
+
}
|
245
|
+
|
246
|
+
.col-9-12 {
|
247
|
+
width: 75%;
|
248
|
+
}
|
249
|
+
|
250
|
+
.col-5-12 {
|
251
|
+
width: 41.66%;
|
252
|
+
}
|
253
|
+
|
254
|
+
.col-7-12 {
|
255
|
+
width: 58.33%
|
256
|
+
}
|
257
|
+
|
258
|
+
@media handheld, only screen and (max-width: 767px) {
|
259
|
+
|
260
|
+
|
261
|
+
.grid {
|
262
|
+
width: 100%;
|
263
|
+
min-width: 0;
|
264
|
+
margin-left: 0px;
|
265
|
+
margin-right: 0px;
|
266
|
+
padding-left: 0px;
|
267
|
+
padding-right: 0px;
|
268
|
+
}
|
269
|
+
|
270
|
+
[class*='col-'] {
|
271
|
+
width: auto;
|
272
|
+
float: none;
|
273
|
+
margin-left: 0px;
|
274
|
+
margin-right: 0px;
|
275
|
+
margin-top: 10px;
|
276
|
+
margin-bottom: 10px;
|
277
|
+
padding-right: 0px;
|
278
|
+
padding-left: 0px;
|
279
|
+
}
|
280
|
+
}
|
281
|
+
</style>
|
282
|
+
|
283
|
+
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
|
284
|
+
<%= javascript_include_tag "//www.google.com/jsapi", "chartkick" %>
|
285
|
+
</head>
|
286
|
+
<body>
|
287
|
+
<div class="container">
|
288
|
+
<div id="header">
|
289
|
+
<div class="grid">
|
290
|
+
<div class="col-1-2">
|
291
|
+
<ul class="nav">
|
292
|
+
<li id="brand">Searchjoy</li>
|
293
|
+
<li><%= link_to "Live Stream", searchjoy.root_path, class: ("active" if !@search_type) %></li>
|
294
|
+
<% @search_types.each do |search_type| %>
|
295
|
+
<li><%= link_to search_type, searchjoy.overview_searches_path(search_type: search_type), class: ("active" if @search_type == search_type) %></li>
|
296
|
+
<% end %>
|
297
|
+
</ul>
|
298
|
+
</div>
|
299
|
+
|
300
|
+
<div class="col-1-2" style="text-align: right;">
|
301
|
+
<% if @time_range %>
|
302
|
+
<%= @time_range.first.strftime("%b %-e, %Y") %> to <%= @time_range.last.strftime("%b %-e, %Y") %>
|
303
|
+
<span class="text-muted"><%= @time_zone.name.sub(" (US & Canada)", "") %></span>
|
304
|
+
<% end %>
|
305
|
+
</div>
|
306
|
+
</div>
|
307
|
+
</div>
|
308
|
+
|
309
|
+
<%= yield %>
|
310
|
+
</div>
|
311
|
+
</body>
|
312
|
+
</html>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<h1>
|
2
|
+
<%= @search_type %> Searches
|
3
|
+
<small>Top <%= @limit %></small>
|
4
|
+
</h1>
|
5
|
+
|
6
|
+
<table>
|
7
|
+
<thead>
|
8
|
+
<tr>
|
9
|
+
<th>Query</th>
|
10
|
+
<th class="num" style="width: 20%;"><%= link_to "Searches", params.except(:sort), class: ("active" if params[:sort] != "conversion_rate") %></th>
|
11
|
+
<th class="num" style="width: 20%;">Conversions</th>
|
12
|
+
<th class="num" style="width: 20%;"><%= link_to "%", params.merge(sort: "conversion_rate"), class: ("active" if params[:sort] == "conversion_rate") %></th>
|
13
|
+
<th class="num" style="width: 20%;">Avg Results</th>
|
14
|
+
</tr>
|
15
|
+
</thead>
|
16
|
+
<tbody>
|
17
|
+
<% @searches.each do |search| %>
|
18
|
+
<tr>
|
19
|
+
<td><%= search["normalized_query"] %></td>
|
20
|
+
<td class="num"><%= search["searches_count"] %></td>
|
21
|
+
<td class="num"><%= search["conversions_count"] %></td>
|
22
|
+
<td class="num"><%= number_to_percentage search["conversion_rate"].to_f, precision: 0 %></td>
|
23
|
+
<td class="num"><%= search["avg_results_count"].to_f.round %></td>
|
24
|
+
</tr>
|
25
|
+
<% end %>
|
26
|
+
</tbody>
|
27
|
+
</table>
|
@@ -0,0 +1,48 @@
|
|
1
|
+
<h1><%= @search_type %> Overview</h1>
|
2
|
+
|
3
|
+
<div class="grid">
|
4
|
+
<div class="col-1-2">
|
5
|
+
<table>
|
6
|
+
<thead>
|
7
|
+
<tr>
|
8
|
+
<th>Top Searches</th>
|
9
|
+
<th class="num"><%= link_to "View all", searchjoy.searches_path(search_type: @search_type) %></th>
|
10
|
+
</tr>
|
11
|
+
</thead>
|
12
|
+
<tbody>
|
13
|
+
<% @top_searches.each do |row| %>
|
14
|
+
<tr>
|
15
|
+
<td><%= row["normalized_query"] %></td>
|
16
|
+
<td class="num"><%= row["searches_count"] %></td>
|
17
|
+
</tr>
|
18
|
+
<% end %>
|
19
|
+
</tbody>
|
20
|
+
</table>
|
21
|
+
</div>
|
22
|
+
<div class="col-1-2">
|
23
|
+
<table>
|
24
|
+
<thead>
|
25
|
+
<tr>
|
26
|
+
<th>Low Conversions</th>
|
27
|
+
<th class="num"><%= link_to "View all", searchjoy.searches_path(search_type: @search_type, sort: "conversion_rate") %></th>
|
28
|
+
</tr>
|
29
|
+
</thead>
|
30
|
+
<tbody>
|
31
|
+
<% @bad_conversion_rate.each do |row| %>
|
32
|
+
<tr>
|
33
|
+
<td><%= row["normalized_query"] %></td>
|
34
|
+
<td class="num"><%= number_to_percentage row["conversion_rate"], precision: 0 %></td>
|
35
|
+
</tr>
|
36
|
+
<% end %>
|
37
|
+
</tbody>
|
38
|
+
</table>
|
39
|
+
</div>
|
40
|
+
</div>
|
41
|
+
|
42
|
+
<h2>Conversion Rate</h2>
|
43
|
+
|
44
|
+
<%= line_chart @conversion_rate_by_week %>
|
45
|
+
|
46
|
+
<h2>Volume</h2>
|
47
|
+
|
48
|
+
<%= line_chart [{name: "Searches", data: @searches_by_week}, {name: "Conversions", data: @conversions_by_week}] %>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<% @searches.each do |search| %>
|
2
|
+
<tr>
|
3
|
+
<td style="width: 15%;">
|
4
|
+
<%= link_to search.search_type, searchjoy.overview_searches_path(search_type: search.search_type), class: "type-link type-link-#{@search_types.index(search.search_type)}" %>
|
5
|
+
</td>
|
6
|
+
<td><%= search.query %></td>
|
7
|
+
<td style="width: 35%; color: #5cb85c;">
|
8
|
+
<% if search.converted_at %>
|
9
|
+
<strong>âś“</strong>
|
10
|
+
<%= search.convertable ? (Searchjoy.conversion_name ? Searchjoy.conversion_name.call(search.convertable) : "#{search.convertable_type} #{search.convertable_id}") : "Converted" %>
|
11
|
+
<% end %>
|
12
|
+
</td>
|
13
|
+
<td style="width: 20%;" class="num">
|
14
|
+
<%= time_ago_in_words search.created_at %> ago
|
15
|
+
<div class="text-muted"><%= pluralize search.results_count, "result" %></div>
|
16
|
+
</td>
|
17
|
+
</tr>
|
18
|
+
<% end %>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<h1>Live Stream</h1>
|
2
|
+
|
3
|
+
<table id="stream"></table>
|
4
|
+
|
5
|
+
<script>
|
6
|
+
function fetchRecentSearches() {
|
7
|
+
$("#stream").load("<%= searchjoy.searches_recent_path %>");
|
8
|
+
setTimeout(fetchRecentSearches, 5 * 1000);
|
9
|
+
}
|
10
|
+
$( function() {
|
11
|
+
fetchRecentSearches();
|
12
|
+
});
|
13
|
+
</script>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# taken from https://github.com/collectiveidea/audited/blob/master/lib/generators/audited/install_generator.rb
|
2
|
+
require "rails/generators"
|
3
|
+
require "rails/generators/migration"
|
4
|
+
require "active_record"
|
5
|
+
require "rails/generators/active_record"
|
6
|
+
|
7
|
+
module Searchjoy
|
8
|
+
module Generators
|
9
|
+
class InstallGenerator < Rails::Generators::Base
|
10
|
+
include Rails::Generators::Migration
|
11
|
+
|
12
|
+
source_root File.expand_path("../templates", __FILE__)
|
13
|
+
|
14
|
+
# Implement the required interface for Rails::Generators::Migration.
|
15
|
+
def self.next_migration_number(dirname) #:nodoc:
|
16
|
+
next_migration_number = current_migration_number(dirname) + 1
|
17
|
+
if ActiveRecord::Base.timestamped_migrations
|
18
|
+
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
|
19
|
+
else
|
20
|
+
"%.3d" % next_migration_number
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def copy_migration
|
25
|
+
migration_template "install.rb", "db/migrate/install_searchjoy.rb"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :searchjoy_searches do |t|
|
4
|
+
t.string :search_type
|
5
|
+
t.string :query
|
6
|
+
t.string :normalized_query
|
7
|
+
t.integer :results_count
|
8
|
+
t.timestamp :created_at
|
9
|
+
t.integer :convertable_id
|
10
|
+
t.string :convertable_type
|
11
|
+
t.timestamp :converted_at
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :searchjoy_searches, [:created_at]
|
15
|
+
add_index :searchjoy_searches, [:search_type, :created_at]
|
16
|
+
add_index :searchjoy_searches, [:search_type, :normalized_query, :created_at], name: "index_searchjoy_searches_on_search_type_and_normalized_query_an" # autogenerated name is too long
|
17
|
+
add_index :searchjoy_searches, [:convertable_id, :convertable_type]
|
18
|
+
end
|
19
|
+
end
|
data/lib/searchjoy.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require "searchjoy/search"
|
2
|
+
require "searchjoy/track"
|
3
|
+
require "searchjoy/engine"
|
4
|
+
require "searchjoy/version"
|
5
|
+
|
6
|
+
require "chartkick"
|
7
|
+
require "groupdate"
|
8
|
+
|
9
|
+
module Searchjoy
|
10
|
+
# time zone
|
11
|
+
mattr_reader :time_zone
|
12
|
+
def self.time_zone=(time_zone)
|
13
|
+
@@time_zone = time_zone.is_a?(String) ? ActiveSupport::TimeZone.new(time_zone) : time_zone
|
14
|
+
end
|
15
|
+
|
16
|
+
# top searches
|
17
|
+
mattr_accessor :top_searches
|
18
|
+
self.top_searches = 100
|
19
|
+
|
20
|
+
# conversion name
|
21
|
+
mattr_accessor :conversion_name
|
22
|
+
end
|
23
|
+
|
24
|
+
if defined?(Searchkick)
|
25
|
+
module Searchkick
|
26
|
+
module Search
|
27
|
+
include Searchjoy::Track
|
28
|
+
|
29
|
+
alias_method :search_without_track, :search
|
30
|
+
alias_method :search, :search_with_track
|
31
|
+
end
|
32
|
+
|
33
|
+
class Results
|
34
|
+
attr_accessor :search
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Searchjoy
|
2
|
+
class Search < ActiveRecord::Base
|
3
|
+
belongs_to :convertable, polymorphic: true
|
4
|
+
|
5
|
+
before_save :set_normalized_query
|
6
|
+
|
7
|
+
def convert(convertable = nil)
|
8
|
+
if !converted?
|
9
|
+
self.converted_at = Time.now
|
10
|
+
self.convertable = convertable
|
11
|
+
save(validate: false)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def converted?
|
16
|
+
converted_at.present?
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def set_normalized_query
|
22
|
+
self.normalized_query = query.downcase if query
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Searchjoy
|
2
|
+
module Track
|
3
|
+
|
4
|
+
def search_with_track(term, options = {})
|
5
|
+
results = search_without_track(term, options)
|
6
|
+
|
7
|
+
if options[:track]
|
8
|
+
attributes = options[:track] == true ? {} : options[:track]
|
9
|
+
results.search = Searchjoy::Search.create({search_type: self.name, query: term, results_count: results.total_count}.merge(attributes))
|
10
|
+
end
|
11
|
+
|
12
|
+
results
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
data/searchjoy.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'searchjoy/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "searchjoy"
|
8
|
+
spec.version = Searchjoy::VERSION
|
9
|
+
spec.authors = ["Andrew Kane"]
|
10
|
+
spec.email = ["andrew@chartkick.com"]
|
11
|
+
spec.description = %q{Search analytics made easy}
|
12
|
+
spec.summary = %q{Search analytics made easy}
|
13
|
+
spec.homepage = "https://github.com/ankane/searchjoy"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "chartkick"
|
22
|
+
spec.add_dependency "groupdate"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: searchjoy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Kane
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: chartkick
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: groupdate
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Search analytics made easy
|
70
|
+
email:
|
71
|
+
- andrew@chartkick.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- .gitignore
|
77
|
+
- Gemfile
|
78
|
+
- LICENSE.txt
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- app/controllers/searchjoy/searches_controller.rb
|
82
|
+
- app/views/layouts/searchjoy/application.html.erb
|
83
|
+
- app/views/searchjoy/searches/index.html.erb
|
84
|
+
- app/views/searchjoy/searches/overview.html.erb
|
85
|
+
- app/views/searchjoy/searches/recent.html.erb
|
86
|
+
- app/views/searchjoy/searches/stream.html.erb
|
87
|
+
- config/routes.rb
|
88
|
+
- lib/generators/searchjoy/install_generator.rb
|
89
|
+
- lib/generators/searchjoy/templates/install.rb
|
90
|
+
- lib/searchjoy.rb
|
91
|
+
- lib/searchjoy/engine.rb
|
92
|
+
- lib/searchjoy/search.rb
|
93
|
+
- lib/searchjoy/track.rb
|
94
|
+
- lib/searchjoy/version.rb
|
95
|
+
- searchjoy.gemspec
|
96
|
+
homepage: https://github.com/ankane/searchjoy
|
97
|
+
licenses:
|
98
|
+
- MIT
|
99
|
+
metadata: {}
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - '>='
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - '>='
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
requirements: []
|
115
|
+
rubyforge_project:
|
116
|
+
rubygems_version: 2.0.0
|
117
|
+
signing_key:
|
118
|
+
specification_version: 4
|
119
|
+
summary: Search analytics made easy
|
120
|
+
test_files: []
|