searchcraft 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.gem_rbs_collection/actionpack/6.0/.rbs_meta.yaml +9 -0
  3. data/.gem_rbs_collection/actionpack/6.0/actioncontroller.rbs +127 -0
  4. data/.gem_rbs_collection/actionpack/6.0/actionpack-generated.rbs +12094 -0
  5. data/.gem_rbs_collection/actionpack/6.0/patch.rbs +93 -0
  6. data/.gem_rbs_collection/actionview/6.0/.rbs_meta.yaml +9 -0
  7. data/.gem_rbs_collection/actionview/6.0/actionview-generated.rbs +10768 -0
  8. data/.gem_rbs_collection/actionview/6.0/patch.rbs +64 -0
  9. data/.gem_rbs_collection/activemodel/7.0/.rbs_meta.yaml +9 -0
  10. data/.gem_rbs_collection/activemodel/7.0/activemodel-7.0.rbs +1 -0
  11. data/.gem_rbs_collection/activemodel/7.0/activemodel-generated.rbs +4319 -0
  12. data/.gem_rbs_collection/activemodel/7.0/manifest.yaml +12 -0
  13. data/.gem_rbs_collection/activemodel/7.0/patch.rbs +20 -0
  14. data/.gem_rbs_collection/activerecord/7.0/.rbs_meta.yaml +9 -0
  15. data/.gem_rbs_collection/activerecord/7.0/activerecord-7.0.rbs +21 -0
  16. data/.gem_rbs_collection/activerecord/7.0/activerecord-generated.rbs +24943 -0
  17. data/.gem_rbs_collection/activerecord/7.0/activerecord.rbs +563 -0
  18. data/.gem_rbs_collection/activerecord/7.0/manifest.yaml +12 -0
  19. data/.gem_rbs_collection/activerecord/7.0/patch.rbs +41 -0
  20. data/.gem_rbs_collection/activerecord/7.0/railties.rbs +120 -0
  21. data/.gem_rbs_collection/activesupport/7.0/.rbs_meta.yaml +9 -0
  22. data/.gem_rbs_collection/activesupport/7.0/activesupport-7.0.rbs +51 -0
  23. data/.gem_rbs_collection/activesupport/7.0/activesupport-generated.rbs +12672 -0
  24. data/.gem_rbs_collection/activesupport/7.0/activesupport.rbs +237 -0
  25. data/.gem_rbs_collection/activesupport/7.0/manifest.yaml +14 -0
  26. data/.gem_rbs_collection/activesupport/7.0/patch.rbs +89 -0
  27. data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +9 -0
  28. data/.gem_rbs_collection/ast/2.4/ast.rbs +73 -0
  29. data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
  30. data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +26 -0
  31. data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
  32. data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
  33. data/.gem_rbs_collection/i18n/1.10/.rbs_meta.yaml +9 -0
  34. data/.gem_rbs_collection/i18n/1.10/backend.rbs +269 -0
  35. data/.gem_rbs_collection/i18n/1.10/i18n.rbs +117 -0
  36. data/.gem_rbs_collection/listen/3.2/.rbs_meta.yaml +9 -0
  37. data/.gem_rbs_collection/listen/3.2/listen.rbs +25 -0
  38. data/.gem_rbs_collection/listen/3.2/listener.rbs +24 -0
  39. data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +9 -0
  40. data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +2332 -0
  41. data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +4 -0
  42. data/.gem_rbs_collection/rack/2.2/.rbs_meta.yaml +9 -0
  43. data/.gem_rbs_collection/rack/2.2/manifest.yaml +3 -0
  44. data/.gem_rbs_collection/rack/2.2/rack.rbs +1433 -0
  45. data/.gem_rbs_collection/rails-dom-testing/2.0/.rbs_meta.yaml +9 -0
  46. data/.gem_rbs_collection/rails-dom-testing/2.0/rails-dom-testing.rbs +55 -0
  47. data/.gem_rbs_collection/railties/6.0/.rbs_meta.yaml +9 -0
  48. data/.gem_rbs_collection/railties/6.0/manifest.yaml +2 -0
  49. data/.gem_rbs_collection/railties/6.0/patch.rbs +56 -0
  50. data/.gem_rbs_collection/railties/6.0/railties-generated.rbs +4858 -0
  51. data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +9 -0
  52. data/.gem_rbs_collection/rainbow/3.0/global.rbs +7 -0
  53. data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +209 -0
  54. data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +5 -0
  55. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
  56. data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
  57. data/.gem_rbs_collection/rake/13.0/rake.rbs +28 -0
  58. data/.gem_rbs_collection/thor/1.2/.rbs_meta.yaml +9 -0
  59. data/.gem_rbs_collection/thor/1.2/manifest.yaml +7 -0
  60. data/.gem_rbs_collection/thor/1.2/thor.rbs +15 -0
  61. data/.overcommit.yml +10 -27
  62. data/.standard.yml +1 -1
  63. data/CHANGELOG.md +18 -3
  64. data/README.md +85 -1
  65. data/lib/searchcraft/annotate.rb +0 -2
  66. data/lib/searchcraft/builder.rb +5 -2
  67. data/lib/searchcraft/configuration.rb +19 -0
  68. data/lib/searchcraft/depends_on.rb +5 -12
  69. data/lib/searchcraft/dump_schema.rb +0 -2
  70. data/lib/searchcraft/model.rb +34 -32
  71. data/lib/searchcraft/text_search.rb +31 -0
  72. data/lib/searchcraft/version.rb +1 -1
  73. data/lib/searchcraft/view_hash_store.rb +2 -1
  74. data/lib/searchcraft.rb +2 -1
  75. data/rbs_collection.lock.yaml +174 -0
  76. data/rbs_collection.yaml +21 -0
  77. data/sig/ext/misc.rbs +33 -0
  78. data/sig/manifest.yaml +5 -0
  79. data/sig/searchcraft/annotate.rbs +7 -0
  80. data/sig/searchcraft/builder.rbs +58 -0
  81. data/sig/searchcraft/configuration.rbs +31 -0
  82. data/sig/searchcraft/depends_on.rbs +17 -0
  83. data/sig/searchcraft/dump_schema.rbs +5 -0
  84. data/sig/searchcraft/model.rbs +19 -0
  85. data/sig/searchcraft/railtie.rbs +5 -0
  86. data/sig/searchcraft/text_search.rbs +11 -0
  87. data/sig/searchcraft/view_hash_store.rbs +17 -0
  88. data/sig/searchcraft.rbs +17 -1
  89. metadata +86 -14
  90. data/Rakefile +0 -14
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: rainbow
3
+ version: '3.0'
4
+ source:
5
+ type: git
6
+ name: ruby/gem_rbs_collection
7
+ revision: 3e93a59bd5da185b68187991630ac9a54a2468a7
8
+ remote: https://github.com/ruby/gem_rbs_collection.git
9
+ repo_dir: gems
@@ -0,0 +1,7 @@
1
+ module Rainbow
2
+ def self.enabled: () -> bool
3
+
4
+ def self.enabled=: (bool value) -> bool
5
+
6
+ def self.uncolor: (String string) -> String
7
+ end
@@ -0,0 +1,209 @@
1
+ module Rainbow
2
+ class Presenter < String
3
+ # Sets color of this text.
4
+ def color: (*Symbol values) -> instance
5
+
6
+ alias foreground color
7
+
8
+ alias fg color
9
+
10
+ # Sets background color of this text.
11
+ def background: (*Symbol values) -> instance
12
+
13
+ alias bg background
14
+
15
+ # Resets terminal to default colors/backgrounds.
16
+ #
17
+ # It shouldn't be needed to use this method because all methods
18
+ # append terminal reset code to end of string.
19
+ def reset: () -> instance
20
+
21
+ # Turns on bright/bold for this text.
22
+ def bright: () -> instance
23
+
24
+ alias bold bright
25
+
26
+ # Turns on faint/dark for this text (not well supported by terminal
27
+ # emulators).
28
+ def faint: () -> instance
29
+
30
+ # Turns on italic style for this text (not well supported by terminal
31
+ # emulators).
32
+ def italic: () -> instance
33
+
34
+ # Turns on underline decoration for this text.
35
+ def underline: () -> instance
36
+
37
+ # Turns on blinking attribute for this text (not well supported by terminal
38
+ # emulators).
39
+ def blink: () -> instance
40
+
41
+ # Inverses current foreground/background colors.
42
+ def inverse: () -> instance
43
+
44
+ # Hides this text (set its color to the same as background).
45
+ def hide: () -> instance
46
+
47
+ def black: () -> instance
48
+
49
+ def red: () -> instance
50
+
51
+ def green: () -> instance
52
+
53
+ def yellow: () -> instance
54
+
55
+ def blue: () -> instance
56
+
57
+ def magenta: () -> instance
58
+
59
+ def cyan: () -> instance
60
+
61
+ def white: () -> instance
62
+
63
+ # We take care of X11 color method call here.
64
+ # Such as #aqua, #ghostwhite.
65
+ def method_missing: (untyped method_name, *untyped args) -> untyped
66
+
67
+ def respond_to_missing?: (untyped method_name, *untyped args) -> bool
68
+
69
+ def wrap_with_sgr: (untyped codes) -> instance
70
+
71
+ def aliceblue: () -> instance
72
+ def antiquewhite: () -> instance
73
+ def aqua: () -> instance
74
+ def aquamarine: () -> instance
75
+ def azure: () -> instance
76
+ def beige: () -> instance
77
+ def bisque: () -> instance
78
+ def blanchedalmond: () -> instance
79
+ def blueviolet: () -> instance
80
+ def brown: () -> instance
81
+ def burlywood: () -> instance
82
+ def cadetblue: () -> instance
83
+ def chartreuse: () -> instance
84
+ def chocolate: () -> instance
85
+ def coral: () -> instance
86
+ def cornflower: () -> instance
87
+ def cornsilk: () -> instance
88
+ def crimson: () -> instance
89
+ def darkblue: () -> instance
90
+ def darkcyan: () -> instance
91
+ def darkgoldenrod: () -> instance
92
+ def darkgray: () -> instance
93
+ def darkgreen: () -> instance
94
+ def darkkhaki: () -> instance
95
+ def darkmagenta: () -> instance
96
+ def darkolivegreen: () -> instance
97
+ def darkorange: () -> instance
98
+ def darkorchid: () -> instance
99
+ def darkred: () -> instance
100
+ def darksalmon: () -> instance
101
+ def darkseagreen: () -> instance
102
+ def darkslateblue: () -> instance
103
+ def darkslategray: () -> instance
104
+ def darkturquoise: () -> instance
105
+ def darkviolet: () -> instance
106
+ def deeppink: () -> instance
107
+ def deepskyblue: () -> instance
108
+ def dimgray: () -> instance
109
+ def dodgerblue: () -> instance
110
+ def firebrick: () -> instance
111
+ def floralwhite: () -> instance
112
+ def forestgreen: () -> instance
113
+ def fuchsia: () -> instance
114
+ def gainsboro: () -> instance
115
+ def ghostwhite: () -> instance
116
+ def gold: () -> instance
117
+ def goldenrod: () -> instance
118
+ def gray: () -> instance
119
+ def greenyellow: () -> instance
120
+ def honeydew: () -> instance
121
+ def hotpink: () -> instance
122
+ def indianred: () -> instance
123
+ def indigo: () -> instance
124
+ def ivory: () -> instance
125
+ def khaki: () -> instance
126
+ def lavender: () -> instance
127
+ def lavenderblush: () -> instance
128
+ def lawngreen: () -> instance
129
+ def lemonchiffon: () -> instance
130
+ def lightblue: () -> instance
131
+ def lightcoral: () -> instance
132
+ def lightcyan: () -> instance
133
+ def lightgoldenrod: () -> instance
134
+ def lightgray: () -> instance
135
+ def lightgreen: () -> instance
136
+ def lightpink: () -> instance
137
+ def lightsalmon: () -> instance
138
+ def lightseagreen: () -> instance
139
+ def lightskyblue: () -> instance
140
+ def lightslategray: () -> instance
141
+ def lightsteelblue: () -> instance
142
+ def lightyellow: () -> instance
143
+ def lime: () -> instance
144
+ def limegreen: () -> instance
145
+ def linen: () -> instance
146
+ def maroon: () -> instance
147
+ def mediumaquamarine: () -> instance
148
+ def mediumblue: () -> instance
149
+ def mediumorchid: () -> instance
150
+ def mediumpurple: () -> instance
151
+ def mediumseagreen: () -> instance
152
+ def mediumslateblue: () -> instance
153
+ def mediumspringgreen: () -> instance
154
+ def mediumturquoise: () -> instance
155
+ def mediumvioletred: () -> instance
156
+ def midnightblue: () -> instance
157
+ def mintcream: () -> instance
158
+ def mistyrose: () -> instance
159
+ def moccasin: () -> instance
160
+ def navajowhite: () -> instance
161
+ def navyblue: () -> instance
162
+ def oldlace: () -> instance
163
+ def olive: () -> instance
164
+ def olivedrab: () -> instance
165
+ def orange: () -> instance
166
+ def orangered: () -> instance
167
+ def orchid: () -> instance
168
+ def palegoldenrod: () -> instance
169
+ def palegreen: () -> instance
170
+ def paleturquoise: () -> instance
171
+ def palevioletred: () -> instance
172
+ def papayawhip: () -> instance
173
+ def peachpuff: () -> instance
174
+ def peru: () -> instance
175
+ def pink: () -> instance
176
+ def plum: () -> instance
177
+ def powderblue: () -> instance
178
+ def purple: () -> instance
179
+ def rebeccapurple: () -> instance
180
+ def rosybrown: () -> instance
181
+ def royalblue: () -> instance
182
+ def saddlebrown: () -> instance
183
+ def salmon: () -> instance
184
+ def sandybrown: () -> instance
185
+ def seagreen: () -> instance
186
+ def seashell: () -> instance
187
+ def sienna: () -> instance
188
+ def silver: () -> instance
189
+ def skyblue: () -> instance
190
+ def slateblue: () -> instance
191
+ def slategray: () -> instance
192
+ def snow: () -> instance
193
+ def springgreen: () -> instance
194
+ def steelblue: () -> instance
195
+ def tan: () -> instance
196
+ def teal: () -> instance
197
+ def thistle: () -> instance
198
+ def tomato: () -> instance
199
+ def turquoise: () -> instance
200
+ def violet: () -> instance
201
+ def webgray: () -> instance
202
+ def webgreen: () -> instance
203
+ def webmaroon: () -> instance
204
+ def webpurple: () -> instance
205
+ def wheat: () -> instance
206
+ def whitesmoke: () -> instance
207
+ def yellowgreen: () -> instance
208
+ end
209
+ end
@@ -0,0 +1,5 @@
1
+ class Object
2
+ private
3
+
4
+ def Rainbow: (String) -> Rainbow::Presenter
5
+ end
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: rake
3
+ version: '13.0'
4
+ source:
5
+ type: git
6
+ name: ruby/gem_rbs_collection
7
+ revision: 3e93a59bd5da185b68187991630ac9a54a2468a7
8
+ remote: https://github.com/ruby/gem_rbs_collection.git
9
+ repo_dir: gems
@@ -0,0 +1,2 @@
1
+ dependencies:
2
+ - name: fileutils
@@ -0,0 +1,28 @@
1
+ module Rake
2
+ class TaskLib
3
+ include Rake::DSL
4
+ end
5
+
6
+ module DSL
7
+ private
8
+
9
+ include FileUtils
10
+
11
+ def desc: (String description) -> void
12
+ def directory: (*untyped args) ?{ () -> void } -> void
13
+ def file: (*untyped args) ?{ () -> void } -> void
14
+ def file_create: (*untyped args) ?{ () -> void } -> void
15
+ def import: (*String fns) -> void
16
+ def multitask: (*untyped args) ?{ () -> void } -> void
17
+ def namespace: (?untyped name) ?{ () -> void } -> void
18
+ def rule: (*untyped args) ?{ () -> void } -> void
19
+ def task: (*untyped args) ?{ () -> void } -> void
20
+ end
21
+ end
22
+
23
+ module FileUtils
24
+ def sh: (*String cmd, **untyped options) ?{ (bool, Process::Status) -> void } -> void
25
+ def ruby: (*String args, **untyped options) ?{ (bool, Process::Status) -> void } -> void
26
+ def safe_ln: (*untyped args, **untyped options) -> void
27
+ def split_all: (String path) -> Array[String]
28
+ end
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: thor
3
+ version: '1.2'
4
+ source:
5
+ type: git
6
+ name: ruby/gem_rbs_collection
7
+ revision: 3e93a59bd5da185b68187991630ac9a54a2468a7
8
+ remote: https://github.com/ruby/gem_rbs_collection.git
9
+ repo_dir: gems
@@ -0,0 +1,7 @@
1
+ # manifest.yaml describes dependencies which do not appear in the gemspec.
2
+ # If this gem includes such dependencies, comment-out the following lines and
3
+ # declare the dependencies.
4
+ # If all dependencies appear in the gemspec, you should remove this file.
5
+ #
6
+ # dependencies:
7
+ # - name: pathname
@@ -0,0 +1,15 @@
1
+ class Thor
2
+ class Group
3
+ end
4
+
5
+ module Actions
6
+ class CreateFile
7
+ end
8
+
9
+ def create_file: (String destination, String data, ?verbose: bool) -> String
10
+ | (String destination, ?verbose: bool) { () -> String } -> String
11
+ end
12
+
13
+ class Error
14
+ end
15
+ end
data/.overcommit.yml CHANGED
@@ -18,10 +18,20 @@
18
18
  verify_signatures: false
19
19
 
20
20
  PreCommit:
21
+ Signatures:
22
+ enabled: true
23
+ required: true
24
+ command: ['bundle', 'exec', 'steep', 'check']
25
+ flags: []
21
26
  StandardRB:
22
27
  enabled: true
23
28
  required: true
24
29
  command: ['bundle', 'exec', 'standardrb'] # Invoke within Bundler context
30
+ Tests:
31
+ enabled: true
32
+ required: true
33
+ command: ['bundle', 'exec', 'rake', 'test']
34
+ flags: []
25
35
  ErbLint:
26
36
  enabled: true
27
37
  required: true
@@ -32,30 +42,3 @@ PreCommit:
32
42
  '--enable-linters',
33
43
  'space_around_erb_tag,extra_newline',
34
44
  ]
35
- # RustyWind:
36
- # enabled: true
37
- # required: true
38
- # command: ['yarn', 'run', 'rustywind-fix']
39
-
40
- # PrePush:
41
- # RSpec:
42
- # enabled: true
43
- # required: true
44
- # command: ['bundle', 'exec', 'rspec']
45
-
46
- #PreCommit:
47
- # RuboCop:
48
- # enabled: true
49
- # on_warn: fail # Treat all warnings as failures
50
- #
51
- # TrailingWhitespace:
52
- # enabled: true
53
- # exclude:
54
- # - '**/db/structure.sql' # Ignore trailing whitespace in generated files
55
- #
56
- #PostCheckout:
57
- # ALL: # Special hook name that customizes all hooks of this type
58
- # quiet: true # Change all post-checkout hooks to only display output on failure
59
- #
60
- # IndexTags:
61
- # enabled: true # Generate a tags file with `ctags` each time HEAD changes
data/.standard.yml CHANGED
@@ -1,3 +1,3 @@
1
1
  # For available configuration options, see:
2
2
  # https://github.com/testdouble/standard
3
- ruby_version: 2.6
3
+ ruby_version: 3.0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
- ## [Unreleased]
1
+ ## [0.4.1] - 2023-11-01
2
2
 
3
- ## [0.1.0] - 2023-08-19
3
+ - [`SearchCraft::TextSearch`](lib/searchcraft/text_search.rb) module added to `Builder` which provides `tsvector` helpers. See [`test_text_search.rb`](test/searchcraft/builder/test_text_search.rb) for examples.
4
+ - RBS type signatures
5
+ - Convenience `Config.explicit_builder_classes = {"Builder" => "Model"}` if you need to explicitly describe your Builder + Model classes and they are 1:1
6
+ - Override default table name for `ViewHashStore` with:
4
7
 
5
- - Initial release
8
+ ```ruby
9
+ SearchCraft.configure do |config|
10
+ config.view_hash_store_table_name = "'my_schema'.'my_table_name'"`
11
+ end
12
+ ```
13
+
14
+ Fixes:
15
+
16
+ - Fixes for users of `SearchCraft.config.explicit_model_class_names` on initialization
17
+
18
+ ## [0.4.0] - 2023-10-24
19
+
20
+ - Initial public release
data/README.md CHANGED
@@ -18,7 +18,7 @@ See [demo app](https://ykdnr.hatchboxapp.com/searchcraft/products?category_id=54
18
18
 
19
19
  Add lightning quick search capabilities to your Rails apps without external systems like ElasticSearch. It's now magically simple to craft the ActiveRecord/Arel expressions we already know and love, and convert them into SQL materialized views: ready to be queried and composed with ActiveRecord. Everything you love about Rails, but faster.
20
20
 
21
- **What makes Rails slow for search?** Large tables, lots of joins, subqueries, missing or unused indexes, and complex queries.
21
+ **What makes Rails slow for search?** Large tables, lots of joins, subqueries, missing or unused indexes, and complex queries. Also slow? Coordinating data from multiple external systems through Ruby to produce search results.
22
22
 
23
23
  SearchCraft makes it trivial to write and use powerful **SQL materialized views** to pre-calculate the results of your search and reporting queries. It's like a database index, but for complex queries.
24
24
 
@@ -134,6 +134,88 @@ Number.all
134
134
  [#<Number number: 1>, #<Number number: 2>, #<Number number: 3>, #<Number number: 4>, #<Number number: 5>]
135
135
  ```
136
136
 
137
+ ### Indexes
138
+
139
+ A wonderful feature of materialized views is you can add indexes to them; even unique indexes.
140
+
141
+ Currently the mechanism for adding indexes is to add a `view_indexes` method to your builder class.
142
+
143
+ For example, we can add a unique index on `NumberBuilder`'s `number` column:
144
+
145
+ ```ruby
146
+ class NumberBuilder < SearchCraft::Builder
147
+ def view_indexes
148
+ {
149
+ number: {columns: ["number"], unique: true}
150
+ }
151
+ end
152
+ ```
153
+
154
+ Or several indexes on the `ProductSearchBuilder` from earlier:
155
+
156
+ ```ruby
157
+ class ProductSearchBuilder < SearchCraft::Builder
158
+ def view_indexes
159
+ {
160
+ id: {columns: ["product_id"], unique: true},
161
+ product_name: {columns: ["product_name"]},
162
+ reviews_count: {columns: ["reviews_count"]},
163
+ reviews_average: {columns: ["reviews_average"]}
164
+ }
165
+ end
166
+ end
167
+ ```
168
+
169
+ By default the indexes will be `using: :btree` indexing method. You can also use other indexing methods available in rails, such as `:gin`, `:gist`, or if you're using the `trigram` extension you can use `:gin_trgm_ops`. These can be useful when you're looking at setting up text search, as discussed below.
170
+
171
+ ### Search
172
+
173
+ Another benefit of materialized views is we can create columns that are optimised for search. For example above, since we've precalculated the `reviews_average` in `ProductSearchBuilder`, we can easily find products with a certain average rating.
174
+
175
+ ```ruby
176
+ ProductSearch.where("reviews_average > 4")
177
+ ```
178
+
179
+ ### Associations
180
+
181
+ A fabulous feature of ActiveRecord is the ability to join queries together. Since our materialized views are native ActiveRecord models, we can join them together with other queries.
182
+
183
+ Let's setup an association between our MV's `ProductSearch#product_id` and the table `Product#id` primary key:
184
+
185
+ ```ruby
186
+ class ProductSearch < ActiveRecord::Base
187
+ include SearchCraft::Model
188
+
189
+ belongs_to :product, foreign_key: :product_id, primary_key: :id
190
+ end
191
+ ```
192
+
193
+ We can now join, or eager load, the tables together with ActiveRecord queries. To following returns a relation of `ProductSearch` objects, with each of their `ProductSearch#product` association preloaded.
194
+
195
+ ```ruby
196
+ ProductSearch.includes(:product).where("reviews_average > 4")
197
+ ```
198
+
199
+ The following returns `Product` objects, based on seaching the `ProductSearch` materialized view:
200
+
201
+ ```ruby
202
+ class Product
203
+ has_one :product_search, foreign_key: :product_id, primary_key: :id, class_name: "ProductSearch"
204
+ end
205
+
206
+ Product.joins(:product_searches).merge(
207
+ ProductSearch.where("reviews_average > 4")
208
+ )
209
+ ```
210
+
211
+ ### Text search
212
+
213
+ PostgreSQL comes with a solution for [text search](https://www.postgresql.org/docs/current/textsearch.html) using a combination of functions such as `to_tsvector`, `ts_rank`, and `websearch_to_tsquery`.
214
+
215
+ Pending more docs, see the [`test/searchcraft/builder/test_text_search.rb`](test/searchcraft/builder/test_text_search.rb) for an example of how to use these functions in your materialized views.
216
+
217
+ I'm still working on extracting this solution from our code at [Store Connect](https://getstoreconnect.com).
218
+
137
219
  ### Dependencies between views
138
220
 
139
221
  Once you have one SearchCraft materialized view, you might want to create another that depends upon it. You can do this too with the `depends_on` method.
@@ -383,6 +465,7 @@ SearchCraft.load_tasks
383
465
  * Annotates models whenever materialized view is updated, if `annotate` gem is installed
384
466
  * Namespaced models/builders will use the full namesapce + classname for the materialized view name
385
467
  * Rake tasks to refresh all materialized views `rake searchcraft:refresh`, and check if any views need to be recreated `rake searchcraft:rebuild`
468
+ * Rubygem contains RBS type signatures
386
469
 
387
470
  ## Development
388
471
 
@@ -419,5 +502,6 @@ Everyone interacting in the Searchcraft project's codebases, issue trackers, cha
419
502
 
420
503
  ## Credits
421
504
 
505
+ * Thanks to [Store Connect](https://getstoreconnect.com/) for assisting the ideation and development of this project.
422
506
  * [scenic](https://github.com/scenic-views/scenic) gem first allowed me to use materialized views in Rails, but I was iterating on my view schema so frequently that their migration approach - `rails db:rollback`, rebuild migration SQL, `rails db:migrate`, and then test - became slow. It also introduced bugs - I would forget to run the steps, and then see odd behaviour. If you have relatively static views or materialized views, and want to use Rails migrations, please try out `scenic` gem. This `searchcraft` gem still depends on `scenic` for its view `refresh` feature, and adding views into `schema.rb`.
423
507
  * [activerecord](https://github.com/rails/rails) has been one of the most wonderful gifts to the universe since its inception. As a bonus, it allowed me to become "Dr Nic" in 2006 when I performed silly tricks with it in a rubygem called "Dr Nic's Magic Models". I've made many dear friends and had a wonderful career since those days.
@@ -1,6 +1,4 @@
1
1
  module SearchCraft::Annotate
2
- extend ActiveSupport::Concern
3
-
4
2
  # If using annotate gem, then automatically annotate models after rebuilding views
5
3
  # TODO: I'm suspicious this is not working for dependent Builders, e.g. demo_app's OnsaleSearchBuilder
6
4
  def annotate_models!
@@ -1,7 +1,9 @@
1
1
  class SearchCraft::Builder
2
2
  extend SearchCraft::Annotate
3
3
  include SearchCraft::DependsOn
4
+ extend SearchCraft::DependsOn::ClassMethods
4
5
  include SearchCraft::DumpSchema
6
+ include SearchCraft::TextSearch
5
7
 
6
8
  # Subclass must implement view_scope or view_select_sql
7
9
  def view_scope
@@ -75,7 +77,7 @@ class SearchCraft::Builder
75
77
  Dir.glob("#{load_path}/**/*.rb").each do |file|
76
78
  File.readlines(file).each do |line|
77
79
  if (match = line.match(/class\s+([\w:]+)\s*<\s*#{potential_superclass_regex}/))
78
- class_name = match[1]
80
+ class_name = match[1].to_s
79
81
  warn "Found #{class_name} in #{file}" unless known_subclass_names.include?(class_name)
80
82
  subclass_names << class_name
81
83
  end
@@ -119,6 +121,7 @@ class SearchCraft::Builder
119
121
  end
120
122
  return unless dependencies_ready?
121
123
 
124
+ @@dependencies ||= {}
122
125
  dependencies_changed = (@@dependencies[self.class.name] || []) & builders_changed.map(&:name)
123
126
  return false unless dependencies_changed.any? ||
124
127
  SearchCraft::ViewHashStore.changed?(builder: self)
@@ -176,7 +179,7 @@ class SearchCraft::Builder
176
179
 
177
180
  # ProductSearchBuilder name becomes product_searches
178
181
  def base_sql_name
179
- self.class.name.gsub(/Builder$/, "").tableize.tr("/", "_")
182
+ self.class.name.to_s.gsub(/Builder$/, "").tableize.tr("/", "_")
180
183
  end
181
184
 
182
185
  def base_idx_name
@@ -2,11 +2,30 @@ module SearchCraft
2
2
  class Configuration
3
3
  attr_accessor :disable_autorebuild
4
4
  attr_accessor :debug
5
+ attr_reader :explicit_builder_classes
5
6
  attr_accessor :explicit_builder_class_names
6
7
  attr_accessor :explicit_model_class_names
8
+ attr_reader :view_hash_store_table_name
7
9
 
8
10
  def autorebuild?
9
11
  !disable_autorebuild
10
12
  end
13
+
14
+ # If you need to explicitly list the builder + model classes you want to use,
15
+ # then set this to a hash of builder class names => model class names.
16
+ # {
17
+ # "Search::Builder::ContentArticleSearchBuilder" => "Search::ContentArticleSearch",
18
+ # "Search::Builder::ContentPageSearchBuilder" => "Search::ContentPageSearch"
19
+ # }
20
+ def explicit_builder_classes=(builders_and_models)
21
+ @explicit_builder_classes = builders_and_models
22
+ @explicit_builder_class_names = builders_and_models.keys
23
+ @explicit_model_class_names = builders_and_models.values
24
+ end
25
+
26
+ def view_hash_store_table_name=(table_name)
27
+ @view_hash_store_table_name = table_name
28
+ SearchCraft::ViewHashStore.table_name = table_name
29
+ end
11
30
  end
12
31
  end
@@ -1,20 +1,11 @@
1
1
  module SearchCraft::DependsOn
2
- extend ActiveSupport::Concern
3
-
4
- class_methods do
2
+ module ClassMethods
5
3
  @@dependencies = {}
6
4
 
7
5
  def depends_on(*builder_names)
8
6
  @@dependencies[name] = builder_names
9
7
  end
10
8
 
11
- # TODO: implement .add_index instead of #view_indexes below
12
- def add_index(index_name, columns, unique: false, name: nil)
13
- @indexes ||= {}
14
- # TODO: also get indexes from @@dependencies[name]
15
- @indexes[index_name] = {columns: columns, unique: unique, name: name}
16
- end
17
-
18
9
  def sort_builders_by_dependency
19
10
  sorted = []
20
11
  visited = {}
@@ -27,7 +18,7 @@ module SearchCraft::DependsOn
27
18
  end
28
19
 
29
20
  def visit(builder, visited, sorted)
30
- return if visited[builder.name]
21
+ return if visited[builder.name.to_s]
31
22
 
32
23
  dependency_names = @@dependencies[builder.name] || []
33
24
  dependency_names.each do |dependency_name|
@@ -35,8 +26,10 @@ module SearchCraft::DependsOn
35
26
  visit(dependency, visited, sorted)
36
27
  end
37
28
 
38
- visited[builder.name] = true
29
+ visited[builder.name.to_s] = true
39
30
  sorted << builder
40
31
  end
41
32
  end
33
+
34
+ extend ClassMethods
42
35
  end
@@ -1,6 +1,4 @@
1
1
  module SearchCraft::DumpSchema
2
- extend ActiveSupport::Concern
3
-
4
2
  # If in Rails, dump schema.rb after rebuilding views
5
3
  def dump_schema!
6
4
  return unless Rails.env.development?