Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions app/controllers/concerns/filterable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

# A concern to handle filtering for controllers.
# Usage: Include this concern in your controller and call `apply_filters` on your query.
module Filterable
extend ActiveSupport::Concern

# Applies filters to a query based on the given filter parameters.
# @param query [ActiveRecord::Relation] The query to filter.
# @param filters [Hash] A hash of filter parameters (e.g., { rank: 'genus', status: 15 }).
# @return [ActiveRecord::Relation] The filtered query.
def apply_filters(query, filters)
filters.each do |key, value|
next if value.nil?
query = query.where(key => value)
end
query
end

# Applies sorting to a query based on the given sort parameters.
# @param query [ActiveRecord::Relation] The query to sort.
# @param sort_by [String, nil] The field to sort by (defaults to params[:sort]).
# @param sort_direction [String, nil] The direction to sort ('asc' or 'desc').
# @return [ActiveRecord::Relation] The sorted query.
def apply_sort(query, sort_by: nil, sort_direction: nil)
sort_by ||= params[:sort]
sort_direction ||= params[:direction] || 'asc'

return query unless sort_by.present?

# Handle special sorting cases (e.g., 'citations' for Name model)
case sort_by.to_s.downcase
when 'citations'
query.left_joins(:publication_names).group(:id).order("COUNT(publication_names.id) #{sort_direction.upcase}")
when 'date'
# Use validated_at if available, otherwise fall back to created_at
if query.model.column_names.include?('validated_at')
query.order("validated_at #{sort_direction.upcase}")
else
query.order("created_at #{sort_direction.upcase}")
end
else
# Default sorting by the given field
query.order("#{sort_by} #{sort_direction.upcase}")
end
end
end
76 changes: 76 additions & 0 deletions app/controllers/concerns/name_filterable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# frozen_string_literal: true

# A concern to handle Name-specific filtering and sorting for controllers.
# Usage: Include this concern in your controller and call `apply_name_filters` on your query.
module NameFilterable
extend ActiveSupport::Concern

included do
# Default filters for Name model
def name_status_filters
{
'public' => Name.public_status,
'automated' => 0,
'seqcode' => 15,
'icnp' => 20,
'icnafp' => 25,
'valid' => Name.valid_status
}
end

# Maps status strings to their corresponding integer values.
# @param status [String] The status string (e.g., 'SeqCode', 'ICNP').
# @return [Integer, Array<Integer>] The status value(s).
def map_status_to_value(status)
status = status.to_s.downcase
status = 'icnafp' if status == 'icn'
name_status_filters[status] || status
end
end

# Applies Name-specific filters to a query.
# @param query [ActiveRecord::Relation] The query to filter.
# @param filters [Hash] A hash of filter parameters (e.g., { rank: 'genus', status: 'SeqCode' }).
# @return [ActiveRecord::Relation] The filtered query.
def apply_name_filters(query, filters)
filters.each do |key, value|
next if value.nil?

case key.to_sym
when :status
query = query.where(status: map_status_to_value(value))
when :rank
query = query.where(rank: value)
when :redirect
query = query.where(redirect: value)
else
query = query.where(key => value)
end
end
query
end

# Applies Name-specific sorting to a query.
# @param query [ActiveRecord::Relation] The query to sort.
# @param sort_by [String, nil] The field to sort by (defaults to params[:sort]).
# @return [ActiveRecord::Relation] The sorted query.
def apply_name_sort(query, sort_by: nil)
sort_by ||= params[:sort] || 'date'

case sort_by.to_s.downcase
when 'date'
# Use validated_at for SeqCode-validated names, otherwise created_at
if params[:status] == 'SeqCode' || params[:status] == 'seqcode'
query.order(validated_at: :desc)
else
query.order(created_at: :desc)
end
when 'citations'
query.left_joins(:publication_names).group(:id).order('COUNT(publication_names.id) DESC')
when 'alphabetically'
query.order(name: :asc)
else
query.order(sort_by => :asc)
end
end
end
25 changes: 25 additions & 0 deletions app/controllers/concerns/paginatable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

# A concern to handle pagination for controllers.
# Usage: Include this concern in your controller and call `paginate` on your query.
module Paginatable
extend ActiveSupport::Concern

included do
# Default pagination parameters
def default_per_page
30
end
end

# Paginates a query with the given parameters.
# @param query [ActiveRecord::Relation] The query to paginate.
# @param page [Integer, nil] The page number (defaults to params[:page]).
# @param per_page [Integer, nil] The number of items per page (defaults to default_per_page).
# @return [ActiveRecord::Relation] The paginated query.
def paginate(query, page: nil, per_page: nil)
page ||= params[:page]
per_page ||= default_per_page
query.paginate(page: page, per_page: per_page)
end
end
1 change: 0 additions & 1 deletion app/controllers/names_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -684,5 +684,4 @@ def add_automatic_correspondence(message)
user: current_user, name: @name
).save
end

end
71 changes: 71 additions & 0 deletions app/services/name/fuzzy_search.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

module Name
# Service object to handle fuzzy search for names.
# This encapsulates the logic for finding similar names using
# PostgreSQL's similarity functions.
class FuzzySearch
# Performs a fuzzy search for names similar to the given query.
# @param query [String] The search query.
# @param method [Symbol] The search method (:similarity or :levenshtein).
# @param threshold [Float, Integer] The threshold for matching.
# @param limit [Integer] The maximum number of results to return.
# @param selection [Symbol, ActiveRecord::Relation] The selection of names to search.
# Can be :all_valid, :all_public, :valid_genera, :public_genera, or a custom query.
# @return [ActiveRecord::Relation] The matching names.
def self.call(
query,
method: :similarity,
threshold: nil,
limit: 10,
selection: :all_valid
)
return unless ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'

selection = resolve_selection(selection)
clean_query = ActiveRecord::Base.connection.quote(query)

case method.to_sym
when :similarity
perform_similarity_search(selection, clean_query, threshold || 0.7, limit)
when :levenshtein
perform_levenshtein_search(selection, clean_query, threshold || 2, limit)
else
raise ArgumentError, "Unsupported fuzzy match method: #{method}"
end
end

private_class_method def self.resolve_selection(selection)
case selection
when :all_valid
Name.all_valid
when :all_public
Name.all_public
when :valid_genera
Name.all_valid.where(rank: :genus)
when :public_genera
Name.all_public.where(rank: :genus)
when ActiveRecord::Relation
selection
else
raise ArgumentError, "Unsupported selection: #{selection}"
end
end

private_class_method def self.perform_similarity_search(selection, query, threshold, limit)
selection
.select("id, name, similarity(name, #{query}) AS score")
.where('similarity(name, ?) > ?', query, threshold)
.order('score DESC')
.limit(limit)
end

private_class_method def self.perform_levenshtein_search(selection, query, threshold, limit)
selection
.select("id, name, levenshtein(name, #{query}) AS score")
.where('levenshtein(name, ?) <= ?', query, threshold)
.order('score ASC')
.limit(limit)
end
end
end
27 changes: 27 additions & 0 deletions app/services/name/type_resolver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Name
# Service object to resolve the nomenclatural type for a name.
# This encapsulates the logic for determining the type_of_type value
# and handling edge cases.
class TypeResolver
# Resolves the nomenclatural type class for a given object.
# @param object [Object, nil] The object to resolve (e.g., a Name, Genome, or Strain).
# @return [String] The resolved type_of_type value, or 'unknown' if unresolved.
def self.resolve(object)
return 'unknown' if object.nil?
return object.type_of_type if object.respond_to?(:type_of_type)
'unknown'
end

# Resolves the nomenclatural type class for a given object, with additional context.
# @param object [Object, nil] The object to resolve.
# @param context [Hash] Additional context (e.g., { fallback: 'Name' }).
# @return [String] The resolved type_of_type value.
def self.resolve_with_context(object, context = {})
resolved = resolve(object)
return resolved unless resolved == 'unknown' && context[:fallback].present?
context[:fallback]
end
end
end
18 changes: 18 additions & 0 deletions db/migrate/20260511100000_add_indexes_to_names.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class AddIndexesToNames < ActiveRecord::Migration[6.1]
def change
# Add indexes for frequently queried columns
add_index :names, :name
add_index :names, :rank
add_index :names, :status
add_index :names, :redirect_id
add_index :names, :created_by_id
add_index :names, :validated_by_id
add_index :names, :priority_date
add_index :names, :nomenclatural_type_type
add_index :names, :nomenclatural_type_id

# Composite indexes for common query patterns
add_index :names, [:rank, :status]
add_index :names, [:status, :priority_date]
end
end
Loading
Loading