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 @@