Skip to content

Rails Examples

Real-world examples and implementation patterns for using ragdoll-rails in various scenarios.

Complete Application Examples

Knowledge Base Application

A complete knowledge base application using Ragdoll Rails.

Models

# app/models/user.rb
class User < ApplicationRecord
  has_many :articles, dependent: :destroy
  has_many :saved_searches, dependent: :destroy

  enum role: { viewer: 0, editor: 1, admin: 2 }

  def can_upload_documents?
    editor? || admin?
  end
end

# app/models/article.rb
class Article < ApplicationRecord
  include Ragdoll::Searchable

  belongs_to :user
  belongs_to :category, optional: true
  has_many_attached :attachments

  validates :title, presence: true, length: { maximum: 255 }
  validates :content, presence: true
  validates :status, inclusion: { in: %w[draft published archived] }

  ragdoll_searchable do |config|
    config.content_field = :content
    config.title_field = :title
    config.metadata_fields = [:category_name, :tags, :author_name, :status]
    config.chunk_size = 800
    config.auto_process = true
    config.process_on_create = true
    config.process_on_update = true

    config.custom_metadata = ->(article) {
      {
        category_name: article.category&.name,
        author_name: article.user.name,
        word_count: article.content.split.size,
        reading_time: (article.content.split.size / 200.0).ceil,
        tags: article.tag_list,
        last_updated: article.updated_at.iso8601
      }
    }
  end

  scope :published, -> { where(status: 'published') }
  scope :by_category, ->(category) { where(category: category) }
  scope :by_user, ->(user) { where(user: user) }
  scope :recent, ->(days = 30) { where(created_at: days.days.ago..) }

  def category_name
    category&.name
  end

  def author_name
    user.name
  end

  def tag_list
    tags.split(',').map(&:strip) if tags.present?
  end

  def reading_time_minutes
    (content.split.size / 200.0).ceil
  end
end

# app/models/category.rb
class Category < ApplicationRecord
  has_many :articles, dependent: :destroy

  validates :name, presence: true, uniqueness: true
  validates :description, presence: true

  scope :with_articles, -> { joins(:articles).distinct }

  def article_count
    articles.published.count
  end
end

# app/models/saved_search.rb
class SavedSearch < ApplicationRecord
  belongs_to :user

  validates :name, presence: true
  validates :query, presence: true

  scope :public_searches, -> { where(is_public: true) }
  scope :by_user, ->(user) { where(user: user) }

  def execute
    options = filters.present? ? { filters: filters } : {}
    Article.search(query, options)
  end
end

Controllers

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :authenticate_user!
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
    devise_parameter_sanitizer.permit(:account_update, keys: [:name])
  end
end

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  before_action :set_article, only: [:show, :edit, :update, :destroy]
  before_action :check_edit_permission, only: [:edit, :update, :destroy]

  def index
    @articles = Article.published.includes(:user, :category)
    @articles = @articles.by_category(params[:category_id]) if params[:category_id].present?
    @articles = @articles.page(params[:page]).per(20)

    @categories = Category.with_articles
  end

  def show
    @related_articles = @article.find_similar(limit: 5)
      .where.not(id: @article.id)
      .where(status: 'published')
  end

  def new
    @article = current_user.articles.build
    @categories = Category.all
  end

  def create
    @article = current_user.articles.build(article_params)

    if @article.save
      redirect_to @article, notice: 'Article was successfully created.'
    else
      @categories = Category.all
      render :new
    end
  end

  def edit
    @categories = Category.all
  end

  def update
    if @article.update(article_params)
      redirect_to @article, notice: 'Article was successfully updated.'
    else
      @categories = Category.all
      render :edit
    end
  end

  def destroy
    @article.destroy
    redirect_to articles_url, notice: 'Article was successfully deleted.'
  end

  private

  def set_article
    @article = Article.find(params[:id])
  end

  def article_params
    params.require(:article).permit(:title, :content, :category_id, :tags, :status, attachments: [])
  end

  def check_edit_permission
    redirect_to articles_path, alert: 'Access denied.' unless can_edit_article?(@article)
  end

  def can_edit_article?(article)
    current_user.admin? || article.user == current_user
  end
end

# app/controllers/search_controller.rb
class SearchController < ApplicationController
  before_action :set_search_params, only: [:index, :suggestions]

  def index
    return unless @query.present?

    @search_service = ArticleSearchService.new(@query, search_options)
    @results = @search_service.call
    @facets = @search_service.facets
    @suggestions = @search_service.suggestions if @results.empty?

    track_search_analytics
  end

  def suggestions
    @suggestions = ArticleSuggestionService.new(@query).call
    render json: @suggestions
  end

  def saved_searches
    @saved_searches = current_user.saved_searches.order(:name)
    @public_searches = SavedSearch.public_searches.includes(:user).limit(10)
  end

  def save_search
    @saved_search = current_user.saved_searches.build(saved_search_params)

    if @saved_search.save
      render json: { success: true, id: @saved_search.id }
    else
      render json: { success: false, errors: @saved_search.errors }
    end
  end

  private

  def set_search_params
    @query = params[:q]
    @filters = params[:filters] || {}
    @sort = params[:sort] || 'relevance'
    @page = params[:page] || 1
  end

  def search_options
    {
      filters: @filters,
      sort: @sort,
      page: @page,
      per_page: 15,
      include_facets: true,
      search_type: params[:search_type] || 'hybrid'
    }
  end

  def track_search_analytics
    SearchAnalytic.create!(
      user: current_user,
      query: @query,
      results_count: @results.size,
      search_type: params[:search_type] || 'hybrid',
      filters: @filters,
      response_time: @search_service.search_time
    )
  end

  def saved_search_params
    params.require(:saved_search).permit(:name, :query, :is_public, filters: {})
  end
end

Services

# app/services/article_search_service.rb
class ArticleSearchService
  attr_reader :search_time

  def initialize(query, options = {})
    @query = query
    @options = options
    @search_time = 0
  end

  def call
    start_time = Time.current

    results = perform_search
    results = apply_filters(results) if @options[:filters].present?
    results = apply_sorting(results)
    results = paginate_results(results)

    @search_time = Time.current - start_time

    add_facets(results) if @options[:include_facets]

    results
  end

  def facets
    return {} unless @options[:include_facets]

    {
      'category' => category_facets,
      'author' => author_facets,
      'status' => status_facets,
      'created_at' => date_facets
    }
  end

  def suggestions
    return [] if @query.blank?

    # Use a suggestion service or implement basic suggestions
    ArticleSuggestionService.new(@query).call
  end

  private

  def perform_search
    case @options[:search_type]
    when 'semantic'
      Article.semantic_search(@query, limit: 100)
    when 'keyword'
      Article.keyword_search(@query).limit(100)
    else
      Article.search(@query, limit: 100)
    end
  end

  def apply_filters(results)
    filtered = results

    if @options[:filters][:category].present?
      category = Category.find(@options[:filters][:category])
      filtered = filtered.where(category: category)
    end

    if @options[:filters][:author].present?
      user = User.find(@options[:filters][:author])
      filtered = filtered.where(user: user)
    end

    if @options[:filters][:date_range].present?
      case @options[:filters][:date_range]
      when 'week'
        filtered = filtered.where(created_at: 1.week.ago..)
      when 'month'
        filtered = filtered.where(created_at: 1.month.ago..)
      when 'year'
        filtered = filtered.where(created_at: 1.year.ago..)
      end
    end

    filtered
  end

  def apply_sorting(results)
    case @options[:sort]
    when 'date_desc'
      results.order(created_at: :desc)
    when 'date_asc'
      results.order(created_at: :asc)
    when 'title'
      results.order(:title)
    else
      results # Keep relevance sorting from search
    end
  end

  def paginate_results(results)
    page = @options[:page] || 1
    per_page = @options[:per_page] || 15

    results.page(page).per(per_page)
  end

  def add_facets(results)
    # Add facet information to results
    results.define_singleton_method(:facets) { facets }
  end

  def category_facets
    Article.published.joins(:category)
      .group('categories.name')
      .count
      .map { |name, count| { name: name, count: count } }
  end

  def author_facets
    Article.published.joins(:user)
      .group('users.name')
      .count
      .map { |name, count| { name: name, count: count } }
      .sort_by { |item| -item[:count] }
      .first(10)
  end

  def status_facets
    Article.group(:status).count
      .map { |status, count| { name: status.humanize, value: status, count: count } }
  end

  def date_facets
    [
      { name: 'Past Week', value: 'week', count: Article.where(created_at: 1.week.ago..).count },
      { name: 'Past Month', value: 'month', count: Article.where(created_at: 1.month.ago..).count },
      { name: 'Past Year', value: 'year', count: Article.where(created_at: 1.year.ago..).count }
    ]
  end
end

# app/services/article_suggestion_service.rb
class ArticleSuggestionService
  def initialize(query)
    @query = query.to_s.downcase.strip
  end

  def call
    return [] if @query.blank? || @query.length < 3

    suggestions = []

    # Title-based suggestions
    suggestions += title_suggestions

    # Tag-based suggestions
    suggestions += tag_suggestions

    # Category-based suggestions
    suggestions += category_suggestions

    suggestions.uniq.first(5)
  end

  private

  def title_suggestions
    Article.published
      .where("LOWER(title) LIKE ?", "%#{@query}%")
      .limit(3)
      .pluck(:title)
  end

  def tag_suggestions
    # Assuming tags are stored as comma-separated strings
    Article.published
      .where("LOWER(tags) LIKE ?", "%#{@query}%")
      .pluck(:tags)
      .flat_map { |tags| tags.split(',').map(&:strip) }
      .select { |tag| tag.downcase.include?(@query) }
      .uniq
      .first(2)
  end

  def category_suggestions
    Category.where("LOWER(name) LIKE ?", "%#{@query}%")
      .pluck(:name)
      .first(2)
  end
end

Views

<!-- app/views/articles/index.html.erb -->
<div class="articles-index">
  <div class="page-header">
    <h1>Knowledge Base</h1>
    <div class="header-actions">
      <%= link_to "Search Articles", search_path, class: "btn btn-outline-primary" %>
      <% if current_user.can_upload_documents? %>
        <%= link_to "New Article", new_article_path, class: "btn btn-primary" %>
      <% end %>
    </div>
  </div>

  <div class="filters-section">
    <%= form_with url: articles_path, method: :get, local: true, class: "filter-form" do |form| %>
      <div class="filter-group">
        <%= form.select :category_id, 
            options_from_collection_for_select(@categories, :id, :name, params[:category_id]), 
            { prompt: 'All Categories' }, 
            { class: "form-select" } %>
      </div>
      <%= form.submit "Filter", class: "btn btn-outline-primary" %>
    <% end %>
  </div>

  <div class="articles-grid">
    <% @articles.each do |article| %>
      <div class="article-card">
        <div class="article-content">
          <h3 class="article-title">
            <%= link_to article.title, article_path(article) %>
          </h3>

          <p class="article-excerpt">
            <%= truncate(strip_tags(article.content), length: 150) %>
          </p>

          <div class="article-meta">
            <span class="author">By <%= article.author_name %></span>
            <span class="date"><%= article.created_at.strftime("%B %d, %Y") %></span>
            <% if article.category %>
              <span class="category"><%= article.category.name %></span>
            <% end %>
            <span class="reading-time"><%= article.reading_time_minutes %> min read</span>
          </div>

          <% if article.tag_list.present? %>
            <div class="article-tags">
              <% article.tag_list.each do |tag| %>
                <span class="tag"><%= tag %></span>
              <% end %>
            </div>
          <% end %>
        </div>
      </div>
    <% end %>
  </div>

  <div class="pagination-wrapper">
    <%= paginate @articles %>
  </div>
</div>
<!-- app/views/articles/show.html.erb -->
<div class="article-show">
  <div class="article-header">
    <div class="breadcrumb">
      <%= link_to "Articles", articles_path %>
      <% if @article.category %>
<%= link_to @article.category.name, articles_path(category_id: @article.category.id) %>
      <% end %>
<%= @article.title %>
    </div>

    <h1 class="article-title"><%= @article.title %></h1>

    <div class="article-meta">
      <div class="author-info">
        <strong><%= @article.author_name %></strong>
        <span class="publish-date">
          Published <%= @article.created_at.strftime("%B %d, %Y") %>
        </span>
        <% if @article.updated_at > @article.created_at + 1.hour %>
          <span class="update-date">
            (Updated <%= @article.updated_at.strftime("%B %d, %Y") %>)
          </span>
        <% end %>
      </div>

      <div class="article-stats">
        <span class="reading-time">
          <i class="bi bi-clock"></i> <%= @article.reading_time_minutes %> min read
        </span>
        <% if @article.category %>
          <span class="category">
            <i class="bi bi-tag"></i> <%= @article.category.name %>
          </span>
        <% end %>
      </div>
    </div>

    <% if can_edit_article?(@article) %>
      <div class="article-actions">
        <%= link_to "Edit", edit_article_path(@article), class: "btn btn-secondary" %>
        <%= link_to "Delete", article_path(@article), method: :delete,
                    confirm: "Are you sure?", class: "btn btn-outline-danger" %>
      </div>
    <% end %>
  </div>

  <div class="article-content">
    <%= simple_format(@article.content) %>
  </div>

  <% if @article.attachments.any? %>
    <div class="article-attachments">
      <h3>Attachments</h3>
      <% @article.attachments.each do |attachment| %>
        <div class="attachment">
          <%= link_to attachment.filename, rails_blob_path(attachment, disposition: "attachment") %>
          <span class="file-size">(<%= number_to_human_size(attachment.byte_size) %>)</span>
        </div>
      <% end %>
    </div>
  <% end %>

  <% if @article.tag_list.present? %>
    <div class="article-tags">
      <h4>Tags:</h4>
      <% @article.tag_list.each do |tag| %>
        <span class="tag"><%= tag %></span>
      <% end %>
    </div>
  <% end %>

  <% if @related_articles.any? %>
    <div class="related-articles">
      <h3>Related Articles</h3>
      <div class="related-grid">
        <% @related_articles.each do |article| %>
          <div class="related-card">
            <h4><%= link_to article.title, article_path(article) %></h4>
            <p><%= truncate(strip_tags(article.content), length: 100) %></p>
            <small>By <%= article.author_name %></small>
          </div>
        <% end %>
      </div>
    </div>
  <% end %>
</div>
<!-- app/views/search/index.html.erb -->
<div class="search-page">
  <div class="search-header">
    <h1>Search Knowledge Base</h1>
  </div>

  <div class="search-form">
    <%= form_with url: search_path, method: :get, local: true, 
                  data: { controller: "search", search_suggestions_url_value: suggestions_search_path } do |form| %>
      <div class="search-input-group">
        <%= form.text_field :q, value: params[:q], 
                           placeholder: "Search articles, categories, or tags...", 
                           class: "search-input form-control",
                           data: { 
                             action: "input->search#suggest",
                             search_target: "input"
                           } %>
        <%= form.submit "Search", class: "btn btn-primary" %>
      </div>

      <div class="search-options">
        <div class="row">
          <div class="col-md-4">
            <%= form.select :search_type, [
              ['Smart Search (Recommended)', 'hybrid'],
              ['Meaning-based Search', 'semantic'],
              ['Exact Word Search', 'keyword']
            ], { selected: params[:search_type] || 'hybrid' }, { class: "form-select" } %>
          </div>
          <div class="col-md-4">
            <%= form.select :sort, [
              ['Most Relevant', 'relevance'],
              ['Newest First', 'date_desc'],
              ['Oldest First', 'date_asc'],
              ['Alphabetical', 'title']
            ], { selected: params[:sort] || 'relevance' }, { class: "form-select" } %>
          </div>
        </div>
      </div>
    <% end %>

    <div class="search-suggestions" data-search-target="suggestions" style="display: none;">
      <!-- Populated via Stimulus -->
    </div>
  </div>

  <% if @results.present? %>
    <div class="search-results-section">
      <div class="search-meta">
        <p>Found <strong><%= pluralize(@results.total_count, 'article') %></strong> 
           <% if params[:q].present? %>for "<strong><%= params[:q] %></strong>"<% end %>
           <span class="search-time">in <%= number_with_precision(@search_service.search_time, precision: 3) %>s</span>
        </p>
      </div>

      <div class="search-content">
        <% if @facets.present? %>
          <div class="search-facets">
            <h4>Filter Results</h4>

            <% @facets.each do |facet_name, facet_data| %>
              <div class="facet-group">
                <h5><%= facet_name.humanize %></h5>
                <% facet_data.each do |item| %>
                  <div class="facet-item">
                    <%= link_to "#{item[:name]} (#{item[:count]})", 
                                search_path(q: params[:q], filters: { facet_name => item[:value] || item[:name] }),
                                class: "facet-link" %>
                  </div>
                <% end %>
              </div>
            <% end %>
          </div>
        <% end %>

        <div class="search-results">
          <% @results.each do |article| %>
            <div class="search-result">
              <div class="result-header">
                <h3 class="result-title">
                  <%= link_to article.title, article_path(article) %>
                </h3>
                <div class="result-meta">
                  <% if respond_to?(:similarity_score) && article.respond_to?(:similarity_score) %>
                    <span class="similarity-score">
                      <%= number_to_percentage(article.similarity_score * 100, precision: 1) %> match
                    </span>
                  <% end %>
                  <span class="author">by <%= article.author_name %></span>
                  <span class="date"><%= article.created_at.strftime("%b %d, %Y") %></span>
                  <% if article.category %>
                    <span class="category"><%= article.category.name %></span>
                  <% end %>
                </div>
              </div>

              <div class="result-content">
                <p class="result-snippet">
                  <%= search_result_snippet(article.content, params[:q]) %>
                </p>
              </div>
            </div>
          <% end %>

          <div class="search-pagination">
            <%= paginate @results %>
          </div>
        </div>
      </div>
    </div>
  <% elsif params[:q].present? %>
    <div class="no-results">
      <h3>No articles found</h3>
      <p>Try different keywords or check your spelling.</p>

      <% if @suggestions.present? %>
        <div class="search-suggestions">
          <p>Did you mean:</p>
          <ul>
            <% @suggestions.each do |suggestion| %>
              <li><%= link_to suggestion, search_path(q: suggestion) %></li>
            <% end %>
          </ul>
        </div>
      <% end %>
    </div>
  <% end %>
</div>

Document Management System

A corporate document management system with advanced features.

Advanced Model with Versioning

# app/models/document.rb
class Document < ApplicationRecord
  include Ragdoll::Searchable

  belongs_to :user
  belongs_to :department
  has_many :document_versions, dependent: :destroy
  has_many :document_shares, dependent: :destroy
  has_many :shared_with_users, through: :document_shares, source: :user
  has_many :document_comments, dependent: :destroy
  has_one_attached :file

  validates :title, presence: true
  validates :visibility, inclusion: { in: %w[private department company public] }

  ragdoll_searchable do |config|
    config.content_field = :extracted_content
    config.title_field = :title
    config.metadata_fields = [:department_name, :document_type, :tags, :visibility]
    config.chunk_size = 1200
    config.auto_process = true

    config.custom_metadata = ->(doc) {
      {
        department_name: doc.department.name,
        document_type: doc.document_type,
        file_extension: doc.file_extension,
        version: doc.current_version,
        shared_count: doc.document_shares.count,
        comment_count: doc.document_comments.count,
        last_accessed: doc.last_accessed_at,
        security_level: doc.security_level
      }
    }
  end

  enum document_type: {
    policy: 0,
    procedure: 1,
    manual: 2,
    report: 3,
    presentation: 4,
    specification: 5,
    other: 99
  }

  enum security_level: {
    public: 0,
    internal: 1,
    confidential: 2,
    restricted: 3
  }

  scope :accessible_by, ->(user) {
    where(
      "(visibility = 'public') OR " \
      "(visibility = 'company' AND :user_id IS NOT NULL) OR " \
      "(visibility = 'department' AND department_id = :dept_id) OR " \
      "(user_id = :user_id) OR " \
      "(id IN (SELECT document_id FROM document_shares WHERE user_id = :user_id))",
      user_id: user&.id,
      dept_id: user&.department_id
    )
  }

  def create_version!
    self.current_version += 1
    document_versions.create!(
      version_number: current_version,
      title: title,
      content: extracted_content,
      file_data: file.attached? ? file.blob : nil,
      created_by: self.user
    )
    save!
  end

  def share_with!(user, permission: 'read')
    document_shares.find_or_create_by(user: user) do |share|
      share.permission = permission
    end
  end

  def accessible_by?(user)
    return true if user == self.user

    case visibility
    when 'public' then true
    when 'company' then user.present?
    when 'department' then user&.department == department
    when 'private' then false
    else false
    end || document_shares.exists?(user: user)
  end

  def track_access!(user)
    update!(
      last_accessed_at: Time.current,
      access_count: access_count + 1
    )

    DocumentAccess.create!(
      document: self,
      user: user,
      accessed_at: Time.current,
      ip_address: Current.ip_address,
      user_agent: Current.user_agent
    )
  end
end

# app/models/document_version.rb
class DocumentVersion < ApplicationRecord
  belongs_to :document
  belongs_to :created_by, class_name: 'User'
  has_one_attached :file

  validates :version_number, presence: true, uniqueness: { scope: :document_id }

  scope :ordered, -> { order(version_number: :desc) }

  def restore!
    document.update!(
      title: title,
      extracted_content: content,
      current_version: version_number
    )

    if file_data
      document.file.attach(file_data)
      document.ragdoll_process!
    end
  end
end

# app/models/document_share.rb
class DocumentShare < ApplicationRecord
  belongs_to :document
  belongs_to :user

  validates :permission, inclusion: { in: %w[read write admin] }
  validates :user_id, uniqueness: { scope: :document_id }

  scope :with_write_access, -> { where(permission: %w[write admin]) }
  scope :with_admin_access, -> { where(permission: 'admin') }
end

Advanced Search with Filters

# app/services/advanced_document_search_service.rb
class AdvancedDocumentSearchService
  include ActionView::Helpers::DateHelper

  attr_reader :search_time, :total_results

  def initialize(query, user, options = {})
    @query = query
    @user = user
    @options = options
    @search_time = 0
    @total_results = 0
  end

  def call
    start_time = Time.current

    results = perform_search
    results = apply_security_filters(results)
    results = apply_advanced_filters(results)
    results = apply_sorting(results)
    results = paginate_results(results)

    @search_time = Time.current - start_time
    @total_results = results.total_count

    enhance_results(results)
  end

  def facets
    {
      'document_type' => document_type_facets,
      'department' => department_facets,
      'security_level' => security_level_facets,
      'file_type' => file_type_facets,
      'date_range' => date_range_facets,
      'author' => author_facets
    }
  end

  private

  def perform_search
    if @query.present?
      Document.search(@query, search_options)
    else
      Document.accessible_by(@user)
    end
  end

  def search_options
    {
      limit: 1000, # Get more results for filtering
      search_type: @options[:search_type] || 'hybrid',
      threshold: @options[:threshold] || 0.5
    }
  end

  def apply_security_filters(results)
    # Additional security filtering beyond basic accessibility
    filtered = results.accessible_by(@user)

    # Filter by security clearance
    if @user.security_clearance.present?
      max_level = security_level_mapping[@user.security_clearance]
      filtered = filtered.where(security_level: 0..max_level)
    end

    filtered
  end

  def apply_advanced_filters(results)
    filtered = results

    # Document type filter
    if @options[:document_type].present?
      filtered = filtered.where(document_type: @options[:document_type])
    end

    # Department filter
    if @options[:department_id].present?
      filtered = filtered.where(department_id: @options[:department_id])
    end

    # File type filter
    if @options[:file_type].present?
      filtered = filtered.joins(:file_attachment)
        .where(active_storage_blobs: { content_type: @options[:file_type] })
    end

    # Date range filter
    if @options[:date_range].present?
      filtered = apply_date_filter(filtered, @options[:date_range])
    end

    # Security level filter
    if @options[:security_level].present?
      filtered = filtered.where(security_level: @options[:security_level])
    end

    # Author filter
    if @options[:author_id].present?
      filtered = filtered.where(user_id: @options[:author_id])
    end

    # Tags filter
    if @options[:tags].present?
      tag_conditions = @options[:tags].map { "tags ILIKE ?" }
      tag_values = @options[:tags].map { |tag| "%#{tag}%" }
      filtered = filtered.where(tag_conditions.join(' AND '), *tag_values)
    end

    # File size filter
    if @options[:min_size].present? || @options[:max_size].present?
      filtered = apply_file_size_filter(filtered)
    end

    filtered
  end

  def apply_date_filter(results, date_range)
    case date_range
    when 'today'
      results.where(created_at: Date.current.beginning_of_day..)
    when 'week'
      results.where(created_at: 1.week.ago..)
    when 'month'
      results.where(created_at: 1.month.ago..)
    when 'quarter'
      results.where(created_at: 3.months.ago..)
    when 'year'
      results.where(created_at: 1.year.ago..)
    when 'custom'
      if @options[:start_date] && @options[:end_date]
        start_date = Date.parse(@options[:start_date])
        end_date = Date.parse(@options[:end_date])
        results.where(created_at: start_date.beginning_of_day..end_date.end_of_day)
      else
        results
      end
    else
      results
    end
  end

  def apply_file_size_filter(results)
    joins_clause = <<~SQL
      LEFT JOIN active_storage_attachments asa ON asa.record_id = documents.id 
        AND asa.record_type = 'Document' AND asa.name = 'file'
      LEFT JOIN active_storage_blobs asb ON asb.id = asa.blob_id
    SQL

    filtered = results.joins(joins_clause)

    if @options[:min_size].present?
      min_bytes = parse_file_size(@options[:min_size])
      filtered = filtered.where('asb.byte_size >= ?', min_bytes)
    end

    if @options[:max_size].present?
      max_bytes = parse_file_size(@options[:max_size])
      filtered = filtered.where('asb.byte_size <= ?', max_bytes)
    end

    filtered
  end

  def apply_sorting(results)
    case @options[:sort]
    when 'created_desc'
      results.order(created_at: :desc)
    when 'created_asc'
      results.order(created_at: :asc)
    when 'updated_desc'
      results.order(updated_at: :desc)
    when 'title_asc'
      results.order(:title)
    when 'title_desc'
      results.order(title: :desc)
    when 'size_desc'
      results.joins(:file_attachment, :file_blob).order('active_storage_blobs.byte_size DESC')
    when 'access_count'
      results.order(access_count: :desc)
    else
      results # Keep relevance ordering from search
    end
  end

  def paginate_results(results)
    page = @options[:page] || 1
    per_page = [@options[:per_page] || 20, 100].min

    results.page(page).per(per_page)
  end

  def enhance_results(results)
    # Add additional data to results
    results.each do |document|
      # Track that this document appeared in search results
      SearchResult.create!(
        user: @user,
        document: document,
        query: @query,
        position: results.index(document) + 1,
        similarity_score: document.respond_to?(:similarity_score) ? document.similarity_score : nil
      )
    end

    results
  end

  def document_type_facets
    accessible_documents.group(:document_type).count
      .map { |type, count| { name: type.humanize, value: type, count: count } }
  end

  def department_facets
    accessible_documents.joins(:department)
      .group('departments.name')
      .count
      .map { |name, count| { name: name, value: name, count: count } }
  end

  def security_level_facets
    accessible_documents.group(:security_level).count
      .map { |level, count| { name: level.humanize, value: level, count: count } }
  end

  def file_type_facets
    accessible_documents.joins(:file_attachment, :file_blob)
      .group('active_storage_blobs.content_type')
      .count
      .map { |type, count| { name: format_content_type(type), value: type, count: count } }
      .sort_by { |item| -item[:count] }
      .first(10)
  end

  def date_range_facets
    [
      { name: 'Today', value: 'today', count: accessible_documents.where(created_at: Date.current.beginning_of_day..).count },
      { name: 'This Week', value: 'week', count: accessible_documents.where(created_at: 1.week.ago..).count },
      { name: 'This Month', value: 'month', count: accessible_documents.where(created_at: 1.month.ago..).count },
      { name: 'This Quarter', value: 'quarter', count: accessible_documents.where(created_at: 3.months.ago..).count },
      { name: 'This Year', value: 'year', count: accessible_documents.where(created_at: 1.year.ago..).count }
    ]
  end

  def author_facets
    accessible_documents.joins(:user)
      .group('users.name')
      .count
      .map { |name, count| { name: name, count: count } }
      .sort_by { |item| -item[:count] }
      .first(15)
  end

  def accessible_documents
    @accessible_documents ||= Document.accessible_by(@user)
  end

  def security_level_mapping
    {
      'basic' => 0,
      'standard' => 1,
      'elevated' => 2,
      'top_secret' => 3
    }
  end

  def format_content_type(content_type)
    case content_type
    when 'application/pdf' then 'PDF Documents'
    when /^image\// then 'Images'
    when /word|doc/ then 'Word Documents'
    when /excel|sheet/ then 'Spreadsheets'
    when /powerpoint|presentation/ then 'Presentations'
    else content_type.humanize
    end
  end

  def parse_file_size(size_string)
    # Parse size strings like "10MB", "500KB", "2GB"
    size_string = size_string.to_s.upcase
    number = size_string.scan(/\d+/).first.to_f
    unit = size_string.scan(/[A-Z]+/).first

    case unit
    when 'KB' then number * 1024
    when 'MB' then number * 1024 * 1024
    when 'GB' then number * 1024 * 1024 * 1024
    else number
    end
  end
end

This comprehensive example demonstrates how to build sophisticated document management applications using ragdoll-rails with advanced search, security, versioning, and analytics features.