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_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_call` so a single AS::N subscription tree can cover query, cache, agent, and embedding spend.
"parse.embeddings.embed"
Instance Method Summary collapse
-
#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, **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.
-
#inspect ⇒ Object
Default #inspect that allowlists safe instance vars.
-
#inspect_attrs ⇒ Hash
Attributes safe to surface in #inspect.
-
#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_query` and `:search_document` inputs.
-
#validate_response!(input_count, vectors) ⇒ Array<Array<Float>>
Validate a provider response before returning it from ‘embed_*`.
Instance Method Details
#dimensions ⇒ Integer
Returns fixed output width of this provider’s vectors.
79 80 81 |
# File 'lib/parse/embeddings/provider.rb', line 79 def dimensions raise NotImplementedError, "#{self.class}#dimensions must be implemented" end |
#embed_batch_size ⇒ Integer?
Returns provider-recommended batch size, or nil.
95 96 97 |
# File 'lib/parse/embeddings/provider.rb', line 95 def nil end |
#embed_image(sources, input_type: :search_document, **opts) ⇒ Array<Array<Float>>
Returns vectors aligned 1:1 with ‘sources`.
51 52 53 |
# File 'lib/parse/embeddings/provider.rb', line 51 def (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`.
38 39 40 |
# File 'lib/parse/embeddings/provider.rb', line 38 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.
65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/parse/embeddings/provider.rb', line 65 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 |
#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.
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_attrs ⇒ Hash
Returns 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 (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.
100 101 102 |
# File 'lib/parse/embeddings/provider.rb', line 100 def max_input_tokens nil end |
#modalities ⇒ Array<Symbol>
Returns subset of [:text, :image, :audio, :video].
90 91 92 |
# File 'lib/parse/embeddings/provider.rb', line 90 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`.
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`).
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.
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:
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 |