Class: Vangrail::Embeddings

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

Overview

One OpenAI-compatible embeddings call.

The sibling of Chat, and it exists for one reason: the lexicon rails read words, and a synonym nobody listed is a miss. An embedding is the only cheap way to compare two sentences by what they mean rather than by what they spell, and every local proxy that serves chat can usually serve this too.

Local first, exactly as with Chat. Sending every retrieved document to a third party to be embedded is a data-flow decision an application should make deliberately, and on a loopback proxy it is not one at all.

Batched, because the cost that matters is round trips rather than tokens: a page has a few dozen clauses, and thirty small requests to score one page is what makes a rail too slow to leave on.

Constant Summary collapse

PATH =
'/embeddings'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model:, base_url: nil, api_key: nil, http: nil, open_timeout: HTTP::DEFAULT_OPEN_TIMEOUT, read_timeout: 20) ⇒ Embeddings

Returns a new instance of Embeddings.

Raises:

  • (ArgumentError)


26
27
28
29
30
31
32
33
# File 'lib/vangrail/embeddings.rb', line 26

def initialize(model:, base_url: nil, api_key: nil, http: nil,
               open_timeout: HTTP::DEFAULT_OPEN_TIMEOUT, read_timeout: 20)
  raise ArgumentError, 'an Embeddings needs a base_url or an http client' if http.nil? && base_url.to_s.strip.empty?

  @model = model
  @http = http || HTTP.new(base_url: base_url, api_key: api_key,
                           open_timeout: open_timeout, read_timeout: read_timeout)
end

Instance Attribute Details

#httpObject (readonly)

Returns the value of attribute http.



24
25
26
# File 'lib/vangrail/embeddings.rb', line 24

def http
  @http
end

#modelObject (readonly)

Returns the value of attribute model.



24
25
26
# File 'lib/vangrail/embeddings.rb', line 24

def model
  @model
end

Class Method Details

.cosine(left, right) ⇒ Object

Cosine similarity, which is what an embedding comparison is. Two vectors of different length is a provider that changed model mid-call, and it is an error rather than a zero.

Raises:



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/vangrail/embeddings.rb', line 55

def self.cosine(left, right)
  raise ProtocolError, 'vectors of different lengths' unless left.size == right.size

  dot = 0.0
  left_norm = 0.0
  right_norm = 0.0
  left.each_with_index do |value, i|
    other = right[i]
    dot += value * other
    left_norm += value * value
    right_norm += other * other
  end
  return 0.0 if left_norm.zero? || right_norm.zero?

  dot / (Math.sqrt(left_norm) * Math.sqrt(right_norm))
end

Instance Method Details

#embed(texts) ⇒ Object

Vectors for each input, in the order given.

The index is read rather than trusted to arrive in order: the API says each datum carries one, and a provider batching internally is entitled to answer out of order.

Raises:



40
41
42
43
44
45
46
47
48
49
50
# File 'lib/vangrail/embeddings.rb', line 40

def embed(texts)
  inputs = Array(texts).map(&:to_s)
  return [] if inputs.empty?

  body = http.post_json(PATH, { 'model' => model, 'input' => inputs })
  vectors = vectors_in(body)
  raise ProtocolError, "embeddings endpoint returned #{vectors.size} vectors for #{inputs.size} inputs" \
    unless vectors.size == inputs.size

  vectors
end