media assets: add ability to search by AI tags.
Add ability to search the /media_assets index by AI tags. Multi-tag searches are supported, including AND/OR/NOT operators, but metatags aren't supported. Multi-tag searches will probably be slow. The default AI tag confidence threshold is 50%. There's a hidden search[min_score] URL param that lets you change this.
This commit is contained in:
57
app/logical/ai_tag_query.rb
Normal file
57
app/logical/ai_tag_query.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -2,22 +2,28 @@
|
||||
<div id="a-index">
|
||||
<h1 class="mb-2">All Uploads</h1>
|
||||
|
||||
<% 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 %>
|
||||
|
||||
<div class="border-b mb-4 flex flex-wrap gap-4">
|
||||
<%= 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")] %>
|
||||
<span class="flex-grow-1"></span>
|
||||
<%= render PreviewSizeMenuComponent.new(current_size: @preview_size) %>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user