Skip to content

Rails Controllers

The ragdoll-rails gem provides a comprehensive set of controllers that handle document management, search functionality, and administrative tasks. These controllers follow Rails conventions and can be easily customized or extended.

Controller Overview

The Ragdoll Rails controllers are organized into logical groups:

  • Document Management: Upload, view, edit, and delete documents
  • Search: Perform searches and manage search interfaces
  • Content: Handle content chunks and embeddings
  • Admin: Administrative functions and monitoring
  • API: RESTful API endpoints

Core Controllers

Ragdoll::DocumentsController

The main controller for document operations.

Actions

class Ragdoll::DocumentsController < Ragdoll::ApplicationController
  before_action :authenticate_user!, except: [:show] if Ragdoll.configuration.require_authentication
  before_action :set_document, only: [:show, :edit, :update, :destroy, :download, :reprocess]
  before_action :authorize_document!, only: [:show, :edit, :update, :destroy]

  # GET /documents
  def index
    @documents = documents_scope
      .includes(:user, :contents)
      .page(params[:page])
      .per(params[:per_page] || 20)

    @documents = @documents.where(status: params[:status]) if params[:status].present?
    @documents = @documents.where(content_type: params[:content_type]) if params[:content_type].present?

    respond_to do |format|
      format.html
      format.json { render json: @documents }
      format.csv { send_data documents_csv, filename: "documents-#{Date.current}.csv" }
    end
  end

  # GET /documents/1
  def show
    @content_preview = @document.content_preview(1000)
    @related_documents = @document.find_similar(limit: 5)

    respond_to do |format|
      format.html
      format.json { render json: @document, include: [:contents, :embeddings] }
      format.pdf { render_pdf }
    end
  end

  # GET /documents/new
  def new
    @document = documents_scope.build
  end

  # GET /documents/1/edit
  def edit
  end

  # POST /documents
  def create
    @document = documents_scope.build(document_params)
    @document.user = current_user if respond_to?(:current_user)

    respond_to do |format|
      if @document.save
        enqueue_processing if should_auto_process?
        format.html { redirect_to @document, notice: 'Document was successfully created.' }
        format.json { render json: @document, status: :created }
      else
        format.html { render :new }
        format.json { render json: @document.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /documents/1
  def update
    respond_to do |format|
      if @document.update(document_params)
        enqueue_reprocessing if file_changed?
        format.html { redirect_to @document, notice: 'Document was successfully updated.' }
        format.json { render json: @document }
      else
        format.html { render :edit }
        format.json { render json: @document.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /documents/1
  def destroy
    @document.destroy
    respond_to do |format|
      format.html { redirect_to documents_url, notice: 'Document was successfully deleted.' }
      format.json { head :no_content }
    end
  end

  # GET /documents/1/download
  def download
    authorize_download!
    redirect_to rails_blob_path(@document.file, disposition: "attachment")
  end

  # POST /documents/1/reprocess
  def reprocess
    authorize_reprocess!
    Ragdoll::ProcessDocumentJob.perform_later(@document)
    redirect_to @document, notice: 'Document reprocessing has been queued.'
  end

  private

  def documents_scope
    if respond_to?(:current_user) && current_user
      Ragdoll::Document.accessible_by(current_user)
    else
      Ragdoll::Document.public_documents
    end
  end

  def set_document
    @document = documents_scope.find(params[:id])
  end

  def document_params
    params.require(:document).permit(:title, :description, :file, :metadata, images: [])
  end

  def authorize_document!
    return if Ragdoll.configuration.authorize_document_access.call(@document, current_user)

    respond_to do |format|
      format.html { redirect_to root_path, alert: 'Access denied.' }
      format.json { render json: { error: 'Access denied' }, status: :forbidden }
    end
  end

  def should_auto_process?
    Ragdoll.configuration.auto_process_documents && @document.file.attached?
  end

  def enqueue_processing
    Ragdoll::ProcessDocumentJob.perform_later(@document)
  end

  def enqueue_reprocessing
    return unless @document.file.attached?

    @document.update!(status: 'pending')
    Ragdoll::ProcessDocumentJob.perform_later(@document)
  end

  def file_changed?
    @document.saved_change_to_attribute?(:file) || @document.file.attachment.changed?
  end

  def render_pdf
    # Custom PDF rendering logic
    pdf = Ragdoll::PdfRenderer.new(@document).render
    send_data pdf, filename: "#{@document.title}.pdf", type: 'application/pdf'
  end

  def documents_csv
    Ragdoll::CsvExporter.new(@documents).export
  end
end

Custom Actions

class Ragdoll::DocumentsController < Ragdoll::ApplicationController
  # GET /documents/bulk_upload
  def bulk_upload
    @upload_session = Ragdoll::UploadSession.new
  end

  # POST /documents/bulk_create
  def bulk_create
    @upload_session = Ragdoll::UploadSession.new(bulk_upload_params)

    if @upload_session.valid?
      @upload_session.process_files
      redirect_to documents_path, notice: "#{@upload_session.file_count} files uploaded successfully."
    else
      render :bulk_upload
    end
  end

  # GET /documents/stats
  def stats
    @stats = {
      total_documents: documents_scope.count,
      processed_documents: documents_scope.processed.count,
      total_size: documents_scope.sum(:file_size),
      recent_uploads: documents_scope.recent(7).count,
      top_content_types: documents_scope.group(:content_type).count.sort_by(&:last).reverse.first(5)
    }

    respond_to do |format|
      format.html
      format.json { render json: @stats }
    end
  end

  private

  def bulk_upload_params
    params.require(:upload_session).permit(files: [], metadata: {})
  end
end

Ragdoll::SearchController

Handles search functionality and interfaces.

class Ragdoll::SearchController < Ragdoll::ApplicationController
  before_action :authenticate_user!, if: :authentication_required?
  before_action :authorize_search!, if: :authorization_required?
  before_action :set_search_params, only: [:index, :suggestions, :facets]

  # GET /search
  def index
    @search_service = Ragdoll::SearchService.new(@query, search_options)
    @results = @search_service.call
    @facets = @search_service.facets if params[:include_facets]
    @suggestions = @search_service.suggestions if @results.empty?

    # Track search analytics
    track_search_event if Ragdoll.configuration.track_search_analytics

    respond_to do |format|
      format.html
      format.json { render json: search_response }
      format.turbo_stream { render :index }
    end
  end

  # GET /search/suggestions
  def suggestions
    @suggestions = Ragdoll::SuggestionService.new(@query).call

    respond_to do |format|
      format.json { render json: @suggestions }
    end
  end

  # GET /search/facets
  def facets
    @facets = Ragdoll::FacetService.new(@query, facet_options).call

    respond_to do |format|
      format.json { render json: @facets }
      format.html { render partial: 'facets', locals: { facets: @facets } }
    end
  end

  # POST /search/save
  def save
    @saved_search = current_user.saved_searches.build(saved_search_params)

    if @saved_search.save
      render json: @saved_search, status: :created
    else
      render json: @saved_search.errors, status: :unprocessable_entity
    end
  end

  private

  def set_search_params
    @query = params[:q] || params[:query]
    @filters = params[:filters] || {}
    @sort = params[:sort] || 'relevance'
    @page = params[:page] || 1
    @per_page = [params[:per_page].to_i, 50].min.positive? || 10
  end

  def search_options
    {
      filters: @filters,
      sort: @sort,
      page: @page,
      per_page: @per_page,
      include_facets: params[:include_facets],
      search_type: params[:search_type] || 'hybrid',
      threshold: params[:threshold]&.to_f || Ragdoll.configuration.similarity_threshold,
      user: current_user
    }
  end

  def facet_options
    {
      facet_fields: params[:facet_fields] || ['content_type', 'user', 'created_at'],
      facet_limit: params[:facet_limit] || 10
    }
  end

  def search_response
    {
      query: @query,
      results: @results.map { |result| serialize_search_result(result) },
      total_count: @results.total_count,
      page: @page,
      per_page: @per_page,
      facets: @facets,
      suggestions: @suggestions,
      search_time: @search_service.search_time
    }
  end

  def serialize_search_result(result)
    {
      id: result.id,
      title: result.title,
      description: result.description,
      content_preview: result.content_preview,
      similarity_score: result.similarity_score,
      url: document_path(result),
      metadata: result.metadata,
      user: result.user&.name,
      created_at: result.created_at
    }
  end

  def track_search_event
    Ragdoll::SearchAnalytics.track(
      query: @query,
      user: current_user,
      results_count: @results.size,
      search_type: params[:search_type],
      response_time: @search_service.search_time
    )
  end

  def authentication_required?
    !Ragdoll.configuration.allow_anonymous_search
  end

  def authorization_required?
    Ragdoll.configuration.authorize_search.present?
  end

  def authorize_search!
    return if Ragdoll.configuration.authorize_search.call(current_user)

    respond_to do |format|
      format.html { redirect_to root_path, alert: 'Search access denied.' }
      format.json { render json: { error: 'Search access denied' }, status: :forbidden }
    end
  end

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

Ragdoll::Admin::AdminController

Base controller for administrative functions.

class Ragdoll::Admin::AdminController < Ragdoll::ApplicationController
  before_action :authenticate_admin!
  layout 'ragdoll/admin'

  protected

  def authenticate_admin!
    return if current_user&.admin?

    respond_to do |format|
      format.html { redirect_to root_path, alert: 'Admin access required.' }
      format.json { render json: { error: 'Admin access required' }, status: :forbidden }
    end
  end
end

class Ragdoll::Admin::DashboardController < Ragdoll::Admin::AdminController
  # GET /admin/dashboard
  def index
    @stats = {
      total_documents: Ragdoll::Document.count,
      processed_documents: Ragdoll::Document.processed.count,
      pending_documents: Ragdoll::Document.pending.count,
      failed_documents: Ragdoll::Document.failed.count,
      total_storage: Ragdoll::Document.sum(:file_size),
      active_users: active_users_count,
      recent_searches: recent_searches_count,
      system_health: system_health_check
    }

    @recent_uploads = Ragdoll::Document.recent(7).limit(10)
    @processing_queue_size = processing_queue_size
    @error_documents = Ragdoll::Document.failed.limit(5)
  end

  # GET /admin/system_info
  def system_info
    @system_info = {
      ragdoll_version: Ragdoll::VERSION,
      rails_version: Rails.version,
      ruby_version: RUBY_VERSION,
      database_version: database_version,
      redis_version: redis_version,
      background_job_adapter: Ragdoll.configuration.job_adapter,
      llm_provider: Ragdoll.configuration.llm_provider,
      storage_service: Ragdoll.configuration.storage_service
    }

    respond_to do |format|
      format.html
      format.json { render json: @system_info }
    end
  end

  private

  def active_users_count
    # Implementation depends on your user model
    User.joins(:documents).where(documents: { created_at: 1.week.ago.. }).distinct.count
  end

  def recent_searches_count
    # Implementation depends on search analytics
    Ragdoll::SearchAnalytics.where(created_at: 1.day.ago..).count
  end

  def system_health_check
    {
      database: database_healthy?,
      llm_provider: llm_provider_healthy?,
      background_jobs: background_jobs_healthy?,
      storage: storage_healthy?
    }
  end

  def processing_queue_size
    case Ragdoll.configuration.job_adapter
    when :sidekiq
      Sidekiq::Queue.new(Ragdoll.configuration.processing_queue).size
    when :resque
      Resque.size(Ragdoll.configuration.processing_queue)
    else
      0
    end
  end

  def database_healthy?
    ActiveRecord::Base.connection.active?
  rescue
    false
  end

  def llm_provider_healthy?
    Ragdoll::LLMService.new.health_check
  rescue
    false
  end

  def background_jobs_healthy?
    case Ragdoll.configuration.job_adapter
    when :sidekiq
      Sidekiq.redis(&:ping) == 'PONG'
    else
      true
    end
  rescue
    false
  end

  def storage_healthy?
    ActiveStorage::Blob.service.exist?('health_check')
  rescue
    false
  end
end

API Controllers

Ragdoll::Api::V1::BaseController

Base API controller with common functionality.

class Ragdoll::Api::V1::BaseController < ActionController::API
  include ActionController::HttpAuthentication::Token::ControllerMethods

  before_action :authenticate_api_user!
  before_action :set_default_format

  rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
  rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity
  rescue_from Ragdoll::AuthorizationError, with: :render_forbidden

  protected

  def authenticate_api_user!
    authenticate_or_request_with_http_token do |token, options|
      @current_api_user = User.find_by(api_token: token)
    end
  end

  def current_api_user
    @current_api_user
  end

  def set_default_format
    request.format = :json unless params[:format]
  end

  def render_success(data = nil, message = nil, status = :ok)
    response = { success: true }
    response[:message] = message if message
    response[:data] = data if data
    render json: response, status: status
  end

  def render_error(message, status = :bad_request, errors = nil)
    response = { success: false, message: message }
    response[:errors] = errors if errors
    render json: response, status: status
  end

  def render_not_found(exception)
    render_error("Record not found", :not_found)
  end

  def render_unprocessable_entity(exception)
    render_error("Validation failed", :unprocessable_entity, exception.record.errors)
  end

  def render_forbidden(exception)
    render_error("Access denied", :forbidden)
  end
end

Ragdoll::Api::V1::DocumentsController

RESTful API for document operations.

class Ragdoll::Api::V1::DocumentsController < Ragdoll::Api::V1::BaseController
  before_action :set_document, only: [:show, :update, :destroy, :reprocess]
  before_action :authorize_document!, only: [:show, :update, :destroy]

  # GET /api/v1/documents
  def index
    @documents = documents_scope
      .includes(:user, :contents)
      .page(params[:page])
      .per(params[:per_page] || 20)

    apply_filters

    render json: {
      documents: @documents.map { |doc| serialize_document(doc) },
      pagination: pagination_meta(@documents)
    }
  end

  # GET /api/v1/documents/:id
  def show
    render json: {
      document: serialize_document(@document, include_content: true)
    }
  end

  # POST /api/v1/documents
  def create
    @document = documents_scope.build(document_params)
    @document.user = current_api_user

    if @document.save
      enqueue_processing if should_auto_process?
      render_success(serialize_document(@document), "Document created successfully", :created)
    else
      render_error("Document creation failed", :unprocessable_entity, @document.errors)
    end
  end

  # PATCH /api/v1/documents/:id
  def update
    if @document.update(document_params)
      enqueue_reprocessing if file_changed?
      render_success(serialize_document(@document), "Document updated successfully")
    else
      render_error("Document update failed", :unprocessable_entity, @document.errors)
    end
  end

  # DELETE /api/v1/documents/:id
  def destroy
    @document.destroy
    render_success(nil, "Document deleted successfully")
  end

  # POST /api/v1/documents/:id/reprocess
  def reprocess
    Ragdoll::ProcessDocumentJob.perform_later(@document)
    render_success(nil, "Document reprocessing queued")
  end

  # POST /api/v1/documents/bulk_upload
  def bulk_upload
    files = params[:files] || []
    metadata = params[:metadata] || {}

    results = []
    errors = []

    files.each_with_index do |file, index|
      document = documents_scope.build(
        title: file.original_filename,
        file: file,
        metadata: metadata,
        user: current_api_user
      )

      if document.save
        enqueue_processing if should_auto_process?
        results << serialize_document(document)
      else
        errors << { index: index, filename: file.original_filename, errors: document.errors }
      end
    end

    render json: {
      success: errors.empty?,
      uploaded: results.size,
      failed: errors.size,
      documents: results,
      errors: errors
    }
  end

  private

  def documents_scope
    Ragdoll::Document.accessible_by(current_api_user)
  end

  def set_document
    @document = documents_scope.find(params[:id])
  end

  def document_params
    params.require(:document).permit(:title, :description, :file, metadata: {})
  end

  def apply_filters
    @documents = @documents.where(status: params[:status]) if params[:status].present?
    @documents = @documents.where(content_type: params[:content_type]) if params[:content_type].present?
    @documents = @documents.where('created_at >= ?', params[:created_after]) if params[:created_after].present?
    @documents = @documents.where('created_at <= ?', params[:created_before]) if params[:created_before].present?
  end

  def serialize_document(document, include_content: false)
    result = {
      id: document.id,
      title: document.title,
      description: document.description,
      content_type: document.content_type,
      file_size: document.file_size,
      status: document.status,
      metadata: document.metadata,
      created_at: document.created_at,
      updated_at: document.updated_at,
      user: {
        id: document.user&.id,
        name: document.user&.name
      },
      urls: {
        self: api_v1_document_url(document),
        download: document_url(document) + '/download'
      }
    }

    if include_content
      result[:content] = {
        preview: document.content_preview,
        chunks_count: document.contents.count,
        embeddings_count: document.embeddings.count
      }
    end

    result
  end

  def pagination_meta(collection)
    {
      current_page: collection.current_page,
      total_pages: collection.total_pages,
      total_count: collection.total_count,
      per_page: collection.limit_value
    }
  end

  def authorize_document!
    return if Ragdoll.configuration.authorize_document_access.call(@document, current_api_user)

    raise Ragdoll::AuthorizationError, "Access denied to document #{@document.id}"
  end

  def should_auto_process?
    Ragdoll.configuration.auto_process_documents && @document.file.attached?
  end

  def enqueue_processing
    Ragdoll::ProcessDocumentJob.perform_later(@document)
  end

  def enqueue_reprocessing
    return unless @document.file.attached?

    @document.update!(status: 'pending')
    Ragdoll::ProcessDocumentJob.perform_later(@document)
  end

  def file_changed?
    @document.saved_change_to_attribute?(:file) || @document.file.attachment.changed?
  end
end

Controller Customization

Inheriting from Ragdoll Controllers

class DocumentsController < Ragdoll::DocumentsController
  before_action :set_company_scope

  private

  def documents_scope
    current_user.company.documents
  end

  def set_company_scope
    @company = current_user.company
  end

  def document_params
    super.merge(company_id: @company.id)
  end
end

Custom Authorization

class Ragdoll::DocumentsController < Ragdoll::ApplicationController
  include Pundit::Authorization

  def show
    authorize @document
    # ... rest of action
  end

  def create
    @document = documents_scope.build(document_params)
    authorize @document
    # ... rest of action
  end
end

# app/policies/ragdoll/document_policy.rb
class Ragdoll::DocumentPolicy < ApplicationPolicy
  def show?
    user.admin? || record.user == user || record.public?
  end

  def create?
    user.present? && user.can_upload_documents?
  end

  def update?
    user.admin? || record.user == user
  end

  def destroy?
    user.admin? || record.user == user
  end
end

Adding Custom Actions

class Ragdoll::DocumentsController < Ragdoll::ApplicationController
  # GET /documents/:id/preview
  def preview
    @document = documents_scope.find(params[:id])
    authorize_document!

    @preview_content = Ragdoll::PreviewService.new(@document).generate

    respond_to do |format|
      format.html { render layout: false }
      format.json { render json: { preview: @preview_content } }
    end
  end

  # POST /documents/:id/share
  def share
    @document = documents_scope.find(params[:id])
    authorize_document!

    @share_link = Ragdoll::ShareLinkService.new(@document, current_user).create

    respond_to do |format|
      format.json { render json: { share_url: @share_link.url } }
    end
  end

  # POST /documents/:id/favorite
  def favorite
    @document = documents_scope.find(params[:id])
    current_user.favorite(@document)

    respond_to do |format|
      format.json { render json: { favorited: true } }
    end
  end
end

This comprehensive controller documentation provides everything you need to understand, use, and customize the Ragdoll Rails controllers for your specific needs.