diff options
| author | Miquel Sabaté Solà <msabate@suse.com> | 2024-03-13 18:03:43 +0100 |
|---|---|---|
| committer | Miquel Sabaté Solà <msabate@suse.com> | 2024-03-13 18:03:43 +0100 |
| commit | a7791635aa98f012d35f97c3c5288494bbb28e38 (patch) | |
| tree | bad987e6b4d10ad2f7cd02e40658d464907595ee /app/models/search.rb | |
| download | operum-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.rb | 99 |
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 |
