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 +4 -4
- data/Gemfile +4 -0
- data/README.md +170 -19
- data/lib/emb/client.rb +59 -16
- data/lib/emb/multi.rb +7 -11
- data/lib/emb/proxy.rb +8 -14
- data/lib/emb/version.rb +1 -6
- data/lib/emb.rb +27 -18
- metadata +2 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da3d0c2517e4def78e7d887bd480cf3db14d2c83ba66eff0853ddf075fc1b785
|
|
4
|
+
data.tar.gz: 586bd53ce50bd1d1b427aaa82811676e193009155991dbe20f3496bf45816c7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 55aebcfa8f1ccdeca346309bd91d20e8527988593d1e76a4996d002e692ff2bfabf04119e65fa360c05ec04c3b8fa39f34166b72f1e3651eba071b97f024fc16
|
|
7
|
+
data.tar.gz: 2921b88b9cdb672f3ddcb54fba2ceb441772fc5791890487a30f17483abca57dd0a4e04f69534bdc0f33f5df1e20c0bb680394ef2a7822ddbe3164915914e82c
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -1,45 +1,196 @@
|
|
|
1
1
|
# emb — Ruby client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://rubygems.org/gems/emb)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
# → [{name: "minilm", dim: 384, status: "ready"}, ...]
|
|
93
|
+
### Global convenience API
|
|
17
94
|
|
|
18
|
-
|
|
19
|
-
# → {dim: 384, workers: 10, ...}
|
|
95
|
+
When you don't need multiple clients, use the module-level methods:
|
|
20
96
|
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
+
```bash
|
|
180
|
+
bundle exec rubocop
|
|
181
|
+
```
|
|
31
182
|
|
|
32
|
-
|
|
33
|
-
- Ruby 3.3+ with bundler
|
|
183
|
+
### Tests
|
|
34
184
|
|
|
35
|
-
|
|
185
|
+
Start the emb server, then run the test suite:
|
|
36
186
|
|
|
37
187
|
```bash
|
|
38
|
-
# From the repo root
|
|
188
|
+
# From the repo root:
|
|
39
189
|
./bin/emb -config test-two-models.yaml &
|
|
40
190
|
|
|
41
|
-
# From gems/emb
|
|
191
|
+
# From gems/emb/:
|
|
42
192
|
bundle exec rake
|
|
43
193
|
```
|
|
44
194
|
|
|
45
|
-
|
|
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
|
|
4
|
-
require
|
|
3
|
+
require 'connection_pool'
|
|
4
|
+
require 'redis_client'
|
|
5
5
|
|
|
6
6
|
module Emb
|
|
7
|
-
|
|
7
|
+
DEFAULTS = { host: 'localhost', port: 6379, pool: 5 }.freeze
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
35
|
+
$stdout.puts "[EMB] #{args.map(&:inspect).join(' ')} (#{format('%.2f', elapsed)}ms)"
|
|
36
|
+
|
|
37
|
+
result
|
|
27
38
|
end
|
|
28
39
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
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
|
|
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
|
-
|
|
12
|
+
def setup(...)
|
|
13
|
+
@default_client = Client.new(...)
|
|
22
14
|
end
|
|
23
15
|
|
|
24
|
-
|
|
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
|
-
|
|
33
|
+
private
|
|
27
34
|
|
|
28
|
-
def
|
|
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.
|
|
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-
|
|
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
|