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

Constant Summary collapse

DEFAULT_LIMIT =

The default #limit. Subclasses whose #autocomplete honours #limit set their own.

20
DELEGATED_DEPTH =

How deep a restricted #delegated_autocomplete takes its inner autocomplete

5000
DELEGATED_MINIMUM_LENGTH =

The shortest query string a restricted #delegated_autocomplete runs its inner autocomplete for

3
LITERAL_RESTRICTION_MAX =

The most ids #asserted_object_restriction passes literally

1000

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, restrict_to: nil, limit: nil, **keyword_args) ⇒ Autocomplete

Returns a new instance of Autocomplete.

Parameters:

  • args (Hash)


67
68
69
70
71
72
73
74
75
76
# File 'lib/queries/query/autocomplete.rb', line 67

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

  @project_id = project_id
  @restrict_to = restrict_to
  @limit = limit

  # should not need this
  # build_terms # TODO - should remove this for accessors
end

Instance Attribute Details

#delegated_results ⇒ Hash?

Returns optional, a store for the inner results of restricted #delegated_autocomplete calls. An autocomplete that creates several autocompletes delegating to the same inner autocomplete (e.g. one per side of a biological association) gives them all the same Hash, so the DELEGATED_DEPTH fetch runs once. nil (default) is no sharing.

Returns:

  • (Hash, nil) —

    optional, a store for the inner results of restricted #delegated_autocomplete calls. An autocomplete that creates several autocompletes delegating to the same inner autocomplete (e.g. one per side of a biological association) gives them all the same Hash, so the DELEGATED_DEPTH fetch runs once. nil (default) is no sharing.



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

def delegated_results
  @delegated_results
end

#dynamic_limit ⇒ Integer

Returns:

  • (Integer)


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

def dynamic_limit
  @dynamic_limit
end

#limit ⇒ Integer

Returns the maximum number of results #autocomplete returns, the given limit or the subclass' DEFAULT_LIMIT.

Returns:

  • (Integer) —

    the maximum number of results #autocomplete returns, the given limit or the subclass' DEFAULT_LIMIT



85
86
87
# File 'lib/queries/query/autocomplete.rb', line 85

def limit
  @limit || self.class::DEFAULT_LIMIT
end

#project_id ⇒ Array

Returns:

  • (Array)


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

def project_id
  @project_id
end

#query_string ⇒ String?

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

#restrict_to ⇒ ActiveRecord::Relation?

Returns optional, restricts results to these records of referenced_klass. nil (default) is no restriction. Must be a relation of referenced_klass (or a subclass), e.g. ::CollectingEvent.where(...); any select on it is replaced with select(:id). Anything else raises ArgumentError. See #apply_restriction.

Returns:

  • (ActiveRecord::Relation, nil) —

    optional, restricts results to these records of referenced_klass. nil (default) is no restriction. Must be a relation of referenced_klass (or a subclass), e.g. ::CollectingEvent.where(...); any select on it is replaced with select(:id). Anything else raises ArgumentError. See #apply_restriction.



45
46
47
# File 'lib/queries/query/autocomplete.rb', line 45

def restrict_to
  @restrict_to
end

Instance Method Details

#apply_restriction(query) ⇒ ActiveRecord::Relation

Apply #restrict_to to query. Subclasses should call this where they assemble their individual queries (i.e. alongside where project_id is applied), rather than in base_query, so that it covers queries not built from base_query (e.g. referenced_klass.joins(:identifiers)).

Use this when the caller only wants a subset of the model's records, typically when one autocomplete delegates to another: restricting the inner autocomplete to candidates the outer one can use keeps it fast, and keeps unusable candidates from filling its result limits.

The restriction is table.id IN (<restrict_to>). Requiring a relation of referenced_klass means the ids are always the model's own, and reselect(:id) guarantees the single column an IN subquery needs.

Parameters:

  • query (ActiveRecord::Relation) —

    of referenced_klass

Returns:

  • (ActiveRecord::Relation)


105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/queries/query/autocomplete.rb', line 105

def apply_restriction(query)
  return query if restrict_to.nil?

  r = restrict_to
  unless r.is_a?(ActiveRecord::Relation) && r.klass <= referenced_klass
    given = r.is_a?(ActiveRecord::Relation) ? r.klass.name : r.class.name
    raise ArgumentError,
      "restrict_to must be a relation of #{referenced_klass.name}, not #{given}"
  end

  query.where(table[:id].in(r.reselect(:id).arel))
end

#asserted_object_autocomplete(object_type:, object_autocomplete_class:, object_association:) ⇒ ActiveRecord::Relation?

For autocompletes of records asserting something about a polymorphic object (e.g. AssertedDistribution, AssertedEnvironment): match the objects of object_type with their own autocomplete, then return the asserted records of those objects, in the object autocomplete's rank order.

The combinatorics are not great for joining each object autocomplete option with the asserted records directly, so the object autocomplete is run restricted to objects that have asserted records (in #project_id, within #restrict_to). This keeps it fast and keeps objects without (usable) asserted records from filling its result limit: every one has one, so #limit of them can fill the results.

Parameters:

  • object_type (String) —

    e.g. 'BiologicalAssociation'

  • object_autocomplete_class (Class) —

    accepting restrict_to: and limit:

  • object_association (Symbol) —

    the polymorphic association, e.g. :asserted_distribution_object

Returns:

  • (ActiveRecord::Relation, nil) —

    of referenced_klass, at most #limit, nil if there are no matches



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/queries/query/autocomplete.rb', line 247

def asserted_object_autocomplete(object_type:, object_autocomplete_class:, object_association:)
  type_column = "#{object_association}_type"
  id_column = "#{object_association}_id"

  asserted = referenced_klass.where(type_column => object_type)
  asserted = asserted.where(project_id:) if project_id.present?
  asserted = apply_restriction(asserted)

  objects = asserted_object_restriction(asserted, object_type.constantize, id_column)
  return nil if objects.nil?

  object_ids = object_autocomplete_class
    .new(query_string, project_id:, restrict_to: objects, limit:)
    .autocomplete.map(&:id)
  return nil if object_ids.empty?

  order_by_id_rank(
    asserted.where(id_column => object_ids), "#{table.name}.#{id_column}", object_ids
  ).limit(limit)
end

#asserted_object_restriction(asserted, object_klass, object_id_column) ⇒ ActiveRecord::Relation?

Returns the object_klass records that have an asserted record, nil if there are none.

Parameters:

  • asserted (ActiveRecord::Relation) —

    of referenced_klass, the asserted records of one object type

  • object_klass (Class)
  • object_id_column (String) —

    the column of asserted holding the object ids

Returns:

  • (ActiveRecord::Relation, nil) —

    the object_klass records that have an asserted record, nil if there are none



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/queries/query/autocomplete.rb', line 207

def asserted_object_restriction(asserted, object_klass, object_id_column)
  # The restriction is re-applied in every query of the object
  # autocomplete, and of those it delegates to (e.g. BA -> Otu/CO/... ->
  # TaxonName, dozens of queries): when there are few ids, passing them
  # literally is much cheaper than re-evaluating the subquery in each;
  # when there are many, the subquery is cheaper. Only the caller can
  # tell cheaply (one pluck here); checking the size at every level of
  # the chain costs more than it saves.
  ids = asserted.distinct
    .limit(LITERAL_RESTRICTION_MAX + 1).pluck(object_id_column)
  return nil if ids.empty?

  if ids.size <= LITERAL_RESTRICTION_MAX
    object_klass.where(id: ids)
  else
    object_klass.where(id: asserted.select(object_id_column))
  end
end

#autocomplete ⇒ Array

Returns default the autocomplete result to all TODO: eliminate.

Returns:

  • (Array) —

    default the autocomplete result to all TODO: eliminate



460
461
462
463
# File 'lib/queries/query/autocomplete.rb', line 460

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

#autocomplete_cached ⇒ ActiveRecord::Relation

Returns:

  • (ActiveRecord::Relation)


498
499
500
501
502
503
504
# File 'lib/queries/query/autocomplete.rb', line 498

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

#autocomplete_cached_wildcard_anywhere ⇒ ActiveRecord::Relation

Returns removes years/integers!.

Returns:

  • (ActiveRecord::Relation) —

    removes years/integers!



482
483
484
485
486
# File 'lib/queries/query/autocomplete.rb', line 482

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_exact ⇒ Object



530
531
532
533
# File 'lib/queries/query/autocomplete.rb', line 530

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_like ⇒ Object

TODO: GIN/similarity



536
537
538
539
# File 'lib/queries/query/autocomplete.rb', line 536

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_id ⇒ ActiveRecord::Relation

Returns:

  • (ActiveRecord::Relation)


466
467
468
469
470
471
472
# File 'lib/queries/query/autocomplete.rb', line 466

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_named ⇒ ActiveRecord::Relation

Returns:

  • (ActiveRecord::Relation)


507
508
509
510
# File 'lib/queries/query/autocomplete.rb', line 507

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

#autocomplete_named ⇒ ActiveRecord::Relation

Returns:

  • (ActiveRecord::Relation)


513
514
515
516
# File 'lib/queries/query/autocomplete.rb', line 513

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

#autocomplete_ordered_wildcard_pieces_in_cached ⇒ ActiveRecord::Relation

Returns:

  • (ActiveRecord::Relation)


475
476
477
478
# File 'lib/queries/query/autocomplete.rb', line 475

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_facet ⇒ ActiveRecord::Relation?

TODO: Used in taxon_name, source, identifier

Returns:

  • (ActiveRecord::Relation, nil) —

    cached matches full query string wildcarded



491
492
493
494
495
# File 'lib/queries/query/autocomplete.rb', line 491

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)


442
443
444
445
446
447
448
449
450
# File 'lib/queries/query/autocomplete.rb', line 442

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_name ⇒ Object



522
523
524
# File 'lib/queries/query/autocomplete.rb', line 522

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

#common_name_table ⇒ Object



518
519
520
# File 'lib/queries/query/autocomplete.rb', line 518

def common_name_table
  ::CommonName.arel_table
end

#common_name_wild_pieces ⇒ Object



526
527
528
# File 'lib/queries/query/autocomplete.rb', line 526

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

#delegated_autocomplete(build:, keep:, key:) ⇒ Array

Run another model's (inner) autocomplete on behalf of this one. Use it when an autocomplete delegates to another, instead of translating #restrict_to for the inner model.

Unrestricted, the inner autocomplete is simply run, #limit deep.

Restricted, translating the restriction for the inner model is relation-specific and easy to get subtly wrong (e.g. Otus -> the TaxonNames, and Combinations, they resolve to), and pushing it into each of the inner autocomplete's queries is expensive for large restrictions (it's re-built in each query), so instead the inner autocomplete is run once, unrestricted, DELEGATED_DEPTH deep, and its results filtered by keep (one query). Best effort: usable matches ranked below DELEGATED_DEPTH (the least relevant) are missed. A LIMIT costs nothing for terms that match fewer rows, so only broad terms pay for the depth; one deep fetch was faster overall than starting shallower and deepening.

Restricted, query strings shorter than DELEGATED_MINIMUM_LENGTH return no inner results: they match so broadly that the DELEGATED_DEPTH fetched are an arbitrary sample (largely unordered wildcard matches), and the slowest to fetch.

The DELEGATED_DEPTH results depend only on the inner autocomplete, not on #restrict_to, so autocompletes given the same #delegated_results share them, by key.

The inner autocomplete must accept limit: and give its queries that can return many rows a ranking order, so that the first DELEGATED_DEPTH are the best ranked (see lib/queries/ARCHITECTURE.md). So far only TaxonName autocomplete is used this way (its #autocomplete_wildcard_joined_strings is unordered).

Parameters:

  • build (Proc) —

    given a limit (this autocomplete's #limit, not the inner autocomplete's default), returns the unrestricted inner autocomplete

  • keep (Proc) —

    given inner results, returns those usable under #restrict_to, in order (e.g. those that map to an #apply_restriction record)

  • key (Array) —

    everything, other than the limit, that build passes the inner autocomplete, i.e. what its results depend on

Returns:

  • (Array) —

    inner results, when restricted at most #limit of them



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

def delegated_autocomplete(build:, keep:, key:)
  inner = build.call(limit)
  return inner.autocomplete if restrict_to.nil?
  return [] if query_string.to_s.length < DELEGATED_MINIMUM_LENGTH

  inner.limit = DELEGATED_DEPTH
  results = (delegated_results || {})[[inner.class, *key]] ||= inner.autocomplete
  keep.call(results).first(limit)
end

#exactly_named ⇒ Arel::Nodes::Matches

Returns:

  • (Arel::Nodes::Matches)


397
398
399
# File 'lib/queries/query/autocomplete.rb', line 397

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

#fragments ⇒ Array

Used in unordered AND searches

Returns:

  • (Array) —

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



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

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

#integers ⇒ Array

Returns of strings representing integers.

Returns:

  • (Array) —

    of strings representing integers



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

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



545
546
547
548
# File 'lib/queries/query/autocomplete.rb', line 545

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_cached ⇒ Arel::Nodes::Matches

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

Returns:

  • (Arel::Nodes::Matches)


429
430
431
# File 'lib/queries/query/autocomplete.rb', line 429

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

#match_wildcard_in_cached ⇒ Arel::Nodes::Matches

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

Returns:

  • (Arel::Nodes::Matches)


435
436
437
438
439
# File 'lib/queries/query/autocomplete.rb', line 435

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

#named ⇒ Arel::Nodes::Matches

Returns:

  • (Arel::Nodes::Matches)


392
393
394
# File 'lib/queries/query/autocomplete.rb', line 392

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

#only_ids ⇒ Arek::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.



383
384
385
386
387
388
389
# File 'lib/queries/query/autocomplete.rb', line 383

def only_ids
  if only_integers?
    with_id
  else
    nil
  end
end

#only_integers? ⇒ Boolean

Returns:

  • (Boolean)


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

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

#order_by_id_rank(query, column, ids) ⇒ ActiveRecord::Relation

Order query by the position of column in ids, e.g. another autocomplete's ranked results, so that a limit keeps the best matches. Ties (e.g. several records per id) are broken by #table id.

Parameters:

  • query (ActiveRecord::Relation) —

    of referenced_klass

  • column (String) —

    a qualified column holding the ids, e.g. 'otus.id'

  • ids (Array) —

    not empty, in rank order

Returns:

  • (ActiveRecord::Relation)


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

def order_by_id_rank(query, column, ids)
  query.order(Arel.sql(
    "array_position(ARRAY[#{ids.map(&:to_i).join(',')}], #{column}), #{table.name}.id"
  ))
end

#parent ⇒ Arel::Nodes::TableAlias

Returns used in heirarchy joins.

Returns:

  • (Arel::Nodes::TableAlias) —

    used in heirarchy joins



403
404
405
# File 'lib/queries/query/autocomplete.rb', line 403

def parent
  table.alias
end

#parent_child_join ⇒ Scope

Returns:

  • (Scope)


359
360
361
# File 'lib/queries/query/autocomplete.rb', line 359

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

#parent_child_where ⇒ Arel::Nodes::Grouping

Match at two levels, for example, 'wa te" will match "Washington Co., Texas"

Returns:

  • (Arel::Nodes::Grouping)


365
366
367
368
369
# File 'lib/queries/query/autocomplete.rb', line 365

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

#pieces ⇒ Array

TODO: used?!

Returns:

  • (Array) —

    split on whitespace



332
333
334
# File 'lib/queries/query/autocomplete.rb', line 332

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

#safe_integers ⇒ Array<Integer>

Returns Array of integers parsed from query_string that fit within the 4-byte SQL integer range (1 to 2_147_483_647).

Returns:

  • (Array<Integer>) —

    Array of integers parsed from query_string that fit within the 4-byte SQL integer range (1 to 2_147_483_647)



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

def safe_integers
  integers
    .map(&:to_i)
    .select { |i| i.between?(1, 2_147_483_647) }
end

#scope ⇒ Scope

stub TODO: deprecate? probably unused

Returns:

  • (Scope)


271
272
273
# File 'lib/queries/query/autocomplete.rb', line 271

def scope
  where('1 = 2')
end

#string_fragments ⇒ Array

Used in unordered AND searches

Returns:

  • (Array) —

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



320
321
322
323
324
325
326
327
# File 'lib/queries/query/autocomplete.rb', line 320

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

#wildcard_wrapped_integers ⇒ Array

Returns:

  • (Array)


337
338
339
# File 'lib/queries/query/autocomplete.rb', line 337

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

#wildcard_wrapped_years ⇒ Array

Returns:

  • (Array)


342
343
344
# File 'lib/queries/query/autocomplete.rb', line 342

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

#with_cached ⇒ Arel::Nodes::Matches

Returns:

  • (Arel::Nodes::Matches)


418
419
420
# File 'lib/queries/query/autocomplete.rb', line 418

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

#with_cached_like ⇒ Arel::Nodes::Matches

Returns:

  • (Arel::Nodes::Matches)


423
424
425
# File 'lib/queries/query/autocomplete.rb', line 423

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

#with_id ⇒ Arel::Nodes?

Returns used in or_clauses.

Returns:

  • (Arel::Nodes, nil) —

    used in or_clauses



373
374
375
376
377
378
379
# File 'lib/queries/query/autocomplete.rb', line 373

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

#with_project_id ⇒ Arel::Nodes::Equality

TODO: nil/or clause this

Returns:

  • (Arel::Nodes::Equality)


409
410
411
412
413
414
415
# File 'lib/queries/query/autocomplete.rb', line 409

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

#year_letter ⇒ String?

Returns:

  • (String, nil)


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

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

#years ⇒ Array

Returns:

  • (Array)


276
277
278
# File 'lib/queries/query/autocomplete.rb', line 276

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