ragdoll-rails 0.1.9 → 0.1.12

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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -2
  3. data/app/assets/javascripts/ragdoll/application.js +129 -0
  4. data/app/assets/javascripts/ragdoll/bulk_upload_status.js +454 -0
  5. data/app/assets/stylesheets/ragdoll/application.css +84 -0
  6. data/app/assets/stylesheets/ragdoll/bulk_upload_status.css +379 -0
  7. data/app/channels/application_cable/channel.rb +6 -0
  8. data/app/channels/application_cable/connection.rb +6 -0
  9. data/app/channels/ragdoll/bulk_upload_status_channel.rb +27 -0
  10. data/app/channels/ragdoll/file_processing_channel.rb +26 -0
  11. data/app/components/ragdoll/alert_component.html.erb +4 -0
  12. data/app/components/ragdoll/alert_component.rb +32 -0
  13. data/app/components/ragdoll/application_component.rb +6 -0
  14. data/app/components/ragdoll/card_component.html.erb +15 -0
  15. data/app/components/ragdoll/card_component.rb +21 -0
  16. data/app/components/ragdoll/document_list_component.html.erb +41 -0
  17. data/app/components/ragdoll/document_list_component.rb +13 -0
  18. data/app/components/ragdoll/document_table_component.html.erb +76 -0
  19. data/app/components/ragdoll/document_table_component.rb +13 -0
  20. data/app/components/ragdoll/empty_state_component.html.erb +12 -0
  21. data/app/components/ragdoll/empty_state_component.rb +17 -0
  22. data/app/components/ragdoll/flash_messages_component.html.erb +3 -0
  23. data/app/components/ragdoll/flash_messages_component.rb +37 -0
  24. data/app/components/ragdoll/navbar_component.html.erb +24 -0
  25. data/app/components/ragdoll/navbar_component.rb +31 -0
  26. data/app/components/ragdoll/page_header_component.html.erb +13 -0
  27. data/app/components/ragdoll/page_header_component.rb +15 -0
  28. data/app/components/ragdoll/stats_card_component.html.erb +11 -0
  29. data/app/components/ragdoll/stats_card_component.rb +17 -0
  30. data/app/components/ragdoll/status_badge_component.html.erb +3 -0
  31. data/app/components/ragdoll/status_badge_component.rb +30 -0
  32. data/app/controllers/ragdoll/api/v1/analytics_controller.rb +72 -0
  33. data/app/controllers/ragdoll/api/v1/base_controller.rb +29 -0
  34. data/app/controllers/ragdoll/api/v1/documents_controller.rb +148 -0
  35. data/app/controllers/ragdoll/api/v1/search_controller.rb +87 -0
  36. data/app/controllers/ragdoll/api/v1/system_controller.rb +97 -0
  37. data/app/controllers/ragdoll/application_controller.rb +17 -0
  38. data/app/controllers/ragdoll/configuration_controller.rb +82 -0
  39. data/app/controllers/ragdoll/dashboard_controller.rb +98 -0
  40. data/app/controllers/ragdoll/documents_controller.rb +460 -0
  41. data/app/controllers/ragdoll/documents_controller_backup.rb +68 -0
  42. data/app/controllers/ragdoll/jobs_controller.rb +116 -0
  43. data/app/controllers/ragdoll/search_controller.rb +374 -0
  44. data/app/jobs/application_job.rb +9 -0
  45. data/app/jobs/ragdoll/bulk_document_processing_job.rb +280 -0
  46. data/app/jobs/ragdoll/process_file_job.rb +166 -0
  47. data/app/services/ragdoll/worker_health_service.rb +111 -0
  48. data/app/views/layouts/ragdoll/application.html.erb +162 -0
  49. data/app/views/ragdoll/dashboard/analytics.html.erb +333 -0
  50. data/app/views/ragdoll/dashboard/index.html.erb +208 -0
  51. data/app/views/ragdoll/documents/edit.html.erb +91 -0
  52. data/app/views/ragdoll/documents/index.html.erb +305 -0
  53. data/app/views/ragdoll/documents/new.html.erb +1518 -0
  54. data/app/views/ragdoll/documents/show.html.erb +188 -0
  55. data/app/views/ragdoll/documents/upload_results.html.erb +248 -0
  56. data/app/views/ragdoll/jobs/index.html.erb +669 -0
  57. data/app/views/ragdoll/jobs/show.html.erb +129 -0
  58. data/app/views/ragdoll/search/index.html.erb +327 -0
  59. data/config/cable.yml +12 -0
  60. data/config/routes.rb +56 -1
  61. data/lib/generators/ragdoll/init/templates/ragdoll_config.rb +17 -4
  62. data/lib/ragdoll/rails/configuration.rb +2 -1
  63. data/lib/ragdoll/rails/engine.rb +32 -1
  64. data/lib/ragdoll/rails/version.rb +1 -1
  65. metadata +90 -4
@@ -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,374 @@
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
+ # NOTE: Document type filtering is deprecated in unified text-based architecture
103
+ # All media types are now converted to text for unified cross-modal search
104
+ if @filters[:document_type].present?
105
+ search_params[:document_type] = @filters[:document_type]
106
+ ::Rails.logger.warn "⚠️ Document type filtering is deprecated in unified text-based RAG architecture"
107
+ end
108
+
109
+ # Add status filter if specified
110
+ if @filters[:status].present?
111
+ search_params[:status] = @filters[:status]
112
+ end
113
+
114
+ search_response = ::Ragdoll.search(**search_params.merge(track_search: false))
115
+
116
+ # The search returns a hash with :results and :statistics
117
+ @results = search_response.is_a?(Hash) ? search_response[:results] || [] : []
118
+ @similarity_stats = search_response.is_a?(Hash) ? search_response[:statistics] || {} : {}
119
+
120
+ # Add similarity search results
121
+ @results.each do |result|
122
+ if result[:embedding_id] && result[:document_id]
123
+ embedding = ::Ragdoll::Embedding.find(result[:embedding_id])
124
+ document = ::Ragdoll::Document.find(result[:document_id])
125
+ @detailed_results << {
126
+ embedding: embedding,
127
+ document: document,
128
+ similarity: result[:similarity],
129
+ content: result[:content],
130
+ usage_count: embedding.usage_count,
131
+ last_used: embedding.returned_at,
132
+ search_type: 'similarity'
133
+ }
134
+ end
135
+ end
136
+
137
+ # Mark that similarity search was attempted
138
+ @similarity_search_attempted = true
139
+
140
+ # Always gather statistics about all possible matches when similarity search returns limited results
141
+ similarity_results_count = @detailed_results.select { |r| r[:search_type] == 'similarity' }.count
142
+ ::Rails.logger.debug "🔍 Similarity results found: #{similarity_results_count}"
143
+
144
+ # Gather statistics if we have no results OR if the threshold is relatively high (> 0.1)
145
+ # This ensures we provide helpful feedback even when the search succeeds with a lower threshold
146
+ should_gather_stats = similarity_results_count == 0 || @filters[:threshold] > 0.1
147
+ ::Rails.logger.debug "🔍 Should gather stats: #{should_gather_stats} (results: #{similarity_results_count}, threshold: #{@filters[:threshold]})"
148
+
149
+ if should_gather_stats
150
+ ::Rails.logger.debug "🔍 Gathering below-threshold statistics..."
151
+ begin
152
+ # Search again with minimal threshold to get all potential matches
153
+ stats_params = search_params.merge(threshold: 0.0, limit: 100)
154
+ stats_response = ::Ragdoll.search(**stats_params)
155
+
156
+ ::Rails.logger.debug "🔍 Stats response: #{stats_response.inspect}"
157
+
158
+ if stats_response.is_a?(Hash) && stats_response[:results]
159
+ all_similarities = []
160
+ stats_response[:results].each do |result|
161
+ if result[:similarity]
162
+ all_similarities << result[:similarity]
163
+ # Store below-threshold results
164
+ if result[:similarity] < @filters[:threshold] && result[:similarity] > 0
165
+ @below_threshold_results << {
166
+ document_id: result[:document_id],
167
+ similarity: result[:similarity],
168
+ content: result[:content]
169
+ }
170
+ end
171
+ end
172
+ end
173
+
174
+ ::Rails.logger.debug "🔍 All similarities collected: #{all_similarities.inspect}"
175
+ ::Rails.logger.debug "🔍 Threshold: #{@filters[:threshold]}"
176
+
177
+ # Calculate statistics for display
178
+ if all_similarities.any?
179
+ below_threshold_count = all_similarities.count { |s| s < @filters[:threshold] && s > 0 }
180
+ @below_threshold_stats = {
181
+ count: below_threshold_count,
182
+ highest: all_similarities.max,
183
+ lowest: all_similarities.select { |s| s > 0 }.min,
184
+ average: all_similarities.sum / all_similarities.size.to_f,
185
+ suggested_threshold: all_similarities.select { |s| s > 0 }.min.round(3)
186
+ }
187
+ ::Rails.logger.debug "🔍 Below threshold stats: #{@below_threshold_stats.inspect}"
188
+ else
189
+ ::Rails.logger.debug "🔍 No similarities found in stats response"
190
+ end
191
+ else
192
+ ::Rails.logger.debug "🔍 Stats response was not in expected format or had no results"
193
+ end
194
+ rescue => stats_error
195
+ ::Rails.logger.error "Stats gathering error: #{stats_error.message}"
196
+ end
197
+ end
198
+
199
+ rescue => e
200
+ ::Rails.logger.error "Similarity search error: #{e.message}"
201
+ # Continue with fulltext search even if similarity search fails
202
+ end
203
+ end
204
+
205
+ # Perform full-text search if enabled
206
+ if use_fulltext
207
+ fulltext_params = {
208
+ limit: @filters[:limit],
209
+ threshold: @filters[:threshold]
210
+ }
211
+
212
+ # Add document type filter if specified
213
+ # NOTE: Document type filtering is deprecated in unified text-based architecture
214
+ # All media types are now converted to text for unified cross-modal search
215
+ if @filters[:document_type].present?
216
+ fulltext_params[:document_type] = @filters[:document_type]
217
+ ::Rails.logger.warn "⚠️ Document type filtering is deprecated in unified text-based RAG architecture"
218
+ end
219
+
220
+ # Add status filter if specified
221
+ if @filters[:status].present?
222
+ fulltext_params[:status] = @filters[:status]
223
+ end
224
+
225
+ fulltext_results = ::Ragdoll::Document.search_content(@query, **fulltext_params)
226
+
227
+ # Collect fulltext similarities for statistics
228
+ fulltext_similarities = []
229
+ fulltext_results.each do |document|
230
+ # Avoid duplicates if document was already found in similarity search
231
+ unless @detailed_results.any? { |r| r[:document].id == document.id }
232
+ # Use the fulltext_similarity score from the enhanced search
233
+ fulltext_similarity = document.respond_to?(:fulltext_similarity) ? document.fulltext_similarity.to_f : 0.0
234
+ fulltext_similarities << fulltext_similarity if fulltext_similarity > 0
235
+
236
+ @detailed_results << {
237
+ document: document,
238
+ content: document.metadata&.dig('summary') || document.title || "No summary available",
239
+ search_type: 'fulltext',
240
+ similarity: fulltext_similarity
241
+ }
242
+ end
243
+ end
244
+
245
+ # Gather fulltext statistics if we have few results OR if threshold is high (> 0.1)
246
+ # This ensures consistent feedback regardless of which search types are enabled
247
+ fulltext_results_count = @detailed_results.select { |r| r[:search_type] == 'fulltext' }.count
248
+ should_gather_fulltext_stats = fulltext_results_count == 0 || @filters[:threshold] > 0.1
249
+
250
+ if should_gather_fulltext_stats && !@below_threshold_stats
251
+ ::Rails.logger.debug "🔍 Gathering fulltext below-threshold statistics..."
252
+ begin
253
+ # Search again with lower threshold to get all potential matches
254
+ stats_params = fulltext_params.merge(threshold: 0.0, limit: 100)
255
+ all_fulltext_results = ::Ragdoll::Document.search_content(@query, **stats_params)
256
+
257
+ all_fulltext_similarities = []
258
+ all_fulltext_results.each do |document|
259
+ similarity = document.respond_to?(:fulltext_similarity) ? document.fulltext_similarity.to_f : 0.0
260
+ if similarity > 0
261
+ all_fulltext_similarities << similarity
262
+ # Store below-threshold results
263
+ if similarity < @filters[:threshold]
264
+ @below_threshold_results << {
265
+ document_id: document.id,
266
+ similarity: similarity,
267
+ content: document.metadata&.dig('summary') || document.title || "No summary available"
268
+ }
269
+ end
270
+ end
271
+ end
272
+
273
+ ::Rails.logger.debug "🔍 Fulltext similarities collected: #{all_fulltext_similarities.inspect}"
274
+ ::Rails.logger.debug "🔍 Threshold: #{@filters[:threshold]}"
275
+
276
+ # Calculate statistics for display
277
+ if all_fulltext_similarities.any?
278
+ below_threshold_count = all_fulltext_similarities.count { |s| s < @filters[:threshold] && s > 0 }
279
+ @below_threshold_stats = {
280
+ count: below_threshold_count,
281
+ highest: all_fulltext_similarities.max,
282
+ lowest: all_fulltext_similarities.select { |s| s > 0 }.min,
283
+ average: all_fulltext_similarities.sum / all_fulltext_similarities.size.to_f,
284
+ suggested_threshold: all_fulltext_similarities.select { |s| s > 0 }.min.round(3)
285
+ }
286
+ ::Rails.logger.debug "🔍 Fulltext below threshold stats: #{@below_threshold_stats.inspect}"
287
+ else
288
+ ::Rails.logger.debug "🔍 No fulltext similarities found in stats response"
289
+ end
290
+ rescue => stats_error
291
+ ::Rails.logger.error "Fulltext stats gathering error: #{stats_error.message}"
292
+ end
293
+ end
294
+ end
295
+
296
+ # Sort results by similarity score if available, otherwise by relevance
297
+ @detailed_results.sort_by! { |r| r[:similarity] ? -r[:similarity] : 0 }
298
+
299
+ # Save search for analytics
300
+ search_type = case
301
+ when use_similarity && use_fulltext then 'hybrid'
302
+ when use_similarity then 'similarity'
303
+ when use_fulltext then 'fulltext'
304
+ else 'unknown'
305
+ end
306
+
307
+ similarity_results = @detailed_results.select { |r| r[:search_type] == 'similarity' }
308
+ similarities = similarity_results.map { |r| r[:similarity] }.compact
309
+
310
+ # Save search for analytics without query embedding (which is optional)
311
+ begin
312
+ ::Ragdoll::Search.create!(
313
+ query: @query,
314
+ search_type: search_type,
315
+ results_count: @detailed_results.count,
316
+ max_similarity_score: similarities.any? ? similarities.max : nil,
317
+ min_similarity_score: similarities.any? ? similarities.min : nil,
318
+ avg_similarity_score: similarities.any? ? (similarities.sum / similarities.size.to_f) : nil,
319
+ search_filters: @filters.to_json,
320
+ search_options: {
321
+ threshold_used: @filters[:threshold],
322
+ similarity_results: similarity_results.count,
323
+ fulltext_results: @detailed_results.select { |r| r[:search_type] == 'fulltext' }.count,
324
+ use_similarity: use_similarity,
325
+ use_fulltext: use_fulltext,
326
+ # Store original form parameters for reconstruction
327
+ form_params: {
328
+ use_similarity_search: params[:use_similarity_search],
329
+ use_fulltext_search: params[:use_fulltext_search],
330
+ limit: @filters[:limit],
331
+ threshold: @filters[:threshold],
332
+ document_type: @filters[:document_type],
333
+ status: @filters[:status]
334
+ }
335
+ }.to_json
336
+ )
337
+ ::Rails.logger.debug "🔍 Search saved successfully"
338
+ rescue => e
339
+ ::Rails.logger.error "🔍 Failed to save search: #{e.message}"
340
+ # Continue without failing the search
341
+ end
342
+
343
+ ::Rails.logger.debug "🔍 Search completed successfully. Results count: #{@detailed_results.count}"
344
+ ::Rails.logger.debug "🔍 Similarity search attempted: #{@similarity_search_attempted}"
345
+ ::Rails.logger.debug "🔍 Below threshold stats: #{@below_threshold_stats.inspect}"
346
+ ::Rails.logger.debug "🔍 Threshold used: #{@similarity_threshold_used}"
347
+ @search_performed = true
348
+
349
+ rescue => e
350
+ ::Rails.logger.error "🔍 Search error: #{e.message}"
351
+ ::Rails.logger.error e.backtrace.join("\n")
352
+ @error = e.message
353
+ @search_performed = false
354
+ end
355
+ else
356
+ @search_performed = false
357
+ end
358
+
359
+ respond_to do |format|
360
+ format.html { render :index }
361
+ format.json {
362
+ json_response = { results: @detailed_results, error: @error }
363
+ if @similarity_search_attempted && @similarity_stats
364
+ json_response[:similarity_statistics] = {
365
+ threshold_used: @similarity_threshold_used,
366
+ stats: @similarity_stats
367
+ }
368
+ end
369
+ render json: json_response
370
+ }
371
+ end
372
+ end
373
+ end
374
+ 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