Class: Queries::BiologicalAssociation::Autocomplete

Inherits:
Query::Autocomplete show all
Defined in:
lib/queries/biological_association/autocomplete.rb

Constant Summary collapse

DEFAULT_LIMIT =
50

Constants inherited from Query::Autocomplete

Query::Autocomplete::DELEGATED_DEPTH, Query::Autocomplete::DELEGATED_MINIMUM_LENGTH, Query::Autocomplete::LITERAL_RESTRICTION_MAX

Instance Attribute Summary

Attributes inherited from Query::Autocomplete

#delegated_results, #dynamic_limit, #limit, #project_id, #query_string, #restrict_to

Attributes inherited from Query

#query_string, #terms

Instance Method Summary collapse

Methods inherited from Query::Autocomplete

#apply_restriction, #asserted_object_autocomplete, #asserted_object_restriction, #autocomplete_cached, #autocomplete_cached_wildcard_anywhere, #autocomplete_common_name_exact, #autocomplete_common_name_like, #autocomplete_exact_id, #autocomplete_exactly_named, #autocomplete_named, #autocomplete_ordered_wildcard_pieces_in_cached, #cached_facet, #combine_or_clauses, #common_name_name, #common_name_table, #common_name_wild_pieces, #delegated_autocomplete, #exactly_named, #fragments, #integers, #least_levenshtein, #match_wildcard_end_in_cached, #match_wildcard_in_cached, #named, #only_ids, #only_integers?, #order_by_id_rank, #parent, #parent_child_join, #parent_child_where, #pieces, #safe_integers, #scope, #string_fragments, #wildcard_wrapped_integers, #wildcard_wrapped_years, #with_cached, #with_cached_like, #with_id, #with_project_id, #year_letter, #years

Methods inherited from Query

#alphabetic_strings, #alphanumeric_strings, base_name, #base_name, #build_terms, #cached_facet, #end_wildcard, #levenshtein_distance, #match_ordered_wildcard_pieces_in_cached, #no_terms?, referenced_klass, #referenced_klass, #referenced_klass_except, #referenced_klass_intersection, #referenced_klass_union, #start_and_end_wildcard, #start_wildcard, #table, #wildcard_pieces

Constructor Details

#initialize(string, project_id: nil, restrict_to: nil, limit: nil) ⇒ Autocomplete

Returns a new instance of Autocomplete.



7
8
9
# File 'lib/queries/biological_association/autocomplete.rb', line 7

def initialize(string, project_id: nil, restrict_to: nil, limit: nil)
  super(string, project_id:, restrict_to:, limit:)
end

Instance Method Details

#anatomical_part_autocomplete(side) ⇒ Queries::AnatomicalPart::Autocomplete



71
72
73
# File 'lib/queries/biological_association/autocomplete.rb', line 71

def anatomical_part_autocomplete(side)
  side_autocomplete(Queries::AnatomicalPart::Autocomplete, ::AnatomicalPart, side)
end

#anatomical_part_matches(anatomical_part_query, side, results_allowed) ⇒ Object



114
115
116
117
# File 'lib/queries/biological_association/autocomplete.rb', line 114

def anatomical_part_matches(anatomical_part_query, side, results_allowed)
  ids = anatomical_part_query.limit(results_allowed).pluck(:id)
  joined_matches('anatomical_parts', 'AnatomicalPart', side, ids, results_allowed)
end

#autocomplete ⇒ Array

Returns:

  • (Array)


169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/queries/biological_association/autocomplete.rb', line 169

def autocomplete
  result = []
  ordered_lazy_queries.each do |q|
    remaining = limit - result.count
    break if remaining <= 0

    result += q.call(remaining)
    result.uniq!
  end

  result.first(limit)
end

#base_query ⇒ Scope

Returns scoped to the project, so that queries that aren't joined to project-scoped records (e.g. #autocomplete_exact_id) are.

Returns:

  • (Scope) —

    scoped to the project, so that queries that aren't joined to project-scoped records (e.g. #autocomplete_exact_id) are



14
15
16
17
18
# File 'lib/queries/biological_association/autocomplete.rb', line 14

def base_query
  q = super
  q = q.where(project_id:) if project_id.any?
  q
end

#biological_relationship_autocomplete ⇒ Queries::BiologicalRelationship::Autocomplete



65
66
67
68
# File 'lib/queries/biological_association/autocomplete.rb', line 65

def biological_relationship_autocomplete
  @biological_relationship_autocomplete ||= Queries::BiologicalRelationship::Autocomplete
    .new(query_string, project_id: project_id)
end

#biological_relationship_matches(results_allowed) ⇒ Object



119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/queries/biological_association/autocomplete.rb', line 119

def biological_relationship_matches(results_allowed)
  ids = biological_relationship_autocomplete.all.limit(results_allowed).pluck(:id)
  return [] if ids.empty?

  # Keep the relationship autocomplete's ranking, so that the results
  # cap keeps the best matches
  q = order_by_id_rank(
    base_query.where(biological_relationship_id: ids),
    'biological_associations.biological_relationship_id', ids
  ).limit(results_allowed)

  apply_restriction(q).to_a
end

#collection_object_autocomplete(side) ⇒ Queries::CollectionObject::Autocomplete



55
56
57
# File 'lib/queries/biological_association/autocomplete.rb', line 55

def collection_object_autocomplete(side)
  side_autocomplete(Queries::CollectionObject::Autocomplete, ::CollectionObject, side)
end

#collection_object_matches(collection_object_query, side, results_allowed) ⇒ Object



104
105
106
107
# File 'lib/queries/biological_association/autocomplete.rb', line 104

def collection_object_matches(collection_object_query, side, results_allowed)
  ids = collection_object_query.limit(results_allowed).pluck(:id)
  joined_matches('collection_objects', 'CollectionObject', side, ids, results_allowed)
end

#field_occurrence_autocomplete(side) ⇒ Queries::FieldOccurrence::Autocomplete



60
61
62
# File 'lib/queries/biological_association/autocomplete.rb', line 60

def field_occurrence_autocomplete(side)
  side_autocomplete(Queries::FieldOccurrence::Autocomplete, ::FieldOccurrence, side)
end

#field_occurrence_matches(field_occurrence_query, side, results_allowed) ⇒ Object



109
110
111
112
# File 'lib/queries/biological_association/autocomplete.rb', line 109

def field_occurrence_matches(field_occurrence_query, side, results_allowed)
  ids = field_occurrence_query.limit(results_allowed).pluck(:id)
  joined_matches('field_occurrences', 'FieldOccurrence', side, ids, results_allowed)
end

#joined_matches(related_table_name, related_type, side, ids, results_allowed) ⇒ Array<BiologicalAssociation>

Returns biological_associations where the subject or object (on side) is one of the related_klass records identified by ids, in ids order, at most results_allowed.

Returns:

  • (Array<BiologicalAssociation>) —

    biological_associations where the subject or object (on side) is one of the related_klass records identified by ids, in ids order, at most results_allowed



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/queries/biological_association/autocomplete.rb', line 79

def joined_matches(related_table_name, related_type, side, ids, results_allowed)
  return [] if ids.empty?

  foreign_key_column = "biological_association_#{side}_id"
  type_column = "biological_association_#{side}_type"

  q = base_query
    .joins(
      "JOIN #{related_table_name} ON biological_associations.#{foreign_key_column} = #{related_table_name}.id " \
      "AND biological_associations.#{type_column} = '#{related_type}'"
    )
    .where(related_table_name.to_sym => { id: ids })

  # Keep the related autocomplete's ranking, so that the results cap
  # keeps the best matches
  q = order_by_id_rank(q, "#{related_table_name}.id", ids).limit(results_allowed)

  apply_restriction(q).to_a
end

#ordered_lazy_queries ⇒ Array<Proc>

Returns An ordered list of thunks, highest priority first. Each, when called with the number of results still wanted, lazily runs its own query and returns an Array. Kept as thunks (rather than eagerly building/running every branch, as this used to) so autocomplete can stop pulling further branches, and cap how many candidate ids a branch even asks for, once enough results are found.

Returns:

  • (Array<Proc>) —

    An ordered list of thunks, highest priority first. Each, when called with the number of results still wanted, lazily runs its own query and returns an Array. Kept as thunks (rather than eagerly building/running every branch, as this used to) so autocomplete can stop pulling further branches, and cap how many candidate ids a branch even asks for, once enough results are found.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/queries/biological_association/autocomplete.rb', line 139

def ordered_lazy_queries
  queries = [->(n) {
    q = autocomplete_exact_id
    q ? apply_restriction(q).to_a : []
  }]

  queries << ->(n) { otu_matches(:subject, n) }
  queries << ->(n) { otu_matches(:object, n) }

  %i{subject object}.each do |side|
    collection_object_autocomplete(side).base_queries
      .each { |q| queries << ->(n) { collection_object_matches(q, side, n) } }
  end

  %i{subject object}.each do |side|
    field_occurrence_autocomplete(side).base_queries
      .each { |q| queries << ->(n) { field_occurrence_matches(q, side, n) } }
  end

  queries << ->(n) { biological_relationship_matches(n) }

  %i{subject object}.each do |side|
    anatomical_part_autocomplete(side).updated_queries
      .each { |q| queries << ->(n) { anatomical_part_matches(q, side, n) } }
  end

  queries
end

#otu_autocomplete(side) ⇒ Queries::Otu::Autocomplete



50
51
52
# File 'lib/queries/biological_association/autocomplete.rb', line 50

def otu_autocomplete(side)
  side_autocomplete(Queries::Otu::Autocomplete, ::Otu, side)
end

#otu_matches(side, results_allowed) ⇒ Object



99
100
101
102
# File 'lib/queries/biological_association/autocomplete.rb', line 99

def otu_matches(side, results_allowed)
  ids = otu_autocomplete(side).distinct_autocomplete_base.limit(results_allowed).pluck(:id)
  joined_matches('otus', 'Otu', side, ids, results_allowed)
end

#side_autocomplete(autocomplete_klass, klass, side) ⇒ Query::Autocomplete

Returns an autocomplete_klass autocomplete restricted to klass records on side (see #side_restriction), and limited to #limit, so that it returns enough candidates to fill the results. Both sides share delegated results (e.g. the Otu autocompletes' TaxonName fetch), see Query::Autocomplete#delegated_results.

Returns:

  • (Query::Autocomplete) —

    an autocomplete_klass autocomplete restricted to klass records on side (see #side_restriction), and limited to #limit, so that it returns enough candidates to fill the results. Both sides share delegated results (e.g. the Otu autocompletes' TaxonName fetch), see Query::Autocomplete#delegated_results.



42
43
44
45
46
47
# File 'lib/queries/biological_association/autocomplete.rb', line 42

def side_autocomplete(autocomplete_klass, klass, side)
  @side_autocompletes ||= {}
  @side_autocompletes[[autocomplete_klass, side]] ||= autocomplete_klass
    .new(query_string, project_id:, restrict_to: side_restriction(klass, side), limit:)
    .tap { |a| a.delegated_results = (@delegated_results ||= {}) }
end

#side_restriction(klass, side) ⇒ ActiveRecord::Relation

Returns the klass records that are the side (:subject or :object) of a biological_association in the project (and in restrict_to, when given). A candidate on a side is only useful if it is on that side of some biological association; unrestricted, candidates that aren't fill the subject/object autocompletes' limits and matching biological associations are missed (e.g. a genus search matching hundreds of species, few of which are in one).

Returns:

  • (ActiveRecord::Relation) —

    the klass records that are the side (:subject or :object) of a biological_association in the project (and in restrict_to, when given). A candidate on a side is only useful if it is on that side of some biological association; unrestricted, candidates that aren't fill the subject/object autocompletes' limits and matching biological associations are missed (e.g. a genus search matching hundreds of species, few of which are in one).



28
29
30
31
32
33
34
# File 'lib/queries/biological_association/autocomplete.rb', line 28

def side_restriction(klass, side)
  klass.where(
    id: apply_restriction(base_query)
      .where("biological_association_#{side}_type": klass.base_class.name)
      .reselect("biological_association_#{side}_id")
  )
end