Class: Parse::Embeddings::Jina
- Defined in:
- lib/parse/embeddings/jina.rb
Overview
Jina AI embeddings provider. Wraps POST /v1/embeddings.
Supported text-capable models:
- v5 text family —
jina-embeddings-v5-text-small,jina-embeddings-v5-text-nano. - v5 omni family (text mode) —
jina-embeddings-v5-omni-small,jina-embeddings-v5-omni-nano. These models are multimodal at the network boundary but accept plain-text inputs through this provider just like the text-only variants. - v4 —
jina-embeddings-v4(Matryoshka, multimodal; text inputs only here). - v3 —
jina-embeddings-v3(Matryoshka, 32–1024). - code embeddings —
jina-code-embeddings-0.5b,jina-code-embeddings-1.5b.
Rerankers (jina-reranker-*), VLM (jina-vlm),
image-only (jina-clip-v2), and ReaderLM-v2 are NOT exposed
through this provider — they don't fit the embed_text contract.
They'll surface through forthcoming embed_image / rerank /
generation hooks.
== Asymmetric input types
Jina uses a task request field with the following canonical
values (mapped from SDK-canonical input_type: Symbols):
:search_query→"retrieval.query":search_document→"retrieval.passage":classification→"classification":clustering→"separation"
The Provider#supports_input_type? flag returns true here so
cache-keying middleware can branch on it. Code-embedding models
accept the task field and use it to bias the head.
== Matryoshka dimensions
jina-embeddings-v3, jina-embeddings-v4, and the v5 family
support Matryoshka-style output-width truncation via the
dimensions request field. Pass dimensions: to the constructor
to set the desired width (must be ≤ the model's native width).
Defined Under Namespace
Classes: AuthenticationError, BadRequestError, RateLimitError, TransientError
Constant Summary collapse
- DEFAULT_BASE_URL =
"https://api.jina.ai/v1"- DEFAULT_MODEL =
"jina-embeddings-v3"- DEFAULT_TIMEOUT =
30- DEFAULT_OPEN_TIMEOUT =
5- DEFAULT_MAX_RETRIES =
3- DEFAULT_BATCH_SIZE =
100- MAX_RESPONSE_BYTES =
16 * 1024 * 1024
- MODEL_DEFAULT_DIMENSIONS =
Native vector widths. The Matryoshka-capable rows allow the caller to truncate via the
dimensions:kwarg. { "jina-embeddings-v5-omni-small" => 1024, "jina-embeddings-v5-omni-nano" => 512, "jina-embeddings-v5-text-small" => 1024, "jina-embeddings-v5-text-nano" => 512, "jina-embeddings-v4" => 2048, "jina-embeddings-v3" => 1024, "jina-code-embeddings-1.5b" => 1024, "jina-code-embeddings-0.5b" => 1024, }.freeze
- MODEL_MAX_INPUT_TOKENS =
{ "jina-embeddings-v5-omni-small" => 32_000, "jina-embeddings-v5-omni-nano" => 32_000, "jina-embeddings-v5-text-small" => 32_000, "jina-embeddings-v5-text-nano" => 32_000, "jina-embeddings-v4" => 32_000, "jina-embeddings-v3" => 8_192, "jina-code-embeddings-1.5b" => 32_000, "jina-code-embeddings-0.5b" => 32_000, }.freeze
- MATRYOSHKA_MODELS =
Models that accept the Matryoshka
dimensionsfield. Other rows must pass the native width or no override. %w[ jina-embeddings-v5-omni-small jina-embeddings-v5-omni-nano jina-embeddings-v5-text-small jina-embeddings-v5-text-nano jina-embeddings-v4 jina-embeddings-v3 ].freeze
- INPUT_TYPE_WIRE_VALUES =
Map SDK-canonical input_type symbols to Jina
taskstrings. { search_query: "retrieval.query", search_document: "retrieval.passage", classification: "classification", clustering: "separation", }.freeze
Constants inherited from Provider
Provider::AS_NOTIFICATION_NAME
Instance Method Summary collapse
- #backoff_seconds(attempt) ⇒ Object protected
- #build_connection ⇒ Object protected
- #dimensions ⇒ Object
- #embed_batch_size ⇒ Object
-
#embed_text(strings, input_type: :search_document) ⇒ Array<Array<Float>>
Vectors aligned 1:1 with
strings. - #extract_vectors!(payload, input_count) ⇒ Object protected
-
#initialize(api_key:, model: DEFAULT_MODEL, dimensions: nil, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, open_timeout: DEFAULT_OPEN_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES, embed_batch_size: DEFAULT_BATCH_SIZE, allow_faraday_proxy: false, allow_insecure_base_url: false, connection: nil) ⇒ Jina
constructor
A new instance of Jina.
- #inspect_attrs ⇒ Object
- #max_input_tokens ⇒ Object
- #model_name ⇒ Object
- #normalize? ⇒ Boolean
- #parse_json_body!(body) ⇒ Object protected
- #post_embeddings(body) ⇒ Object protected
- #retry_after_seconds(response) ⇒ Object protected
- #supports_input_type? ⇒ Boolean
Methods inherited from Provider
#embed_image, #embed_text_batched, #inspect, #instrument_embed, #modalities, #validate_response!
Constructor Details
#initialize(api_key:, model: DEFAULT_MODEL, dimensions: nil, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, open_timeout: DEFAULT_OPEN_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES, embed_batch_size: DEFAULT_BATCH_SIZE, allow_faraday_proxy: false, allow_insecure_base_url: false, connection: nil) ⇒ Jina
Returns a new instance of Jina.
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/parse/embeddings/jina.rb', line 132 def initialize( api_key:, model: DEFAULT_MODEL, dimensions: nil, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, open_timeout: DEFAULT_OPEN_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES, embed_batch_size: DEFAULT_BATCH_SIZE, allow_faraday_proxy: false, allow_insecure_base_url: false, connection: nil ) validate_api_key!(api_key) validate_model!(model) validate_dimensions!(model, dimensions) sanitized_base_url = validate_base_url!(base_url, allow_insecure_base_url) validate_positive_integer!(:timeout, timeout) validate_positive_integer!(:open_timeout, open_timeout) validate_non_negative_integer!(:max_retries, max_retries) validate_positive_integer!(:embed_batch_size, ) @api_key = api_key @model = model @dimensions = dimensions || MODEL_DEFAULT_DIMENSIONS.fetch(model) @base_url = sanitized_base_url @timeout = timeout @open_timeout = open_timeout @max_retries = max_retries @embed_batch_size = @allow_faraday_proxy = allow_faraday_proxy @connection = connection || build_connection end |
Instance Method Details
#backoff_seconds(attempt) ⇒ Object (protected)
354 355 356 |
# File 'lib/parse/embeddings/jina.rb', line 354 def backoff_seconds(attempt) [0.5 * (2**(attempt - 1)), 30.0].min end |
#build_connection ⇒ Object (protected)
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/parse/embeddings/jina.rb', line 248 def build_connection headers = { "Authorization" => "Bearer #{@api_key}", "Content-Type" => "application/json", "Accept" => "application/json", "User-Agent" => "parse-stack-embeddings/#{user_agent_version}", } faraday_opts = { url: @base_url, headers: headers } faraday_opts[:proxy] = nil unless @allow_faraday_proxy conn = Faraday.new(**faraday_opts) do |f| f..timeout = @timeout f..open_timeout = @open_timeout f.adapter Faraday.default_adapter end conn.proxy = nil if !@allow_faraday_proxy && conn.respond_to?(:proxy=) conn end |
#dimensions ⇒ Object
166 167 168 |
# File 'lib/parse/embeddings/jina.rb', line 166 def dimensions @dimensions end |
#embed_batch_size ⇒ Object
174 175 176 |
# File 'lib/parse/embeddings/jina.rb', line 174 def @embed_batch_size end |
#embed_text(strings, input_type: :search_document) ⇒ Array<Array<Float>>
Returns vectors aligned 1:1 with strings.
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/parse/embeddings/jina.rb', line 194 def (strings, input_type: :search_document) unless strings.is_a?(Array) raise ArgumentError, "Parse::Embeddings::Jina#embed_text expects Array<String> (got #{strings.class})." end return [] if strings.empty? strings.each_with_index do |s, i| unless s.is_a?(String) raise ArgumentError, "Parse::Embeddings::Jina#embed_text strings[#{i}] is not a String (#{s.class})." end if s.empty? raise ArgumentError, "Parse::Embeddings::Jina#embed_text strings[#{i}] is empty; Jina rejects empty inputs." end end unless INPUT_TYPE_WIRE_VALUES.key?(input_type) raise ArgumentError, "Parse::Embeddings::Jina#embed_text input_type #{input_type.inspect} not in " \ "#{INPUT_TYPE_WIRE_VALUES.keys.inspect}." end task_value = INPUT_TYPE_WIRE_VALUES[input_type] body = { model: @model, input: strings, task: task_value, embedding_type: "float", } # Forward `dimensions` only for Matryoshka-capable models whose # active width differs from native. Sending it to a non-Matryoshka # model would yield a 400 from Jina. if MATRYOSHKA_MODELS.include?(@model) && @dimensions != MODEL_DEFAULT_DIMENSIONS.fetch(@model) body[:dimensions] = @dimensions end (strings.length, input_type) do |emit_payload| payload = (body) if payload.is_a?(Hash) && payload["usage"].is_a?(Hash) tt = payload["usage"]["total_tokens"] emit_payload[:total_tokens] = tt if tt.is_a?(Integer) && tt >= 0 end vectors = extract_vectors!(payload, strings.length) validate_response!(strings.length, vectors) end end |
#extract_vectors!(payload, input_count) ⇒ Object (protected)
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 |
# File 'lib/parse/embeddings/jina.rb', line 321 def extract_vectors!(payload, input_count) unless payload.is_a?(Hash) raise InvalidResponseError, "Parse::Embeddings::Jina: response body is not a JSON object." end data = payload["data"] unless data.is_a?(Array) raise InvalidResponseError, "Parse::Embeddings::Jina: response.data is not an Array." end if data.length != input_count raise InvalidResponseError, "Parse::Embeddings::Jina: response.data.length #{data.length} != input count #{input_count}." end sorted = data.each_with_index.map do |entry, i| unless entry.is_a?(Hash) raise InvalidResponseError, "Parse::Embeddings::Jina: response.data[#{i}] is not a JSON object." end idx = entry["index"] unless idx.is_a?(Integer) && idx >= 0 && idx < input_count raise InvalidResponseError, "Parse::Embeddings::Jina: response.data[#{i}].index #{idx.inspect} out of range." end [idx, entry["embedding"]] end indices = sorted.map(&:first) if indices.uniq.length != indices.length raise InvalidResponseError, "Parse::Embeddings::Jina: duplicate index in response.data." end sorted.sort_by(&:first).map(&:last) end |
#inspect_attrs ⇒ Object
242 243 244 |
# File 'lib/parse/embeddings/jina.rb', line 242 def inspect_attrs super.merge(base: safe_base_host, retries: @max_retries) end |
#max_input_tokens ⇒ Object
178 179 180 |
# File 'lib/parse/embeddings/jina.rb', line 178 def max_input_tokens MODEL_MAX_INPUT_TOKENS[@model] end |
#model_name ⇒ Object
170 171 172 |
# File 'lib/parse/embeddings/jina.rb', line 170 def model_name @model end |
#normalize? ⇒ Boolean
182 183 184 185 |
# File 'lib/parse/embeddings/jina.rb', line 182 def normalize? # Jina's v3/v4/v5 embeddings are documented unit-normalized. true end |
#parse_json_body!(body) ⇒ Object (protected)
308 309 310 311 312 313 314 315 316 317 318 319 |
# File 'lib/parse/embeddings/jina.rb', line 308 def parse_json_body!(body) s = body.to_s if s.bytesize > MAX_RESPONSE_BYTES raise InvalidResponseError, "Parse::Embeddings::Jina: response body exceeds #{MAX_RESPONSE_BYTES} bytes " \ "(#{s.bytesize}). Refusing to parse." end JSON.parse(s, max_nesting: 32) rescue JSON::ParserError => e raise InvalidResponseError, "Parse::Embeddings::Jina: response is not valid JSON (#{e.})." end |
#post_embeddings(body) ⇒ Object (protected)
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 |
# File 'lib/parse/embeddings/jina.rb', line 268 def (body) attempts = 0 loop do attempts += 1 begin response = @connection.post("embeddings") do |req| req.body = body.to_json end rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e if attempts > @max_retries raise TransientError, "Parse::Embeddings::Jina: #{e.class} after #{attempts} attempt(s)." end sleep(backoff_seconds(attempts)) next end status = response.status return parse_json_body!(response.body) if status >= 200 && status < 300 if status == 401 raise AuthenticationError, "Parse::Embeddings::Jina: 401 Unauthorized — check api_key." end if status == 429 if attempts > @max_retries raise RateLimitError, "Parse::Embeddings::Jina: 429 rate limited after #{attempts} attempt(s)." end sleep(retry_after_seconds(response) || backoff_seconds(attempts)) next end if status >= 500 if attempts > @max_retries raise TransientError, "Parse::Embeddings::Jina: #{status} after #{attempts} attempt(s)." end sleep(backoff_seconds(attempts)) next end raise BadRequestError, "Parse::Embeddings::Jina: #{status} from POST /embeddings." end end |
#retry_after_seconds(response) ⇒ Object (protected)
358 359 360 361 362 363 |
# File 'lib/parse/embeddings/jina.rb', line 358 def retry_after_seconds(response) ra = response.respond_to?(:headers) ? response.headers["retry-after"] || response.headers["Retry-After"] : nil return nil unless ra v = ra.to_f v.positive? ? [v, 60.0].min : nil end |
#supports_input_type? ⇒ Boolean
187 188 189 |
# File 'lib/parse/embeddings/jina.rb', line 187 def supports_input_type? true end |