diff --git a/app/logical/ai_tag_query.rb b/app/logical/ai_tag_query.rb new file mode 100644 index 000000000..119e3bf3f --- /dev/null +++ b/app/logical/ai_tag_query.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# An AITagQuery is a tag search performed on media assets using AI tags. Only +# basic tags are allowed, no metatags. +class AITagQuery + extend Memoist + + attr_reader :search_string + delegate :to_infix, :to_pretty_string, to: :ast + alias_method :to_s, :to_infix + + def initialize(search_string) + @search_string = search_string.to_s.strip + end + + def self.search(search_string, **options) + new(search_string).search(**options) + end + + def ast + @ast ||= PostQuery::Parser.parse(search_string) + end + + def normalized_ast + @normalized_ast ||= ast.to_cnf.rewrite_opts.trim + end + + def search(relation: MediaAsset.all, foreign_key: :id, score_range: (50..)) + normalized_ast.visit do |node, *children| + case node.type + in :all + relation.all + in :none + relation.none + in :tag + ai_tag = AITag.named(node.name).where(score: score_range) + relation.where(ai_tag.where(AITag.arel_table[:media_asset_id].eq(relation.arel_table[foreign_key])).arel.exists) + in :metatag + relation.none + in :wildcard + relation.none + in :not + children.first.negate_relation + in :and + joins = children.flat_map(&:joins_values) + orders = children.flat_map(&:order_values) + nodes = children.map { |child| child.joins(joins).order(orders) } + nodes.reduce(&:and) + in :or + joins = children.flat_map(&:joins_values) + orders = children.flat_map(&:order_values) + nodes = children.map { |child| child.joins(joins).order(orders) } + nodes.reduce(&:or) + end + end + end +end diff --git a/app/models/media_asset.rb b/app/models/media_asset.rb index 890b6b333..f8b113b03 100644 --- a/app/models/media_asset.rb +++ b/app/models/media_asset.rb @@ -195,13 +195,22 @@ class MediaAsset < ApplicationRecord concerning :SearchMethods do class_methods do + def ai_tags_match(tag_string, score_range: (50..)) + AITagQuery.search(tag_string, relation: self, score_range: score_range) + end + def search(params) - q = search_attributes(params, :id, :created_at, :updated_at, :md5, :file_ext, :file_size, :image_width, :image_height, :file_key, :is_public) + q = search_attributes(params, :id, :created_at, :updated_at, :status, :md5, :file_ext, :file_size, :image_width, :image_height, :file_key, :is_public) if params[:metadata].present? q = q.joins(:media_metadata).merge(MediaMetadata.search(metadata: params[:metadata])) end + if params[:ai_tags_match].present? + min_score = params.fetch(:min_score, 50).to_i + q = q.ai_tags_match(params[:ai_tags_match], score_range: (min_score..)) + end + if params[:is_posted].to_s.truthy? #q = q.where.associated(:post) q = q.where(Post.where("posts.md5 = media_assets.md5").arel.exists) @@ -210,7 +219,16 @@ class MediaAsset < ApplicationRecord q = q.where.not(Post.where("posts.md5 = media_assets.md5").arel.exists) end - q.apply_default_order(params) + case params[:order] + when "id", "id_desc" + q = q.order(id: :desc) + when "id_asc" + q = q.order(id: :asc) + else + q = q.apply_default_order(params) + end + + q end end end diff --git a/app/views/media_assets/index.html.erb b/app/views/media_assets/index.html.erb index 7d204cefd..034dc9eb6 100644 --- a/app/views/media_assets/index.html.erb +++ b/app/views/media_assets/index.html.erb @@ -2,22 +2,28 @@

All Uploads

- <% if search_params[:metadata].present? %> - <%= search_form_for(media_assets_path) do |f| %> + <%= search_form_for(media_assets_path) do |f| %> + <%= f.input :ai_tags_match, label: "Tags", input_html: { value: params.dig(:search, :ai_tags_match), data: { autocomplete: "tag-query" } } %> + <%= f.input :status, collection: MediaAsset.statuses.keys.map(&:capitalize), include_blank: true, selected: params.dig(:search, :status) %> + <%= f.input :is_posted, as: :hidden, input_html: { value: params.dig(:search, :is_posted) } %> + <%= f.input :min_score, as: :hidden, input_html: { value: params.dig(:search, :min_score) } %> + + <% if search_params[:metadata].present? %> <%= f.simple_fields_for :metadata do |meta| %> <% params.dig(:search, :metadata).to_h.each do |key, value| %> <%= meta.input key, label: key, input_html: { value: value } %> <% end %> <% end %> - - <%= f.submit "Search" %> <% end %> + + <%= f.input :order, collection: [%w[Newest id], %w[Oldest id_asc]], include_blank: true, selected: params[:search][:order] %> + <%= f.submit "Search" %> <% end %>
- <%= link_to "All", current_page_path(search: search_params.to_h.without("is_posted")), class: ["inline-block p-1 pb-2", (search_params[:is_posted].nil? ? "border-current border-b-2 -mb-px" : "inactive-link")] %> - <%= link_to "Posted", current_page_path(search: { is_posted: true }), class: ["inline-block p-1 pb-2", (search_params[:is_posted].to_s.truthy? ? "border-current border-b-2 -mb-px" : "inactive-link")] %> - <%= link_to "Unposted", current_page_path(search: { is_posted: false }), class: ["inline-block p-1 pb-2", (search_params[:is_posted].to_s.falsy? ? "border-current border-b-2 -mb-px" : "inactive-link")] %> + <%= link_to "All", current_page_path(search: search_params.merge(is_posted: nil)), class: ["inline-block p-1 pb-2", (search_params[:is_posted].nil? ? "border-current border-b-2 -mb-px" : "inactive-link")] %> + <%= link_to "Posted", current_page_path(search: search_params.merge(is_posted: true)), class: ["inline-block p-1 pb-2", (search_params[:is_posted].to_s.truthy? ? "border-current border-b-2 -mb-px" : "inactive-link")] %> + <%= link_to "Unposted", current_page_path(search: search_params.merge(is_posted: false)), class: ["inline-block p-1 pb-2", (search_params[:is_posted].to_s.falsy? ? "border-current border-b-2 -mb-px" : "inactive-link")] %> <%= render PreviewSizeMenuComponent.new(current_size: @preview_size) %>