ragdoll-rails 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/javascripts/ragdoll/application.js +129 -0
- data/app/assets/javascripts/ragdoll/bulk_upload_status.js +454 -0
- data/app/assets/stylesheets/ragdoll/application.css +84 -0
- data/app/assets/stylesheets/ragdoll/bulk_upload_status.css +379 -0
- data/app/channels/application_cable/channel.rb +6 -0
- data/app/channels/application_cable/connection.rb +6 -0
- data/app/channels/ragdoll/bulk_upload_status_channel.rb +27 -0
- data/app/channels/ragdoll/file_processing_channel.rb +26 -0
- data/app/components/ragdoll/alert_component.html.erb +4 -0
- data/app/components/ragdoll/alert_component.rb +32 -0
- data/app/components/ragdoll/application_component.rb +6 -0
- data/app/components/ragdoll/card_component.html.erb +15 -0
- data/app/components/ragdoll/card_component.rb +21 -0
- data/app/components/ragdoll/document_list_component.html.erb +41 -0
- data/app/components/ragdoll/document_list_component.rb +13 -0
- data/app/components/ragdoll/document_table_component.html.erb +76 -0
- data/app/components/ragdoll/document_table_component.rb +13 -0
- data/app/components/ragdoll/empty_state_component.html.erb +12 -0
- data/app/components/ragdoll/empty_state_component.rb +17 -0
- data/app/components/ragdoll/flash_messages_component.html.erb +3 -0
- data/app/components/ragdoll/flash_messages_component.rb +37 -0
- data/app/components/ragdoll/navbar_component.html.erb +24 -0
- data/app/components/ragdoll/navbar_component.rb +31 -0
- data/app/components/ragdoll/page_header_component.html.erb +13 -0
- data/app/components/ragdoll/page_header_component.rb +15 -0
- data/app/components/ragdoll/stats_card_component.html.erb +11 -0
- data/app/components/ragdoll/stats_card_component.rb +17 -0
- data/app/components/ragdoll/status_badge_component.html.erb +3 -0
- data/app/components/ragdoll/status_badge_component.rb +30 -0
- data/app/controllers/ragdoll/api/v1/analytics_controller.rb +72 -0
- data/app/controllers/ragdoll/api/v1/base_controller.rb +29 -0
- data/app/controllers/ragdoll/api/v1/documents_controller.rb +148 -0
- data/app/controllers/ragdoll/api/v1/search_controller.rb +87 -0
- data/app/controllers/ragdoll/api/v1/system_controller.rb +97 -0
- data/app/controllers/ragdoll/application_controller.rb +17 -0
- data/app/controllers/ragdoll/configuration_controller.rb +82 -0
- data/app/controllers/ragdoll/dashboard_controller.rb +98 -0
- data/app/controllers/ragdoll/documents_controller.rb +460 -0
- data/app/controllers/ragdoll/documents_controller_backup.rb +68 -0
- data/app/controllers/ragdoll/jobs_controller.rb +116 -0
- data/app/controllers/ragdoll/search_controller.rb +368 -0
- data/app/jobs/application_job.rb +9 -0
- data/app/jobs/ragdoll/bulk_document_processing_job.rb +280 -0
- data/app/jobs/ragdoll/process_file_job.rb +166 -0
- data/app/services/ragdoll/worker_health_service.rb +111 -0
- data/app/views/layouts/ragdoll/application.html.erb +162 -0
- data/app/views/ragdoll/dashboard/analytics.html.erb +333 -0
- data/app/views/ragdoll/dashboard/index.html.erb +208 -0
- data/app/views/ragdoll/documents/edit.html.erb +91 -0
- data/app/views/ragdoll/documents/index.html.erb +302 -0
- data/app/views/ragdoll/documents/new.html.erb +1518 -0
- data/app/views/ragdoll/documents/show.html.erb +188 -0
- data/app/views/ragdoll/documents/upload_results.html.erb +248 -0
- data/app/views/ragdoll/jobs/index.html.erb +669 -0
- data/app/views/ragdoll/jobs/show.html.erb +129 -0
- data/app/views/ragdoll/search/index.html.erb +324 -0
- data/config/cable.yml +12 -0
- data/config/routes.rb +56 -1
- data/lib/ragdoll/rails/engine.rb +32 -1
- data/lib/ragdoll/rails/version.rb +1 -1
- metadata +86 -1
@@ -0,0 +1,68 @@
|
|
1
|
+
# Backup controller for simple upload without ActionCable/jobs
|
2
|
+
# This is a fallback version for when job queues are not available
|
3
|
+
|
4
|
+
module Ragdoll
|
5
|
+
class DocumentsControllerBackup < ApplicationController
|
6
|
+
def upload_simple
|
7
|
+
Rails.logger.info "upload_simple called with params: #{params.inspect}"
|
8
|
+
|
9
|
+
if params[:ragdoll_document] && params[:ragdoll_document][:files].present?
|
10
|
+
uploaded_files = params[:ragdoll_document][:files]
|
11
|
+
uploaded_files = [uploaded_files] unless uploaded_files.is_a?(Array)
|
12
|
+
|
13
|
+
results = []
|
14
|
+
uploaded_files.each_with_index do |file, index|
|
15
|
+
next unless file.respond_to?(:original_filename)
|
16
|
+
|
17
|
+
begin
|
18
|
+
# Save uploaded file temporarily
|
19
|
+
temp_path = Rails.root.join('tmp', 'uploads', file.original_filename)
|
20
|
+
FileUtils.mkdir_p(File.dirname(temp_path))
|
21
|
+
File.binwrite(temp_path, file.read)
|
22
|
+
|
23
|
+
# Process document directly (synchronously)
|
24
|
+
result = ::Ragdoll.add_document(path: temp_path.to_s)
|
25
|
+
|
26
|
+
if result[:success] && result[:document_id]
|
27
|
+
document = ::Ragdoll::Document.find(result[:document_id])
|
28
|
+
results << {
|
29
|
+
file: file.original_filename,
|
30
|
+
success: true,
|
31
|
+
document_id: document.id,
|
32
|
+
message: 'Document processed successfully'
|
33
|
+
}
|
34
|
+
else
|
35
|
+
results << {
|
36
|
+
file: file.original_filename,
|
37
|
+
success: false,
|
38
|
+
error: result[:error] || 'Unknown error'
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
# Clean up temp file
|
43
|
+
File.delete(temp_path) if File.exist?(temp_path)
|
44
|
+
rescue => e
|
45
|
+
Rails.logger.error "Error processing file #{file.original_filename}: #{e.message}"
|
46
|
+
results << {
|
47
|
+
file: file.original_filename,
|
48
|
+
success: false,
|
49
|
+
error: e.message
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
render json: {
|
55
|
+
success: true,
|
56
|
+
results: results,
|
57
|
+
message: "Processed #{results.count} file(s)"
|
58
|
+
}
|
59
|
+
else
|
60
|
+
render json: { success: false, error: "No files provided" }, status: :bad_request
|
61
|
+
end
|
62
|
+
rescue => e
|
63
|
+
Rails.logger.error "Error in upload_simple: #{e.message}"
|
64
|
+
Rails.logger.error e.backtrace.join("\n")
|
65
|
+
render json: { success: false, error: e.message }, status: :internal_server_error
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ragdoll
|
4
|
+
class JobsController < ApplicationController
|
5
|
+
skip_before_action :verify_authenticity_token, only: [:restart_workers, :destroy, :bulk_delete, :bulk_retry, :cancel_all_pending]
|
6
|
+
|
7
|
+
def index
|
8
|
+
@pending_jobs = SolidQueue::Job.where(finished_at: nil).order(created_at: :desc).limit(50)
|
9
|
+
@completed_jobs = SolidQueue::Job.where.not(finished_at: nil).order(finished_at: :desc).limit(50)
|
10
|
+
@failed_jobs = SolidQueue::FailedExecution.order(created_at: :desc).limit(50)
|
11
|
+
|
12
|
+
@stats = {
|
13
|
+
pending: SolidQueue::Job.where(finished_at: nil).count,
|
14
|
+
completed: SolidQueue::Job.where.not(finished_at: nil).count,
|
15
|
+
failed: SolidQueue::FailedExecution.count,
|
16
|
+
total: SolidQueue::Job.count
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def show
|
21
|
+
@job = SolidQueue::Job.find(params[:id])
|
22
|
+
end
|
23
|
+
|
24
|
+
def retry
|
25
|
+
failed_execution = SolidQueue::FailedExecution.find(params[:id])
|
26
|
+
failed_execution.retry
|
27
|
+
redirect_to ragdoll.jobs_path, notice: 'Job retried successfully'
|
28
|
+
rescue => e
|
29
|
+
redirect_to ragdoll.jobs_path, alert: "Failed to retry job: #{e.message}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def destroy
|
33
|
+
if params[:type] == 'failed'
|
34
|
+
SolidQueue::FailedExecution.find(params[:id]).destroy
|
35
|
+
else
|
36
|
+
SolidQueue::Job.find(params[:id]).destroy
|
37
|
+
end
|
38
|
+
redirect_to ragdoll.jobs_path, notice: 'Job deleted successfully'
|
39
|
+
rescue => e
|
40
|
+
redirect_to ragdoll.jobs_path, alert: "Failed to delete job: #{e.message}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def health
|
44
|
+
health_status = WorkerHealthService.check_worker_health
|
45
|
+
render json: health_status
|
46
|
+
end
|
47
|
+
|
48
|
+
def restart_workers
|
49
|
+
if WorkerHealthService.needs_restart?
|
50
|
+
# Process stuck jobs first
|
51
|
+
processed_count = WorkerHealthService.process_stuck_jobs!(10)
|
52
|
+
|
53
|
+
# Restart workers
|
54
|
+
WorkerHealthService.restart_workers!
|
55
|
+
|
56
|
+
redirect_to ragdoll.jobs_path, notice: "Workers restarted! Processed #{processed_count} stuck jobs."
|
57
|
+
else
|
58
|
+
redirect_to ragdoll.jobs_path, alert: "Workers appear to be healthy."
|
59
|
+
end
|
60
|
+
rescue => e
|
61
|
+
redirect_to ragdoll.jobs_path, alert: "Failed to restart workers: #{e.message}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def bulk_delete
|
65
|
+
job_ids = params[:job_ids] || []
|
66
|
+
job_type = params[:job_type] || 'pending'
|
67
|
+
|
68
|
+
deleted_count = 0
|
69
|
+
|
70
|
+
job_ids.each do |job_id|
|
71
|
+
begin
|
72
|
+
if job_type == 'failed'
|
73
|
+
SolidQueue::FailedExecution.find(job_id).destroy
|
74
|
+
else
|
75
|
+
SolidQueue::Job.find(job_id).destroy
|
76
|
+
end
|
77
|
+
deleted_count += 1
|
78
|
+
rescue => e
|
79
|
+
Rails.logger.error "Failed to delete job #{job_id}: #{e.message}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
redirect_to ragdoll.jobs_path, notice: "Successfully deleted #{deleted_count} job(s)."
|
84
|
+
rescue => e
|
85
|
+
redirect_to ragdoll.jobs_path, alert: "Bulk delete failed: #{e.message}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def bulk_retry
|
89
|
+
job_ids = params[:job_ids] || []
|
90
|
+
retried_count = 0
|
91
|
+
|
92
|
+
job_ids.each do |job_id|
|
93
|
+
begin
|
94
|
+
failed_execution = SolidQueue::FailedExecution.find(job_id)
|
95
|
+
failed_execution.retry
|
96
|
+
retried_count += 1
|
97
|
+
rescue => e
|
98
|
+
Rails.logger.error "Failed to retry job #{job_id}: #{e.message}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
redirect_to ragdoll.jobs_path, notice: "Successfully retried #{retried_count} job(s)."
|
103
|
+
rescue => e
|
104
|
+
redirect_to ragdoll.jobs_path, alert: "Bulk retry failed: #{e.message}"
|
105
|
+
end
|
106
|
+
|
107
|
+
def cancel_all_pending
|
108
|
+
begin
|
109
|
+
deleted_count = SolidQueue::Job.where(finished_at: nil).delete_all
|
110
|
+
redirect_to ragdoll.jobs_path, notice: "Successfully canceled #{deleted_count} pending job(s)."
|
111
|
+
rescue => e
|
112
|
+
redirect_to ragdoll.jobs_path, alert: "Failed to cancel all pending jobs: #{e.message}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,368 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ragdoll
|
4
|
+
class SearchController < ApplicationController
|
5
|
+
skip_before_action :verify_authenticity_token, only: [:search]
|
6
|
+
|
7
|
+
def index
|
8
|
+
# Load popular queries for sidebar
|
9
|
+
@popular_queries = ::Ragdoll::Search.group(:query).count.sort_by { |query, count| -count }.first(10).to_h
|
10
|
+
|
11
|
+
# Check if we're reconstructing a previous search
|
12
|
+
if params[:search_id].present?
|
13
|
+
begin
|
14
|
+
previous_search = ::Ragdoll::Search.find(params[:search_id])
|
15
|
+
@reconstructed_search = previous_search
|
16
|
+
|
17
|
+
# Extract stored form parameters
|
18
|
+
search_options = previous_search.search_options.is_a?(Hash) ? previous_search.search_options :
|
19
|
+
(previous_search.search_options.present? ? JSON.parse(previous_search.search_options) : {})
|
20
|
+
search_filters = previous_search.search_filters.is_a?(Hash) ? previous_search.search_filters :
|
21
|
+
(previous_search.search_filters.present? ? JSON.parse(previous_search.search_filters) : {})
|
22
|
+
|
23
|
+
form_params = search_options.dig('form_params') || {}
|
24
|
+
|
25
|
+
# Reconstruct query and filters from stored search
|
26
|
+
@query = previous_search.query
|
27
|
+
@filters = {
|
28
|
+
document_type: form_params['document_type'] || search_filters['document_type'],
|
29
|
+
status: form_params['status'] || search_filters['status'],
|
30
|
+
limit: form_params['limit'] || search_filters['limit'] || 10,
|
31
|
+
threshold: form_params['threshold'] || search_filters['threshold'] || 0.001
|
32
|
+
}
|
33
|
+
|
34
|
+
# Reconstruct boolean search options
|
35
|
+
@use_similarity_search = form_params['use_similarity_search'] || search_options['use_similarity'] || 'true'
|
36
|
+
@use_fulltext_search = form_params['use_fulltext_search'] || search_options['use_fulltext'] || 'true'
|
37
|
+
|
38
|
+
::Rails.logger.debug "🔍 Reconstructed search from ID #{params[:search_id]}: #{@query}"
|
39
|
+
|
40
|
+
rescue ActiveRecord::RecordNotFound
|
41
|
+
::Rails.logger.warn "🔍 Search ID #{params[:search_id]} not found"
|
42
|
+
# Fall back to default behavior
|
43
|
+
rescue => e
|
44
|
+
::Rails.logger.error "🔍 Error reconstructing search: #{e.message}"
|
45
|
+
# Fall back to default behavior
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Default values if not reconstructing a search
|
50
|
+
unless @reconstructed_search
|
51
|
+
@filters = {
|
52
|
+
document_type: params[:document_type],
|
53
|
+
status: params[:status],
|
54
|
+
limit: params[:limit]&.to_i || 10,
|
55
|
+
threshold: params[:threshold]&.to_f || (::Rails.env.development? ? 0.001 : 0.7)
|
56
|
+
}
|
57
|
+
@query = params[:query]
|
58
|
+
@use_similarity_search = params[:use_similarity_search] || 'true'
|
59
|
+
@use_fulltext_search = params[:use_fulltext_search] || 'true'
|
60
|
+
end
|
61
|
+
|
62
|
+
@search_performed = false
|
63
|
+
end
|
64
|
+
|
65
|
+
def search
|
66
|
+
::Rails.logger.debug "🔍 Search called with params: #{params.inspect}"
|
67
|
+
::Rails.logger.debug "🔍 Use similarity search: #{params[:use_similarity_search]}"
|
68
|
+
::Rails.logger.debug "🔍 Use fulltext search: #{params[:use_fulltext_search]}"
|
69
|
+
@query = params[:query]
|
70
|
+
@filters = {
|
71
|
+
document_type: params[:document_type],
|
72
|
+
status: params[:status],
|
73
|
+
limit: params[:limit]&.to_i || 10,
|
74
|
+
threshold: params[:threshold]&.to_f || (::Rails.env.development? ? 0.001 : 0.7) # Much lower threshold for development
|
75
|
+
}
|
76
|
+
::Rails.logger.debug "🔍 Query: #{@query.inspect}, Filters: #{@filters.inspect}"
|
77
|
+
|
78
|
+
# Initialize data needed for the view sidebar - load popular queries
|
79
|
+
@popular_queries = ::Ragdoll::Search.group(:query).count.sort_by { |query, count| -count }.first(10).to_h
|
80
|
+
|
81
|
+
if @query.present?
|
82
|
+
begin
|
83
|
+
# Check which search types are enabled (default to both if neither param is set)
|
84
|
+
use_similarity = params[:use_similarity_search] != 'false'
|
85
|
+
use_fulltext = params[:use_fulltext_search] != 'false'
|
86
|
+
|
87
|
+
@detailed_results = []
|
88
|
+
@below_threshold_results = []
|
89
|
+
@similarity_search_attempted = false
|
90
|
+
@similarity_threshold_used = @filters[:threshold]
|
91
|
+
|
92
|
+
# Perform similarity search if enabled
|
93
|
+
if use_similarity
|
94
|
+
begin
|
95
|
+
search_params = {
|
96
|
+
query: @query,
|
97
|
+
limit: @filters[:limit],
|
98
|
+
threshold: @filters[:threshold]
|
99
|
+
}
|
100
|
+
|
101
|
+
# Add document type filter if specified
|
102
|
+
if @filters[:document_type].present?
|
103
|
+
search_params[:document_type] = @filters[:document_type]
|
104
|
+
end
|
105
|
+
|
106
|
+
# Add status filter if specified
|
107
|
+
if @filters[:status].present?
|
108
|
+
search_params[:status] = @filters[:status]
|
109
|
+
end
|
110
|
+
|
111
|
+
search_response = ::Ragdoll.search(**search_params.merge(track_search: false))
|
112
|
+
|
113
|
+
# The search returns a hash with :results and :statistics
|
114
|
+
@results = search_response.is_a?(Hash) ? search_response[:results] || [] : []
|
115
|
+
@similarity_stats = search_response.is_a?(Hash) ? search_response[:statistics] || {} : {}
|
116
|
+
|
117
|
+
# Add similarity search results
|
118
|
+
@results.each do |result|
|
119
|
+
if result[:embedding_id] && result[:document_id]
|
120
|
+
embedding = ::Ragdoll::Embedding.find(result[:embedding_id])
|
121
|
+
document = ::Ragdoll::Document.find(result[:document_id])
|
122
|
+
@detailed_results << {
|
123
|
+
embedding: embedding,
|
124
|
+
document: document,
|
125
|
+
similarity: result[:similarity],
|
126
|
+
content: result[:content],
|
127
|
+
usage_count: embedding.usage_count,
|
128
|
+
last_used: embedding.returned_at,
|
129
|
+
search_type: 'similarity'
|
130
|
+
}
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Mark that similarity search was attempted
|
135
|
+
@similarity_search_attempted = true
|
136
|
+
|
137
|
+
# Always gather statistics about all possible matches when similarity search returns limited results
|
138
|
+
similarity_results_count = @detailed_results.select { |r| r[:search_type] == 'similarity' }.count
|
139
|
+
::Rails.logger.debug "🔍 Similarity results found: #{similarity_results_count}"
|
140
|
+
|
141
|
+
# Gather statistics if we have no results OR if the threshold is relatively high (> 0.1)
|
142
|
+
# This ensures we provide helpful feedback even when the search succeeds with a lower threshold
|
143
|
+
should_gather_stats = similarity_results_count == 0 || @filters[:threshold] > 0.1
|
144
|
+
::Rails.logger.debug "🔍 Should gather stats: #{should_gather_stats} (results: #{similarity_results_count}, threshold: #{@filters[:threshold]})"
|
145
|
+
|
146
|
+
if should_gather_stats
|
147
|
+
::Rails.logger.debug "🔍 Gathering below-threshold statistics..."
|
148
|
+
begin
|
149
|
+
# Search again with minimal threshold to get all potential matches
|
150
|
+
stats_params = search_params.merge(threshold: 0.0, limit: 100)
|
151
|
+
stats_response = ::Ragdoll.search(**stats_params)
|
152
|
+
|
153
|
+
::Rails.logger.debug "🔍 Stats response: #{stats_response.inspect}"
|
154
|
+
|
155
|
+
if stats_response.is_a?(Hash) && stats_response[:results]
|
156
|
+
all_similarities = []
|
157
|
+
stats_response[:results].each do |result|
|
158
|
+
if result[:similarity]
|
159
|
+
all_similarities << result[:similarity]
|
160
|
+
# Store below-threshold results
|
161
|
+
if result[:similarity] < @filters[:threshold] && result[:similarity] > 0
|
162
|
+
@below_threshold_results << {
|
163
|
+
document_id: result[:document_id],
|
164
|
+
similarity: result[:similarity],
|
165
|
+
content: result[:content]
|
166
|
+
}
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
::Rails.logger.debug "🔍 All similarities collected: #{all_similarities.inspect}"
|
172
|
+
::Rails.logger.debug "🔍 Threshold: #{@filters[:threshold]}"
|
173
|
+
|
174
|
+
# Calculate statistics for display
|
175
|
+
if all_similarities.any?
|
176
|
+
below_threshold_count = all_similarities.count { |s| s < @filters[:threshold] && s > 0 }
|
177
|
+
@below_threshold_stats = {
|
178
|
+
count: below_threshold_count,
|
179
|
+
highest: all_similarities.max,
|
180
|
+
lowest: all_similarities.select { |s| s > 0 }.min,
|
181
|
+
average: all_similarities.sum / all_similarities.size.to_f,
|
182
|
+
suggested_threshold: all_similarities.select { |s| s > 0 }.min.round(3)
|
183
|
+
}
|
184
|
+
::Rails.logger.debug "🔍 Below threshold stats: #{@below_threshold_stats.inspect}"
|
185
|
+
else
|
186
|
+
::Rails.logger.debug "🔍 No similarities found in stats response"
|
187
|
+
end
|
188
|
+
else
|
189
|
+
::Rails.logger.debug "🔍 Stats response was not in expected format or had no results"
|
190
|
+
end
|
191
|
+
rescue => stats_error
|
192
|
+
::Rails.logger.error "Stats gathering error: #{stats_error.message}"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
rescue => e
|
197
|
+
::Rails.logger.error "Similarity search error: #{e.message}"
|
198
|
+
# Continue with fulltext search even if similarity search fails
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Perform full-text search if enabled
|
203
|
+
if use_fulltext
|
204
|
+
fulltext_params = {
|
205
|
+
limit: @filters[:limit],
|
206
|
+
threshold: @filters[:threshold]
|
207
|
+
}
|
208
|
+
|
209
|
+
# Add document type filter if specified
|
210
|
+
if @filters[:document_type].present?
|
211
|
+
fulltext_params[:document_type] = @filters[:document_type]
|
212
|
+
end
|
213
|
+
|
214
|
+
# Add status filter if specified
|
215
|
+
if @filters[:status].present?
|
216
|
+
fulltext_params[:status] = @filters[:status]
|
217
|
+
end
|
218
|
+
|
219
|
+
fulltext_results = ::Ragdoll::Document.search_content(@query, **fulltext_params)
|
220
|
+
|
221
|
+
# Collect fulltext similarities for statistics
|
222
|
+
fulltext_similarities = []
|
223
|
+
fulltext_results.each do |document|
|
224
|
+
# Avoid duplicates if document was already found in similarity search
|
225
|
+
unless @detailed_results.any? { |r| r[:document].id == document.id }
|
226
|
+
# Use the fulltext_similarity score from the enhanced search
|
227
|
+
fulltext_similarity = document.respond_to?(:fulltext_similarity) ? document.fulltext_similarity.to_f : 0.0
|
228
|
+
fulltext_similarities << fulltext_similarity if fulltext_similarity > 0
|
229
|
+
|
230
|
+
@detailed_results << {
|
231
|
+
document: document,
|
232
|
+
content: document.metadata&.dig('summary') || document.title || "No summary available",
|
233
|
+
search_type: 'fulltext',
|
234
|
+
similarity: fulltext_similarity
|
235
|
+
}
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Gather fulltext statistics if we have few results OR if threshold is high (> 0.1)
|
240
|
+
# This ensures consistent feedback regardless of which search types are enabled
|
241
|
+
fulltext_results_count = @detailed_results.select { |r| r[:search_type] == 'fulltext' }.count
|
242
|
+
should_gather_fulltext_stats = fulltext_results_count == 0 || @filters[:threshold] > 0.1
|
243
|
+
|
244
|
+
if should_gather_fulltext_stats && !@below_threshold_stats
|
245
|
+
::Rails.logger.debug "🔍 Gathering fulltext below-threshold statistics..."
|
246
|
+
begin
|
247
|
+
# Search again with lower threshold to get all potential matches
|
248
|
+
stats_params = fulltext_params.merge(threshold: 0.0, limit: 100)
|
249
|
+
all_fulltext_results = ::Ragdoll::Document.search_content(@query, **stats_params)
|
250
|
+
|
251
|
+
all_fulltext_similarities = []
|
252
|
+
all_fulltext_results.each do |document|
|
253
|
+
similarity = document.respond_to?(:fulltext_similarity) ? document.fulltext_similarity.to_f : 0.0
|
254
|
+
if similarity > 0
|
255
|
+
all_fulltext_similarities << similarity
|
256
|
+
# Store below-threshold results
|
257
|
+
if similarity < @filters[:threshold]
|
258
|
+
@below_threshold_results << {
|
259
|
+
document_id: document.id,
|
260
|
+
similarity: similarity,
|
261
|
+
content: document.metadata&.dig('summary') || document.title || "No summary available"
|
262
|
+
}
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
::Rails.logger.debug "🔍 Fulltext similarities collected: #{all_fulltext_similarities.inspect}"
|
268
|
+
::Rails.logger.debug "🔍 Threshold: #{@filters[:threshold]}"
|
269
|
+
|
270
|
+
# Calculate statistics for display
|
271
|
+
if all_fulltext_similarities.any?
|
272
|
+
below_threshold_count = all_fulltext_similarities.count { |s| s < @filters[:threshold] && s > 0 }
|
273
|
+
@below_threshold_stats = {
|
274
|
+
count: below_threshold_count,
|
275
|
+
highest: all_fulltext_similarities.max,
|
276
|
+
lowest: all_fulltext_similarities.select { |s| s > 0 }.min,
|
277
|
+
average: all_fulltext_similarities.sum / all_fulltext_similarities.size.to_f,
|
278
|
+
suggested_threshold: all_fulltext_similarities.select { |s| s > 0 }.min.round(3)
|
279
|
+
}
|
280
|
+
::Rails.logger.debug "🔍 Fulltext below threshold stats: #{@below_threshold_stats.inspect}"
|
281
|
+
else
|
282
|
+
::Rails.logger.debug "🔍 No fulltext similarities found in stats response"
|
283
|
+
end
|
284
|
+
rescue => stats_error
|
285
|
+
::Rails.logger.error "Fulltext stats gathering error: #{stats_error.message}"
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Sort results by similarity score if available, otherwise by relevance
|
291
|
+
@detailed_results.sort_by! { |r| r[:similarity] ? -r[:similarity] : 0 }
|
292
|
+
|
293
|
+
# Save search for analytics
|
294
|
+
search_type = case
|
295
|
+
when use_similarity && use_fulltext then 'hybrid'
|
296
|
+
when use_similarity then 'similarity'
|
297
|
+
when use_fulltext then 'fulltext'
|
298
|
+
else 'unknown'
|
299
|
+
end
|
300
|
+
|
301
|
+
similarity_results = @detailed_results.select { |r| r[:search_type] == 'similarity' }
|
302
|
+
similarities = similarity_results.map { |r| r[:similarity] }.compact
|
303
|
+
|
304
|
+
# Save search for analytics without query embedding (which is optional)
|
305
|
+
begin
|
306
|
+
::Ragdoll::Search.create!(
|
307
|
+
query: @query,
|
308
|
+
search_type: search_type,
|
309
|
+
results_count: @detailed_results.count,
|
310
|
+
max_similarity_score: similarities.any? ? similarities.max : nil,
|
311
|
+
min_similarity_score: similarities.any? ? similarities.min : nil,
|
312
|
+
avg_similarity_score: similarities.any? ? (similarities.sum / similarities.size.to_f) : nil,
|
313
|
+
search_filters: @filters.to_json,
|
314
|
+
search_options: {
|
315
|
+
threshold_used: @filters[:threshold],
|
316
|
+
similarity_results: similarity_results.count,
|
317
|
+
fulltext_results: @detailed_results.select { |r| r[:search_type] == 'fulltext' }.count,
|
318
|
+
use_similarity: use_similarity,
|
319
|
+
use_fulltext: use_fulltext,
|
320
|
+
# Store original form parameters for reconstruction
|
321
|
+
form_params: {
|
322
|
+
use_similarity_search: params[:use_similarity_search],
|
323
|
+
use_fulltext_search: params[:use_fulltext_search],
|
324
|
+
limit: @filters[:limit],
|
325
|
+
threshold: @filters[:threshold],
|
326
|
+
document_type: @filters[:document_type],
|
327
|
+
status: @filters[:status]
|
328
|
+
}
|
329
|
+
}.to_json
|
330
|
+
)
|
331
|
+
::Rails.logger.debug "🔍 Search saved successfully"
|
332
|
+
rescue => e
|
333
|
+
::Rails.logger.error "🔍 Failed to save search: #{e.message}"
|
334
|
+
# Continue without failing the search
|
335
|
+
end
|
336
|
+
|
337
|
+
::Rails.logger.debug "🔍 Search completed successfully. Results count: #{@detailed_results.count}"
|
338
|
+
::Rails.logger.debug "🔍 Similarity search attempted: #{@similarity_search_attempted}"
|
339
|
+
::Rails.logger.debug "🔍 Below threshold stats: #{@below_threshold_stats.inspect}"
|
340
|
+
::Rails.logger.debug "🔍 Threshold used: #{@similarity_threshold_used}"
|
341
|
+
@search_performed = true
|
342
|
+
|
343
|
+
rescue => e
|
344
|
+
::Rails.logger.error "🔍 Search error: #{e.message}"
|
345
|
+
::Rails.logger.error e.backtrace.join("\n")
|
346
|
+
@error = e.message
|
347
|
+
@search_performed = false
|
348
|
+
end
|
349
|
+
else
|
350
|
+
@search_performed = false
|
351
|
+
end
|
352
|
+
|
353
|
+
respond_to do |format|
|
354
|
+
format.html { render :index }
|
355
|
+
format.json {
|
356
|
+
json_response = { results: @detailed_results, error: @error }
|
357
|
+
if @similarity_search_attempted && @similarity_stats
|
358
|
+
json_response[:similarity_statistics] = {
|
359
|
+
threshold_used: @similarity_threshold_used,
|
360
|
+
stats: @similarity_stats
|
361
|
+
}
|
362
|
+
end
|
363
|
+
render json: json_response
|
364
|
+
}
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ApplicationJob < ActiveJob::Base
|
4
|
+
# Automatically retry jobs that encountered a deadlock
|
5
|
+
# retry_on ActiveRecord::Deadlocked
|
6
|
+
|
7
|
+
# Most jobs are safe to ignore if the underlying records are no longer available
|
8
|
+
# discard_on ActiveJob::DeserializationError
|
9
|
+
end
|