Module: Knitsearch::Model

Extended by:
ActiveSupport::Concern
Defined in:
lib/knitsearch/model.rb

Overview

The user-facing concern. Include in an ActiveRecord model and call ‘searchable_by` with the columns you want indexed:

class Article < ApplicationRecord
  include Knitsearch::Model
  searchable_by against: { title: 'A', body: 'B' }
end

Sync happens via SQLite triggers, not ActiveRecord callbacks. Triggers are created in the migration and fire atomically inside the source transaction.

Instance Method Summary collapse

Instance Method Details

#knitsearch_cascade_to_childrenObject



562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
# File 'lib/knitsearch/model.rb', line 562

def knitsearch_cascade_to_children
  dependents = Knitsearch.belongs_to_dependents[self.class]
  return unless dependents

  dependents.each do |dependent|
    child_model = dependent[:model]
    fk = dependent[:foreign_key]
    shadow_map = dependent[:columns]

    # Build SET clause for update_all: { shadow_col => new_parent_value, ... }
    updates = {}
    shadow_map.each do |shadow_col, source_col|
      value = send(source_col)&.to_s
      updates[shadow_col] = value
    end

    child_model.where(fk => id).update_all(updates)
  end
end

#search_highlight(column) ⇒ Object



546
547
548
549
550
# File 'lib/knitsearch/model.rb', line 546

def search_highlight(column)
  raw = self["searchable_highlight_#{column}"]
  return nil if raw.nil?
  Knitsearch::Highlighter.render(raw)
end

#search_snippet(column) ⇒ Object



552
553
554
555
556
# File 'lib/knitsearch/model.rb', line 552

def search_snippet(column)
  raw = self["searchable_snippet_#{column}"]
  return nil if raw.nil?
  Knitsearch::Highlighter.render(raw)
end

#searchable_scoreObject



558
559
560
# File 'lib/knitsearch/model.rb', line 558

def searchable_score
  self["searchable_score"]
end

#should_sync_associated?Boolean

Returns:

  • (Boolean)


515
516
517
# File 'lib/knitsearch/model.rb', line 515

def should_sync_associated?
  self.class.associated_mapping.any?
end

#should_sync_rich_text?Boolean

Returns:

  • (Boolean)


499
500
501
# File 'lib/knitsearch/model.rb', line 499

def should_sync_rich_text?
  self.class.rich_text_mapping.any?
end

#sync_associated_to_shadow_columnsObject



519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
# File 'lib/knitsearch/model.rb', line 519

def sync_associated_to_shadow_columns
  self.class.associated_mapping.each do |assoc_name, columns|
    reflection = self.class.reflect_on_association(assoc_name)

    columns.each do |col, _weight|
      shadow_column = "#{assoc_name}_#{col}_plain_text"

      if reflection.macro == :belongs_to
        # belongs_to: sync the parent's value
        assoc_object = send(assoc_name)
        value = if assoc_object.nil?
                  nil
                else
                  assoc_object.send(col)&.to_s
                end
        send("#{shadow_column}=", value)
      elsif reflection.macro == :has_many
        # has_many (both plain and through): sync from the live association
        # Plain has_many: synced on create via before_save, then updated from child side
        # has_many :through: synced on create via before_save, then updated from join/target side
        values = send(assoc_name).pluck(col).compact.map(&:to_s)
        send("#{shadow_column}=", values.any? ? values.join(" ") : nil)
      end
    end
  end
end

#sync_rich_text_to_shadow_columnsObject



503
504
505
506
507
508
509
510
511
512
513
# File 'lib/knitsearch/model.rb', line 503

def sync_rich_text_to_shadow_columns
  self.class.rich_text_mapping.each do |declared_field, shadow_column|
    rich_text_body = send(declared_field)
    plain_text = if rich_text_body.nil?
                   nil
                 else
                   extract_plain_text_from_action_text(rich_text_body)
                 end
    send("#{shadow_column}=", plain_text)
  end
end