simplec 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3e3189399a08bf6dc14a452311b79f7f48eb6a16
4
- data.tar.gz: 179e5ac787ed6456ac46282808591748ba10e560
3
+ metadata.gz: 96f4e9c1f6b7ebca90065eaafc12867bd2402760
4
+ data.tar.gz: bdbd5d66111b3afa442c3cb9a1d84cd25bc7387a
5
5
  SHA512:
6
- metadata.gz: 61dcc253ee7661aaea00164e559700fedd731d5932049fb9f21e285a65fd4840e35a3c1dbc84f433cd79bf61611cf75b0bffe7abd6876a4db7ce63774dba7f72
7
- data.tar.gz: 8d0115591036074ab888821f17b04fdfdd2692bb941273a8329fa61d25104763ea1630215a06b5fac7ed714bd1ea9bfff5b1a44f93f05a1d1589f825cb51fa1e
6
+ metadata.gz: 2e60a95279a8c1aebbb8cdbfd2a1c17f8f66f8d6598a6d6fd307342a17597f95431a60bfccadf100f0c5df526bb0bc9d8b930741e92218791bcf0617f816ba31
7
+ data.tar.gz: 3a267018544e4d5c9c5990beee28b1d694ce24055d70f7156481830a2c0de3a825d58ccebfbef14f787e3dc4a33b429b94703b8c83bb640393b05e067dbc9ca2
@@ -25,6 +25,7 @@ module Simplec
25
25
  end
26
26
 
27
27
  def validate_slug!
28
+ return if self.slug.nil?
28
29
  similar = self.class.where(slug: self.slug).where.not(id: self.id)
29
30
  return if similar.size == 0
30
31
  return if (
@@ -55,6 +55,7 @@ module Simplec
55
55
  before_validation :match_parent_subdomain
56
56
  before_validation :build_path
57
57
  after_save :link_embedded_images!
58
+ after_save :index!
58
59
 
59
60
  # @!attribute slug
60
61
  # The value is normalized to a string starting without a leading slash
@@ -86,6 +87,37 @@ module Simplec
86
87
  # know what you are doing.
87
88
  # @return [JSON]
88
89
 
90
+ # @!method search(term, options={})
91
+ #
92
+ # If the term is nil or blank, all results will be returned.
93
+ #
94
+ # @param term [String]
95
+ # @param options [Hash]
96
+ # @option options [Class, Array, String] :types
97
+ # A Class or Array of Classes (of page types, typically `Page::Home`) to
98
+ # limit results to.
99
+ # @option options [Symbol, String] all other options
100
+ # All other options are matched to the `query` JSONB field. These are
101
+ # all direct matches on the indexed `query` field. If you want to do
102
+ # anything more complicated, append it to the scope returned.
103
+ #
104
+ # @return [ActiveRecord::Relation] a relation for the query
105
+ # @!scope class
106
+ scope :search, ->(term, options={}) {
107
+ _types = Array(options.delete(:types))
108
+
109
+ query = all
110
+ query = query.where(type: _types) if _types.any?
111
+ options.each { |k,v| query = query.where("query->>:k = :v", k: k, v: v) }
112
+
113
+ if term.blank?
114
+ query
115
+ else
116
+ tsq = tsquery term
117
+ query.where("tsv @@ #{tsq}").order("ts_rank_cd(tsv, #{tsq}) DESC")
118
+ end
119
+ }
120
+
89
121
  # Define a field on the page
90
122
  #
91
123
  # There is as template for each type for customization located in:
@@ -140,12 +172,86 @@ module Simplec
140
172
  _fields = case type
141
173
  when :file
142
174
  fields.select {|k, v| FILE_FIELDS.member?(v[:type])}
175
+ when :textual
176
+ fields.select {|k, v| !FILE_FIELDS.member?(v[:type])}
143
177
  else
144
178
  fields
145
179
  end
146
180
  _fields.keys
147
181
  end
148
182
 
183
+ # Set extra attributes on the record for querying.
184
+ #
185
+ # @example set attributes
186
+ # class Page::Home < Page
187
+ # has_many :tags
188
+ #
189
+ # field :category
190
+ #
191
+ # # Where category is a Simplec::Page::field and tags is a defined
192
+ # # method.
193
+ # search_query_attributes! :category, :tags
194
+ #
195
+ # def tags
196
+ # self.tags.pluck(:name)
197
+ # end
198
+ # end
199
+ #
200
+ # # Built-in matching
201
+ # Page.search('foo', category: 'how-to')
202
+ #
203
+ # # Manual matching
204
+ # Page.search('bar').where("query->>'tags' IN ('home', 'garden')")
205
+ #
206
+ def self.search_query_attributes!(*args)
207
+ @_search_query_attrs = args.map(&:to_sym)
208
+ end
209
+
210
+ # Get extra attributes on the record for querying.
211
+ #
212
+ # See #search_query_attributes! for more information.
213
+ #
214
+ # @return [Array] of attributes
215
+ def self.search_query_attributes
216
+ @_search_query_attrs = Set.new(@_search_query_attrs).add(:id).to_a
217
+ end
218
+
219
+ # Index every record.
220
+ #
221
+ # Internally this method iterates over all pages in batches of 3.
222
+ #
223
+ # @return [NilClass]
224
+ def self.index!
225
+ find_each(batch_size: 3) { |page| page.index! }
226
+ end
227
+
228
+ # Create a to_tsquery statement.
229
+ #
230
+ # Mainly used internally, but could be used in custom queries.
231
+ #
232
+ # @param input [String] string to be queried
233
+ # @param options [Hash] optional
234
+ # @option options [String] :language defaults to 'english'
235
+ # This is really a future addition, all of the tsvector fields are set to
236
+ # 'english'.
237
+ #
238
+ # @return [String] a to_tsquery statement
239
+ def self.tsquery(input, options={})
240
+ options[:language] ||= 'english'
241
+ value = input.to_s.strip
242
+ value = value.
243
+ gsub('(', '').
244
+ gsub(')', '').
245
+ gsub(%q('), '').
246
+ gsub(' ', '\\ ').
247
+ gsub(':', '').
248
+ gsub("\t", '').
249
+ gsub("!", '')
250
+ value << ':*'
251
+ query = "to_tsquery(?, ?)"
252
+ sanitize_sql_array([query, options[:language], value])
253
+ end
254
+
149
255
  # Return field options for building forms.
150
256
  #
151
257
  def field_options
@@ -156,7 +262,6 @@ module Simplec
156
262
  #
157
263
  # This is a recursive, expensive call.
158
264
  #
159
- # @return [Array] of parent Pages
160
265
  def parents
161
266
  page, parents = self, Array.new
162
267
  while page.parent
@@ -225,9 +330,64 @@ module Simplec
225
330
  @layouts ||= Subdomain.new.layouts
226
331
  end
227
332
 
333
+ # Index this record for search.
334
+ #
335
+ # Internally, this method uses update_columns so it can be used in
336
+ # `after_save` callbacks, etc.
337
+ #
338
+ # @return [Boolean] success
339
+ def index!
340
+ set_search_text!
341
+ set_query_attributes!
342
+ update_columns text: self.text, query: self.query
343
+ end
344
+
345
+ # Extract text out of HTML or plain strings. Basically removes html
346
+ # formatting.
347
+ #
348
+ # @param attributes [Symbol, String]
349
+ # variable list of attributes or methods to be extracted for search
350
+ #
351
+ # @return [String] content of each attribute separated by new lines
352
+ def extract_search_text(*attributes)
353
+ Array(attributes).map { |meth|
354
+ Nokogiri::HTML(self.send(meth)).xpath("//text()").
355
+ map {|node| text = node.text; text.try(:strip!); text}.join(" ")
356
+ }.reject(&:blank?).join("\n")
357
+ end
358
+
359
+ # Set the text which will be index.
360
+ #
361
+ # a title, meta_description
362
+ # b slug, path (non-printable, add tags, added terms)
363
+ # c textual fields
364
+ # d (reserved for sub-records, etc)
365
+ #
366
+ # 'a' correlates to 'A' priority in Postgresql. For more information:
367
+ # https://www.postgresql.org/docs/9.6/static/functions-textsearch.html
368
+ #
369
+ def set_search_text!
370
+ self.text['a'] = extract_search_text :title, :meta_description
371
+ self.text['b'] = extract_search_text :slug, :path
372
+ self.text['c'] = extract_search_text *self.class.field_names(:textual)
373
+ self.text['d'] = nil
374
+ end
375
+
376
+ # Build query attribute hash.
377
+ #
378
+ # Internally stored as JSONB.
379
+ #
380
+ # @return [Hash] to be set for query attribute
381
+ def set_query_attributes!
382
+ attr_names = self.class.search_query_attributes.map(&:to_s)
383
+ self.query = attr_names.inject({}) { |memo, attr|
384
+ memo[attr] = self.send(attr)
385
+ memo
386
+ }
387
+ end
388
+
228
389
  # @!visibility private
229
390
  module Normalizers
230
-
231
391
  def slug=(val)
232
392
  val = val ? val.to_s.split('/').reject(&:blank?).join('/') : val
233
393
  super val
@@ -0,0 +1,58 @@
1
+ class AddSearchToSimplecPages < ActiveRecord::Migration[5.0]
2
+ def change
3
+ add_column :simplec_pages, :tsv, :tsvector
4
+ add_column :simplec_pages, :query, :jsonb
5
+ add_column :simplec_pages, :text, :jsonb
6
+
7
+ add_index :simplec_pages, :tsv, using: :gin
8
+
9
+ reversible do |dir|
10
+ dir.up {
11
+ execute <<-SQL.gsub(/\s+/, " ").strip
12
+ ALTER TABLE simplec_pages ALTER COLUMN query
13
+ SET DEFAULT '{}'::JSONB;
14
+ ALTER TABLE simplec_pages ALTER COLUMN text
15
+ SET DEFAULT '{"a": null, "b": null, "c": null, "d": null}'::JSONB;
16
+
17
+ UPDATE simplec_pages
18
+ SET
19
+ query = '{}'::JSONB,
20
+ text = '{"a": null, "b": null, "c": null, "d": null}'::JSONB;
21
+
22
+ CREATE INDEX simplec_pages_query ON simplec_pages
23
+ USING GIN (query jsonb_path_ops);
24
+
25
+ CREATE FUNCTION simplec_pages_search_tsvector_update_trigger() RETURNS trigger AS $$
26
+ begin
27
+ new.tsv :=
28
+ setweight(
29
+ to_tsvector('pg_catalog.english', coalesce(new.text ->> 'a','')), 'A'
30
+ ) ||
31
+ setweight(
32
+ to_tsvector('pg_catalog.english', coalesce(new.text ->> 'b','')), 'B'
33
+ ) ||
34
+ setweight(
35
+ to_tsvector('pg_catalog.english', coalesce(new.text ->> 'c','')), 'C'
36
+ ) ||
37
+ setweight(
38
+ to_tsvector('pg_catalog.english', coalesce(new.text ->> 'd','')), 'D'
39
+ );
40
+ return new;
41
+ end
42
+
43
+ $$ LANGUAGE plpgsql;
44
+ CREATE TRIGGER simplec_pages_search_tsvector_update BEFORE INSERT OR UPDATE
45
+ ON simplec_pages FOR EACH ROW
46
+ EXECUTE PROCEDURE simplec_pages_search_tsvector_update_trigger();
47
+ SQL
48
+ }
49
+ dir.down {
50
+ execute <<-SQL.gsub(/\s+/, " ").strip
51
+ DROP TRIGGER IF EXISTS simplec_pages_search_tsvector_update ON simplec_pages;
52
+ DROP FUNCTION IF EXISTS simplec_pages_search_tsvector_update_trigger();
53
+ DROP INDEX simplec_pages_query;
54
+ SQL
55
+ }
56
+ end
57
+ end
58
+ end
@@ -1,3 +1,3 @@
1
1
  module Simplec
2
- VERSION = '0.6.0'
2
+ VERSION = '0.7.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simplec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Smith
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-09-16 00:00:00.000000000 Z
11
+ date: 2017-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -153,6 +153,7 @@ files:
153
153
  - db/migrate/20170809210304_create_simplec_embedded_images.rb
154
154
  - db/migrate/20170814211816_create_simplec_documents.rb
155
155
  - db/migrate/20170814211929_create_simplec_document_sets.rb
156
+ - db/migrate/20170917144923_add_search_to_simplec_pages.rb
156
157
  - lib/simplec.rb
157
158
  - lib/simplec/action_controller/extensions.rb
158
159
  - lib/simplec/action_view/helper.rb