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 32
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 63
def _bulk_update_fuzzy(_o)
  trigram_class = _o.trigram_class_name.constantize

  supports_bulk_inserts  =
    connection.class.name !~ /sqlite/i ||
    connection.send(:sqlite_version) >= '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 42
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] }
end
_load_for_ids(ids) click to toggle source
# File lib/fuzzily/searchable.rb, line 57
def _load_for_ids(ids)
  {}.tap do |result|
    find(ids).each { |_r| result[_r.id] = _r }
  end
end
make_field_fuzzily_searchable(field, options={}) click to toggle source
# File lib/fuzzily/searchable.rb, line 108
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("#{field}_changed?".to_sym)
    
    record.send(_o.update_trigrams_method)
  end

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