reranker-ruby 0.1.1 → 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/lib/reranker_ruby/base.rb +55 -15
- data/lib/reranker_ruby/cache/memory.rb +21 -0
- data/lib/reranker_ruby/configuration.rb +15 -15
- data/lib/reranker_ruby/version.rb +1 -1
- data/lib/reranker_ruby/voyage.rb +42 -0
- data/lib/reranker_ruby.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7fd1607f0f0124cf4a8e2fd10e6a643b738d1c65e555fe73becee2ededd360f
|
|
4
|
+
data.tar.gz: 354c66d0a2767171e7c4da774a91ee77287ac7b8c97dc02de290f0a1b1b95f19
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3317a9db00d34aff5c7d06b73a7d0d2ec67bfd6f180212905d64b0a8d4e3a2426d3cb52564350c91b0741cfcc5f7512d127f021fee43a575073c8e1af9fef610
|
|
7
|
+
data.tar.gz: eaf726ae758dca2c28ececff924506470a760232396e20536c00f3051da1cdd681433805d60a90c1b8bc7cf5b066fa613e382508ba5e421024876650fd624477
|
data/lib/reranker_ruby/base.rb
CHANGED
|
@@ -8,11 +8,18 @@ require "digest"
|
|
|
8
8
|
module RerankerRuby
|
|
9
9
|
class Error < StandardError; end
|
|
10
10
|
class APIError < Error; end
|
|
11
|
+
class TimeoutError < Error; end
|
|
12
|
+
class RateLimitError < APIError; end
|
|
13
|
+
class ValidationError < Error; end
|
|
11
14
|
|
|
12
15
|
class Base
|
|
13
|
-
def initialize(api_key: nil, cache: nil)
|
|
16
|
+
def initialize(api_key: nil, cache: nil, timeout: nil, max_retries: nil)
|
|
14
17
|
@api_key = api_key
|
|
15
18
|
@cache = cache
|
|
19
|
+
@timeout = timeout || 30
|
|
20
|
+
@max_retries = max_retries || 3
|
|
21
|
+
@connections = {}
|
|
22
|
+
@conn_mutex = Mutex.new
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
def rerank(query, documents, top_k: 10)
|
|
@@ -22,9 +29,9 @@ module RerankerRuby
|
|
|
22
29
|
private
|
|
23
30
|
|
|
24
31
|
def validate_inputs!(query, documents, top_k)
|
|
25
|
-
raise
|
|
26
|
-
raise
|
|
27
|
-
raise
|
|
32
|
+
raise ValidationError, "query cannot be nil or empty" if query.nil? || query.to_s.strip.empty?
|
|
33
|
+
raise ValidationError, "documents cannot be nil or empty" if documents.nil? || documents.empty?
|
|
34
|
+
raise ValidationError, "top_k must be positive" if top_k && top_k <= 0
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
def instrument(query:, document_count:, top_k:, &block)
|
|
@@ -63,18 +70,32 @@ module RerankerRuby
|
|
|
63
70
|
result
|
|
64
71
|
end
|
|
65
72
|
|
|
73
|
+
def get_connection(uri)
|
|
74
|
+
host_key = "#{uri.host}:#{uri.port}"
|
|
75
|
+
@conn_mutex.synchronize do
|
|
76
|
+
conn = @connections[host_key]
|
|
77
|
+
if conn && conn.started?
|
|
78
|
+
return conn
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
82
|
+
http.use_ssl = uri.scheme == "https"
|
|
83
|
+
http.open_timeout = @timeout
|
|
84
|
+
http.read_timeout = @timeout
|
|
85
|
+
http.write_timeout = @timeout
|
|
86
|
+
http.keep_alive_timeout = 30
|
|
87
|
+
http.start
|
|
88
|
+
@connections[host_key] = http
|
|
89
|
+
http
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
66
93
|
def post(url, body, headers: {})
|
|
67
94
|
uri = URI.parse(url)
|
|
68
95
|
retries = 0
|
|
69
|
-
max_retries = 3
|
|
70
96
|
|
|
71
97
|
begin
|
|
72
|
-
http =
|
|
73
|
-
http.use_ssl = uri.scheme == "https"
|
|
74
|
-
http.open_timeout = 30
|
|
75
|
-
http.read_timeout = 30
|
|
76
|
-
http.write_timeout = 30
|
|
77
|
-
|
|
98
|
+
http = get_connection(uri)
|
|
78
99
|
request = Net::HTTP::Post.new(uri.path)
|
|
79
100
|
request["Content-Type"] = "application/json"
|
|
80
101
|
request["User-Agent"] = "RerankerRuby/#{RerankerRuby::VERSION}"
|
|
@@ -83,7 +104,12 @@ module RerankerRuby
|
|
|
83
104
|
|
|
84
105
|
response = http.request(request)
|
|
85
106
|
|
|
86
|
-
if response.code.to_i == 429
|
|
107
|
+
if response.code.to_i == 429
|
|
108
|
+
retry_after = response["Retry-After"]&.to_f || (2 ** retries)
|
|
109
|
+
raise RateLimitError, "Rate limited (429). Retry after #{retry_after}s"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if response.code.to_i >= 500
|
|
87
113
|
raise APIError, "HTTP #{response.code}: #{response.body}"
|
|
88
114
|
end
|
|
89
115
|
|
|
@@ -92,13 +118,27 @@ module RerankerRuby
|
|
|
92
118
|
end
|
|
93
119
|
|
|
94
120
|
JSON.parse(response.body)
|
|
95
|
-
rescue APIError => e
|
|
121
|
+
rescue RateLimitError, APIError => e
|
|
96
122
|
retries += 1
|
|
97
|
-
if retries <= max_retries
|
|
98
|
-
|
|
123
|
+
if retries <= @max_retries
|
|
124
|
+
wait = if e.is_a?(RateLimitError) && e.message =~ /after ([\d.]+)s/
|
|
125
|
+
$1.to_f
|
|
126
|
+
else
|
|
127
|
+
2 ** (retries - 1)
|
|
128
|
+
end
|
|
129
|
+
sleep(wait)
|
|
99
130
|
retry
|
|
100
131
|
end
|
|
101
132
|
raise
|
|
133
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET => e
|
|
134
|
+
# Reset pooled connection on network errors
|
|
135
|
+
@conn_mutex.synchronize { @connections.delete("#{uri.host}:#{uri.port}") }
|
|
136
|
+
retries += 1
|
|
137
|
+
if retries <= @max_retries
|
|
138
|
+
sleep(2 ** (retries - 1))
|
|
139
|
+
retry
|
|
140
|
+
end
|
|
141
|
+
raise TimeoutError, "Request failed after #{@max_retries} retries: #{e.message}"
|
|
102
142
|
rescue JSON::ParserError => e
|
|
103
143
|
raise APIError, "Invalid JSON response: #{e.message}"
|
|
104
144
|
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module RerankerRuby
|
|
4
4
|
module Cache
|
|
5
5
|
class Memory
|
|
6
|
+
attr_reader :ttl
|
|
7
|
+
|
|
6
8
|
def initialize(ttl: 3600)
|
|
7
9
|
@ttl = ttl
|
|
8
10
|
@store = {}
|
|
@@ -29,6 +31,18 @@ module RerankerRuby
|
|
|
29
31
|
end
|
|
30
32
|
end
|
|
31
33
|
|
|
34
|
+
def invalidate(key)
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
@store.delete(key)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def invalidate_matching(pattern)
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
@store.delete_if { |k, _| k.match?(pattern) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
32
46
|
def clear
|
|
33
47
|
@mutex.synchronize do
|
|
34
48
|
@store.clear
|
|
@@ -40,6 +54,13 @@ module RerankerRuby
|
|
|
40
54
|
@store.size
|
|
41
55
|
end
|
|
42
56
|
end
|
|
57
|
+
|
|
58
|
+
def prune_expired
|
|
59
|
+
@mutex.synchronize do
|
|
60
|
+
now = Time.now.to_f
|
|
61
|
+
@store.delete_if { |_, entry| now - entry[:time] > @ttl }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
43
64
|
end
|
|
44
65
|
end
|
|
45
66
|
end
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module RerankerRuby
|
|
4
4
|
class Configuration
|
|
5
|
-
attr_accessor :default_provider, :cohere_api_key, :jina_api_key,
|
|
5
|
+
attr_accessor :default_provider, :cohere_api_key, :jina_api_key, :voyage_api_key,
|
|
6
6
|
:default_model, :default_top_k, :cache_store, :cache_ttl,
|
|
7
|
-
:logger, :onnx_model, :onnx_model_path, :onnx_cache_dir
|
|
7
|
+
:logger, :onnx_model, :onnx_model_path, :onnx_cache_dir,
|
|
8
|
+
:timeout, :max_retries
|
|
8
9
|
|
|
9
10
|
def initialize
|
|
10
11
|
@default_provider = :cohere
|
|
11
12
|
@cohere_api_key = nil
|
|
12
13
|
@jina_api_key = nil
|
|
14
|
+
@voyage_api_key = nil
|
|
13
15
|
@default_model = nil
|
|
14
16
|
@default_top_k = 10
|
|
15
17
|
@cache_store = nil
|
|
@@ -18,6 +20,8 @@ module RerankerRuby
|
|
|
18
20
|
@onnx_model = nil
|
|
19
21
|
@onnx_model_path = nil
|
|
20
22
|
@onnx_cache_dir = nil
|
|
23
|
+
@timeout = 30
|
|
24
|
+
@max_retries = 3
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
# Build a reranker instance from configuration
|
|
@@ -35,6 +39,11 @@ module RerankerRuby
|
|
|
35
39
|
opts = { api_key: jina_api_key, cache: cache }
|
|
36
40
|
opts[:model] = default_model if default_model
|
|
37
41
|
Jina.new(**opts)
|
|
42
|
+
when :voyage
|
|
43
|
+
raise Error, "voyage_api_key is required" unless voyage_api_key
|
|
44
|
+
opts = { api_key: voyage_api_key, cache: cache, timeout: timeout, max_retries: max_retries }
|
|
45
|
+
opts[:model] = default_model if default_model
|
|
46
|
+
Voyage.new(**opts)
|
|
38
47
|
when :onnx
|
|
39
48
|
opts = { cache: cache }
|
|
40
49
|
opts[:model] = onnx_model if onnx_model
|
|
@@ -66,10 +75,7 @@ module RerankerRuby
|
|
|
66
75
|
|
|
67
76
|
class << self
|
|
68
77
|
def configuration
|
|
69
|
-
@
|
|
70
|
-
@config_mutex.synchronize do
|
|
71
|
-
@configuration ||= Configuration.new
|
|
72
|
-
end
|
|
78
|
+
@configuration ||= Configuration.new
|
|
73
79
|
end
|
|
74
80
|
|
|
75
81
|
def configure
|
|
@@ -77,19 +83,13 @@ module RerankerRuby
|
|
|
77
83
|
end
|
|
78
84
|
|
|
79
85
|
def reset_configuration!
|
|
80
|
-
@
|
|
81
|
-
@
|
|
82
|
-
@configuration = Configuration.new
|
|
83
|
-
@reranker = nil
|
|
84
|
-
end
|
|
86
|
+
@configuration = Configuration.new
|
|
87
|
+
@reranker = nil
|
|
85
88
|
end
|
|
86
89
|
|
|
87
90
|
# Global reranker instance built from configuration
|
|
88
91
|
def reranker
|
|
89
|
-
@
|
|
90
|
-
@config_mutex.synchronize do
|
|
91
|
-
@reranker ||= configuration.build_reranker
|
|
92
|
-
end
|
|
92
|
+
@reranker ||= configuration.build_reranker
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
# Convenience method for quick reranking
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RerankerRuby
|
|
4
|
+
class Voyage < Base
|
|
5
|
+
API_URL = "https://api.voyageai.com/v1/rerank"
|
|
6
|
+
DEFAULT_MODEL = "rerank-2"
|
|
7
|
+
|
|
8
|
+
def initialize(api_key:, model: DEFAULT_MODEL, **options)
|
|
9
|
+
super(**options)
|
|
10
|
+
@api_key = api_key
|
|
11
|
+
@model = model
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def rerank(query, documents, top_k: 10, model: nil)
|
|
15
|
+
validate_inputs!(query, documents, top_k)
|
|
16
|
+
instrument(query: query, document_count: documents.length, top_k: top_k) do
|
|
17
|
+
with_cache(query, documents, top_k: top_k) do
|
|
18
|
+
texts = extract_texts(documents)
|
|
19
|
+
|
|
20
|
+
response = post(API_URL, {
|
|
21
|
+
model: model || @model,
|
|
22
|
+
query: query,
|
|
23
|
+
documents: texts,
|
|
24
|
+
top_k: top_k
|
|
25
|
+
}, headers: {
|
|
26
|
+
"Authorization" => "Bearer #{@api_key}"
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
response["data"].map do |r|
|
|
30
|
+
idx = r["index"]
|
|
31
|
+
Result.new(
|
|
32
|
+
text: texts[idx],
|
|
33
|
+
score: r["relevance_score"],
|
|
34
|
+
index: idx,
|
|
35
|
+
metadata: extract_metadata(documents[idx])
|
|
36
|
+
)
|
|
37
|
+
end.sort
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/reranker_ruby.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "reranker_ruby/result"
|
|
|
5
5
|
require_relative "reranker_ruby/base"
|
|
6
6
|
require_relative "reranker_ruby/cohere"
|
|
7
7
|
require_relative "reranker_ruby/jina"
|
|
8
|
+
require_relative "reranker_ruby/voyage"
|
|
8
9
|
require_relative "reranker_ruby/rrf"
|
|
9
10
|
require_relative "reranker_ruby/model_downloader"
|
|
10
11
|
require_relative "reranker_ruby/score_normalizer"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: reranker-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Johannes Dwi Cahyo
|
|
@@ -94,6 +94,7 @@ files:
|
|
|
94
94
|
- lib/reranker_ruby/rrf.rb
|
|
95
95
|
- lib/reranker_ruby/score_normalizer.rb
|
|
96
96
|
- lib/reranker_ruby/version.rb
|
|
97
|
+
- lib/reranker_ruby/voyage.rb
|
|
97
98
|
homepage: https://github.com/johannesdwicahyo/reranker-ruby
|
|
98
99
|
licenses:
|
|
99
100
|
- MIT
|