module Fuzzily::Searchable::ClassMethods

Public Instance Methods

fuzzily_searchable(*fields) click to toggle source

fuzzily_searchable <field> [, <field>…] [, <options>]

# File lib/fuzzily/searchable.rb, line 31
def fuzzily_searchable(*fields)
  options = fields.last.kind_of?(Hash) ? fields.pop : {}

  fields.each do |field|
    make_field_fuzzily_searchable(field, options)
  end
end

Private Instance Methods

_bulk_update_fuzzy(_o) click to toggle source
# File lib/fuzzily/searchable.rb, line 67
def _bulk_update_fuzzy(_o)
  trigram_class = _o.trigram_class_name.constantize
  supports_bulk_inserts  =
    connection.class.name !~ /sqlite/i ||
    (
      defined?(SQLite3::SQLITE_VERSION) &&
      Gem::Version.new(SQLite3::SQLITE_VERSION) >= Gem::Version.new("3.7.11")
    )

  _with_included_trigrams(_o).find_in_batches(batch_size: 100) do |batch|
    inserts = []
    batch.each do |record|
      data = Fuzzily::String.new(record.send(_o.field))
      data.scored_trigrams.each do |trigram, score|
        inserts << sanitize_sql_array(["(?,?,?,?,?)", self.name, record.id, _o.field.to_s, score, trigram])
      end
    end

    # take care of quoting
    c = trigram_class.connection
    insert_sql = %Q{
      INSERT INTO %s (%s, %s, %s, %s, %s)
      VALUES
    } % [
      c.quote_table_name(trigram_class.table_name),
      c.quote_column_name("owner_type"),
      c.quote_column_name("owner_id"),
      c.quote_column_name("fuzzy_field"),
      c.quote_column_name("score"),
      c.quote_column_name("trigram")
    ]

    trigram_class.transaction do
      batch.each { |record| record.send(_o.trigram_association).delete_all }
      break if inserts.empty?

      if supports_bulk_inserts
        trigram_class.connection.insert(insert_sql + inserts.join(", "))
      else
        inserts.each do |insert|
          trigram_class.connection.insert(insert_sql + insert)
        end
      end
    end
  end
end
_find_by_fuzzy(_o, pattern, options={}) click to toggle source
# File lib/fuzzily/searchable.rb, line 41
def _find_by_fuzzy(_o, pattern, options={})
  options[:limit] ||= 10 unless options.has_key? :limit
  options[:offset] ||= 0

  trigrams = _o.trigram_class_name.constantize.
    limit(options[:limit]).
    offset(options[:offset]).
    for_model(self.name).
    for_field(_o.field.to_s).
    matches_for(pattern)
  records = _load_for_ids(trigrams.map(&:owner_id))
  # order records as per trigram query (no portable way to do this in SQL)
  trigrams.map { |t| records[t.owner_id] }.compact
end
_load_for_ids(ids) click to toggle source
# File lib/fuzzily/searchable.rb, line 56
def _load_for_ids(ids)
  {}.tap do |result|
    results = if respond_to? :where
      where(id: ids)
    else
      find(:all, ids)
    end
    results.each { |_r| result[_r.id] = _r }
  end
end
make_field_fuzzily_searchable(field, options={}) click to toggle source
# File lib/fuzzily/searchable.rb, line 114
def make_field_fuzzily_searchable(field, options={})
  class_variable_defined?(:"@@fuzzily_searchable_#{field}") and return

  _o = OpenStruct.new(
    field:                  field,
    trigram_class_name:     options.fetch(:class_name, "Trigram"),
    trigram_association:    "trigrams_for_#{field}".to_sym,
    update_trigrams_method: "update_fuzzy_#{field}!".to_sym,
    async:                  options.fetch(:async, false)
  )

  _add_trigram_association(_o)

  singleton_class.send(:define_method, "find_by_fuzzy_#{field}".to_sym) do |*args|
    _find_by_fuzzy(_o, *args)
  end

  singleton_class.send(:define_method, "bulk_update_fuzzy_#{field}".to_sym) do
    _bulk_update_fuzzy(_o)
  end

  define_method _o.update_trigrams_method do
    if _o.async && self.respond_to?(:delay)
      self.delay._update_fuzzy!(_o)
    else
      _update_fuzzy!(_o)
    end
  end

  after_save do |record|
    next unless record.send("saved_change_to_#{field}?".to_sym)

    record.send(_o.update_trigrams_method)
  end

  class_variable_set(:"@@fuzzily_searchable_#{field}", true)
  self
end