Class: Parse::Embeddings::Provider Abstract
- Inherits:
-
Object
- Object
- Parse::Embeddings::Provider
- Defined in:
- lib/parse/embeddings/provider.rb
Overview
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:
- #embed_image — v5.1 (multimodal); default
NotImplementedError - #embed_video — v5.6 (multimodal); default
NotImplementedError - #embed_batch_size — provider-recommended batch size hint
- #max_input_tokens — chunker hint
- #normalize? — whether output is unit-normalized
- #modalities — defaults to
[:text] - #supports_input_type? — defaults to
false
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_callso a single AS::N subscription tree can cover query, cache, agent, and embedding spend. "parse.embeddings.embed"
Instance Method Summary collapse
-
#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.
-
#dimensions ⇒ Integer
Fixed output width of this provider's vectors.
-
#embed_batch_size ⇒ Integer?
Provider-recommended batch size, or nil.
-
#embed_image(sources, input_type: :search_document, allow_insecure: false, **opts) ⇒ Array<Array<Float>>
Vectors aligned 1:1 with
sources. -
#embed_text(strings, input_type: :search_document) ⇒ Array<Array<Float>>
Vectors aligned 1:1 with
strings. -
#embed_text_batched(strings, input_type: :search_document) ⇒ Array<Array<Float>>
Batched text embedding.
-
#embed_video(sources, input_type: :search_document, allow_insecure: false, **opts) ⇒ Array<Array<Float>>
Embed video sources.
-
#inspect ⇒ Object
Default #inspect that allowlists safe instance vars.
- #inspect_attrs ⇒ Object
-
#instrument_embed(input_count, input_type, **extra) ⇒ Object
Subscribed payload contract.
-
#max_input_tokens ⇒ Integer?
Chunker hint; max tokens per input.
-
#modalities ⇒ Array<Symbol>
Subset of [:text, :image, :audio, :video].
-
#model_name ⇒ String
Stable model identifier (e.g. "text-embedding-3-small").
-
#normalize? ⇒ Boolean
Whether the provider returns unit-normalized vectors.
-
#supports_input_type? ⇒ Boolean
Whether the provider distinguishes between
:search_queryand:search_documentinputs. -
#supports_modality?(modality) ⇒ Boolean
Whether this provider accepts
modality. -
#validate_response!(input_count, vectors) ⇒ Array<Array<Float>>
Validate a provider response before returning it from
embed_*.
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.
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.
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.
160 161 162 |
# File 'lib/parse/embeddings/provider.rb', line 160 def nil end |
#embed_image(sources, input_type: :search_document, allow_insecure: false, **opts) ⇒ Array<Array<Float>>
Returns vectors aligned 1:1 with sources.
78 79 80 |
# File 'lib/parse/embeddings/provider.rb', line 78 def (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.
41 42 43 |
# File 'lib/parse/embeddings/provider.rb', line 41 def (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.
130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/parse/embeddings/provider.rb', line 130 def (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 = return (strings, input_type: input_type) if size.nil? || strings.length <= size strings.each_slice(size).flat_map do |slice| (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:
- a URL String, forwarded for provider-side fetch after Parse::Embeddings.validate_image_url! screens it (the SDK never downloads the video), and
- a MediaFile, streamed into the request body by StreamingBody without ever being fully resident.
Concrete overrides must accept allow_insecure: explicitly or
absorb it via **opts — see #embed_image for why.
107 108 109 |
# File 'lib/parse/embeddings/provider.rb', line 107 def (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.namewhen 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 (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.
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].
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.
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).
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.
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.
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.
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 |