emb 0.1.2 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc1be692fe3d651429a292c2e2d0b912aa1bb8af22dbf56b9390d7e8a00fe516
4
- data.tar.gz: 433caba4f427a465b9717970884ea64e12c4169fca3f54df8e91858dc1ef21bb
3
+ metadata.gz: da3d0c2517e4def78e7d887bd480cf3db14d2c83ba66eff0853ddf075fc1b785
4
+ data.tar.gz: 586bd53ce50bd1d1b427aaa82811676e193009155991dbe20f3496bf45816c7b
5
5
  SHA512:
6
- metadata.gz: 553e753f5a90217312bd7f6e2e9240c16747b6fcc01e876cdabdc2b33a2c430cb937cb214926ee8a4d3fb77a66a45439ad6fb6c1be1885617fb0fa68026ee336
7
- data.tar.gz: 478a17bbf82026da26580ead85fae29bd6f8b1350bbe841aa32b098ce0845e3005641a80f388fd7912df0ba3e2c9652a98d8dfda8769a8d48c3ceb1d3eed1ae4
6
+ metadata.gz: 55aebcfa8f1ccdeca346309bd91d20e8527988593d1e76a4996d002e692ff2bfabf04119e65fa360c05ec04c3b8fa39f34166b72f1e3651eba071b97f024fc16
7
+ data.tar.gz: 2921b88b9cdb672f3ddcb54fba2ceb441772fc5791890487a30f17483abca57dd0a4e04f69534bdc0f33f5df1e20c0bb680394ef2a7822ddbe3164915914e82c
data/Gemfile CHANGED
@@ -9,3 +9,7 @@ gem 'redis-client'
9
9
 
10
10
  gem 'rake', require: false
11
11
  gem 'rspec', require: false
12
+
13
+ gem 'rubocop', require: false
14
+ gem 'rubocop-rake', require: false
15
+ gem 'rubocop-rspec', require: false
data/README.md CHANGED
@@ -20,16 +20,95 @@ gem install emb
20
20
 
21
21
  ## Setup
22
22
 
23
- Configure the connection pool (defaults shown):
23
+ The client connects to an emb server via the Redis protocol (RESP2). Configure with a URL,
24
+ host/port, or rely on defaults and environment variables:
24
25
 
25
26
  ```ruby
26
27
  require "emb"
27
28
 
28
- Emb.setup(host: "localhost", port: 6379, pool: 5)
29
+ # URL (Redis URL format)
30
+ Emb.setup(url: "redis://localhost:6379")
31
+
32
+ # Or individual params
33
+ Emb.setup(host: "localhost", port: 6379)
34
+
35
+ # Or rely on defaults
36
+ Emb.setup
29
37
  ```
30
38
 
31
39
  `Emb.config` is an alias for `Emb.setup`.
32
40
 
41
+ ### Configuration sources (priority order)
42
+
43
+ 1. Explicit `url:` or `host:`/`port:` arguments
44
+ 2. `EMB_URL` environment variable
45
+ 3. Default: `redis://localhost:6379`
46
+
47
+ ### Connection pool
48
+
49
+ ```ruby
50
+ Emb.setup(url: "redis://localhost:6379", pool: 10)
51
+ ```
52
+
53
+ ### Authentication
54
+
55
+ If the server is configured with a password, include it in the URL:
56
+
57
+ ```ruby
58
+ # Password as URL userinfo
59
+ Emb.setup(url: "redis://:hunter2@localhost:6379")
60
+ ```
61
+
62
+ The `RedisClient` gem handles `AUTH` automatically on connect when a password
63
+ is embedded in the URL. This works correctly with connection pooling — every
64
+ connection in the pool authenticates on creation.
65
+
66
+ Manual authentication is also possible but not recommended for pooled connections:
67
+
68
+ ```ruby
69
+ Emb.send_command("AUTH", "hunter2") # only authenticates one connection
70
+ ```
71
+
72
+ ## Instance-based clients
73
+
74
+ Create independent clients to connect to multiple servers or use different configurations:
75
+
76
+ ```ruby
77
+ default = Emb.setup(url: "redis://localhost:6379")
78
+ other = Emb.new(url: "redis://:hunter2@10.0.0.1:6380")
79
+
80
+ default.ping # => "PONG"
81
+ other.ping # => "PONG"
82
+ ```
83
+
84
+ Each client has its own connection pool and model proxy registry:
85
+
86
+ ```ruby
87
+ c1 = Emb.new(url: "redis://server1:6379")
88
+ c2 = Emb.new(url: "redis://server2:6379")
89
+
90
+ c1[:minilm] != c2[:minilm] # separate proxies
91
+ ```
92
+
93
+ ### Global convenience API
94
+
95
+ When you don't need multiple clients, use the module-level methods:
96
+
97
+ ```ruby
98
+ Emb.setup
99
+
100
+ Emb[:minilm]["hello"] # proxy access
101
+ Emb.models # list models
102
+ Emb.info(:minilm) # model info
103
+ Emb.stats # server stats
104
+ Emb.help # command reference
105
+ Emb.ping # health check
106
+ ```
107
+
108
+ These all delegate to a lazily-initialized default client. No explicit `setup` call
109
+ is required for simple cases — the default client connects to `redis://localhost:6379`
110
+ automatically.
111
+
33
112
  ## Usage
34
113
 
35
114
  ### Single text
@@ -39,6 +118,13 @@ result = Emb[:minilm]["hello world"]
39
118
  # => [0.0123, -0.0456, 0.0789, ...] (384 floats)
40
119
  ```
41
120
 
121
+ With an instance-based client:
122
+
123
+ ```ruby
124
+ client = Emb.new(url: "redis://localhost:6379")
125
+ result = client[:minilm]["hello world"]
126
+ ```
127
+
42
128
  ### Multiple texts
43
129
 
44
130
  ```ruby
@@ -51,11 +137,21 @@ results = Emb[:minilm]["hello", "world"]
51
137
  Send texts to different models in one round trip:
52
138
 
53
139
  ```ruby
54
- Emb.multi do |m|
140
+ results = Emb.multi do |m|
141
+ m[:minilm]["hello"]
142
+ m[:bge]["world"]
143
+ end
144
+ # => [[0.0123, ...], [-0.0456, ...]]
145
+ # Results are unpacked from float32 binary — same format as single embeddings
146
+ ```
147
+
148
+ Works the same on instance clients:
149
+
150
+ ```ruby
151
+ client.multi do |m|
55
152
  m[:minilm]["hello"]
56
153
  m[:bge]["world"]
57
154
  end
58
- # => EMB.MULTI minilm "hello" bge "world"
59
155
  ```
60
156
 
61
157
  ### Commands
@@ -68,7 +164,23 @@ Emb.help # => command reference string
68
164
  Emb.ping # => "PONG"
69
165
  ```
70
166
 
71
- ## Testing end to end
167
+ ## Development
168
+
169
+ ### Console
170
+
171
+ Start an IRB session with the gem loaded:
172
+
173
+ ```bash
174
+ bundle exec rake console
175
+ ```
176
+
177
+ ### Lint
178
+
179
+ ```bash
180
+ bundle exec rubocop
181
+ ```
182
+
183
+ ### Tests
72
184
 
73
185
  Start the emb server, then run the test suite:
74
186
 
@@ -80,4 +192,5 @@ Start the emb server, then run the test suite:
80
192
  bundle exec rake
81
193
  ```
82
194
 
83
- Tests cover all commands: `EMB`, `EMB.MODELS`, `EMB.INFO`, `EMB.HELP`, `PING`, and `EMB.MULTI`.
195
+ Tests cover all commands: `EMB`, `EMB.MODELS`, `EMB.INFO`, `EMB.HELP`, `PING`,
196
+ and `EMB.MULTI`, plus instance-based clients, URL configuration, and connection pooling.
data/lib/emb/client.rb CHANGED
@@ -1,36 +1,78 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "connection_pool"
4
- require "redis_client"
3
+ require 'connection_pool'
4
+ require 'redis_client'
5
5
 
6
6
  module Emb
7
- @pool = nil
7
+ DEFAULTS = { host: 'localhost', port: 6379, pool: 5 }.freeze
8
8
 
9
- DEFAULTS = { host: "localhost", port: 6379, pool: 5 }.freeze
9
+ class Client
10
+ attr_reader :pool
11
+
12
+ def initialize(url: nil, host: nil, port: nil, pool: DEFAULTS[:pool])
13
+ url ||= ENV['EMB_URL']
14
+ host ||= DEFAULTS[:host]
15
+ port ||= DEFAULTS[:port]
10
16
 
11
- class << self
12
- def setup(host: DEFAULTS[:host], port: DEFAULTS[:port], pool: DEFAULTS[:pool])
13
17
  @pool = ConnectionPool.new(size: pool) do
14
- RedisClient.new(host: host, port: port, protocol: 2, reconnect_attempts: 3)
18
+ if url
19
+ RedisClient.new(url: url, protocol: 2, reconnect_attempts: 3)
20
+ else
21
+ RedisClient.new(host: host, port: port, protocol: 2, reconnect_attempts: 3)
22
+ end
15
23
  end
16
- end
17
24
 
18
- alias_method :config, :setup
25
+ @registry = {}
26
+ end
19
27
 
20
28
  def send_command(*args)
21
- pool.with { |r| r.call(*args) }
22
- end
29
+ return @pool.with { |r| r.call(*args) } unless Emb.debug?
30
+
31
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
32
+ result = @pool.with { |r| r.call(*args) }
33
+ elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
23
34
 
24
- private
35
+ $stdout.puts "[EMB] #{args.map(&:inspect).join(' ')} (#{format('%.2f', elapsed)}ms)"
25
36
 
26
- def pool
27
- @pool ||= default_pool
37
+ result
28
38
  end
29
39
 
30
- def default_pool
31
- ConnectionPool.new(size: DEFAULTS[:pool]) do
32
- RedisClient.new(host: DEFAULTS[:host], port: DEFAULTS[:port], protocol: 2, reconnect_attempts: 3)
40
+ def [](name)
41
+ @registry[name] ||= Proxy.new(self, name.to_sym)
42
+ end
43
+
44
+ def models
45
+ raw = send_command('EMB.MODELS')
46
+ return [] if raw.nil?
47
+
48
+ raw.map do |name, dim, status|
49
+ { name: name, dim: dim.to_i, status: status }
33
50
  end
34
51
  end
52
+
53
+ def info(name)
54
+ raw = send_command('EMB.INFO', name.to_s)
55
+ return {} if raw.nil?
56
+
57
+ raw
58
+ .each_slice(2)
59
+ .to_h { |k, v| [k.to_sym, v] }
60
+ end
61
+
62
+ def stats = send_command('EMB.STATS')
63
+
64
+ def help = send_command('EMB.HELP')
65
+
66
+ def ping = send_command('PING')
67
+
68
+ def reset_registry!
69
+ @registry = {}
70
+ end
71
+
72
+ def multi(&)
73
+ mp = MultiProxy.new(self)
74
+ yield mp
75
+ mp.run
76
+ end
35
77
  end
36
78
  end
data/lib/emb/multi.rb CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Emb
4
4
  class MultiProxy
5
- def initialize
5
+ def initialize(client)
6
+ @client = client
6
7
  @pairs = []
7
8
  end
8
9
 
@@ -13,7 +14,9 @@ module Emb
13
14
  def run
14
15
  args = @pairs.flat_map { |pair| [pair[:model].to_s, pair[:text]] }
15
16
 
16
- Emb.send_command("EMB.MULTI", *args)
17
+ @client
18
+ .send_command('EMB.MULTI', *args)
19
+ .map { |entry| entry.unpack('e*') }
17
20
  end
18
21
 
19
22
  class PairCollector
@@ -23,19 +26,8 @@ module Emb
23
26
  end
24
27
 
25
28
  def [](text)
26
- @pairs << {
27
- model: @model, text: text }
29
+ @pairs << { model: @model, text: text }
28
30
  end
29
31
  end
30
32
  end
31
-
32
- class << self
33
- def multi
34
- mp = MultiProxy.new
35
-
36
- yield mp
37
-
38
- mp.run
39
- end
40
- end
41
33
  end
data/lib/emb/proxy.rb CHANGED
@@ -1,28 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Emb
4
- @registry = {}
5
-
6
- class << self
7
- def [](name)
8
- @registry[name] ||= Proxy.new(name.to_sym)
9
- end
10
-
11
- def reset_registry!
12
- @registry.clear
13
- end
14
- end
15
-
16
4
  class Proxy
17
5
  attr_reader :name
18
6
 
19
- def initialize(name)
7
+ def initialize(client, name)
8
+ @client = client
20
9
  @name = name
21
10
  end
22
11
 
23
12
  def [](text, *texts)
24
- set = Array(Emb.send_command("EMB", @name.to_s, text, *texts))
25
- result = set.map { |entry| entry.unpack("e*") }
13
+ set = Array(@client.send_command('EMB', @name.to_s, text, *texts))
14
+ result = set.map { |entry| entry.unpack('e*') }
26
15
 
27
16
  return result.first if result.size == 1
28
17
 
data/lib/emb/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Emb
4
- VERSION = Gem.loaded_specs['emb'].version.to_s
4
+ VERSION = Gem.loaded_specs['emb']&.version&.to_s || '0.0.0'
5
5
  end
data/lib/emb.rb CHANGED
@@ -1,36 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "emb/version"
4
- require_relative "emb/client"
5
- require_relative "emb/proxy"
6
- require_relative "emb/multi"
3
+ require_relative 'emb/version'
4
+ require_relative 'emb/client'
5
+ require_relative 'emb/proxy'
6
+ require_relative 'emb/multi'
7
7
 
8
8
  module Emb
9
9
  class << self
10
- def models
11
- raw = send_command("EMB.MODELS")
10
+ def new(...) = Client.new(...)
12
11
 
13
- return [] if raw.nil?
14
-
15
- raw.map do |name, dim, status|
16
- { name: name, dim: dim.to_i, status: status }
17
- end
12
+ def setup(...)
13
+ @default_client = Client.new(...)
18
14
  end
19
15
 
20
- def info(name)
21
- raw = send_command("EMB.INFO", name.to_s)
22
-
23
- return {} if raw.nil?
24
-
25
- raw
26
- .each_slice(2)
27
- .to_h { |k, v| [k.to_sym, v] }
16
+ alias config setup
17
+
18
+ def [](name) = default_client[name]
19
+ def models = default_client.models
20
+ def info(name) = default_client.info(name)
21
+ def stats = default_client.stats
22
+ def help = default_client.help
23
+ def ping = default_client.ping
24
+ def multi(&) = default_client.multi(&)
25
+ def reset_registry! = default_client.reset_registry!
26
+ def debug? = @debug
27
+ def send_command(*) = default_client.send_command(*)
28
+
29
+ def debug!
30
+ @debug = true
28
31
  end
29
32
 
30
- def stats = send_command("EMB.STATS")
33
+ private
31
34
 
32
- def help = send_command("EMB.HELP")
33
-
34
- def ping = send_command("PING")
35
+ def default_client
36
+ @default_client ||= Client.new
37
+ end
35
38
  end
36
39
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: emb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - elcuervo
@@ -38,20 +38,6 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.24'
41
- - !ruby/object:Gem::Dependency
42
- name: rspec
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '3.13'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '3.13'
55
41
  description:
56
42
  email:
57
43
  - elcuervo@elcuervo.net