Class: Pikuri::VectorDb::Embedder

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/vector_db/embedder.rb

Overview

Thin wrapper over RubyLLM.embed. Holds an optional model kwarg, exposes a single #embed(texts) method that always takes an Array<String> and always returns an Array<Array<Float>> (parallel to the input).

Why a wrapper at all

RubyLLM.embed‘s shape isn’t fixed: a single-String input returns vectors as a flat Array<Float>; an array input returns Array<Array<Float>>. The Indexer (Phase 7) only ever wants the second shape, and would otherwise have to branch on input type at every call site. Pinning the contract here once means downstream code (Indexer, Search query path) stays simple.

The other reason is testability: stubbing RubyLLM.embed in every consumer’s spec via define_singleton_method is repetitive and fragile (the stub leaks across examples if after blocks aren’t disciplined). Routing through this class lets each consumer inject a fake #embed object via dependency injection — much cheaper than monkey-patching ruby_llm in dozens of tests.

Why no batching policy

RubyLLM.embed accepts the array verbatim and the provider decides what to do with it (OpenAI batches up to 2048 inputs; local llama.cpp processes one at a time but in a single HTTP call). Batching strategy is the Indexer‘s concern, not this wrapper’s — the Indexer slices a large corpus into batches and calls #embed once per batch.

Errors are loud

RubyLLM‘s exceptions (Faraday network failures, provider 4xx / 5xx, auth errors) propagate verbatim. This is internal pikuri code, not an LLM-facing tool, so no “Error: …” string return path.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model: nil, provider: nil, assume_model_exists: false) ⇒ Embedder

Parameters:

  • model (String, nil) (defaults to: nil)

    embedding model name, e.g. “nomic-ai/nomic-embed-text-v1.5-GGUF:Q8_0” for a local llama-server router, “text-embedding-3-small” for hosted OpenAI. nil defers to RubyLLM.config.default_embedding_model.

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

    explicit provider symbol (e.g. :openai). Required when assume_model_exists is true. nil lets RubyLLM infer from the model id.

  • assume_model_exists (Boolean) (defaults to: false)

    mirrors the chat-side Agent::ChatTransport#assume_model_exists flag. Set to true for local llama-server endpoints whose model identifiers are not in RubyLLM’s catalog; otherwise RubyLLM.embed raises ModelNotFoundError before any HTTP call is made.

Raises:

  • (ArgumentError)

    if assume_model_exists is true but provider is nil (RubyLLM requires the pair).



81
82
83
84
85
86
87
88
89
# File 'lib/pikuri/vector_db/embedder.rb', line 81

def initialize(model: nil, provider: nil, assume_model_exists: false)
  if assume_model_exists && provider.nil?
    raise ArgumentError, 'provider: must be specified when assume_model_exists is true'
  end

  @model               = model
  @provider            = provider
  @assume_model_exists = assume_model_exists
end

Instance Attribute Details

#assume_model_existsBoolean (readonly)

Returns whether to bypass RubyLLM’s local model registry. true when pointing at a local llama.cpp endpoint whose model identifiers (router aliases, HF repo paths like “nomic-ai/nomic-embed-text-v1.5-GGUF:Q8_0”) are not in RubyLLM’s built-in catalog.

Returns:

  • (Boolean)

    whether to bypass RubyLLM’s local model registry. true when pointing at a local llama.cpp endpoint whose model identifiers (router aliases, HF repo paths like “nomic-ai/nomic-embed-text-v1.5-GGUF:Q8_0”) are not in RubyLLM’s built-in catalog.



62
63
64
# File 'lib/pikuri/vector_db/embedder.rb', line 62

def assume_model_exists
  @assume_model_exists
end

#modelString? (readonly)

Returns the embedding model name, or nil to use whichever default RubyLLM.config.default_embedding_model resolves to at call time.

Returns:

  • (String, nil)

    the embedding model name, or nil to use whichever default RubyLLM.config.default_embedding_model resolves to at call time.



49
50
51
# File 'lib/pikuri/vector_db/embedder.rb', line 49

def model
  @model
end

#providerSymbol? (readonly)

Returns the explicit provider symbol (e.g. :openai), or nil to let RubyLLM infer from the model name. Required by RubyLLM when assume_model_exists is true.

Returns:

  • (Symbol, nil)

    the explicit provider symbol (e.g. :openai), or nil to let RubyLLM infer from the model name. Required by RubyLLM when assume_model_exists is true.



55
56
57
# File 'lib/pikuri/vector_db/embedder.rb', line 55

def provider
  @provider
end

Instance Method Details

#embed(texts) ⇒ Array<Array<Float>>

Embed texts as a single call to the underlying provider. Empty input short-circuits to [] — no HTTP round-trip.

Parameters:

  • texts (Array<String>)

    non-nil; every element must be a String.

Returns:

  • (Array<Array<Float>>)

    parallel to texts. The i-th vector is the embedding of texts[i].

Raises:

  • (ArgumentError)

    if texts isn’t an Array, or any element isn’t a String.



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/pikuri/vector_db/embedder.rb', line 100

def embed(texts)
  unless texts.is_a?(Array)
    raise ArgumentError, "expected Array<String>, got #{texts.class}"
  end
  return [] if texts.empty?
  unless texts.all? { |t| t.is_a?(String) }
    bad = texts.reject { |t| t.is_a?(String) }.first
    raise ArgumentError, "all elements must be String, got #{bad.class}"
  end

  kwargs = {}
  kwargs[:model]               = @model if @model
  kwargs[:provider]            = @provider if @provider
  kwargs[:assume_model_exists] = true if @assume_model_exists

  embedding = kwargs.empty? ? RubyLLM.embed(texts) : RubyLLM.embed(texts, **kwargs)
  embedding.vectors
end