emb 0.1.0 → 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: b4a415f04bd5c26d788aed81f94684054a563b26884c74d8ef7fa7f6eec909c7
4
- data.tar.gz: 752cd3992f5cd48f31ec7f6d9387f04f36990532b7d9fbce88098c3a87e8eb9d
3
+ metadata.gz: da3d0c2517e4def78e7d887bd480cf3db14d2c83ba66eff0853ddf075fc1b785
4
+ data.tar.gz: 586bd53ce50bd1d1b427aaa82811676e193009155991dbe20f3496bf45816c7b
5
5
  SHA512:
6
- metadata.gz: 7ccade7ad0c808e237a382f4a3172c35ca651b21082135c2a1d4202bdb03acd18c47ff1bcc1a06767a07cf4050d5d3b7fdfd5eaa0789c0bcfd78f24cd31784f1
7
- data.tar.gz: d95c5f9c7064ecf7d61d1a81211909152ce8e465cf2c0e0f39d9d1585e7798210b68b6c4a76a450d641201ecafe834c5909d57c489204328a9404e4d014d8991
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
@@ -1,45 +1,196 @@
1
1
  # emb — Ruby client
2
2
 
3
- Thin Ruby wrapper for [emb](https://github.com/elcuervo/emb), a Redis-compatible embedding server.
3
+ [![emb gem](https://img.shields.io/gem/v/emb?logo=rubygems&color=red&label=emb)](https://rubygems.org/gems/emb)
4
4
 
5
- ## Usage
5
+ Thin Ruby wrapper for [emb](https://github.com/elcuervo/emb), a Redis-compatible embedding server. Auto-decodes float32 binary responses to Ruby arrays.
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem "emb"
13
+ ```
14
+
15
+ Or install globally:
16
+
17
+ ```bash
18
+ gem install emb
19
+ ```
20
+
21
+ ## Setup
22
+
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:
6
25
 
7
26
  ```ruby
8
27
  require "emb"
9
28
 
10
- 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
37
+ ```
38
+
39
+ `Emb.config` is an alias for `Emb.setup`.
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
+ ```
11
71
 
12
- Emb[:minilm]["hello world"]
13
- # → raw float32 bytes
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
+ ```
14
92
 
15
- Emb.models
16
- # → [{name: "minilm", dim: 384, status: "ready"}, ...]
93
+ ### Global convenience API
17
94
 
18
- Emb.info(:minilm)
19
- # → {dim: 384, workers: 10, ...}
95
+ When you don't need multiple clients, use the module-level methods:
20
96
 
21
- Emb.multi do |m|
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
+
112
+ ## Usage
113
+
114
+ ### Single text
115
+
116
+ ```ruby
117
+ result = Emb[:minilm]["hello world"]
118
+ # => [0.0123, -0.0456, 0.0789, ...] (384 floats)
119
+ ```
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
+
128
+ ### Multiple texts
129
+
130
+ ```ruby
131
+ results = Emb[:minilm]["hello", "world"]
132
+ # => [[0.0123, ...], [-0.0456, ...]]
133
+ ```
134
+
135
+ ### Multi-model queries
136
+
137
+ Send texts to different models in one round trip:
138
+
139
+ ```ruby
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|
22
152
  m[:minilm]["hello"]
23
153
  m[:bge]["world"]
24
154
  end
25
- # → EMB.MULTI minilm "hello" bge "world"
26
155
  ```
27
156
 
28
- ## Testing end to end
157
+ ### Commands
158
+
159
+ ```ruby
160
+ Emb.models # => [{name: "minilm", dim: 384, status: "ready"}, ...]
161
+ Emb.info(:minilm) # => {dim: 384, workers: 10, requests: 42, ...}
162
+ Emb.stats # => server statistics hash
163
+ Emb.help # => command reference string
164
+ Emb.ping # => "PONG"
165
+ ```
166
+
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
29
178
 
30
- ### Prerequisites
179
+ ```bash
180
+ bundle exec rubocop
181
+ ```
31
182
 
32
- - An `emb` binary built from the repo root (`just build` or `go build`)
33
- - Ruby 3.3+ with bundler
183
+ ### Tests
34
184
 
35
- ### Running tests
185
+ Start the emb server, then run the test suite:
36
186
 
37
187
  ```bash
38
- # From the repo root, start the emb server:
188
+ # From the repo root:
39
189
  ./bin/emb -config test-two-models.yaml &
40
190
 
41
- # From gems/emb/, run the test suite:
191
+ # From gems/emb/:
42
192
  bundle exec rake
43
193
  ```
44
194
 
45
- This starts the full test suite against a real running `emb` server. 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,35 +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
24
+
25
+ @registry = {}
16
26
  end
17
- alias_method :config, :setup
18
27
 
19
28
  def send_command(*args)
20
- pool.with { |r| r.call(*args) }
21
- end
29
+ return @pool.with { |r| r.call(*args) } unless Emb.debug?
22
30
 
23
- private
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
24
34
 
25
- def pool
26
- @pool ||= default_pool
35
+ $stdout.puts "[EMB] #{args.map(&:inspect).join(' ')} (#{format('%.2f', elapsed)}ms)"
36
+
37
+ result
27
38
  end
28
39
 
29
- def default_pool
30
- ConnectionPool.new(size: DEFAULTS[:pool]) do
31
- 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 }
32
50
  end
33
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
34
77
  end
35
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
 
@@ -12,7 +13,10 @@ module Emb
12
13
 
13
14
  def run
14
15
  args = @pairs.flat_map { |pair| [pair[:model].to_s, pair[:text]] }
15
- Emb.send_command("EMB.MULTI", *args)
16
+
17
+ @client
18
+ .send_command('EMB.MULTI', *args)
19
+ .map { |entry| entry.unpack('e*') }
16
20
  end
17
21
 
18
22
  class PairCollector
@@ -22,16 +26,8 @@ module Emb
22
26
  end
23
27
 
24
28
  def [](text)
25
- @pairs << {model: @model, text: text}
29
+ @pairs << { model: @model, text: text }
26
30
  end
27
31
  end
28
32
  end
29
-
30
- class << self
31
- def multi
32
- mp = MultiProxy.new
33
- yield mp
34
- mp.run
35
- end
36
- end
37
33
  end
data/lib/emb/proxy.rb CHANGED
@@ -1,27 +1,21 @@
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
- Emb.send_command("EMB", @name.to_s, text, *texts)
13
+ set = Array(@client.send_command('EMB', @name.to_s, text, *texts))
14
+ result = set.map { |entry| entry.unpack('e*') }
15
+
16
+ return result.first if result.size == 1
17
+
18
+ result
25
19
  end
26
20
 
27
21
  def inspect
data/lib/emb/version.rb CHANGED
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Emb
4
- VERSION =
5
- begin
6
- File.read(File.expand_path('../../../../VERSION', __dir__)).strip
7
- rescue Errno::ENOENT
8
- Gem::Specification.find_by_name("emb").version.to_s
9
- end
4
+ VERSION = Gem.loaded_specs['emb']&.version&.to_s || '0.0.0'
10
5
  end
data/lib/emb.rb CHANGED
@@ -1,30 +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")
12
- return [] if raw.nil?
13
- raw.map { |name, dim, status| {name: name, dim: dim.to_i, status: status} }
14
- end
15
-
16
- def info(name)
17
- raw = send_command("EMB.INFO", name.to_s)
18
-
19
- return {} if raw.nil?
10
+ def new(...) = Client.new(...)
20
11
 
21
- raw.each_slice(2).map { |k, v| [k.to_sym, v] }.to_h
12
+ def setup(...)
13
+ @default_client = Client.new(...)
22
14
  end
23
15
 
24
- def stats = send_command("EMB.STATS")
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
31
+ end
25
32
 
26
- def help = send_command("EMB.HELP")
33
+ private
27
34
 
28
- def ping = send_command("PING")
35
+ def default_client
36
+ @default_client ||= Client.new
37
+ end
29
38
  end
30
39
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: emb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - elcuervo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-01 00:00:00.000000000 Z
11
+ date: 2026-07-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: connection_pool
@@ -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