Class: Queries::Query::Autocomplete

Inherits:
Queries::Query show all
Includes:
Arel::Nodes, Concerns::Identifiers
Defined in:
lib/queries/query/autocomplete.rb

Overview

Requires significant refactor.

To consider: In general our optimization follows this pattern:

a: Names that match exactly, full string b: Names that match exactly, full Identifier (cached) c: Names that match start of string exactly (cached), wildcard end of string, minimum 2 characters d: Names that have a very high cuttoff [good wildcard anywhere] ? d.1: Names that have wildcard either side (limit to 2 characters). Are results optimally better than d? e: Names that have exact ID (internal) (will come to top automatically) f: Names that match some special pattern (e.g. First letter, second name in taxon name search). These

may need higher priority in the stack.

May also consider length, priority, similarity

Instance Attribute Summary collapse

Attributes inherited from Queries::Query

#terms

Instance Method Summary collapse

Methods inherited from Queries::Query

#alphabetic_strings, #alphanumeric_strings, base_name, #base_name, #base_query, #build_terms, #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, **keyword_args) ⇒ Autocomplete

Returns a new instance of Autocomplete.

Parameters:

  • args (Hash)
[View source]

40
41
42
43
44
45
# File 'lib/queries/query/autocomplete.rb', line 40

def initialize(string, project_id: nil, **keyword_args)
  @query_string = ::ApplicationRecord.sanitize_sql(string)&.delete("\u0000") # remove null bytes

  @project_id = project_id
  build_terms # TODO - should remove this for accessors
end

Instance Attribute Details

#dynamic_limitInteger

Returns:

  • (Integer)

34
35
36
# File 'lib/queries/query/autocomplete.rb', line 34

def dynamic_limit
  @dynamic_limit
end

#project_idArray

Returns:

  • (Array)

26
27
28
# File 'lib/queries/query/autocomplete.rb', line 26

def project_id
  @project_id
end

#query_stringString?

Returns the initial, unparsed value, sanitized.

Returns:

  • (String, nil)

    the initial, unparsed value, sanitized


30
31
32
# File 'lib/queries/query/autocomplete.rb', line 30

def query_string
  @query_string
end

Instance Method Details

#autocompleteArray

Returns default the autocomplete result to all TODO: eliminate.

Returns:

  • (Array)

    default the autocomplete result to all TODO: eliminate

[View source]

234
235
236
237
# File 'lib/queries/query/autocomplete.rb', line 234

def autocomplete
  return [] if query_string.blank?
  all.to_a
end

#autocomplete_cachedActiveRecord::Relation

Returns:

  • (ActiveRecord::Relation)
[View source]

272
273
274
275
276
277
278
# File 'lib/queries/query/autocomplete.rb', line 272

def autocomplete_cached
  if a = cached_facet
    base_query.where(a.to_sql).limit(20)
  else
    nil
  end
end

#autocomplete_cached_wildcard_anywhereActiveRecord::Relation

Returns removes years/integers!.

Returns:

  • (ActiveRecord::Relation)

    removes years/integers!

[View source]

256
257
258
259
260
# File 'lib/queries/query/autocomplete.rb', line 256

def autocomplete_cached_wildcard_anywhere
  a = match_wildcard_in_cached
  return nil if a.nil?
  base_query.where(a.to_sql)
end

#autocomplete_common_name_exactObject

[View source]

304
305
306
307
# File 'lib/queries/query/autocomplete.rb', line 304

def autocomplete_common_name_exact
  return nil if no_terms?
  base_query.joins(:common_names).where(common_name_name.to_sql).limit(1)
end

#autocomplete_common_name_likeObject

TODO: GIN/similarity

[View source]

310
311
312
313
# File 'lib/queries/query/autocomplete.rb', line 310

def autocomplete_common_name_like
  return nil if no_terms?
  base_query.joins(:common_names).where(common_name_wild_pieces.to_sql).limit(5)
end

#autocomplete_exact_idActiveRecord::Relation

Returns:

  • (ActiveRecord::Relation)
[View source]

240
241
242
243
244
245
246
# File 'lib/queries/query/autocomplete.rb', line 240

def autocomplete_exact_id
  if i = ::Utilities::Strings::only_integer(query_string)
    base_query.where(id: i).limit(1)
  else
    nil
  end
end

#autocomplete_exactly_namedActiveRecord::Relation

Returns:

  • (ActiveRecord::Relation)
[View source]

281
282
283
284
# File 'lib/queries/query/autocomplete.rb', line 281

def autocomplete_exactly_named
  return nil if no_terms?
  base_query.where(exactly_named.to_sql).limit(20)
end

#autocomplete_namedActiveRecord::Relation

Returns:

  • (ActiveRecord::Relation)
[View source]

287
288
289
290
# File 'lib/queries/query/autocomplete.rb', line 287

def autocomplete_named
  return nil if no_terms?
  base_query.where(named.to_sql).limit(20)
end

#autocomplete_ordered_wildcard_pieces_in_cachedActiveRecord::Relation

Returns:

  • (ActiveRecord::Relation)
[View source]

249
250
251
252
# File 'lib/queries/query/autocomplete.rb', line 249

def autocomplete_ordered_wildcard_pieces_in_cached
  return nil if no_terms?
  base_query.where(match_ordered_wildcard_pieces_in_cached.to_sql)
end

#cached_facetActiveRecord::Relation?

TODO: Used in taxon_name, source, identifier

Returns:

  • (ActiveRecord::Relation, nil)

    cached matches full query string wildcarded

[View source]

265
266
267
268
269
# File 'lib/queries/query/autocomplete.rb', line 265

def cached_facet
  return nil if no_terms?
  # TODO: or is redundant with terms in many cases
  (table[:cached].matches_any(terms)).or(match_ordered_wildcard_pieces_in_cached)
end

#combine_or_clauses(clauses) ⇒ Arel::Nodes::Grouping

Returns:

  • (Arel::Nodes::Grouping)

Raises:

  • (TaxonWorks::Error)
[View source]

216
217
218
219
220
221
222
223
224
# File 'lib/queries/query/autocomplete.rb', line 216

def combine_or_clauses(clauses)
  clauses.compact!
  raise TaxonWorks::Error, 'combine_or_clauses called without a clause, ensure at least one exists' unless !clauses.empty?
  a = clauses.shift
  clauses.each do |b|
    a = a.or(b)
  end
  a
end

#common_name_nameObject

[View source]

296
297
298
# File 'lib/queries/query/autocomplete.rb', line 296

def common_name_name
  common_name_table[:name].eq(query_string)
end

#common_name_tableObject

[View source]

292
293
294
# File 'lib/queries/query/autocomplete.rb', line 292

def common_name_table
  ::CommonName.arel_table
end

#common_name_wild_piecesObject

[View source]

300
301
302
# File 'lib/queries/query/autocomplete.rb', line 300

def common_name_wild_pieces
  common_name_table[:name].matches(wildcard_pieces)
end

#exactly_namedArel::Nodes::Matches

Returns:

  • (Arel::Nodes::Matches)
[View source]

171
172
173
# File 'lib/queries/query/autocomplete.rb', line 171

def exactly_named
  table[:name].eq(query_string) if query_string.present?
end

#fragmentsArray

Used in unordered AND searches

Returns:

  • (Array)

    if 1-5 alphanumeric_strings, those alphabetic_strings wrapped in wildcards, else none.

[View source]

82
83
84
85
86
87
88
89
# File 'lib/queries/query/autocomplete.rb', line 82

def fragments
  a = alphanumeric_strings
  if a.size > 0 && a.size < 6
    a.collect{|a| "%#{a}%"}
  else
    []
  end
end

#integersArray

Returns of strings representing integers.

Returns:

  • (Array)

    of strings representing integers

[View source]

70
71
72
# File 'lib/queries/query/autocomplete.rb', line 70

def integers
  Utilities::Strings.integers(query_string)
end

#least_levenshtein(fields, value) ⇒ Object

Calculate the levenshtein distance for a value across multiple columns, and keep the smallest.

Parameters:

  • fields (Array)

    the table column names to take strings from

  • value (String)

    the string to calculate distances to

[View source]

319
320
321
322
# File 'lib/queries/query/autocomplete.rb', line 319

def least_levenshtein(fields, value)
  levenshtein_sql = fields.map {|f| levenshtein_distance(f, value).to_sql }
  Arel.sql("least(#{levenshtein_sql.join(", ")})")
end

#match_wildcard_end_in_cachedArel::Nodes::Matches

match ALL wildcards, but unordered, if 2 - 6 pieces provided

Returns:

  • (Arel::Nodes::Matches)
[View source]

203
204
205
# File 'lib/queries/query/autocomplete.rb', line 203

def match_wildcard_end_in_cached
  table[:cached].matches(end_wildcard)
end

#match_wildcard_in_cachedArel::Nodes::Matches

match ALL wildcards, but unordered, if 2 - 6 pieces provided

Returns:

  • (Arel::Nodes::Matches)
[View source]

209
210
211
212
213
# File 'lib/queries/query/autocomplete.rb', line 209

def match_wildcard_in_cached
  b = fragments
  return nil if b.empty?
  table[:cached].matches_all(b)
end

#namedArel::Nodes::Matches

Returns:

  • (Arel::Nodes::Matches)
[View source]

166
167
168
# File 'lib/queries/query/autocomplete.rb', line 166

def named
  table[:name].matches_any(terms) if terms.any?
end

#only_idsArek::Npdes?

Returns used in or_clauses, match on id only if integers alone provided.

Returns:

  • (Arek::Npdes, nil)

    used in or_clauses, match on id only if integers alone provided.

[View source]

157
158
159
160
161
162
163
# File 'lib/queries/query/autocomplete.rb', line 157

def only_ids
  if only_integers?
    with_id
  else
    nil
  end
end

#only_integers?Boolean

Returns:

  • (Boolean)
[View source]

75
76
77
# File 'lib/queries/query/autocomplete.rb', line 75

def only_integers?
  Utilities::Strings.only_integers?(query_string)
end

#parentArel::Nodes::TableAlias

Returns used in heirarchy joins.

Returns:

  • (Arel::Nodes::TableAlias)

    used in heirarchy joins

[View source]

177
178
179
# File 'lib/queries/query/autocomplete.rb', line 177

def parent
  table.alias
end

#parent_child_joinScope

Returns:

  • (Scope)
[View source]

133
134
135
# File 'lib/queries/query/autocomplete.rb', line 133

def parent_child_join
  table.join(parent).on(table[:parent_id].eq(parent[:id])).join_sources
end

#parent_child_whereArel::Nodes::Grouping

Match at two levels, for example, ‘wa te“ will match ”Washington Co., Texas“

Returns:

  • (Arel::Nodes::Grouping)
[View source]

139
140
141
142
143
# File 'lib/queries/query/autocomplete.rb', line 139

def parent_child_where
  a,b = query_string.split(/\s+/, 2)
  return table[:id].eq(-1) if a.nil? || b.nil?
  table[:name].matches("#{a}%").and(parent[:name].matches("#{b}%"))
end

#piecesArray

TODO: used?!

Returns:

  • (Array)

    split on whitespace

[View source]

106
107
108
# File 'lib/queries/query/autocomplete.rb', line 106

def pieces
  query_string.split(/\s+/)
end

#scopeScope

stub TODO: deprecate? probably unused

Returns:

  • (Scope)
[View source]

54
55
56
# File 'lib/queries/query/autocomplete.rb', line 54

def scope
  where('1 = 2')
end

#string_fragmentsArray

Used in unordered AND searches

Returns:

  • (Array)

    if 1-5 alphabetic_strings, those alphabetic_strings wrapped in wildcards, else none.

[View source]

94
95
96
97
98
99
100
101
# File 'lib/queries/query/autocomplete.rb', line 94

def string_fragments
  a = alphabetic_strings
  if a.size > 0 && a.size < 6
    a.collect{|a| "%#{a}%"}
  else
    []
  end
end

#wildcard_wrapped_integersArray

Returns:

  • (Array)
[View source]

111
112
113
# File 'lib/queries/query/autocomplete.rb', line 111

def wildcard_wrapped_integers
  integers.collect{|i| "%#{i}%"}
end

#wildcard_wrapped_yearsArray

Returns:

  • (Array)
[View source]

116
117
118
# File 'lib/queries/query/autocomplete.rb', line 116

def wildcard_wrapped_years
  years.collect{|i| "%#{i}%"}
end

#with_cachedArel::Nodes::Matches

Returns:

  • (Arel::Nodes::Matches)
[View source]

192
193
194
# File 'lib/queries/query/autocomplete.rb', line 192

def with_cached
  table[:cached].eq(query_string)
end

#with_cached_likeArel::Nodes::Matches

Returns:

  • (Arel::Nodes::Matches)
[View source]

197
198
199
# File 'lib/queries/query/autocomplete.rb', line 197

def with_cached_like
  table[:cached].matches(start_and_end_wildcard)
end

#with_idArel::Nodes?

Returns used in or_clauses.

Returns:

  • (Arel::Nodes, nil)

    used in or_clauses

[View source]

147
148
149
150
151
152
153
# File 'lib/queries/query/autocomplete.rb', line 147

def with_id
  if integers.any?
    table[:id].in(integers)
  else
    nil
  end
end

#with_project_idArel::Nodes::Equality

TODO: nil/or clause this

Returns:

  • (Arel::Nodes::Equality)
[View source]

183
184
185
186
187
188
189
# File 'lib/queries/query/autocomplete.rb', line 183

def with_project_id
  if project_id.present?
    table[:project_id].in(project_id)
  else
    nil
  end
end

#year_letterString?

Returns:

  • (String, nil)
[View source]

64
65
66
# File 'lib/queries/query/autocomplete.rb', line 64

def year_letter
  Utilities::Strings.year_letter(query_string)
end

#yearsArray

Returns:

  • (Array)
[View source]

59
60
61
# File 'lib/queries/query/autocomplete.rb', line 59

def years
  Utilities::Strings.years(query_string)
end