searchcraft 0.4.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gem_rbs_collection/actionpack/6.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/actionpack/6.0/actioncontroller.rbs +127 -0
- data/.gem_rbs_collection/actionpack/6.0/actionpack-generated.rbs +12094 -0
- data/.gem_rbs_collection/actionpack/6.0/patch.rbs +93 -0
- data/.gem_rbs_collection/actionview/6.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/actionview/6.0/actionview-generated.rbs +10768 -0
- data/.gem_rbs_collection/actionview/6.0/patch.rbs +64 -0
- data/.gem_rbs_collection/activemodel/7.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/activemodel/7.0/activemodel-7.0.rbs +1 -0
- data/.gem_rbs_collection/activemodel/7.0/activemodel-generated.rbs +4319 -0
- data/.gem_rbs_collection/activemodel/7.0/manifest.yaml +12 -0
- data/.gem_rbs_collection/activemodel/7.0/patch.rbs +20 -0
- data/.gem_rbs_collection/activerecord/7.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/activerecord/7.0/activerecord-7.0.rbs +21 -0
- data/.gem_rbs_collection/activerecord/7.0/activerecord-generated.rbs +24943 -0
- data/.gem_rbs_collection/activerecord/7.0/activerecord.rbs +563 -0
- data/.gem_rbs_collection/activerecord/7.0/manifest.yaml +12 -0
- data/.gem_rbs_collection/activerecord/7.0/patch.rbs +41 -0
- data/.gem_rbs_collection/activerecord/7.0/railties.rbs +120 -0
- data/.gem_rbs_collection/activesupport/7.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/activesupport/7.0/activesupport-7.0.rbs +51 -0
- data/.gem_rbs_collection/activesupport/7.0/activesupport-generated.rbs +12672 -0
- data/.gem_rbs_collection/activesupport/7.0/activesupport.rbs +237 -0
- data/.gem_rbs_collection/activesupport/7.0/manifest.yaml +14 -0
- data/.gem_rbs_collection/activesupport/7.0/patch.rbs +89 -0
- data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/ast/2.4/ast.rbs +73 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +26 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
- data/.gem_rbs_collection/i18n/1.10/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/i18n/1.10/backend.rbs +269 -0
- data/.gem_rbs_collection/i18n/1.10/i18n.rbs +117 -0
- data/.gem_rbs_collection/listen/3.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/listen/3.2/listen.rbs +25 -0
- data/.gem_rbs_collection/listen/3.2/listener.rbs +24 -0
- data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +2332 -0
- data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +4 -0
- data/.gem_rbs_collection/rack/2.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rack/2.2/manifest.yaml +3 -0
- data/.gem_rbs_collection/rack/2.2/rack.rbs +1433 -0
- data/.gem_rbs_collection/rails-dom-testing/2.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rails-dom-testing/2.0/rails-dom-testing.rbs +55 -0
- data/.gem_rbs_collection/railties/6.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/railties/6.0/manifest.yaml +2 -0
- data/.gem_rbs_collection/railties/6.0/patch.rbs +56 -0
- data/.gem_rbs_collection/railties/6.0/railties-generated.rbs +4858 -0
- data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rainbow/3.0/global.rbs +7 -0
- data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +209 -0
- data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +5 -0
- data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
- data/.gem_rbs_collection/rake/13.0/rake.rbs +28 -0
- data/.gem_rbs_collection/thor/1.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/thor/1.2/manifest.yaml +7 -0
- data/.gem_rbs_collection/thor/1.2/thor.rbs +15 -0
- data/.overcommit.yml +10 -27
- data/.standard.yml +1 -1
- data/CHANGELOG.md +18 -3
- data/README.md +85 -1
- data/lib/searchcraft/annotate.rb +0 -2
- data/lib/searchcraft/builder.rb +5 -2
- data/lib/searchcraft/configuration.rb +19 -0
- data/lib/searchcraft/depends_on.rb +5 -12
- data/lib/searchcraft/dump_schema.rb +0 -2
- data/lib/searchcraft/model.rb +34 -32
- data/lib/searchcraft/text_search.rb +31 -0
- data/lib/searchcraft/version.rb +1 -1
- data/lib/searchcraft/view_hash_store.rb +2 -1
- data/lib/searchcraft.rb +2 -1
- data/rbs_collection.lock.yaml +174 -0
- data/rbs_collection.yaml +21 -0
- data/sig/ext/misc.rbs +33 -0
- data/sig/manifest.yaml +5 -0
- data/sig/searchcraft/annotate.rbs +7 -0
- data/sig/searchcraft/builder.rbs +58 -0
- data/sig/searchcraft/configuration.rbs +31 -0
- data/sig/searchcraft/depends_on.rbs +17 -0
- data/sig/searchcraft/dump_schema.rbs +5 -0
- data/sig/searchcraft/model.rbs +19 -0
- data/sig/searchcraft/railtie.rbs +5 -0
- data/sig/searchcraft/text_search.rbs +11 -0
- data/sig/searchcraft/view_hash_store.rbs +17 -0
- data/sig/searchcraft.rbs +17 -1
- metadata +86 -14
- data/Rakefile +0 -14
@@ -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,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,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
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
|
-
## [
|
1
|
+
## [0.4.1] - 2023-11-01
|
2
2
|
|
3
|
-
|
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
|
-
|
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.
|
data/lib/searchcraft/annotate.rb
CHANGED
@@ -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!
|
data/lib/searchcraft/builder.rb
CHANGED
@@ -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
|
-
|
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
|