ragdoll-cli 0.1.8 → 0.1.9
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/lib/ragdoll/cli/commands/analytics.rb +222 -0
- data/lib/ragdoll/cli/commands/search.rb +61 -3
- data/lib/ragdoll/cli/commands/update.rb +1 -1
- data/lib/ragdoll/cli/standalone_client.rb +69 -2
- data/lib/ragdoll/cli/version.rb +1 -1
- data/lib/ragdoll/cli.rb +174 -10
- metadata +171 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 843beb22244ce386dadbe959585717710940767c066307b2a92d9cfeafa6c2c9
|
4
|
+
data.tar.gz: 3a47b7c95155ab97644063746998f3e2f9888f0ad7e757af1a35240baed67537
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d15dd454cec4f80ce738ffa100c6a0c00e8642e7881443aca0247436dcb9e321761467b7554439324b3ba5c9b1b232f76d87ce18a4a90547b88e899ea2e196b4
|
7
|
+
data.tar.gz: 14d92726298d5d0419075bc8d9f87bc0be0d0a34d2128c20da4fb768d68770cf0ded31735c588347cce462c94c705dc8252734317db7c09fafe25d73b19f29c6
|
@@ -0,0 +1,222 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'thor'
|
5
|
+
|
6
|
+
module Ragdoll
|
7
|
+
module CLI
|
8
|
+
class Analytics < Thor
|
9
|
+
desc 'overview', 'Show search analytics overview'
|
10
|
+
method_option :days, type: :numeric, default: 30, aliases: '-d',
|
11
|
+
desc: 'Number of days to analyze (default: 30)'
|
12
|
+
method_option :format, type: :string, default: 'table', aliases: '-f',
|
13
|
+
desc: 'Output format (table, json)'
|
14
|
+
def overview
|
15
|
+
client = StandaloneClient.new
|
16
|
+
days = (options && options[:days]) || 30
|
17
|
+
analytics = client.search_analytics(days: days)
|
18
|
+
|
19
|
+
case (options && options[:format]) || 'table'
|
20
|
+
when 'json'
|
21
|
+
puts JSON.pretty_generate(analytics)
|
22
|
+
else
|
23
|
+
puts "Search Analytics (last #{days} days):"
|
24
|
+
puts
|
25
|
+
puts 'Metric'.ljust(30) + 'Value'
|
26
|
+
puts '-' * 50
|
27
|
+
|
28
|
+
analytics.each do |key, value|
|
29
|
+
metric = format_metric_name(key).ljust(30)
|
30
|
+
formatted_value = format_metric_value(key, value)
|
31
|
+
puts "#{metric}#{formatted_value}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
rescue StandardError => e
|
35
|
+
puts "Error retrieving analytics: #{e.message}"
|
36
|
+
end
|
37
|
+
|
38
|
+
desc 'history', 'Show recent search history'
|
39
|
+
method_option :limit, type: :numeric, default: 20, aliases: '-l',
|
40
|
+
desc: 'Number of searches to show (default: 20)'
|
41
|
+
method_option :user_id, type: :string, aliases: '-u',
|
42
|
+
desc: 'Filter by user ID'
|
43
|
+
method_option :session_id, type: :string, aliases: '-s',
|
44
|
+
desc: 'Filter by session ID'
|
45
|
+
method_option :format, type: :string, default: 'table', aliases: '-f',
|
46
|
+
desc: 'Output format (table, json, plain)'
|
47
|
+
def history
|
48
|
+
client = StandaloneClient.new
|
49
|
+
limit = (options && options[:limit]) || 20
|
50
|
+
filter_options = {}
|
51
|
+
filter_options[:user_id] = options[:user_id] if options && options[:user_id]
|
52
|
+
filter_options[:session_id] = options[:session_id] if options && options[:session_id]
|
53
|
+
|
54
|
+
searches = client.search_history(limit: limit, **filter_options)
|
55
|
+
|
56
|
+
case (options && options[:format]) || 'table'
|
57
|
+
when 'json'
|
58
|
+
puts JSON.pretty_generate(searches || [])
|
59
|
+
when 'plain'
|
60
|
+
if searches.nil? || searches.empty?
|
61
|
+
puts 'No search history found.'
|
62
|
+
return
|
63
|
+
end
|
64
|
+
searches.each_with_index do |search, index|
|
65
|
+
puts "#{index + 1}. #{search[:query]} (#{search[:search_type]})"
|
66
|
+
puts " Time: #{search[:created_at]}"
|
67
|
+
puts " Results: #{search[:results_count]}"
|
68
|
+
puts " Execution: #{search[:execution_time_ms]}ms"
|
69
|
+
puts " Session: #{search[:session_id]}" if search[:session_id]
|
70
|
+
puts " User: #{search[:user_id]}" if search[:user_id]
|
71
|
+
puts
|
72
|
+
end
|
73
|
+
else
|
74
|
+
# Table format
|
75
|
+
if searches.nil? || searches.empty?
|
76
|
+
puts 'No search history found.'
|
77
|
+
return
|
78
|
+
end
|
79
|
+
|
80
|
+
puts "Recent Search History (#{searches.length} searches):"
|
81
|
+
puts
|
82
|
+
puts 'Time'.ljust(20) + 'Query'.ljust(30) + 'Type'.ljust(10) + 'Results'.ljust(8) + 'Time(ms)'
|
83
|
+
puts '-' * 80
|
84
|
+
|
85
|
+
searches.each do |search|
|
86
|
+
time = format_time(search[:created_at]).ljust(20)
|
87
|
+
query = (search[:query] || 'N/A')[0..29].ljust(30)
|
88
|
+
type = (search[:search_type] || 'N/A')[0..9].ljust(10)
|
89
|
+
results = (search[:results_count] || 0).to_s.ljust(8)
|
90
|
+
exec_time = (search[:execution_time_ms] || 0).to_s
|
91
|
+
|
92
|
+
puts "#{time}#{query}#{type}#{results}#{exec_time}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
rescue StandardError => e
|
96
|
+
puts "Error retrieving search history: #{e.message}"
|
97
|
+
end
|
98
|
+
|
99
|
+
desc 'trending', 'Show trending search queries'
|
100
|
+
method_option :limit, type: :numeric, default: 10, aliases: '-l',
|
101
|
+
desc: 'Number of queries to show (default: 10)'
|
102
|
+
method_option :days, type: :numeric, default: 7, aliases: '-d',
|
103
|
+
desc: 'Time period in days (default: 7)'
|
104
|
+
method_option :format, type: :string, default: 'table', aliases: '-f',
|
105
|
+
desc: 'Output format (table, json)'
|
106
|
+
def trending
|
107
|
+
client = StandaloneClient.new
|
108
|
+
limit = (options && options[:limit]) || 10
|
109
|
+
days = (options && options[:days]) || 7
|
110
|
+
trending_queries = client.trending_queries(limit: limit, days: days)
|
111
|
+
|
112
|
+
case (options && options[:format]) || 'table'
|
113
|
+
when 'json'
|
114
|
+
puts JSON.pretty_generate(trending_queries || [])
|
115
|
+
else
|
116
|
+
if trending_queries.nil? || trending_queries.empty?
|
117
|
+
puts "No trending queries found for the last #{days} days."
|
118
|
+
return
|
119
|
+
end
|
120
|
+
|
121
|
+
puts "Trending Search Queries (last #{days} days):"
|
122
|
+
puts
|
123
|
+
puts 'Rank'.ljust(5) + 'Query'.ljust(40) + 'Count'.ljust(8) + 'Avg Results'
|
124
|
+
puts '-' * 80
|
125
|
+
|
126
|
+
trending_queries.each_with_index do |query_data, index|
|
127
|
+
rank = (index + 1).to_s.ljust(5)
|
128
|
+
query = (query_data[:query] || 'N/A')[0..39].ljust(40)
|
129
|
+
count = (query_data[:count] || 0).to_s.ljust(8)
|
130
|
+
avg_results = (query_data[:avg_results] || 0).round(1).to_s
|
131
|
+
|
132
|
+
puts "#{rank}#{query}#{count}#{avg_results}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
rescue StandardError => e
|
136
|
+
puts "Error retrieving trending queries: #{e.message}"
|
137
|
+
end
|
138
|
+
|
139
|
+
desc 'cleanup', 'Cleanup old search records'
|
140
|
+
method_option :days, type: :numeric, default: 30, aliases: '-d',
|
141
|
+
desc: 'Remove searches older than N days (default: 30)'
|
142
|
+
method_option :dry_run, type: :boolean, default: true, aliases: '-n',
|
143
|
+
desc: 'Show what would be deleted without actually deleting (default: true)'
|
144
|
+
method_option :force, type: :boolean, default: false, aliases: '-f',
|
145
|
+
desc: 'Actually perform the cleanup (overrides dry_run)'
|
146
|
+
def cleanup
|
147
|
+
client = StandaloneClient.new
|
148
|
+
|
149
|
+
days = (options && options[:days]) || 30
|
150
|
+
dry_run = if options && options.key?(:dry_run)
|
151
|
+
options[:dry_run]
|
152
|
+
else
|
153
|
+
true # Default to true
|
154
|
+
end
|
155
|
+
force = (options && options[:force]) == true
|
156
|
+
|
157
|
+
cleanup_options = {
|
158
|
+
days: days,
|
159
|
+
dry_run: dry_run && !force
|
160
|
+
}
|
161
|
+
|
162
|
+
if cleanup_options[:dry_run]
|
163
|
+
puts "DRY RUN: Showing what would be cleaned up (use --force to actually clean up)"
|
164
|
+
puts
|
165
|
+
else
|
166
|
+
puts "Performing actual cleanup of search records older than #{days} days..."
|
167
|
+
puts
|
168
|
+
end
|
169
|
+
|
170
|
+
result = client.cleanup_searches(**cleanup_options)
|
171
|
+
|
172
|
+
if result.is_a?(Hash)
|
173
|
+
puts "Cleanup Results:"
|
174
|
+
puts " Orphaned searches: #{result[:orphaned_count] || 0}"
|
175
|
+
puts " Old unused searches: #{result[:unused_count] || 0}"
|
176
|
+
puts " Total cleaned: #{(result[:orphaned_count] || 0) + (result[:unused_count] || 0)}"
|
177
|
+
else
|
178
|
+
puts "Searches cleaned up: #{result}"
|
179
|
+
end
|
180
|
+
|
181
|
+
if cleanup_options[:dry_run]
|
182
|
+
puts
|
183
|
+
puts "Use --force to actually perform the cleanup"
|
184
|
+
end
|
185
|
+
rescue StandardError => e
|
186
|
+
puts "Error during cleanup: #{e.message}"
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def format_metric_name(key)
|
192
|
+
key.to_s.tr('_', ' ').split.map(&:capitalize).join(' ')
|
193
|
+
end
|
194
|
+
|
195
|
+
def format_metric_value(key, value)
|
196
|
+
case key.to_s
|
197
|
+
when /time/
|
198
|
+
"#{value}ms"
|
199
|
+
when /rate/
|
200
|
+
"#{value}%"
|
201
|
+
when /count/
|
202
|
+
value.to_s
|
203
|
+
else
|
204
|
+
value.to_s
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def format_time(timestamp)
|
209
|
+
return 'N/A' unless timestamp
|
210
|
+
|
211
|
+
if timestamp.respond_to?(:strftime)
|
212
|
+
timestamp.strftime('%m/%d %H:%M')
|
213
|
+
else
|
214
|
+
# Handle string timestamps
|
215
|
+
Time.parse(timestamp.to_s).strftime('%m/%d %H:%M')
|
216
|
+
end
|
217
|
+
rescue
|
218
|
+
'N/A'
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -14,21 +14,79 @@ module Ragdoll
|
|
14
14
|
|
15
15
|
search_options = {}
|
16
16
|
search_options[:limit] = options[:limit] if options[:limit]
|
17
|
+
search_options[:threshold] = options[:threshold] if options[:threshold]
|
17
18
|
search_options[:content_type] = options[:content_type] if options[:content_type]
|
18
19
|
search_options[:classification] = options[:classification] if options[:classification]
|
19
20
|
search_options[:keywords] = options[:keywords].split(',').map(&:strip) if options[:keywords]
|
20
21
|
search_options[:tags] = options[:tags].split(',').map(&:strip) if options[:tags]
|
22
|
+
|
23
|
+
# Add search tracking options
|
24
|
+
search_options[:session_id] = options[:session_id] if options[:session_id]
|
25
|
+
search_options[:user_id] = options[:user_id] if options[:user_id]
|
26
|
+
search_options[:track_search] = options[:track_search] if options.respond_to?(:key?) ? options.key?(:track_search) : options.track_search
|
21
27
|
|
22
|
-
|
28
|
+
# Select search method based on search_type
|
29
|
+
search_response = case options[:search_type]
|
30
|
+
when 'hybrid'
|
31
|
+
client.hybrid_search(query: query, **search_options)
|
32
|
+
when 'fulltext'
|
33
|
+
# Note: fulltext search would need to be implemented in client
|
34
|
+
client.search(query: query, **search_options)
|
35
|
+
else
|
36
|
+
client.search(query: query, **search_options)
|
37
|
+
end
|
23
38
|
|
24
39
|
# Extract the actual results array from the response
|
25
40
|
results = search_response[:results] || search_response['results'] || []
|
26
41
|
|
27
42
|
if results.empty?
|
43
|
+
# Get statistics for better feedback
|
44
|
+
statistics = search_response[:statistics] || search_response['statistics']
|
45
|
+
execution_time = search_response[:execution_time_ms] || search_response['execution_time_ms']
|
28
46
|
total = search_response[:total_results] || search_response['total_results'] || 0
|
47
|
+
|
29
48
|
puts "No results found for '#{query}'"
|
30
|
-
puts
|
31
|
-
|
49
|
+
puts
|
50
|
+
|
51
|
+
if statistics
|
52
|
+
threshold = statistics[:threshold_used] || statistics['threshold_used']
|
53
|
+
highest = statistics[:highest_similarity] || statistics['highest_similarity']
|
54
|
+
lowest = statistics[:lowest_similarity] || statistics['lowest_similarity']
|
55
|
+
average = statistics[:average_similarity] || statistics['average_similarity']
|
56
|
+
above_threshold = statistics[:similarities_above_threshold] || statistics['similarities_above_threshold']
|
57
|
+
total_checked = statistics[:total_embeddings_checked] || statistics['total_embeddings_checked']
|
58
|
+
|
59
|
+
puts "Search Analysis:"
|
60
|
+
puts " • Similarity threshold: #{threshold&.round(3) || 'N/A'}"
|
61
|
+
puts " • Embeddings analyzed: #{total_checked || 0}"
|
62
|
+
if highest && lowest && average
|
63
|
+
puts " • Similarity range: #{lowest.round(3)} - #{highest.round(3)} (avg: #{average.round(3)})"
|
64
|
+
end
|
65
|
+
puts " • Results above threshold: #{above_threshold || 0}"
|
66
|
+
puts " • Search time: #{execution_time || 0}ms"
|
67
|
+
puts
|
68
|
+
|
69
|
+
# Provide actionable suggestions
|
70
|
+
if highest && threshold
|
71
|
+
if highest < threshold
|
72
|
+
suggested_threshold = (highest * 0.9).round(3)
|
73
|
+
puts "💡 Suggestions:"
|
74
|
+
puts " • Lower the similarity threshold (highest found: #{highest.round(3)})"
|
75
|
+
puts " • Try: ragdoll search '#{query}' --threshold=#{suggested_threshold}"
|
76
|
+
if highest < 0.3
|
77
|
+
puts " • Your query might not match the document content well"
|
78
|
+
puts " • Try different or more specific search terms"
|
79
|
+
end
|
80
|
+
elsif above_threshold > 0
|
81
|
+
puts "💡 Note: Found #{above_threshold} similar content above threshold #{threshold}"
|
82
|
+
puts " This suggests an issue with result processing."
|
83
|
+
end
|
84
|
+
end
|
85
|
+
else
|
86
|
+
puts "(Total documents in system: #{total})" if total > 0
|
87
|
+
puts "Try adjusting your search terms or check if documents have been processed."
|
88
|
+
end
|
89
|
+
|
32
90
|
return
|
33
91
|
end
|
34
92
|
|
@@ -4,6 +4,7 @@ module Ragdoll
|
|
4
4
|
module CLI
|
5
5
|
class StandaloneClient
|
6
6
|
include DebugMe
|
7
|
+
|
7
8
|
def add_document(path, **options)
|
8
9
|
Ragdoll.add_document(path: path, **options)
|
9
10
|
end
|
@@ -34,8 +35,12 @@ module Ragdoll
|
|
34
35
|
end
|
35
36
|
|
36
37
|
|
37
|
-
def search(query, **options)
|
38
|
-
|
38
|
+
def search(query = nil, **options)
|
39
|
+
if query
|
40
|
+
Ragdoll.search(query: query, **options)
|
41
|
+
else
|
42
|
+
Ragdoll.search(**options)
|
43
|
+
end
|
39
44
|
end
|
40
45
|
|
41
46
|
|
@@ -53,6 +58,68 @@ module Ragdoll
|
|
53
58
|
Ragdoll.stats
|
54
59
|
end
|
55
60
|
|
61
|
+
def search_analytics(days: 30)
|
62
|
+
# TODO: This will delegate to Ragdoll core when analytics are implemented
|
63
|
+
if defined?(Ragdoll) && Ragdoll.respond_to?(:search_analytics)
|
64
|
+
Ragdoll.search_analytics(days: days)
|
65
|
+
else
|
66
|
+
# Placeholder response for now
|
67
|
+
{
|
68
|
+
total_searches: 0,
|
69
|
+
unique_queries: 0,
|
70
|
+
avg_results_per_search: 0.0,
|
71
|
+
avg_execution_time: 0.0,
|
72
|
+
search_types: {},
|
73
|
+
searches_with_results: 0,
|
74
|
+
avg_click_through_rate: 0.0
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def search_history(limit: 20, **options)
|
80
|
+
# TODO: This will delegate to Ragdoll core when analytics are implemented
|
81
|
+
if defined?(Ragdoll) && Ragdoll.respond_to?(:search_history)
|
82
|
+
Ragdoll.search_history(limit: limit, **options)
|
83
|
+
else
|
84
|
+
# Placeholder response for now
|
85
|
+
[]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def trending_queries(limit: 10, days: 7)
|
90
|
+
# TODO: This will delegate to Ragdoll core when analytics are implemented
|
91
|
+
if defined?(Ragdoll) && Ragdoll.respond_to?(:trending_queries)
|
92
|
+
Ragdoll.trending_queries(limit: limit, days: days)
|
93
|
+
else
|
94
|
+
# Placeholder response for now
|
95
|
+
[]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def cleanup_searches(**options)
|
100
|
+
# TODO: This will delegate to Ragdoll core when analytics are implemented
|
101
|
+
if defined?(Ragdoll) && Ragdoll.respond_to?(:cleanup_searches)
|
102
|
+
Ragdoll.cleanup_searches(**options)
|
103
|
+
else
|
104
|
+
# Placeholder response for now
|
105
|
+
{ orphaned_count: 0, unused_count: 0 }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def hybrid_search(query = nil, **options)
|
110
|
+
# TODO: This will delegate to Ragdoll core when hybrid search is implemented
|
111
|
+
if defined?(Ragdoll) && Ragdoll.respond_to?(:hybrid_search)
|
112
|
+
if query
|
113
|
+
Ragdoll.hybrid_search(query: query, **options)
|
114
|
+
else
|
115
|
+
Ragdoll.hybrid_search(**options)
|
116
|
+
end
|
117
|
+
else
|
118
|
+
# Fallback to regular search for now
|
119
|
+
result = search(query, **options)
|
120
|
+
result.is_a?(Hash) ? result.merge(search_type: 'hybrid') : { search_type: 'hybrid', results: [] }
|
121
|
+
end
|
122
|
+
end
|
56
123
|
|
57
124
|
def healthy?
|
58
125
|
Ragdoll.healthy?
|
data/lib/ragdoll/cli/version.rb
CHANGED
data/lib/ragdoll/cli.rb
CHANGED
@@ -14,6 +14,7 @@ require_relative 'cli/commands/search'
|
|
14
14
|
require_relative 'cli/commands/config'
|
15
15
|
require_relative 'cli/commands/delete'
|
16
16
|
require_relative 'cli/commands/update'
|
17
|
+
require_relative 'cli/commands/analytics'
|
17
18
|
|
18
19
|
module Ragdoll
|
19
20
|
module CLI
|
@@ -37,6 +38,8 @@ module Ragdoll
|
|
37
38
|
desc 'search QUERY', 'Search for documents matching the query'
|
38
39
|
method_option :limit, type: :numeric, default: 10, aliases: '-l',
|
39
40
|
desc: 'Maximum number of results to return'
|
41
|
+
method_option :threshold, type: :numeric,
|
42
|
+
desc: 'Similarity threshold (0.0-1.0, lower = more results)'
|
40
43
|
method_option :content_type, type: :string, aliases: '-c',
|
41
44
|
desc: 'Filter by content type (text, image, audio)'
|
42
45
|
method_option :classification, type: :string, aliases: '-C',
|
@@ -47,6 +50,14 @@ module Ragdoll
|
|
47
50
|
desc: 'Filter by tags (comma-separated)'
|
48
51
|
method_option :format, type: :string, default: 'table', aliases: '-f',
|
49
52
|
desc: 'Output format (table, json, plain)'
|
53
|
+
method_option :session_id, type: :string, aliases: '-s',
|
54
|
+
desc: 'Session ID for search tracking'
|
55
|
+
method_option :user_id, type: :string, aliases: '-u',
|
56
|
+
desc: 'User ID for search tracking'
|
57
|
+
method_option :track_search, type: :boolean, default: true, aliases: '-t',
|
58
|
+
desc: 'Enable search tracking (default: true)'
|
59
|
+
method_option :search_type, type: :string, default: 'semantic', aliases: '-S',
|
60
|
+
desc: 'Search type: semantic, hybrid, fulltext (default: semantic)'
|
50
61
|
def search(query)
|
51
62
|
Search.new.call(query, options)
|
52
63
|
end
|
@@ -54,6 +65,9 @@ module Ragdoll
|
|
54
65
|
desc 'config SUBCOMMAND', 'Manage configuration'
|
55
66
|
subcommand 'config', Config
|
56
67
|
|
68
|
+
desc 'analytics SUBCOMMAND', 'Search analytics and reporting'
|
69
|
+
subcommand 'analytics', Analytics
|
70
|
+
|
57
71
|
desc 'stats', 'Show document and embedding statistics'
|
58
72
|
def stats
|
59
73
|
client = StandaloneClient.new
|
@@ -78,11 +92,36 @@ module Ragdoll
|
|
78
92
|
end
|
79
93
|
end
|
80
94
|
|
81
|
-
|
95
|
+
if stats[:content_types]
|
96
|
+
puts "\nContent Types:"
|
97
|
+
stats[:content_types].each do |type, count|
|
98
|
+
puts " #{type}: #{count}"
|
99
|
+
end
|
100
|
+
end
|
82
101
|
|
83
|
-
|
84
|
-
|
85
|
-
|
102
|
+
# Add search analytics if available
|
103
|
+
begin
|
104
|
+
search_analytics = client.search_analytics(days: 30)
|
105
|
+
if search_analytics && !search_analytics.empty?
|
106
|
+
puts "\nSearch Analytics (last 30 days):"
|
107
|
+
puts " Total searches: #{search_analytics[:total_searches] || 0}"
|
108
|
+
puts " Unique queries: #{search_analytics[:unique_queries] || 0}"
|
109
|
+
puts " Avg results per search: #{search_analytics[:avg_results_per_search] || 0}"
|
110
|
+
puts " Avg execution time: #{search_analytics[:avg_execution_time] || 0}ms"
|
111
|
+
|
112
|
+
if search_analytics[:search_types]
|
113
|
+
puts " Search types:"
|
114
|
+
search_analytics[:search_types].each do |type, count|
|
115
|
+
puts " #{type}: #{count}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
puts " Searches with results: #{search_analytics[:searches_with_results] || 0}"
|
120
|
+
puts " Avg click-through rate: #{search_analytics[:avg_click_through_rate] || 0}%"
|
121
|
+
end
|
122
|
+
rescue StandardError => e
|
123
|
+
# Search analytics not available - silently continue
|
124
|
+
puts "\nSearch analytics: Not available (#{e.message})"
|
86
125
|
end
|
87
126
|
end
|
88
127
|
|
@@ -354,27 +393,152 @@ module Ragdoll
|
|
354
393
|
end
|
355
394
|
|
356
395
|
desc 'context QUERY', 'Get context for RAG applications'
|
357
|
-
method_option :limit, type: :numeric, default:
|
396
|
+
method_option :limit, type: :numeric, default: 10, aliases: '-l', desc: 'Maximum number of context chunks'
|
397
|
+
method_option :threshold, type: :numeric, desc: 'Similarity threshold (0.0-1.0, lower = more results)'
|
358
398
|
def context(query)
|
359
399
|
client = StandaloneClient.new
|
360
|
-
|
361
|
-
|
400
|
+
context_options = { limit: options[:limit] }
|
401
|
+
context_options[:threshold] = options[:threshold] if options[:threshold]
|
402
|
+
ctx = client.get_context(query, **context_options)
|
403
|
+
|
404
|
+
# Check if no context was found and provide enhanced feedback
|
405
|
+
if ctx[:context_chunks].empty?
|
406
|
+
# Get the underlying search response for statistics
|
407
|
+
search_response = client.search(query, **context_options)
|
408
|
+
display_no_results_feedback(query, search_response, 'context')
|
409
|
+
else
|
410
|
+
puts JSON.pretty_generate(ctx)
|
411
|
+
end
|
362
412
|
end
|
363
413
|
|
364
414
|
desc 'enhance PROMPT', 'Enhance a prompt with context'
|
365
|
-
method_option :
|
415
|
+
method_option :limit, type: :numeric, default: 10, aliases: '-l', desc: 'Maximum number of context chunks to include'
|
416
|
+
method_option :threshold, type: :numeric, desc: 'Similarity threshold (0.0-1.0, lower = more results)'
|
366
417
|
def enhance(prompt)
|
367
418
|
client = StandaloneClient.new
|
368
|
-
|
369
|
-
|
419
|
+
enhance_options = { context_limit: options[:limit] }
|
420
|
+
enhance_options[:threshold] = options[:threshold] if options[:threshold]
|
421
|
+
enhanced = client.enhance_prompt(prompt, **enhance_options)
|
422
|
+
|
423
|
+
# Check if no context was found and provide enhanced feedback
|
424
|
+
if enhanced[:context_count] == 0
|
425
|
+
# Get the underlying search response for statistics
|
426
|
+
search_response = client.search(prompt, limit: enhance_options[:context_limit], threshold: enhance_options[:threshold])
|
427
|
+
display_no_results_feedback(prompt, search_response, 'enhance')
|
428
|
+
else
|
429
|
+
puts enhanced[:enhanced_prompt]
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
desc 'search-history', 'Show recent search history'
|
434
|
+
method_option :limit, type: :numeric, default: 20, aliases: '-l',
|
435
|
+
desc: 'Number of searches to show (default: 20)'
|
436
|
+
method_option :user_id, type: :string, aliases: '-u',
|
437
|
+
desc: 'Filter by user ID'
|
438
|
+
method_option :session_id, type: :string, aliases: '-s',
|
439
|
+
desc: 'Filter by session ID'
|
440
|
+
method_option :format, type: :string, default: 'table', aliases: '-f',
|
441
|
+
desc: 'Output format (table, json, plain)'
|
442
|
+
def search_history
|
443
|
+
analytics = Analytics.new
|
444
|
+
analytics.options = options
|
445
|
+
analytics.history
|
446
|
+
end
|
447
|
+
|
448
|
+
desc 'search-stats', 'Show detailed search analytics'
|
449
|
+
method_option :days, type: :numeric, default: 30, aliases: '-d',
|
450
|
+
desc: 'Number of days to analyze (default: 30)'
|
451
|
+
method_option :format, type: :string, default: 'table', aliases: '-f',
|
452
|
+
desc: 'Output format (table, json)'
|
453
|
+
def search_stats
|
454
|
+
analytics = Analytics.new
|
455
|
+
analytics.options = options
|
456
|
+
analytics.overview
|
370
457
|
end
|
371
458
|
|
459
|
+
desc 'trending', 'Show trending search queries'
|
460
|
+
method_option :limit, type: :numeric, default: 10, aliases: '-l',
|
461
|
+
desc: 'Number of queries to show (default: 10)'
|
462
|
+
method_option :days, type: :numeric, default: 7, aliases: '-d',
|
463
|
+
desc: 'Time period in days (default: 7)'
|
464
|
+
method_option :format, type: :string, default: 'table', aliases: '-f',
|
465
|
+
desc: 'Output format (table, json)'
|
466
|
+
def trending
|
467
|
+
analytics = Analytics.new
|
468
|
+
analytics.options = options
|
469
|
+
analytics.trending
|
470
|
+
end
|
471
|
+
|
472
|
+
desc 'cleanup-searches', 'Cleanup old search records'
|
473
|
+
method_option :days, type: :numeric, default: 30, aliases: '-d',
|
474
|
+
desc: 'Remove searches older than N days (default: 30)'
|
475
|
+
method_option :dry_run, type: :boolean, default: true, aliases: '-n',
|
476
|
+
desc: 'Show what would be deleted without actually deleting (default: true)'
|
477
|
+
method_option :force, type: :boolean, default: false, aliases: '-f',
|
478
|
+
desc: 'Actually perform the cleanup (overrides dry_run)'
|
479
|
+
def cleanup_searches
|
480
|
+
analytics = Analytics.new
|
481
|
+
analytics.options = options
|
482
|
+
analytics.cleanup
|
483
|
+
end
|
372
484
|
|
373
485
|
private
|
374
486
|
|
375
487
|
def load_configuration
|
376
488
|
ConfigurationLoader.new.load
|
377
489
|
end
|
490
|
+
|
491
|
+
def display_no_results_feedback(query, search_response, command_type)
|
492
|
+
# Extract the actual results array from the response
|
493
|
+
results = search_response[:results] || search_response['results'] || []
|
494
|
+
|
495
|
+
puts "No results found for '#{query}'"
|
496
|
+
puts
|
497
|
+
|
498
|
+
# Get statistics for better feedback
|
499
|
+
statistics = search_response[:statistics] || search_response['statistics']
|
500
|
+
execution_time = search_response[:execution_time_ms] || search_response['execution_time_ms']
|
501
|
+
total = search_response[:total_results] || search_response['total_results'] || 0
|
502
|
+
|
503
|
+
if statistics
|
504
|
+
threshold = statistics[:threshold_used] || statistics['threshold_used']
|
505
|
+
highest = statistics[:highest_similarity] || statistics['highest_similarity']
|
506
|
+
lowest = statistics[:lowest_similarity] || statistics['lowest_similarity']
|
507
|
+
average = statistics[:average_similarity] || statistics['average_similarity']
|
508
|
+
above_threshold = statistics[:similarities_above_threshold] || statistics['similarities_above_threshold']
|
509
|
+
total_checked = statistics[:total_embeddings_checked] || statistics['total_embeddings_checked']
|
510
|
+
|
511
|
+
puts "Search Analysis:"
|
512
|
+
puts " • Similarity threshold: #{threshold&.round(3) || 'N/A'}"
|
513
|
+
puts " • Embeddings analyzed: #{total_checked || 0}"
|
514
|
+
if highest && lowest && average
|
515
|
+
puts " • Similarity range: #{lowest.round(3)} - #{highest.round(3)} (avg: #{average.round(3)})"
|
516
|
+
end
|
517
|
+
puts " • Results above threshold: #{above_threshold || 0}"
|
518
|
+
puts " • Search time: #{execution_time || 0}ms"
|
519
|
+
puts
|
520
|
+
|
521
|
+
# Provide actionable suggestions
|
522
|
+
if highest && threshold
|
523
|
+
if highest < threshold
|
524
|
+
suggested_threshold = (highest * 0.9).round(3)
|
525
|
+
puts "💡 Suggestions:"
|
526
|
+
puts " • Lower the similarity threshold (highest found: #{highest.round(3)})"
|
527
|
+
puts " • Try: ragdoll #{command_type} '#{query}' --threshold=#{suggested_threshold}"
|
528
|
+
if highest < 0.3
|
529
|
+
puts " • Your query might not match the document content well"
|
530
|
+
puts " • Try different or more specific search terms"
|
531
|
+
end
|
532
|
+
elsif above_threshold > 0
|
533
|
+
puts "💡 Note: Found #{above_threshold} similar content above threshold #{threshold}"
|
534
|
+
puts " This suggests an issue with result processing."
|
535
|
+
end
|
536
|
+
end
|
537
|
+
else
|
538
|
+
puts "No similarity statistics available."
|
539
|
+
puts "💡 Try lowering the similarity threshold with --threshold=0.5"
|
540
|
+
end
|
541
|
+
end
|
378
542
|
end
|
379
543
|
end
|
380
544
|
end
|
metadata
CHANGED
@@ -1,14 +1,182 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ragdoll-cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dewayne VanHoozer
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
-
dependencies:
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: ragdoll
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: ruby-progressbar
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: thor
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: bundler
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: debug_me
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: minitest
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: rake
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rubocop
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: rubocop-minitest
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: rubocop-rake
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
type: :development
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
- !ruby/object:Gem::Dependency
|
153
|
+
name: simplecov
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
type: :development
|
160
|
+
prerelease: false
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
- !ruby/object:Gem::Dependency
|
167
|
+
name: undercover
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
173
|
+
type: :development
|
174
|
+
prerelease: false
|
175
|
+
version_requirements: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
12
180
|
description: Under development. Contributors welcome.
|
13
181
|
email:
|
14
182
|
- dvanhoozer@gmail.com
|
@@ -21,6 +189,7 @@ files:
|
|
21
189
|
- Rakefile
|
22
190
|
- bin/ragdoll
|
23
191
|
- lib/ragdoll/cli.rb
|
192
|
+
- lib/ragdoll/cli/commands/analytics.rb
|
24
193
|
- lib/ragdoll/cli/commands/config.rb
|
25
194
|
- lib/ragdoll/cli/commands/delete.rb
|
26
195
|
- lib/ragdoll/cli/commands/health.rb
|