searchjoy 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|