Class: Match::Otu::TaxonName
- Inherits:
-
Object
- Object
- Match::Otu::TaxonName
- Defined in:
- lib/match/otu/taxon_name.rb
Constant Summary collapse
- MAX_NAMES =
3000- MATCHABLE_COLUMNS =
[ :cached, :cached_secondary_homonym, :cached_primary_homonym ].freeze
Instance Attribute Summary collapse
-
#levenshtein_distance ⇒ Object
readonly
Returns the value of attribute levenshtein_distance.
-
#names ⇒ Object
readonly
Returns the value of attribute names.
-
#project_id ⇒ Object
readonly
Returns the value of attribute project_id.
-
#resolve_synonyms ⇒ Object
readonly
Returns the value of attribute resolve_synonyms.
-
#taxon_name_id ⇒ Object
readonly
Returns the value of attribute taxon_name_id.
-
#try_without_subgenus ⇒ Object
readonly
Returns the value of attribute try_without_subgenus.
Instance Method Summary collapse
-
#base_scope ⇒ ActiveRecord::Relation
private
Build the base TaxonName scope, optionally constrained to descendants of taxon_name_id.
- #call ⇒ Array<Hash>
- #find_taxon_names(name, column: :cached) ⇒ Array<TaxonName> private
- #find_taxon_names_exact(name, column: :cached) ⇒ Array<TaxonName> private
- #find_taxon_names_fuzzy(name, column: :cached) ⇒ Array<TaxonName> private
-
#genuinely_ambiguous?(ranked) ⇒ Boolean
private
Multiple candidate rows aren't ambiguous if they all resolve to the same valid taxon (e.g. a Combination alongside its own Protonym) — ranking always picks correctly there.
-
#initialize(names:, project_id:, levenshtein_distance: 0, taxon_name_id: nil, resolve_synonyms: false, try_without_subgenus: false) ⇒ TaxonName
constructor
A new instance of TaxonName.
- #match_name(name) ⇒ Hash private
-
#rank_taxon_names(taxon_names) ⇒ Array<TaxonName>
private
Rank candidate TaxonNames: 1.
Constructor Details
#initialize(names:, project_id:, levenshtein_distance: 0, taxon_name_id: nil, resolve_synonyms: false, try_without_subgenus: false) ⇒ TaxonName
Returns a new instance of TaxonName.
42 43 44 45 46 47 48 49 |
# File 'lib/match/otu/taxon_name.rb', line 42 def initialize(names:, project_id:, levenshtein_distance: 0, taxon_name_id: nil, resolve_synonyms: false, try_without_subgenus: false) @names = names.first(MAX_NAMES) @project_id = project_id @levenshtein_distance = levenshtein_distance.to_i.clamp(0, 8) @taxon_name_id = taxon_name_id @resolve_synonyms = resolve_synonyms @try_without_subgenus = try_without_subgenus end |
Instance Attribute Details
#levenshtein_distance ⇒ Object (readonly)
Returns the value of attribute levenshtein_distance.
34 35 36 |
# File 'lib/match/otu/taxon_name.rb', line 34 def levenshtein_distance @levenshtein_distance end |
#names ⇒ Object (readonly)
Returns the value of attribute names.
34 35 36 |
# File 'lib/match/otu/taxon_name.rb', line 34 def names @names end |
#project_id ⇒ Object (readonly)
Returns the value of attribute project_id.
34 35 36 |
# File 'lib/match/otu/taxon_name.rb', line 34 def project_id @project_id end |
#resolve_synonyms ⇒ Object (readonly)
Returns the value of attribute resolve_synonyms.
34 35 36 |
# File 'lib/match/otu/taxon_name.rb', line 34 def resolve_synonyms @resolve_synonyms end |
#taxon_name_id ⇒ Object (readonly)
Returns the value of attribute taxon_name_id.
34 35 36 |
# File 'lib/match/otu/taxon_name.rb', line 34 def taxon_name_id @taxon_name_id end |
#try_without_subgenus ⇒ Object (readonly)
Returns the value of attribute try_without_subgenus.
34 35 36 |
# File 'lib/match/otu/taxon_name.rb', line 34 def try_without_subgenus @try_without_subgenus end |
Instance Method Details
#base_scope ⇒ ActiveRecord::Relation (private)
Build the base TaxonName scope, optionally constrained to descendants of taxon_name_id.
157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/match/otu/taxon_name.rb', line 157 def base_scope scope = ::TaxonName.where(project_id: project_id) if taxon_name_id.present? scope = scope .joins('JOIN taxon_name_hierarchies ON taxon_names.id = taxon_name_hierarchies.descendant_id') .where(taxon_name_hierarchies: { ancestor_id: taxon_name_id }) end scope end |
#call ⇒ Array<Hash>
52 53 54 55 56 57 58 59 60 61 |
# File 'lib/match/otu/taxon_name.rb', line 52 def call unique_names = names.uniq match_cache = {} unique_names.each do |name| match_cache[name] = match_name(name) end names.map { |name| match_cache[name].merge(scientific_name: name) } end |
#find_taxon_names(name, column: :cached) ⇒ Array<TaxonName> (private)
112 113 114 115 116 117 118 119 120 |
# File 'lib/match/otu/taxon_name.rb', line 112 def find_taxon_names(name, column: :cached) raise ArgumentError, "Invalid column: #{column}" unless MATCHABLE_COLUMNS.include?(column) if levenshtein_distance > 0 find_taxon_names_fuzzy(name, column:) else find_taxon_names_exact(name, column:) end end |
#find_taxon_names_exact(name, column: :cached) ⇒ Array<TaxonName> (private)
125 126 127 128 |
# File 'lib/match/otu/taxon_name.rb', line 125 def find_taxon_names_exact(name, column: :cached) scope = base_scope scope.where(column => name).to_a end |
#find_taxon_names_fuzzy(name, column: :cached) ⇒ Array<TaxonName> (private)
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/match/otu/taxon_name.rb', line 133 def find_taxon_names_fuzzy(name, column: :cached) scope = base_scope truncated_name = name[0..254] qualified_column = "taxon_names.#{column}" scope .where( "levenshtein(left(#{qualified_column}, 255), ?) <= ?", truncated_name, levenshtein_distance ) .order( Arel.sql( ::TaxonName.sanitize_sql_array( ["levenshtein(left(#{qualified_column}, 255), ?)", truncated_name] ) ) ) .limit(10) .to_a end |
#genuinely_ambiguous?(ranked) ⇒ Boolean (private)
Multiple candidate rows aren't ambiguous if they all resolve to the same valid taxon (e.g. a Combination alongside its own Protonym) — ranking always picks correctly there. Only flag it when candidates point to genuinely different valid taxa (e.g. true homonyms).
105 106 107 |
# File 'lib/match/otu/taxon_name.rb', line 105 def genuinely_ambiguous?(ranked) ranked.map(&:cached_valid_taxon_name_id).uniq.length > 1 end |
#match_name(name) ⇒ Hash (private)
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/match/otu/taxon_name.rb', line 67 def match_name(name) taxon_names = find_taxon_names(name) if taxon_names.empty? && try_without_subgenus taxon_names = find_taxon_names(name, column: :cached_secondary_homonym) if taxon_names.empty? taxon_names = find_taxon_names(name, column: :cached_primary_homonym) end end return { taxon_name_id: nil, taxon_name: nil, otus: [], ambiguous: false, matched: false } if taxon_names.empty? ranked = rank_taxon_names(taxon_names) matched = ranked.first resolved = matched if resolve_synonyms && matched.cached_valid_taxon_name_id != matched.id valid = ::TaxonName.where(project_id: project_id).find_by(id: matched.cached_valid_taxon_name_id) resolved = valid if valid end otus = ::Otu.where(project_id: project_id, taxon_name_id: resolved.id).to_a { taxon_name_id: resolved.id, taxon_name: resolved, otus: otus, ambiguous: genuinely_ambiguous?(ranked), matched: true } end |
#rank_taxon_names(taxon_names) ⇒ Array<TaxonName> (private)
Rank candidate TaxonNames:
1. Prefer those with OTUs
2. Prefer valid names
174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/match/otu/taxon_name.rb', line 174 def rank_taxon_names(taxon_names) taxon_name_ids = taxon_names.map(&:id) ids_with_otus = ::Otu.where(project_id: project_id, taxon_name_id: taxon_name_ids).distinct.pluck(:taxon_name_id).to_set taxon_names.sort_by do |tn| [ ids_with_otus.include?(tn.id) ? 0 : 1, tn.cached_valid_taxon_name_id == tn.id ? 0 : 1 ] end end |