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

#dimensionsInteger

Returns fixed output width of this provider’s vectors.

Returns:

  • (Integer)

    fixed output width of this provider’s vectors.

Raises:

  • (NotImplementedError)


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

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

#embed_batch_sizeInteger?

Returns provider-recommended batch size, or nil.

Returns:

  • (Integer, nil)

    provider-recommended batch size, or nil.



95
96
97
# File 'lib/parse/embeddings/provider.rb', line 95

def embed_batch_size
  nil
end

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

Returns vectors aligned 1:1 with ‘sources`.

Parameters:

  • sources (Array<URI, IO, String>)

    image sources — URI for remote, IO for streamed bytes, String for base64. Concrete providers document which forms they accept.

  • input_type (Symbol) (defaults to: :search_document)

    ‘:search_query` or `:search_document`, parallel to #embed_text.

  • 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.



51
52
53
# File 'lib/parse/embeddings/provider.rb', line 51

def embed_image(sources, input_type: :search_document, **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.



38
39
40
# File 'lib/parse/embeddings/provider.rb', line 38

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`.



65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/parse/embeddings/provider.rb', line 65

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

#inspectObject

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.



175
176
177
178
# File 'lib/parse/embeddings/provider.rb', line 175

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

#inspect_attrsHash

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

Returns:

  • (Hash)

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



182
183
184
185
186
187
# File 'lib/parse/embeddings/provider.rb', line 182

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.



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/parse/embeddings/provider.rb', line 225

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_tokensInteger?

Returns chunker hint; max tokens per input.

Returns:

  • (Integer, nil)

    chunker hint; max tokens per input.



100
101
102
# File 'lib/parse/embeddings/provider.rb', line 100

def max_input_tokens
  nil
end

#modalitiesArray<Symbol>

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

Returns:

  • (Array<Symbol>)

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



90
91
92
# File 'lib/parse/embeddings/provider.rb', line 90

def modalities
  [:text]
end

#model_nameString

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)


85
86
87
# File 'lib/parse/embeddings/provider.rb', line 85

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`).



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

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.



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

def supports_input_type?
  false
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` is not an Array.

  • vectors.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:



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
165
166
167
168
169
# File 'lib/parse/embeddings/provider.rb', line 134

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