hayfork 1.0.0

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
+ SHA256:
3
+ metadata.gz: f4244a8852812afa0efbd85f9d0c70e0baac4567a50ef5a4b7b24cf6c447cd0f
4
+ data.tar.gz: 8911ea611f7a67d9b7573c9b64080bdd827e3e68b23c8788d870c74033f3acdb
5
+ SHA512:
6
+ metadata.gz: 85bf188fc4e268f5ca08283071cc7c6137a5f360731a35bf82ac99c7e945be9efcca1cfa2199973cfa86d619de6ba24edac72f550c03d614bc855f9c6df1df99
7
+ data.tar.gz: d0457753638b6c4e13cd1a551dd0c8dbace0d901214dda978733133904a6a790b1195c41abddc3e06987e11efc040f3b83c6542952d45c21a11e9a67b154430b
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /Gemfile.lock
3
+ /.yardoc
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.5.3
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.3.8
5
+ - 2.5.3
6
+ - 2.6.0
7
+
8
+ sudo: required
9
+ dist: xenial
10
+ addons:
11
+ postgresql: 10
12
+
13
+ services:
14
+ - postgresql
15
+
16
+ before_install: gem install bundler -v 1.16.1
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in hayfork.gemspec
6
+ gemspec
7
+
8
+ gem "rails", "~> 5.2"
9
+
10
+ gem "pry"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Bob Lail
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # Hayfork
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/hayfork.svg)](https://rubygems.org/gems/hayfork)
4
+ [![Build Status](https://travis-ci.org/boblail/hayfork.svg)](https://travis-ci.org/boblail/hayfork)
5
+
6
+ Full-Text search for ActiveRecord and Postgres.
7
+
8
+ Hayfork generates triggers to maintain a **Haystack** of all searchable fields that Postgres can index easily and efficiently.
9
+
10
+
11
+
12
+ <br/>
13
+
14
+ ## About
15
+
16
+ #### How Hayfork works
17
+
18
+ You define the tables and fields that are to be searchable. Hayfork defines triggers that watch those tables for INSERTs, UPDATEs, and DELETEs. In response, the triggers insert, update, or delete corresponding rows in the haystack: one row per searchable field.
19
+
20
+ They Haystack has a column named `search_vector` that can be indexed, optimizing searches.
21
+
22
+ A query against the Haystack returns a list of **hits** — one result may have more than one hit (as when a search string is found in both the text and title of a book).
23
+
24
+
25
+ #### Why Hayfork?
26
+
27
+ Hayfork is designed to:
28
+
29
+ - optimize searches by:
30
+ - executing one query to search any number of fields or tables
31
+ - writing `search_vector` when hits are inserted so that the column may be indexed
32
+ - rebuild the haystack at the database level so that it works with bulk-inserted records
33
+ - support extension so, by adding metadata to a hit, you can:
34
+ - provide additional context about a result in the UI
35
+ - search only within a particular field (e.g. enable users to search `author:Potok` to find books whether the author's name includes "Potok")
36
+ - scope searches by a user or tenant or feature
37
+
38
+
39
+ <br/>
40
+
41
+ ## Setup
42
+
43
+ Generate a haystack table and model for your application.
44
+
45
+ $ rails generate hayfork:haystack
46
+
47
+ This will generate several files:
48
+
49
+ - `app/models/haystack.rb` and `db/migrate/000_create_haystack.rb` define the Haystack
50
+ - `app/models/query.rb` (and several models in the `Query` namespace) are responsible for parsing a query string and constructing the SQL to execute it.
51
+ - `lib/haystack_triggers.rb` is where you will define the tables and fields to be added to the Haystack.
52
+
53
+
54
+ <br/>
55
+
56
+ ## lib/haystack_triggers.rb
57
+
58
+ #### Basic Example
59
+
60
+ This basic example allows you to search all your employees and projects with one search box:
61
+
62
+ ```ruby
63
+ Hayfork.maintain(Haystack) do
64
+ foreach(Employee) do
65
+ insert(:full_name)
66
+ end
67
+ foreach(Project) do
68
+ insert(:title)
69
+ end
70
+ end
71
+ ```
72
+
73
+ <br/>
74
+
75
+ #### Multiple Fields
76
+
77
+ To allow finding employees by multiple traits (e.g by name, job title, or short biography), you can define multiple `insert` statements per employee:
78
+
79
+ ```ruby
80
+ Hayfork.maintain(Haystack) do
81
+ foreach(Employee) do
82
+ insert(:full_name)
83
+ insert(:position)
84
+ insert(:short_bio)
85
+ end
86
+ end
87
+ ```
88
+
89
+ <br/>
90
+
91
+ #### Scoping Searches
92
+
93
+ Additional columns on `haystack` can also be useful for scoping searches. Suppose we're maintaining a database of employees for multiple companies. We would want to scope searches by company. If we've added `company_id` to our haystack, we can populate it like this:
94
+
95
+ ```ruby
96
+ Hayfork.maintain(Haystack) do
97
+ foreach(Employee) do
98
+ set :company_id, row[:company_id]
99
+
100
+ insert(:full_name)
101
+ insert(:position)
102
+ insert(:short_bio)
103
+ end
104
+ end
105
+ ```
106
+
107
+ In this line,
108
+
109
+ ```ruby
110
+ set :company_id, row[:company_id]
111
+ ```
112
+
113
+ 1. `row` is an instance of `Arel::Table` that represents the row passed to the trigger; `row` is present in every `foreach` block.
114
+ 2. `set` assigns a value that will be inserted in the haystack for all following `insert` statements.
115
+
116
+ <br/>
117
+
118
+ #### belongs_to
119
+
120
+ If a book `belongs_to :author`, you can find the book by _either_ its title or its author's name like this:
121
+
122
+ ```ruby
123
+ Hayfork.maintain(Haystack) do
124
+ foreach(Book) do
125
+ insert(:title)
126
+ insert(author: :name)
127
+ end
128
+ end
129
+ ```
130
+
131
+ When a book is inserted, this will add an entry to the haystack for the book's title and another entry for its author's name. If `book.author_id` is changed, it'll replace the appropriate entry in the haystack; but what if `authors.name` is modified? We also need to watch the `authors` table for changes to modify the haystack:
132
+
133
+ ```ruby
134
+ Hayfork.maintain(Haystack) do
135
+ foreach(Book) do
136
+ insert(:title)
137
+ insert(author: :name)
138
+ end
139
+ foreach(Author) do
140
+ joins :books
141
+ set :search_result_type, "Book"
142
+ set :search_result_id, Book.arel_table[:id]
143
+
144
+ insert(:name)
145
+ end
146
+ end
147
+ ```
148
+
149
+ In the examples seen before, we haven't set `search_result_type` and `search_result_id`. If these values aren't defined, Hayfork assumes that the model passed to `foreach` — the table being watched — is the search result; but for an associated record, we need to explicitly declare the result. In this case, an entry is added to the haystack for every book that belongs to an author.
150
+
151
+ <br/>
152
+
153
+ #### has_many
154
+
155
+ `has_many` and `has_many :through` associations work much the same way. If an article `has_many :comments`, you can find an article by any of its comments like this:
156
+
157
+ ```ruby
158
+ Hayfork.maintain(Haystack) do
159
+ foreach(Article) do
160
+ insert(comments: :text)
161
+ end
162
+ foreach(Comment) do
163
+ joins :article
164
+ set :search_result_type, "Article"
165
+ set :search_result_id, Article.arel_table[:id]
166
+
167
+ insert(:text)
168
+ end
169
+ end
170
+ ```
171
+
172
+ <br/>
173
+
174
+ #### Rebuild Triggers
175
+
176
+ After making changes to `lib/haystack_triggers.rb` or to the default scopes of any of the models being used by the Triggers File, you'll need to replace the triggers in your database and rebuild the Haystack. Hayfork generates a migration to do that:
177
+
178
+ $ rails generate hayfork:rebuild
179
+
180
+
181
+
182
+ <br/>
183
+
184
+ ## Development
185
+
186
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
187
+
188
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
189
+
190
+ <br/>
191
+
192
+ ## Contributing
193
+
194
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cph/hayfork.
195
+
196
+ <br/>
197
+
198
+ ## License
199
+
200
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "hayfork"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/hayfork.gemspec ADDED
@@ -0,0 +1,33 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "hayfork/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "hayfork"
8
+ spec.version = Hayfork::VERSION
9
+ spec.authors = ["Bob Lail"]
10
+ spec.email = ["bob.lailfamily@gmail.com"]
11
+
12
+ spec.summary = %q{ Full-Text search for ActiveRecord and Postgres }
13
+ spec.description = %q{ Hayfork generates triggers to maintain a Haystack of all searchable fields that Postgres can index easily and efficiently }
14
+ spec.homepage = "https://github.com/cph/hayfork"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "railties", ">= 5.2.0", "< 6.0"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.16"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "minitest", "~> 5.0"
27
+ spec.add_development_dependency "minitest-reporters"
28
+ spec.add_development_dependency "minitest-reporters-turn_reporter"
29
+ spec.add_development_dependency "database_cleaner"
30
+ spec.add_development_dependency "pg"
31
+ spec.add_development_dependency "rr"
32
+ spec.add_development_dependency "shoulda-context"
33
+ end
@@ -0,0 +1,45 @@
1
+ require "rails"
2
+ require "rails/generators"
3
+ require "rails/generators/active_record"
4
+
5
+ module Hayfork
6
+ module Generators
7
+ class HaystackGenerator < ActiveRecord::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ # `argument :name` is defined in ::NamedBase,
11
+ # but we override it to provide a default value.
12
+ argument :name, type: :string, default: "haystack"
13
+
14
+ def copy_model
15
+ template "model.rb", "app/models/#{file_name}.rb"
16
+ end
17
+
18
+ def copy_migration
19
+ migration_template "migrations/create.rb", "#{db_migrate_path}/create_#{table_name}.rb", migration_version: migration_version
20
+ end
21
+
22
+ def copy_triggers
23
+ template "triggers.rb", "lib/#{file_name}_triggers.rb"
24
+ end
25
+
26
+ def copy_query_models
27
+ template "query.rb", "app/models/query.rb"
28
+ template "query/exact_phrase.rb", "app/models/query/exact_phrase.rb"
29
+ template "query/object.rb", "app/models/query/object.rb"
30
+ template "query/parser.rb", "app/models/query/parser.rb"
31
+ end
32
+
33
+ def table_name
34
+ return "haystack" if class_name == "Haystack"
35
+ super
36
+ end
37
+
38
+ def migration_version
39
+ return unless Rails::VERSION::MAJOR >= 5
40
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,49 @@
1
+ require "rails"
2
+ require "rails/generators"
3
+ require "rails/generators/active_record"
4
+
5
+ module Hayfork
6
+ module Generators
7
+ class CreateOrReplaceMigration < Rails::Generators::Actions::CreateMigration
8
+ def initialize(base, destination, data, config = {})
9
+ config[:force] = true
10
+ super
11
+ end
12
+
13
+ def identical?
14
+ false
15
+ end
16
+ end
17
+
18
+ module CreateOrReplaceMigrationConcern
19
+ def create_migration(destination, data, config = {}, &block)
20
+ action CreateOrReplaceMigration.new(self, destination, block || data.to_s, config)
21
+ end
22
+ end
23
+
24
+ class RebuildGenerator < ActiveRecord::Generators::Base
25
+ include CreateOrReplaceMigrationConcern
26
+
27
+ source_root File.expand_path("templates", __dir__)
28
+
29
+ # `argument :name` is defined in ::NamedBase,
30
+ # but we override it to provide a default value.
31
+ argument :name, type: :string, default: "haystack"
32
+
33
+ def copy_migration
34
+ migration_template "migrations/rebuild.rb", "#{db_migrate_path}/rebuild_#{table_name}.rb", migration_version: migration_version
35
+ end
36
+
37
+ def table_name
38
+ return "haystack" if class_name == "Haystack"
39
+ super
40
+ end
41
+
42
+ def migration_version
43
+ return unless Rails::VERSION::MAJOR >= 5
44
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
45
+ end
46
+
47
+ end
48
+ end
49
+ end