3 Commits

Author SHA1 Message Date
Dylan Knutson
f63d8cabe7 more dense post index pages 2025-07-23 02:34:36 +00:00
Dylan Knutson
1470a21bbe improve similar post lists, fallback creator models 2025-07-23 02:09:45 +00:00
Dylan Knutson
931e736bbf fix index names 2025-07-23 02:04:25 +00:00
16 changed files with 161 additions and 134 deletions

View File

@@ -92,7 +92,7 @@ class Domain::PostsController < DomainController
@posts_index_view_config =
PostsIndexViewConfig.new(
show_domain_filters: false,
show_creator_links: true,
show_creator_links: false,
index_type_header: "user_created",
)

View File

@@ -35,18 +35,4 @@ module Domain::DomainModelHelper
"SF"
end
end
sig { params(model: Domain::Post).returns(String) }
def title_for_post_model(model)
case model
when Domain::Post::FaPost
model.title
when Domain::Post::E621Post
model.title
when Domain::Post::InkbunnyPost
model.title
when Domain::Post::SofurryPost
model.title
end || "(unknown)"
end
end

View File

@@ -297,10 +297,7 @@ module Domain::PostsHelper
],
find_proc: ->(helper, match, _) do
if post = Domain::Post::FaPost.find_by(fa_id: match[1])
SourceResult.new(
model: post,
title: helper.title_for_post_model(post),
)
SourceResult.new(model: post, title: post.title_for_view)
end
end,
),
@@ -323,9 +320,7 @@ module Domain::PostsHelper
).first
if post_file && (post = post_file.post)
title =
T.bind(self, Domain::PostsHelper).title_for_post_model(post)
SourceResult.new(model: post, title:)
SourceResult.new(model: post, title: post.title_for_view)
end
end,
),
@@ -348,8 +343,7 @@ module Domain::PostsHelper
patterns: [%r{/s/(\d+)/?}, %r{/submissionview\.php\?id=(\d+)/?}],
find_proc: ->(helper, match, _) do
if post = Domain::Post::InkbunnyPost.find_by(ib_id: match[1])
title = helper.title_for_post_model(post)
SourceResult.new(model: post, title:)
SourceResult.new(model: post, title: post.title_for_view)
end
end,
),
@@ -365,8 +359,7 @@ module Domain::PostsHelper
"ib.metapix.net#{url.path}",
).first
if post = post_file.post
title = helper.title_for_post_model(post)
SourceResult.new(model: post, title:)
SourceResult.new(model: post, title: post.title_for_view)
end
end
end,
@@ -394,10 +387,7 @@ module Domain::PostsHelper
patterns: [%r{/posts/(\d+)/?}],
find_proc: ->(helper, match, _) do
if post = Domain::Post::E621Post.find_by(e621_id: match[1])
SourceResult.new(
model: post,
title: helper.title_for_post_model(post),
)
SourceResult.new(model: post, title: post.title_for_view)
end
end,
),

View File

@@ -80,13 +80,15 @@ module Domain
fingerprint_detail_value: String,
limit: Integer,
oversearch: Integer,
includes: T.untyped,
).returns(T::Array[SimilarFingerprintResult])
end
def find_similar_fingerprints(
fingerprint_value:,
fingerprint_detail_value:,
limit: 32,
oversearch: 2
oversearch: 2,
includes: {}
)
ActiveRecord::Base.connection.execute("SET ivfflat.probes = 10")
@@ -95,6 +97,7 @@ module Domain
Arel.sql "(fingerprint_value <~> '#{ActiveRecord::Base.connection.quote_string(fingerprint_value)}')"
)
.limit(limit * oversearch)
.includes(includes)
.to_a
.uniq(&:post_file_id)
.map do |other_fingerprint|

View File

@@ -43,6 +43,11 @@ export const PostHoverPreviewWrapper: React.FC<
href={postPath}
className={anchorClassNamesForVisualStyle(visualStyle, true)}
>
<img
src={postDomainIcon}
alt={postThumbnailAlt}
className={iconClassNamesForSize('small')}
/>
{visualStyle === 'description-section-link' && (
<img
src={postDomainIcon}
@@ -50,7 +55,7 @@ export const PostHoverPreviewWrapper: React.FC<
className={iconClassNamesForSize('small')}
/>
)}
{linkText}
<span className="truncate">{linkText}</span>
</a>
</PostHoverPreview>
);

View File

@@ -17,7 +17,7 @@ export function iconClassNamesForSize(size: IconSize) {
case 'large':
return 'h-8 w-8 flex-shrink-0 rounded-md';
case 'small':
return 'h-5 w-5 flex-shrink-0 rounded-sm';
return 'h-6 w-6 flex-shrink-0 rounded-sm';
}
}
@@ -227,7 +227,8 @@ export function anchorClassNamesForVisualStyle(
visualStyle: string,
hasIcon: boolean = false,
) {
let classNames = ['truncate', 'gap-1'];
// let classNames = ['truncate', 'gap-1'];
let classNames = ['gap-1', 'min-w-0'];
if (hasIcon) {
classNames.push('flex items-center');
}

View File

@@ -216,6 +216,11 @@ class Domain::Post < ReduxApplicationRecord
nil
end
sig { overridable.returns(T.nilable(Domain::User)) }
def primary_fallback_creator_for_view
nil
end
class TagForView < T::Struct
const :category, Symbol
const :value, String

View File

@@ -114,26 +114,16 @@ class Domain::Post::E621Post < Domain::Post
sig { override.returns(T.nilable(String)) }
def primary_creator_name_fallback_for_view
self
.sources_array
.lazy
.filter_map do |source|
model =
T.cast(
T.unsafe(ApplicationController.helpers).link_for_source(source),
T.nilable(Domain::PostsHelper::LinkForSource),
)&.model
if model && model.is_a?(Domain::Post) && model.class.has_creators?
return T.unsafe(model).creator&.name_for_view
elsif model && model.is_a?(Domain::User)
return model.name_for_view
end
end
.first ||
guess_creator_from_sources&.name_for_view ||
self.class.first_valid_artist_tag_in(self.tags_array&.dig("artist")) ||
self.class.first_valid_artist_tag_in(self.artists_array)
end
sig { override.returns(T.nilable(Domain::User)) }
def primary_fallback_creator_for_view
guess_creator_from_sources
end
sig { override.returns(T.nilable(String)) }
def description_html_for_view
self.description
@@ -192,4 +182,28 @@ class Domain::Post::E621Post < Domain::Post
"Unknown (#{self.rating})"
end
end
private
sig { returns(T.nilable(Domain::User)) }
def guess_creator_from_sources
self
.sources_array
.lazy
.filter_map do |source|
model =
T.cast(
T.unsafe(ApplicationController.helpers).link_for_source(source),
T.nilable(Domain::PostsHelper::LinkForSource),
)&.model
if model && model.is_a?(Domain::Post) && model.class.has_creators?
T.unsafe(model).creator
elsif model && model.is_a?(Domain::User)
model
else
nil
end
end
.first
end
end

View File

@@ -1,15 +1,15 @@
<div
class="m-4 flex h-fit flex-col rounded-lg border border-slate-300 bg-slate-50 shadow-sm"
class="m-1 flex min-h-fit flex-col rounded-lg border border-slate-300 bg-slate-50 divide-y divide-slate-300 shadow-sm"
>
<div class="flex justify-between border-b border-slate-300 p-4">
<div class="flex justify-between p-2">
<%= render "domain/posts/inline_postable_domain_link", post: post %>
</div>
<div class="flex items-center justify-center p-4 border-b border-slate-300">
<div class="flex flex-grow items-center justify-center p-2">
<% if (thumbnail_file = gallery_file_for_post(post)) && (thumbnail_path = thumbnail_for_post_path(post)) %>
<%= link_to domain_post_path(post) do %>
<%= image_tag thumbnail_path,
class:
"max-h-[300px] max-w-[300px] rounded-md border border-slate-300 object-contain shadow-md",
"max-h-[250px] max-w-[250px] rounded-md border border-slate-300 object-contain shadow-md",
alt: post.title_for_view %>
<% end %>
<% elsif file_info = gallery_file_info_for_post(post) %>
@@ -21,20 +21,28 @@
<span class="text-sm text-slate-500 italic">No file available</span>
<% end %>
</div>
<div class="">
<h2 class="p-2 text-center text-lg">
<%= link_to title_for_post_model(post), domain_post_path(post), class: "blue-link" %>
<div>
<h2 class="p-1 pb-0 text-center text-md">
<%= link_to(
post.title_for_view,
domain_post_path(post),
title: post.title_for_view,
class: "blue-link inline-block truncate max-w-[250px]",
) %>
</h2>
<div class="px-4 pb-4 text-sm text-slate-500">
<div class="flex justify-between gap-2">
<div class="px-2 pb-2 text-sm text-slate-500">
<div class="flex justify-between gap-2 items-center">
<% if @posts_index_view_config.show_creator_links %>
<% if creator = post.primary_creator_for_view %>
<% if creator = post.primary_creator_for_view || post.primary_fallback_creator_for_view %>
<span class="flex gap-1 items-center">
<span class="text-slate-500 italic text-sm">by</span>
<%= render "domain/has_description_html/inline_link_domain_user", user: creator, visual_style: "sky-link", size: "small" %>
</span>
<% elsif creator = post.primary_creator_name_fallback_for_view %>
<%= creator %>
<span class="flex gap-1 items-center" title="Guessed from sources">
<i class="fa-solid fa-user text-slate-500"></i>
<%= creator %>
</span>
<% end %>
<% end %>
<span class="flex-grow text-right">
@@ -51,7 +59,9 @@
</div>
</span>
<% elsif post.posted_at %>
<%= time_ago_in_words_no_prefix(post.posted_at) %> ago
<span title="<%= post.posted_at&.strftime("%Y-%m-%d %I:%M:%S %p %Z") %>">
<%= time_ago_in_words_no_prefix(post.posted_at) %> ago
</span>
<% end %>
</span>
</div>

View File

@@ -0,0 +1,4 @@
<% source_url = local_assigns[:source_url] %>
<% source_url = source_url && Addressable::URI.parse(source_url).host %>
<% icon_path = source_url && icon_path_for_domain(source_url) %>
<%= image_tag icon_path, class: "w-6 h-6" if icon_path %>

View File

@@ -3,16 +3,16 @@
<div class="flex min-w-0 items-center gap-1">
<%= image_tag domain_model_icon_path(post), class: "w-6 h-6" %>
<span class="truncate text-lg font-medium">
<%= link_to title_for_post_model(post),
post.external_url_for_view.to_s,
class: "text-blue-600 hover:underline",
target: "_blank",
rel: "noopener noreferrer" %>
<%= link_to post.title_for_view,
post.external_url_for_view.to_s,
class: "text-blue-600 hover:underline",
target: "_blank",
rel: "noopener noreferrer" %>
</span>
<i class="fa-solid fa-arrow-up-right-from-square text-slate-400"></i>
</div>
<div class="flex items-center gap-2 whitespace-nowrap text-slate-600 float-right">
<% if creator = post.primary_creator_for_view %>
<% if creator = post.primary_creator_for_view || post.primary_fallback_creator_for_view %>
<span class="flex gap-1 items-center text-lg">
<span class="text-slate-500 italic text-sm">by</span>
<%= render(
@@ -42,7 +42,7 @@
<i class="fa-solid fa-heart mr-1"></i>
Favorites: <%= num_favorites %>
<% if policy(post).view_faved_by? %>
(<%= link_to "#{pluralize(post.faving_users.count, "fav")} known",
(<%= link_to "#{pluralize(post.user_post_favs.count, "fav")} known",
faved_by_domain_post_users_path(post),
class: "text-blue-600 hover:underline" %>)
<% end %>

View File

@@ -1,26 +1,34 @@
<section class="sky-section">
<div class="section-header">Similar Posts</div>
<div class="grid grid-cols-[1fr,auto] bg-slate-100">
<div class="grid grid-cols-[minmax(10px,1fr),auto] bg-slate-100 divide-y divide-slate-300">
<% factors = Domain::Factors::UserPostFavPostFactors.find_by(post: post) %>
<% if factors %>
<% neighbors = Domain::NeighborFinder.find_neighbors(factors).includes(:post).limit(10) %>
<% neighbors = Domain::NeighborFinder.find_neighbors(factors) %>
<% if post.class.has_creators? %>
<% neighbors = neighbors.includes(post: :creator) %>
<% else %>
<% neighbors = neighbors.includes(:post) %>
<% end %>
<% neighbors = neighbors.limit(10) %>
<% num_neighbors = neighbors.size %>
<% neighbors.each_with_index do |neighbor, index| %>
<% border_classes = index < num_neighbors - 1 ? "border-b border-slate-300" : "" %>
<% border_classes = "" %>
<% post = neighbor.post %>
<% creator = post.class.has_creators? ? post.creator : nil %>
<div class="text-md flex items-center px-4 py-1 <%= border_classes %>">
<%= render "domain/has_description_html/inline_link_domain_post", post: post, visual_style: "sky-link" %>
<% creator = post.class.has_creators? ? post.creator : post.primary_fallback_creator_for_view %>
<div class="grid grid-cols-subgrid col-span-full max-w-max py-1 px-2 gap-1 min-h-10">
<div class="text-md flex items-center">
<%= render "domain/has_description_html/inline_link_domain_post", post: post, visual_style: "sky-link" %>
</div>
<% if creator %>
<div class="text-md items-center">
<%= render "domain/has_description_html/inline_link_domain_user", user: creator, visual_style: "sky-link", icon_size: "large" %>
</div>
<% else %>
<div class="text-md self-center truncate">
<%= post.primary_creator_name_fallback_for_view %>
</div>
<% end %>
</div>
<% if creator %>
<div class="text-md items-center px-4 py-1 <%= border_classes %>">
<%= render "domain/has_description_html/inline_link_domain_user", user: creator, visual_style: "sky-link", icon_size: "large" %>
</div>
<% else %>
<div class="text-md truncate px-4 py-1 <%= border_classes %>">
<%= post.primary_creator_name_fallback_for_view %>
</div>
<% end %>
<% end %>
<% else %>
<div class="col-span-full p-4 text-center text-slate-500">No similar posts found</div>

View File

@@ -1,9 +1,9 @@
<% post_file = post.primary_file_for_view || return%>
<% post_file = post.primary_file_for_view || return %>
<% return unless (content_type = post_file.content_type) && is_thumbable_content_type?(content_type) %>
<section class="sky-section">
<div class="section-header">Visually Similar Posts</div>
<div class="grid grid-cols-[auto,auto,1fr,auto] bg-slate-100">
<% fprint = post.primary_file_for_view&.bit_fingerprints&.first %>
<div class="grid grid-cols-[5rem,minmax(10px,1fr),auto] bg-slate-100 divide-y divide-slate-300">
<% fprint = post_file.bit_fingerprints&.first %>
<% fprint_value = fprint&.fingerprint_value %>
<% fprint_detail_value = fprint&.fingerprint_detail_value %>
<% fprints = fprint && fprint_value && fprint_detail_value && find_similar_fingerprints(
@@ -11,8 +11,10 @@
fingerprint_detail_value: fprint_detail_value,
limit: 5,
oversearch: 5,
includes: {post_file: :post},
)
.reject { |f| f.fingerprint.id == fprint.id }
.reject { |f| f.fingerprint.post_file.post_id == post.id}
.reject { |f| f.similarity_percentage < 70 } || []
%>
<% if fprint.nil? %>
@@ -22,32 +24,27 @@
<% elsif fprints.any? %>
<% num_neighbors = fprints.size %>
<% fprints.each_with_index do |similar_fingerprint, index| %>
<% border_classes = index < num_neighbors - 1 ? "border-b border-slate-300" : "" %>
<% post = similar_fingerprint.fingerprint.post_file.post %>
<% creator = post.class.has_creators? ? post.creator : nil %>
<div class="text-md items-center pl-4 pr-2 py-1 flex justify-end <%= border_classes %>">
<div class="w-full text-center font-medium <%= match_badge_classes(similar_fingerprint.similarity_percentage) %>">
<%= similar_fingerprint.similarity_percentage %>%
<div class="grid grid-cols-subgrid col-span-full max-w-max py-1 px-2 gap-2">
<% post = similar_fingerprint.fingerprint.post_file.post %>
<% creator = post.class.has_creators? ? post.creator : post.primary_fallback_creator_for_view %>
<div class="text-md items-center flex justify-end">
<div class="w-full text-center font-medium <%= match_badge_classes(similar_fingerprint.similarity_percentage) %>">
<%= similar_fingerprint.similarity_percentage %>%
</div>
</div>
</div>
<div class="flex items-center <%= border_classes %>">
<% source_url = post.external_url_for_view&.to_s %>
<% source_url = source_url && Addressable::URI.parse(source_url).host %>
<% icon_path = source_url && icon_path_for_domain(source_url) %>
<%= image_tag icon_path, class: "w-6 h-6 mr-2" if icon_path %>
</div>
<div class="text-md flex items-center pr-2 py-1 <%= border_classes %>">
<%= render "domain/has_description_html/inline_link_domain_post", post: post, visual_style: "sky-link" %>
</div>
<% if creator %>
<div class="text-md items-center px-4 py-1 <%= border_classes %>">
<%= render "domain/has_description_html/inline_link_domain_user", user: creator, visual_style: "sky-link", icon_size: "large" %>
<div class="text-md self-center">
<%= render "domain/has_description_html/inline_link_domain_post", post: post, visual_style: "sky-link" %>
</div>
<% else %>
<div class="text-md truncate px-4 py-1 <%= border_classes %>">
<%= post.primary_creator_name_fallback_for_view %>
</div>
<% end %>
<% if creator %>
<div class="text-md items-center">
<%= render "domain/has_description_html/inline_link_domain_user", user: creator, visual_style: "sky-link", icon_size: "large" %>
</div>
<% else %>
<div class="text-md truncate">
<%= post.primary_creator_name_fallback_for_view %>
</div>
<% end %>
</div>
<% end %>
<% else %>
<div class="col-span-full p-4 text-center text-slate-500">No visually similar posts found</div>

View File

@@ -1,7 +1,7 @@
<% if post.sources_array.any? %>
<section class="sky-section">
<div class="section-header">Sources</div>
<div class="bg-slate-100 p-4">
<div class="bg-slate-100 p-2">
<div class="divide-y divide-slate-200">
<% post.sources_array.each do |source| %>
<%= render partial: "domain/posts/e621/source_link", locals: { source: source } %>

View File

@@ -0,0 +1,17 @@
class DropUnusedEmbeddingIndexes < ActiveRecord::Migration[7.2]
def up
remove_index :domain_user_user_follow_to_factors,
name: "index_domain_user_user_follow_to_factors_on_embedding"
rename_index :domain_user_user_follow_to_factors,
"index_domain_user_user_follow_to_factors_on_embedding_hnsw",
"index_domain_user_user_follow_to_factors_on_embedding"
rename_index :domain_user_post_fav_post_factors,
"domain_user_post_fav_post_factors_hnsw_l2_ops",
"index_domain_user_post_fav_post_factors_on_embedding"
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@@ -10,13 +10,6 @@ SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Name: public; Type: SCHEMA; Schema: -; Owner: -
--
-- *not* creating schema, since initdb creates it
--
-- Name: fuzzystrmatch; Type: EXTENSION; Schema: -; Owner: -
--
@@ -3163,13 +3156,6 @@ ALTER TABLE ONLY public.users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
--
-- Name: domain_user_post_fav_post_factors_hnsw_l2_ops; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX domain_user_post_fav_post_factors_hnsw_l2_ops ON public.domain_user_post_fav_post_factors USING hnsw (embedding public.vector_l2_ops) WITH (m='16', ef_construction='64');
--
-- Name: good_jobs_error_finished_at_idx; Type: INDEX; Schema: public; Owner: -
--
@@ -4017,6 +4003,13 @@ CREATE INDEX index_domain_user_post_creations_on_post_id_and_user_id ON public.d
CREATE UNIQUE INDEX index_domain_user_post_creations_on_user_id_and_post_id ON public.domain_user_post_creations USING btree (user_id, post_id);
--
-- Name: index_domain_user_post_fav_post_factors_on_embedding; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_user_post_fav_post_factors_on_embedding ON public.domain_user_post_fav_post_factors USING hnsw (embedding public.vector_l2_ops) WITH (m='16', ef_construction='64');
--
-- Name: index_domain_user_post_fav_post_factors_on_post_id; Type: INDEX; Schema: public; Owner: -
--
@@ -4105,14 +4098,7 @@ CREATE UNIQUE INDEX index_domain_user_user_follow_from_factors_on_user_id ON pub
-- Name: index_domain_user_user_follow_to_factors_on_embedding; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_user_user_follow_to_factors_on_embedding ON public.domain_user_user_follow_to_factors USING ivfflat (embedding public.vector_cosine_ops) WITH (lists='5000');
--
-- Name: index_domain_user_user_follow_to_factors_on_embedding_hnsw; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_user_user_follow_to_factors_on_embedding_hnsw ON public.domain_user_user_follow_to_factors USING hnsw (embedding public.vector_l2_ops) WITH (m='16', ef_construction='64');
CREATE INDEX index_domain_user_user_follow_to_factors_on_embedding ON public.domain_user_user_follow_to_factors USING hnsw (embedding public.vector_l2_ops) WITH (m='16', ef_construction='64');
--
@@ -5195,6 +5181,7 @@ ALTER TABLE ONLY public.domain_twitter_tweets
SET search_path TO "$user", public;
INSERT INTO "schema_migrations" (version) VALUES
('20250722235434'),
('20250722153048'),
('20250722152949'),
('20250722060701'),