Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions lib/classifier/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ def parse_options
@options[:type] = type
end

opts.on(
'--search TEXT',
'Search remote models by name/description, and local models by name only. Use quotes for multiword search'
) do |text|
@options[:search] = text
end

opts.on('-r', '--remote MODEL', 'Use remote model: name or @user/repo:name') do |model|
@options[:remote] = model
end
Expand Down Expand Up @@ -376,12 +383,20 @@ def list_remote_models

return if @exit_code != 0

if index['models'].empty?
models = index['models']

if @options[:search]
models = models.filter do |name, info|
[name, info['description']].any?(/#{Regexp.escape(@options[:search])}/i)
end
end

if models.empty?
@output << 'No models found in registry'
return
end

index['models'].each do |name, info|
models.each do |name, info|
type = info['type'] || 'unknown'
size = info['size'] || 'unknown'
desc = info['description'] || ''
Expand Down Expand Up @@ -413,14 +428,23 @@ def list_local_models

models = default_models + custom_models #: Array[{name: String, registry: String?, path: String}]

models.each do |model|
model[:info] = load_model_info(model[:path])
end

if @options[:search]
models = models.filter do |model|
[model[:name], model.dig(:info, 'description')].any?(/#{Regexp.escape(@options[:search])}/i)
end
end
Comment on lines +435 to +439

@cardmagic cardmagic Jun 22, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing a local search could match descriptions at near-zero extra cost.

Remote search matches name + description, but local matches name only. That asymmetry is fine and it's documented in the help text, so this isn't a bug. Just noting that the usual reason to skip description matching here. Having to load + parse each model's JSON doesn't actually apply: the display loop below already calls load_model_info(model[:path]) for every model (line 443) to read its type. The JSON is parsed regardless.

So searching descriptions locally wouldn't add the I/O it appears to. One option is to load info once per model up front, then filter on both name and info['description'], reusing the same parsed hash for display.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cardmagic Am I correct in understanding that search by description for local models may be added in the future? That is, for new models, their description will be added to the JSON file.

Fixed, check, please.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've got it right. The cached local files are raw classifier dumps — they carry type but no description (that only lives in the registry's models.json), so local --search matches on name for every model today. Your preload approach is forward-compatible: if a model file ever includes a description key, local search picks it up for free since we already parse each file for the type column.


if models.empty?
@output << 'No local models found'
return
end

models.each do |model|
info = load_model_info(model[:path])
type = info['type'] || 'unknown'
type = model.dig(:info, 'type') || 'unknown'
display_name = model[:registry] ? "@#{model[:registry]}:#{model[:name]}" : model[:name]
size = File.size(model[:path])
@output << format('%-30<name>s (%<type>s, %<size>s)', name: display_name, type: type, size: human_size(size))
Expand Down
58 changes: 58 additions & 0 deletions test/cli/registry_commands_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,64 @@ def test_models_local_shows_no_models_when_cache_dir_missing
assert_match(/no local models found/i, result[:output])
end

def test_models_remote_search_by_name
stub_request(:get, 'https://raw.githubusercontent.com/cardmagic/classifier-models/main/models.json')
.to_return(status: 200, body: @models_json)

result = run_cli('models', '--search', 'spam-filter')

assert_equal 0, result[:exit_code]
assert_match(/spam-filter/, result[:output])
refute_match(/sentiment/, result[:output])
end

def test_models_remote_search_by_description
stub_request(:get, 'https://raw.githubusercontent.com/cardmagic/classifier-models/main/models.json')
.to_return(status: 200, body: @models_json)

result = run_cli('models', '--search', 'Spam Detection')

assert_equal 0, result[:exit_code]
assert_match(/spam-filter/, result[:output])
refute_match(/sentiment/, result[:output])
end

def test_models_remote_search_no_found
stub_request(:get, 'https://raw.githubusercontent.com/cardmagic/classifier-models/main/models.json')
.to_return(status: 200, body: @models_json)

result = run_cli('models', '--search', '[a-z]+')

assert_equal 0, result[:exit_code]
assert_match(/No models found in registry/, result[:output])
end

def test_models_local_search_by_name
# Create some cached models
models_dir = File.join(@cache_dir, 'models')
FileUtils.mkdir_p(models_dir)
File.write(File.join(models_dir, 'spam-filter.json'), @model_json)
File.write(File.join(models_dir, 'sentiment.json'), @model_json)

result = run_cli('models', '--local', '--search', 'spam-filter')

assert_equal 0, result[:exit_code]
assert_match(/spam-filter/, result[:output])
refute_match(/sentiment/, result[:output])
end

def test_models_local_search_no_found
# Create some cached models
models_dir = File.join(@cache_dir, 'models')
FileUtils.mkdir_p(models_dir)
File.write(File.join(models_dir, 'spam-filter.json'), @model_json)

result = run_cli('models', '--local', '--search', '[a-z]+')

assert_equal 0, result[:exit_code]
assert_match(/No local models found/, result[:output])
end

#
# Pull Command
#
Expand Down
Loading