Module: Parse::Core::EmbedManaged

Included in:
Object
Defined in:
lib/parse/model/core/embed_managed.rb

Overview

Class-level embed macro for :vector properties.

Lets a model declare which scalar fields feed into a managed embedding, and arranges for that embedding to be computed automatically on save whenever the source fields change.

Mechanics

The class macro:

  1. Validates that into: names a declared :vector property with provider: metadata.
  2. Auto-declares a <into>_digest :string sibling property (override with digest_field:).
  3. Registers a before_save callback that re-computes the embedding whenever the SHA-256 of the concatenated source fields differs from the stored digest. On first save the digest is blank and the embedding is always populated. On a save where no source field changed the digest matches and the callback is a no-op (zero provider calls).
  4. Prepends a guard module that raises ProtectedFieldError on direct body_embedding= assignment from user code. The guard lifts only inside the managed write path (the before_save callback itself).

Provider calls flow through Embeddings.provider — the provider is resolved by name at save time, so registering a provider can happen any time before the first save. Declaration never makes a network call.

Single vector per record

embed produces exactly one vector per record. All declared source fields are concatenated (joined with "\n\n", blank values skipped) and sent to the provider as a single string. This directive is one-vector-per-record by design: long source text whose concatenation exceeds the provider's per-call token budget is truncated provider-side, and the stored vector represents only the leading portion of the document.

Chunking happens at RETRIEVAL time, not embed time. As of v5.2 the SDK ships Retrieval.retrieve and the semantic_search agent tool, which fetch the top-k whole records and split each record's text field into overlapping chunks for presentation (every chunk inherits its parent record's single score). That is presentation chunking — it does not change how embeddings are computed here.

If you instead want each passage to have its OWN embedding (true embed-time chunking), keep one of these patterns:

  1. Pre-chunk client-side and write each chunk as its own Parse::Object record with its own embed declaration.
  2. Maintain a dedicated chunk subclass that belongs_to the parent record, with embed :content, into: :embedding on the chunk class itself.

Examples:

class Document < Parse::Object
  property :title, :string
  property :body,  :string
  property :body_embedding, :vector, dimensions: 1536, provider: :openai
  embed :title, :body, into: :body_embedding
end

doc = Document.new(title: "hello", body: "world")
doc.save   # provider :openai is called once; body_embedding populated

Defined Under Namespace

Modules: ClassMethods Classes: EmbedDirective, InvalidEmbedDeclaration, ProtectedFieldError

Constant Summary collapse

WRITER_KEY =

Internal: name of the Thread-local key under which the managed writer marks the symbol of the field it is currently writing. The guard module's setter checks this key to permit a single field write; the guard is otherwise closed.

:parse_embed_managed_writer

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.recompute_embedding!(record, directive) ⇒ Object



659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
# File 'lib/parse/model/core/embed_managed.rb', line 659

def self.recompute_embedding!(record, directive)
  input = build_source_input(record, directive)
  stored_digest = record.public_send(directive.digest_field)
  target_present = !record.public_send(directive.into).nil?

  if input.nil? || input.empty?
    if target_present || !stored_digest.nil?
      with_writer(directive.into) do
        record.public_send(:"#{directive.into}=", nil)
      end
      record.public_send(:"#{directive.digest_field}=", nil)
      clear_embed_meta(record, directive)
    end
    return
  end

  # Audit the binding BEFORE the digest early-return, not after.
  # A drifted `model:` is a configuration fault, not a property of
  # this particular record: if the check sat behind the digest
  # gate, an unchanged record would skip it and the mismatch would
  # stay invisible until some unrelated record happened to be
  # edited. Saving any record with this directive should surface
  # it. Skipped silently when the provider is not registered — the
  # resolution below still raises for records that actually embed,
  # so an unchanged save keeps its existing no-provider-needed
  # behavior.
  audit_binding!(record.class, directive)

  digest = digest_for(input)
  return if stored_digest == digest && target_present

  provider = Parse::Embeddings.provider(directive.provider_name)
  vectors = call_provider(provider, directive, input)
  unless vectors.is_a?(Array) && vectors.length == 1 && vectors.first.is_a?(Array)
    raise Parse::Embeddings::InvalidResponseError,
          "Parse::Core::EmbedManaged (#{record.class}##{directive.into}): provider " \
          "#{directive.provider_name.inspect} did not return a single vector " \
          "(got #{vectors.inspect[0, 80]})."
  end
  vector = Parse::Vector.new(vectors.first)
  expected_dims = record.class.vector_properties.dig(directive.into, :dimensions)
  if expected_dims && vector.dimensions != expected_dims
    raise Parse::Embeddings::InvalidResponseError,
          "Parse::Core::EmbedManaged (#{record.class}##{directive.into}): provider " \
          "#{directive.provider_name.inspect} returned #{vector.dimensions}-dim vector " \
          "but property declares dimensions: #{expected_dims}."
  end

  with_writer(directive.into) do
    record.public_send(:"#{directive.into}=", vector)
  end
  record.public_send(:"#{directive.digest_field}=", digest)
  stamp_embed_meta(record, directive, provider, vector)
end

Instance Method Details

#compute_embedding!(field: nil) ⇒ self

Recompute this record's managed embedding(s) in-place, NOW, without a save. Runs the same digest-tracked recompute the before_save callback runs: a provider call happens only when the source text/URL changed since the last embed (digest miss). Useful to populate the vector before inspecting it, or to force a refresh in a console.

Parameters:

  • field (Symbol, nil) (defaults to: nil)

    limit to one embed target; nil recomputes every declared directive.

Returns:

  • (self)

Raises:

  • (ArgumentError)

    when field: names no embed target, or the class declares no embed directives.



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/parse/model/core/embed_managed.rb', line 160

def compute_embedding!(field: nil)
  directives = self.class.embed_directives
  if directives.empty?
    raise ArgumentError, "#{self.class}#compute_embedding!: no `embed` directives declared."
  end
  selected =
    if field
      d = directives[field.to_sym]
      unless d
        raise ArgumentError,
              "#{self.class}#compute_embedding!: :#{field} is not an embed target " \
              "(have #{directives.keys.inspect})."
      end
      [d]
    else
      directives.values
    end
  selected.each { |directive| Parse::Core::EmbedManaged.recompute_embedding!(self, directive) }
  self
end