searchjoy 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in searchjoy.gemspec
4
+ gemspec
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,7 @@
1
+ Searchjoy::Engine.routes.draw do
2
+ resources :searches, only: [:index] do
3
+ get "overview", on: :collection
4
+ end
5
+ get "searches/recent"
6
+ root to: "searches#stream"
7
+ end
@@ -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,5 @@
1
+ module Searchjoy
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Searchjoy
4
+ end
5
+ 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
@@ -0,0 +1,3 @@
1
+ module Searchjoy
2
+ VERSION = "0.0.5"
3
+ 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: []