aboutsummaryrefslogtreecommitdiff
path: root/app/models/search.rb
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <msabate@suse.com>2024-03-13 18:03:43 +0100
committerMiquel Sabaté Solà <msabate@suse.com>2024-03-13 18:03:43 +0100
commita7791635aa98f012d35f97c3c5288494bbb28e38 (patch)
treebad987e6b4d10ad2f7cd02e40658d464907595ee /app/models/search.rb
downloadoperum-a7791635aa98f012d35f97c3c5288494bbb28e38.tar.gz
operum-a7791635aa98f012d35f97c3c5288494bbb28e38.zip
Initial commit
Signed-off-by: Miquel Sabaté Solà <msabate@suse.com>
Diffstat (limited to 'app/models/search.rb')
-rw-r--r--app/models/search.rb99
1 files changed, 99 insertions, 0 deletions
diff --git a/app/models/search.rb b/app/models/search.rb
new file mode 100644
index 0000000..81a1c34
--- /dev/null
+++ b/app/models/search.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+class Search < ApplicationRecord
+ validates :name, presence: true, uniqueness: true
+ validates :body, presence: true, uniqueness: true
+
+ belongs_to :user
+
+ # Returns all the results that can be fetched with the current `body`. It's
+ # returned into a hash which groups the taggable types.
+ def results
+ # This can happen as part of an empty Search.new instance. That is, it's
+ # still invalid, but it's the object being initialized for the default
+ # search (i.e. just show me everything).
+ return { things: Thing.order('rate DESC, created_at'), comments: [] } if body.blank?
+
+ # Parse the body of the search, and return early if nothing was able to be
+ # parsed (e.g. user wrote unknown identifiers).
+ parsed = parse_body
+ return { things: [], comments: [] } if parsed.all? { |_, v| v.blank? }
+
+ # And add everything into the `res` hash with the results.
+ res = find_by_text(plain: parsed[:plain])
+ find_by_tags(res:, tags: parsed[:tag])
+ end
+
+ protected
+
+ # Returns a hash which contains the tags that has been specified (`tag`) and
+ # the plain text (`plain`) to check on the different fields.
+ def parse_body
+ res = { tag: [], plain: [] }
+
+ body.split.each do |part|
+ if part.include?(':')
+ parts = part.split(':', 2)
+ parts[1] = clean_clause(part: parts[1])
+
+ next if parts[0] != 'tag'
+
+ res[parts[0].to_sym] << parts[1]
+ else
+ res[:plain] << part
+ end
+ end
+
+ res
+ end
+
+ # Returns the argument without any leading/trailing quotes.
+ def clean_clause(part:)
+ part.gsub(/^("|')+/, '').gsub(/("|')+$/, '')
+ end
+
+ # Returns a hash which groups into taggable types the results by looking the
+ # `plain` text into different fields.
+ def find_by_text(plain:)
+ res = { things: [], comments: [] }
+
+ plain.each do |text|
+ res[:things] = if res[:things].any?
+ res[:things].and(Thing.like(text:)).order('rate DESC, created_at')
+ else
+ Thing.like(text:).order('rate DESC, created_at')
+ end
+ res[:comments] = if res[:comments].any?
+ res[:comments].and(Comment.like(text:)).order(:created_at)
+ else
+ Comment.like(text:).order(:created_at)
+ end
+ end
+
+ res
+ end
+
+ # Returns a hash which groups into taggable types the results by matching the
+ # given `tags` which their references. It expects `res` to be already
+ # initialized by `find_by_text` (yeah, great design, I know), which is also
+ # the structure that will be returned.
+ def find_by_tags(res:, tags:)
+ return res if tags.blank?
+
+ tags = tags.map { |name| Tag.find_by(name:) }
+
+ query = TagReference.where(tag: tags, taggable_type: 'Thing')
+ query = query.where(taggable_id: res[:things].pluck(:id)) if res[:things].present?
+ res[:things] = query.group(:taggable_id)
+ .having('count(taggable_id) = ?', tags.size)
+ .map(&:taggable)
+
+ query = TagReference.where(tag: tags, taggable_type: 'Comment')
+ query = query.where(taggable_id: res[:comments].pluck(:id)) if res[:comments].present?
+ res[:comments] = query.group(:taggable_id)
+ .having('count(taggable_id) = ?', tags.size)
+ .map(&:taggable)
+
+ res
+ end
+end