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 +4 -4
- data/Gemfile +4 -0
- data/README.md +119 -6
- data/lib/emb/client.rb +59 -17
- data/lib/emb/multi.rb +6 -14
- data/lib/emb/proxy.rb +4 -15
- data/lib/emb/version.rb +1 -1
- data/lib/emb.rb +26 -23
- metadata +1 -15
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
|
@@ -20,16 +20,95 @@ gem install emb
|
|
|
20
20
|
|
|
21
21
|
## Setup
|
|
22
22
|
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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`,
|
|
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
|
|
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
|
|
16
|
-
end
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
@registry = {}
|
|
26
|
+
end
|
|
19
27
|
|
|
20
28
|
def send_command(*args)
|
|
21
|
-
pool.with { |r| r.call(*args) }
|
|
22
|
-
|
|
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
|
-
|
|
35
|
+
$stdout.puts "[EMB] #{args.map(&:inspect).join(' ')} (#{format('%.2f', elapsed)}ms)"
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
@pool ||= default_pool
|
|
37
|
+
result
|
|
28
38
|
end
|
|
29
39
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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(
|
|
25
|
-
result = set.map { |entry| entry.unpack(
|
|
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
data/lib/emb.rb
CHANGED
|
@@ -1,36 +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")
|
|
10
|
+
def new(...) = Client.new(...)
|
|
12
11
|
|
|
13
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
|
|
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
|
-
|
|
33
|
+
private
|
|
31
34
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
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
|