pools: switch from search[name_matches] to search[name_contains].

The previous commit changed it so that `/pools?search[name_matches]`
does a full-text search. So for example, `search[name_matches]=smiling`
will now match pool names containing any of the words "smiling",
"smile", "smiles", or "smiled".

This commit adds a `/pools?search[name_contains]` param that does what
`name_matches` did before, and switches to it in search forms. So for
example, `search[name_contains]=smiling` will only match pool names
containing the exact substring "smiling".

This change is so that `<field>_matches` works consistently across the
site, and so that it's possible to search pool names by either an exact
substring match, or by a looser natural language match.

This is a minor breaking API change. API users can replace
`/pools?search[name_matches]` with `/pools?search[name_contains]` to get
the same behavior as before. The same applies to /favorite_groups.
This commit is contained in:
evazion
2022-09-21 22:52:30 -05:00
parent 3114ef3daf
commit 29a4ca0818
15 changed files with 40 additions and 28 deletions

View File

@@ -84,8 +84,8 @@ class PoolsController < ApplicationController
private private
def item_matches_params(pool) def item_matches_params(pool)
if params[:search][:name_matches] if params[:search][:name_contains]
Pool.normalize_name_for_search(pool.name) == Pool.normalize_name_for_search(params[:search][:name_matches]) Pool.normalize_name_for_search(pool.name) == Pool.normalize_name_for_search(params[:search][:name_contains])
else else
true true
end end

View File

@@ -296,8 +296,7 @@ class AutocompleteService
# @param string [String] the name of the pool # @param string [String] the name of the pool
# @return [Array<Hash>] the autocomplete results # @return [Array<Hash>] the autocomplete results
def autocomplete_pool(string) def autocomplete_pool(string)
string = "*" + string + "*" unless string.include?("*") pools = Pool.undeleted.name_contains(string).search(order: "post_count").limit(limit)
pools = Pool.undeleted.name_matches(string).search(order: "post_count").limit(limit)
pools.map do |pool| pools.map do |pool|
{ type: "pool", label: pool.pretty_name, value: pool.name, id: pool.id, post_count: pool.post_count, category: pool.category } { type: "pool", label: pool.pretty_name, value: pool.name, id: pool.id, post_count: pool.post_count, category: pool.category }
@@ -308,8 +307,7 @@ class AutocompleteService
# @param string [String] the name of the favgroup # @param string [String] the name of the favgroup
# @return [Array<Hash>] the autocomplete results # @return [Array<Hash>] the autocomplete results
def autocomplete_favorite_group(string) def autocomplete_favorite_group(string)
string = "*" + string + "*" unless string.include?("*") favgroups = FavoriteGroup.visible(current_user).where(creator: current_user).name_contains(string).search(order: "post_count").limit(limit)
favgroups = FavoriteGroup.visible(current_user).where(creator: current_user).name_matches(string).search(order: "post_count").limit(limit)
favgroups.map do |favgroup| favgroups.map do |favgroup|
{ label: favgroup.pretty_name, value: favgroup.name, post_count: favgroup.post_count } { label: favgroup.pretty_name, value: favgroup.name, post_count: favgroup.post_count }

View File

@@ -25,7 +25,7 @@ class FavoriteGroup < ApplicationRecord
where_array_includes_any(:post_ids, [post_id]) where_array_includes_any(:post_ids, [post_id])
end end
def name_matches(name) def name_contains(name)
name = normalize_name(name) name = normalize_name(name)
name = "*#{name}*" unless name =~ /\*/ name = "*#{name}*" unless name =~ /\*/
where_ilike(:name, name) where_ilike(:name, name)
@@ -44,8 +44,8 @@ class FavoriteGroup < ApplicationRecord
def search(params) def search(params)
q = search_attributes(params, :id, :created_at, :updated_at, :name, :is_public, :post_ids, :creator) q = search_attributes(params, :id, :created_at, :updated_at, :name, :is_public, :post_ids, :creator)
if params[:name_matches].present? if params[:name_contains].present?
q = q.name_matches(params[:name_matches]) q = q.name_contains(params[:name_contains])
end end
case params[:order] case params[:order]

View File

@@ -21,7 +21,7 @@ class Pool < ApplicationRecord
scope :collection, -> { where(category: "collection") } scope :collection, -> { where(category: "collection") }
module SearchMethods module SearchMethods
def name_matches(name) def name_contains(name)
name = normalize_name_for_search(name) name = normalize_name_for_search(name)
name = "*#{name}*" unless name =~ /\*/ name = "*#{name}*" unless name =~ /\*/
where_ilike(:name, name) where_ilike(:name, name)
@@ -44,8 +44,8 @@ class Pool < ApplicationRecord
q = q.post_tags_match(params[:post_tags_match]) q = q.post_tags_match(params[:post_tags_match])
end end
if params[:name_matches].present? if params[:name_contains].present?
q = q.name_matches(params[:name_matches]) q = q.name_contains(params[:name_contains])
end end
if params[:linked_to].present? if params[:linked_to].present?

View File

@@ -27,7 +27,7 @@ class PoolVersion < ApplicationRecord
where_array_includes_any(:added_post_ids, [post_id]).or(where_array_includes_any(:removed_post_ids, [post_id])) where_array_includes_any(:added_post_ids, [post_id]).or(where_array_includes_any(:removed_post_ids, [post_id]))
end end
def name_matches(name) def name_contains(name)
name = normalize_name_for_search(name) name = normalize_name_for_search(name)
name = "*#{name}*" unless name =~ /\*/ name = "*#{name}*" unless name =~ /\*/
where_ilike(:name, name) where_ilike(:name, name)
@@ -40,8 +40,8 @@ class PoolVersion < ApplicationRecord
q = q.for_post_id(params[:post_id].to_i) q = q.for_post_id(params[:post_id].to_i)
end end
if params[:name_matches].present? if params[:name_contains].present?
q = q.name_matches(params[:name_matches]) q = q.name_contains(params[:name_contains])
end end
if params[:updater_name].present? if params[:updater_name].present?

View File

@@ -1241,7 +1241,7 @@ class Post < ApplicationRecord
when "collection" when "collection"
where(id: Pool.collection.select("unnest(post_ids)")) where(id: Pool.collection.select("unnest(post_ids)"))
when /\*/ when /\*/
where(id: Pool.name_matches(pool_name).select("unnest(post_ids)")) where(id: Pool.name_contains(pool_name).select("unnest(post_ids)"))
else else
where(id: Pool.named(pool_name).select("unnest(post_ids)")) where(id: Pool.named(pool_name).select("unnest(post_ids)"))
end end

View File

@@ -1,7 +1,7 @@
<div id="c-favorite-groups"> <div id="c-favorite-groups">
<div id="a-index"> <div id="a-index">
<%= search_form_for(favorite_groups_path) do |f| %> <%= search_form_for(favorite_groups_path) do |f| %>
<%= f.input :name_matches, label: "Name", input_html: { value: params.dig(:search, :name_matches), "data-autocomplete": "favorite-group" } %> <%= f.input :name_contains, label: "Name", input_html: { value: params.dig(:search, :name_contains), "data-autocomplete": "favorite-group" } %>
<%= f.input :creator_name, label: "Creator", input_html: { value: params.dig(:search, :creator_name), "data-autocomplete": "user" } %> <%= f.input :creator_name, label: "Creator", input_html: { value: params.dig(:search, :creator_name), "data-autocomplete": "user" } %>
<%= f.input :order, collection: [%w[Created created_at], %w[Updated updated_at], %w[Name name], %w[Post\ count post_count]], include_blank: true, selected: params.dig(:search, :order) %> <%= f.input :order, collection: [%w[Created created_at], %w[Updated updated_at], %w[Name name], %w[Post\ count post_count]], include_blank: true, selected: params.dig(:search, :order) %>
<%= f.submit "Search" %> <%= f.submit "Search" %>

View File

@@ -1,5 +1,5 @@
<% content_for(:secondary_links) do %> <% content_for(:secondary_links) do %>
<%= quick_search_form_for(:name_matches, pool_versions_path, "pools", autocomplete: "pool") %> <%= quick_search_form_for(:name_contains, pool_versions_path, "pools", autocomplete: "pool") %>
<%= subnav_link_to "Listing", pool_versions_path %> <%= subnav_link_to "Listing", pool_versions_path %>
<%= subnav_link_to "Search", search_pool_versions_path %> <%= subnav_link_to "Search", search_pool_versions_path %>
<% end %> <% end %>

View File

@@ -4,7 +4,7 @@
<%= search_form_for(pool_versions_path) do |f| %> <%= search_form_for(pool_versions_path) do |f| %>
<%= f.input :updater_name, label: "Updater", input_html: { value: params.dig(:search, :updater_name), "data-autocomplete": "user" } %> <%= f.input :updater_name, label: "Updater", input_html: { value: params.dig(:search, :updater_name), "data-autocomplete": "user" } %>
<%= f.input :name_matches, label: "Pool", input_html: { value: params.dig(:search, :name_matches), "data-autocomplete": "pool" } %> <%= f.input :name_contains, label: "Pool", input_html: { value: params.dig(:search, :name_contains), "data-autocomplete": "pool" } %>
<%= f.input :category, label: "Category", collection: [["Series", "series"], ["Collection", "collection"]], include_blank: true %> <%= f.input :category, label: "Category", collection: [["Series", "series"], ["Collection", "collection"]], include_blank: true %>
<%= f.input :is_new, label: "New?", collection: [["Yes", true], ["No", false]], include_blank: true %> <%= f.input :is_new, label: "New?", collection: [["Yes", true], ["No", false]], include_blank: true %>
<%= f.input :name_changed, label: "Name changed?", collection: [["Yes", true], ["No", false]], include_blank: true %> <%= f.input :name_changed, label: "Name changed?", collection: [["Yes", true], ["No", false]], include_blank: true %>

View File

@@ -1,5 +1,5 @@
<%= search_form_for(path) do |f| %> <%= search_form_for(path) do |f| %>
<%= f.input :name_matches, label: "Name", input_html: { value: params.dig(:search, :name_matches), "data-autocomplete": "pool" } %> <%= f.input :name_contains, label: "Name", input_html: { value: params.dig(:search, :name_contains), "data-autocomplete": "pool" } %>
<%= f.input :description_matches, label: "Description", input_html: { value: params.dig(:search, :description_matches) } %> <%= f.input :description_matches, label: "Description", input_html: { value: params.dig(:search, :description_matches) } %>
<%= f.input :post_tags_match, label: "Post tags", input_html: { value: params.dig(:search, :post_tags_match), "data-autocomplete": "tag-query" } %> <%= f.input :post_tags_match, label: "Post tags", input_html: { value: params.dig(:search, :post_tags_match), "data-autocomplete": "tag-query" } %>
<%= f.input :is_deleted, label: "Deleted?", as: :select, include_blank: true, selected: params[:search][:is_deleted] %> <%= f.input :is_deleted, label: "Deleted?", as: :select, include_blank: true, selected: params[:search][:is_deleted] %>

View File

@@ -1,5 +1,5 @@
<% content_for(:secondary_links) do %> <% content_for(:secondary_links) do %>
<%= quick_search_form_for(:name_matches, pools_path, "pools", autocomplete: "pool", redirect: true) %> <%= quick_search_form_for(:name_contains, pools_path, "pools", autocomplete: "pool", redirect: true) %>
<%= subnav_link_to "Gallery", gallery_pools_path %> <%= subnav_link_to "Gallery", gallery_pools_path %>
<%= subnav_link_to "Listing", pools_path %> <%= subnav_link_to "Listing", pools_path %>
<% if policy(Pool).create? %> <% if policy(Pool).create? %>

View File

@@ -22,8 +22,7 @@ class ArtistVersionsControllerTest < ActionDispatch::IntegrationTest
should respond_to_search({}).with { @versions.reverse } should respond_to_search({}).with { @versions.reverse }
should respond_to_search(name: "masao").with { [@versions[2], @versions[0]] } should respond_to_search(name: "masao").with { [@versions[2], @versions[0]] }
should respond_to_search(name_matches: "(deleted)").with { @versions[1] } should respond_to_search(group_name: "the_best").with { @versions[2] }
should respond_to_search(group_name_matches: "the_best").with { @versions[2] }
should respond_to_search(urls_include_any: "https://www.deviantart.com/masao").with { [@versions[2], @versions[1], @versions[0]] } should respond_to_search(urls_include_any: "https://www.deviantart.com/masao").with { [@versions[2], @versions[1], @versions[0]] }
should respond_to_search(is_deleted: "true").with { @versions[1] } should respond_to_search(is_deleted: "true").with { @versions[1] }

View File

@@ -9,7 +9,7 @@ class FavoriteGroupsControllerTest < ActionDispatch::IntegrationTest
context "index action" do context "index action" do
setup do setup do
@mod_favgroup = create(:favorite_group, name: "monochrome", creator: build(:moderator_user, name: "fumimi")) @mod_favgroup = create(:favorite_group, name: "Beautiful Smile", creator: build(:moderator_user, name: "fumimi"))
@private_favgroup = create(:private_favorite_group) @private_favgroup = create(:private_favorite_group)
end end
@@ -19,7 +19,12 @@ class FavoriteGroupsControllerTest < ActionDispatch::IntegrationTest
end end
should respond_to_search({}).with { [@mod_favgroup, @favgroup] } should respond_to_search({}).with { [@mod_favgroup, @favgroup] }
should respond_to_search(name: "monochrome").with { @mod_favgroup } should respond_to_search(name_contains: "eautiful").with { @mod_favgroup }
should respond_to_search(name_contains: "beautiful smile").with { @mod_favgroup }
should respond_to_search(name_contains: "smiling beauty").with { [] }
should respond_to_search(name_matches: "eautiful").with { [] }
should respond_to_search(name_matches: "beautiful smile").with { @mod_favgroup }
should respond_to_search(name_matches: "smiling beauty").with { @mod_favgroup }
context "using includes" do context "using includes" do
should respond_to_search(creator_name: "fumimi").with { @mod_favgroup } should respond_to_search(creator_name: "fumimi").with { @mod_favgroup }

View File

@@ -9,7 +9,7 @@ class PoolsControllerTest < ActionDispatch::IntegrationTest
end end
as(@user) do as(@user) do
@post = create(:post) @post = create(:post)
@pool = create(:pool, name: "pool", description: "[[touhou]]") @pool = create(:pool, name: "Beautiful Smile", description: "[[touhou]]")
end end
end end
@@ -25,7 +25,12 @@ class PoolsControllerTest < ActionDispatch::IntegrationTest
assert_equal(Pool.count, response.parsed_body.css("urlset url loc").size) assert_equal(Pool.count, response.parsed_body.css("urlset url loc").size)
end end
should respond_to_search(name_matches: "pool").with { @pool } should respond_to_search(name_contains: "eautiful").with { @pool }
should respond_to_search(name_contains: "beautiful smile").with { @pool }
should respond_to_search(name_contains: "smiling beauty").with { [] }
should respond_to_search(name_matches: "eautiful").with { [] }
should respond_to_search(name_matches: "beautiful smile").with { @pool }
should respond_to_search(name_matches: "smiling beauty").with { @pool }
should respond_to_search(linked_to: "touhou").with { @pool } should respond_to_search(linked_to: "touhou").with { @pool }
should respond_to_search(not_linked_to: "touhou").with { [] } should respond_to_search(not_linked_to: "touhou").with { [] }
end end

View File

@@ -17,7 +17,12 @@ class PoolTest < ActiveSupport::TestCase
@pool = FactoryBot.create(:pool, name: "Test Pool") @pool = FactoryBot.create(:pool, name: "Test Pool")
assert_equal(@pool.id, Pool.find_by_name("test pool").id) assert_equal(@pool.id, Pool.find_by_name("test pool").id)
assert_equal(@pool.id, Pool.search(name_matches: "test pool").first.id)
assert_equal([@pool.id], Pool.search(name_contains: "test pool").map(&:id))
assert_equal([@pool.id], Pool.search(name_contains: "tes").map(&:id))
assert_equal([@pool.id], Pool.search(name_matches: "test pool").map(&:id))
assert_equal([@pool.id], Pool.search(name_matches: "testing pool").map(&:id))
assert_equal([], Pool.search(name_matches: "tes").map(&:id))
end end
should "find pools by post id" do should "find pools by post id" do