Class: Parse::Embeddings::Provider Abstract

Inherits:
Object
  • Object
show all
Defined in:
lib/parse/embeddings/provider.rb

Overview

This class is abstract.

Abstract base class for embedding providers. Concrete subclasses implement #embed_text (and, in v5.1+, optionally #embed_image).

Provider responsibilities:

  • Translate a batch of inputs into a batch of float vectors.
  • Return vectors in the same order as inputs.
  • Call #validate_response! before returning so the caller sees a typed InvalidResponseError for off-by-one batches and NaN / ±Inf poisoning at the provider boundary — not deep inside a later $vectorSearch call.

Subclasses MUST override:

  • #embed_text — (strings, input_type:) -> Array<Array<Float>>
  • #dimensions — Integer, the fixed output width
  • #model_name — stable identifier for cache keys / embedding_meta

Subclasses MAY override:

Direct Known Subclasses

Cohere, Fixture, Jina, LocalHTTP, OpenAI, Qwen, Voyage

Constant Summary collapse

AS_NOTIFICATION_NAME =

AS::N event name emitted from #instrument_embed. Subscribers match this exact string. Parallel namespace to parse.mongodb.aggregate / parse.cache.* / parse.agent.tool_call so a single AS::N subscription tree can cover query, cache, agent, and embedding spend.

"parse.embeddings.embed"

Instance Method Summary collapse

Instance Method Details

#cache_identity ⇒ Hash, ...

Identity of the deployment this provider talks to, for use in cache keys: the endpoint's scheme, host, non-default port, and path. Never includes credentials, userinfo, or a query string. Two providers of the same class and model pointed at different deployments (two self-hosted servers, or a provider behind a proxy) can return different vectors for the same input, so their cache entries must not be shared.

The default reads @base_url, which every built-in HTTP provider sets. A provider with no configurable endpoint returns nil, which leaves its cache key unchanged. Override to supply another identity.

Returns:

  • (Hash) —

    attributes safe to surface in #inspect. Override in subclasses to add fields; never add credentials.

  • (String, nil)


260
261
262
263
264
265
266
267
268
269
# File 'lib/parse/embeddings/provider.rb', line 260

def cache_identity
  url = instance_variable_defined?(:@base_url) ? @base_url : nil
  return nil if url.nil? || url.to_s.empty?
  uri = URI.parse(url.to_s)
  return nil if uri.host.nil? || uri.host.empty?
  port = uri.port && uri.port != uri.default_port ? ":#{uri.port}" : ""
  "#{uri.scheme}://#{uri.host.downcase}#{port}#{uri.path.to_s.chomp("/")}"
rescue URI::InvalidURIError
  nil
end

#dimensions ⇒ Integer

Returns fixed output width of this provider's vectors.

Returns:

  • (Integer) —

    fixed output width of this provider's vectors.

Raises:

  • (NotImplementedError)


144
145
146
# File 'lib/parse/embeddings/provider.rb', line 144

def dimensions
  raise NotImplementedError, "#{self.class}#dimensions must be implemented"
end

#embed_batch_size ⇒ Integer?

Returns provider-recommended batch size, or nil.

Returns:

  • (Integer, nil) —

    provider-recommended batch size, or nil.



160
161
162
# File 'lib/parse/embeddings/provider.rb', line 160

def embed_batch_size
  nil
end

#embed_image(sources, input_type: :search_document, allow_insecure: false, **opts) ⇒ Array<Array<Float>>

Returns vectors aligned 1:1 with sources.

Parameters:

  • sources (Array<String, Parse::Embeddings::ImageFetch::FetchedImage>) —

    image sources. Two supported forms (mixable within a batch):

    • String URL (v5.1 URL-forwarding path) — forwarded unchanged from the managed path: Core::EmbedManaged deliberately does NOT validate before calling the provider (validating there would double-resolve every URL). The concrete embed_image override is therefore responsible for calling Parse::Embeddings.validate_image_url! (passing allow_insecure: through) before egress — see the bundled Voyage/Cohere providers, which validate internally.
    • ImageFetch::FetchedImage (v5.5 bytes path) — bytes the SDK already downloaded via File.safe_open_url, magic-byte-verified, and EXIF-stripped. Concrete overrides forward fetched.to_data_uri in their wire body's base64 slot and skip URL validation (there is no provider-side fetch).
  • input_type (Symbol) (defaults to: :search_document) —

    :search_query or :search_document, parallel to #embed_text.

  • allow_insecure (Boolean) (defaults to: false) —

    contract kwarg — Core::EmbedManaged.recompute_embedding! unconditionally forwards this from the directive declaration. Concrete embed_image overrides MUST either accept allow_insecure: explicitly (passing it through to Parse::Embeddings.validate_image_url!) or absorb it via **opts. Dropping **opts from the override signature without accepting allow_insecure: will raise ArgumentError: unknown keyword: allow_insecure from the managed-embedding save path. Default false.

  • opts (Hash) —

    provider-specific options (e.g. dim: for Matryoshka-style truncation). Forward-compatible escape hatch.

Returns:

  • (Array<Array<Float>>) —

    vectors aligned 1:1 with sources.

Raises:

  • (NotImplementedError) —

    image embedding is a v5.1+ feature.



78
79
80
# File 'lib/parse/embeddings/provider.rb', line 78

def embed_image(sources, input_type: :search_document, allow_insecure: false, **opts)
  raise NotImplementedError, "#{self.class} does not support image embedding"
end

#embed_text(strings, input_type: :search_document) ⇒ Array<Array<Float>>

Returns vectors aligned 1:1 with strings.

Returns:

  • (Array<Array<Float>>) —

    vectors aligned 1:1 with strings.

Raises:

  • (NotImplementedError) —

    when the concrete subclass has not overridden the method.



41
42
43
# File 'lib/parse/embeddings/provider.rb', line 41

def embed_text(strings, input_type: :search_document)
  raise NotImplementedError, "#{self.class}#embed_text must be implemented"
end

#embed_text_batched(strings, input_type: :search_document) ⇒ Array<Array<Float>>

Batched text embedding. Splits strings into chunks of size #embed_batch_size (or returns a single-shot call when nil) and concatenates results. Concrete providers should override only when their HTTP shape needs more than naive slicing (e.g. async parallelism, per-request budgets). The default is sufficient for any provider whose embed_text accepts an array directly.

Parameters:

Returns:

  • (Array<Array<Float>>) —

    aligned 1:1 with strings.



130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/parse/embeddings/provider.rb', line 130

def embed_text_batched(strings, input_type: :search_document)
  unless strings.is_a?(Array)
    raise ArgumentError,
          "#{self.class}#embed_text_batched expects Array<String> (got #{strings.class})."
  end
  return [] if strings.empty?
  size = embed_batch_size
  return embed_text(strings, input_type: input_type) if size.nil? || strings.length <= size
  strings.each_slice(size).flat_map do |slice|
    embed_text(slice, input_type: input_type)
  end
end

#embed_video(sources, input_type: :search_document, allow_insecure: false, **opts) ⇒ Array<Array<Float>>

Embed video sources. Same contract and default posture as #embed_image: providers that do not offer video leave this raising, and callers discover support through #modalities rather than by rescuing.

Video payloads are large enough that holding one in memory is a real operational risk, so the two source forms concrete providers should accept are:

Concrete overrides must accept allow_insecure: explicitly or absorb it via **opts — see #embed_image for why.

Parameters:

  • sources (Array<String, Parse::Embeddings::MediaFile>)
  • input_type (Symbol) (defaults to: :search_document) —

    :search_query or :search_document.

  • allow_insecure (Boolean) (defaults to: false) —

    forwarded to the URL validator.

  • opts (Hash) —

    provider-specific options.

Returns:

  • (Array<Array<Float>>) —

    vectors aligned 1:1 with sources.

Raises:

  • (NotImplementedError) —

    unless the provider offers video.



107
108
109
# File 'lib/parse/embeddings/provider.rb', line 107

def embed_video(sources, input_type: :search_document, allow_insecure: false, **opts)
  raise NotImplementedError, "#{self.class} does not support video embedding"
end

#inspect ⇒ Object

Default #inspect that allowlists safe instance vars. Concrete providers holding @api_key, @bearer_token, etc. inherit a safe inspect automatically. Subclasses may extend the allowlist by overriding #inspect_attrs.



240
241
242
243
# File 'lib/parse/embeddings/provider.rb', line 240

def inspect
  attrs = inspect_attrs.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
  attrs.empty? ? "#<#{self.class}>" : "#<#{self.class} #{attrs}>"
end

#inspect_attrs ⇒ Object



271
272
273
274
275
276
# File 'lib/parse/embeddings/provider.rb', line 271

def inspect_attrs
  out = {}
  out[:model] = safe_call(:model_name)
  out[:dim] = safe_call(:dimensions)
  out.compact
end

#instrument_embed(input_count, input_type, **extra) ⇒ Object

Subscribed payload contract. Keys are present on every emit so subscribers can rely on them without key? guards (values may be nil when the provider does not surface usage telemetry — e.g. Fixture has no token cost).

  • :provider [String] — self.class.name
  • :model [String] — #model_name
  • :dimensions [Integer] — #dimensions
  • :input_count [Integer] — number of items in the batch
  • :input_type [Symbol] — :search_query / :search_document
  • :total_tokens [Integer, nil] — provider-reported token usage; nil when N/A
  • :cached [Boolean] — whether the batch was served from cache (always false in v5.0)
  • :error [String, nil] — exception.class.name when the block raised

Subscribers should NOT depend on additional keys appearing — the contract is stable. New keys may be added but existing semantics will not change without a deprecation cycle.

Synchronous-subscriber discipline: AS::N delivers events on the request thread. A slow subscriber blocks every embed call; an exception in a subscriber surfaces as a request failure. Keep subscribers cheap (counters, in-memory accumulators) or push to non-blocking sinks (StatsD-over-UDP, OTel exporters that batch).

The block is yielded the payload Hash so concrete providers can write :total_tokens / :cached from inside the network call (after parsing the provider's usage envelope). Any other field set on the yielded payload also reaches subscribers — but only via the documented keys above. Stick to the contract.



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/parse/embeddings/provider.rb', line 314

def instrument_embed(input_count, input_type, **extra)
  payload = {
    provider: self.class.name,
    model: safe_call(:model_name),
    dimensions: safe_call(:dimensions),
    input_count: input_count,
    input_type: input_type,
    total_tokens: nil,
    cached: false,
    error: nil,
  }.merge(extra)
  # Defensive: AS::N is in active_support, which the wider gem
  # already requires; if a downstream caller has loaded the
  # embeddings module without ActiveSupport (e.g. a sliced
  # require of just `parse/embeddings`), fall through.
  unless defined?(ActiveSupport::Notifications)
    return yield(payload)
  end
  result = nil
  ActiveSupport::Notifications.instrument(AS_NOTIFICATION_NAME, payload) do |emit_payload|
    begin
      result = yield(emit_payload)
    rescue StandardError => e
      emit_payload[:error] = e.class.name
      raise
    end
  end
  result
end

#max_input_tokens ⇒ Integer?

Returns chunker hint; max tokens per input.

Returns:

  • (Integer, nil) —

    chunker hint; max tokens per input.



165
166
167
# File 'lib/parse/embeddings/provider.rb', line 165

def max_input_tokens
  nil
end

#modalities ⇒ Array<Symbol>

Returns subset of [:text, :image, :audio, :video].

Returns:

  • (Array<Symbol>) —

    subset of [:text, :image, :audio, :video].



155
156
157
# File 'lib/parse/embeddings/provider.rb', line 155

def modalities
  [:text]
end

#model_name ⇒ String

Returns stable model identifier (e.g. "text-embedding-3-small"). Used as a cache-key component and persisted to embedding_meta.

Returns:

  • (String) —

    stable model identifier (e.g. "text-embedding-3-small"). Used as a cache-key component and persisted to embedding_meta.

Raises:

  • (NotImplementedError)


150
151
152
# File 'lib/parse/embeddings/provider.rb', line 150

def model_name
  raise NotImplementedError, "#{self.class}#model_name must be implemented"
end

#normalize? ⇒ Boolean

Returns whether the provider returns unit-normalized vectors. Affects similarity-metric selection (:cosine vs :dotProduct).

Returns:

  • (Boolean) —

    whether the provider returns unit-normalized vectors. Affects similarity-metric selection (:cosine vs :dotProduct).



172
173
174
# File 'lib/parse/embeddings/provider.rb', line 172

def normalize?
  false
end

#supports_input_type? ⇒ Boolean

Returns whether the provider distinguishes between :search_query and :search_document inputs. When false the input_type: kwarg is accepted (for forward compatibility and cache-key stability) but has no effect on the returned vector.

Returns:

  • (Boolean) —

    whether the provider distinguishes between :search_query and :search_document inputs. When false the input_type: kwarg is accepted (for forward compatibility and cache-key stability) but has no effect on the returned vector.



180
181
182
# File 'lib/parse/embeddings/provider.rb', line 180

def supports_input_type?
  false
end

#supports_modality?(modality) ⇒ Boolean

Returns whether this provider accepts modality. Prefer this over rescuing NotImplementedError — it answers the question without issuing a call.

Parameters:

  • modality (Symbol) —

    one of :text, :image, :video.

Returns:

  • (Boolean) —

    whether this provider accepts modality. Prefer this over rescuing NotImplementedError — it answers the question without issuing a call.



116
117
118
# File 'lib/parse/embeddings/provider.rb', line 116

def supports_modality?(modality)
  modalities.include?(modality.to_sym)
end

#validate_response!(input_count, vectors) ⇒ Array<Array<Float>>

Validate a provider response before returning it from embed_*.

Raises InvalidResponseError on any of:

  • vectors.length != input_count (off-by-one across batch — the most insidious provider bug, since vectors would be silently misaligned with their inputs).
  • vectors[i] is not an Array.
  • vectors[i].length != dimensions (variable-width response).
  • any element non-Numeric, NaN, or ±Inf.

Parameters:

  • input_count (Integer) —

    number of items in the input batch.

  • vectors (Array<Array<Float>>) —

    the provider's response.

Returns:

  • (Array<Array<Float>>) —

    vectors, unchanged on success.

Raises:



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
# File 'lib/parse/embeddings/provider.rb', line 199

def validate_response!(input_count, vectors)
  unless vectors.is_a?(Array)
    raise InvalidResponseError,
          "#{self.class}: expected Array of vectors, got #{vectors.class}."
  end
  if vectors.length != input_count
    raise InvalidResponseError,
          "#{self.class}: response length #{vectors.length} != input count #{input_count}."
  end
  dims = dimensions
  vectors.each_with_index do |vec, i|
    unless vec.is_a?(Array)
      raise InvalidResponseError,
            "#{self.class}: response[#{i}] is not an Array (#{vec.class})."
    end
    if vec.length != dims
      raise InvalidResponseError,
            "#{self.class}: response[#{i}] length #{vec.length} != declared dimensions #{dims}."
    end
    vec.each_with_index do |x, j|
      # Strictly Float or Integer. Numeric is too loose — Complex
      # has #finite? and would pass; Rational/BigDecimal serialize
      # to BSON in surprising ways. Vector elements are always
      # floats in practice.
      unless x.is_a?(Float) || x.is_a?(Integer)
        raise InvalidResponseError,
              "#{self.class}: response[#{i}][#{j}] is not Float or Integer (#{x.class})."
      end
      unless x.respond_to?(:finite?) && x.finite?
        raise InvalidResponseError,
              "#{self.class}: response[#{i}][#{j}] is not finite (#{x.inspect})."
      end
    end
  end
  vectors
end