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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bcd9ac5bc1d098f8780a6523e648505dbf0259f4335b07e909898203b9723866
4
- data.tar.gz: 12297c4398121fe27b4f0ed148268197f44258dde3b9e5f7efcf305202a5e861
3
+ metadata.gz: c7fd1607f0f0124cf4a8e2fd10e6a643b738d1c65e555fe73becee2ededd360f
4
+ data.tar.gz: 354c66d0a2767171e7c4da774a91ee77287ac7b8c97dc02de290f0a1b1b95f19
5
5
  SHA512:
6
- metadata.gz: 75a6a774a12b00ed332f5f401451a53164f8ef3d8fe23428d70cfc224fa951bf0fd7a655f72178a98194e0578698e9f1de826afcd09b59b2dee2ec2dd0ea494f
7
- data.tar.gz: c46089ed7e3ebedc6aea3bd2455bac70cd3b1666b77519a970510a4d4790d2bb3857a5d295d8c35e88358234d39bee5fee8e276fa5b34d8511b47b47a10f87fb
6
+ metadata.gz: 3317a9db00d34aff5c7d06b73a7d0d2ec67bfd6f180212905d64b0a8d4e3a2426d3cb52564350c91b0741cfcc5f7512d127f021fee43a575073c8e1af9fef610
7
+ data.tar.gz: eaf726ae758dca2c28ececff924506470a760232396e20536c00f3051da1cdd681433805d60a90c1b8bc7cf5b066fa613e382508ba5e421024876650fd624477
@@ -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 ArgumentError, "query cannot be nil or empty" if query.nil? || query.to_s.strip.empty?
26
- raise ArgumentError, "documents cannot be nil or empty" if documents.nil? || documents.empty?
27
- raise ArgumentError, "top_k must be positive" if top_k && top_k <= 0
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 = Net::HTTP.new(uri.host, uri.port)
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 || response.code.to_i >= 500
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 && (e.message.include?("429") || e.message.include?("50"))
98
- sleep(2 ** (retries - 1))
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
- @config_mutex ||= Mutex.new
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
- @config_mutex ||= Mutex.new
81
- @config_mutex.synchronize do
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
- @config_mutex ||= Mutex.new
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RerankerRuby
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -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.1.1
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