Class: Embedding::Gemini

Inherits:
Object
  • Object
show all
Defined in:
app/services/embedding/gemini.rb

Overview

Gemini Embedding 2 service for multimodal embeddings.

Natively embeds images, text, and interleaved image+text into a unified
vector space via the Gemini API embedContent endpoint.

== Architecture

Image Analysis: pHash → Gemini Embedding 2 (image + metadata)
Vision Desc: Gemini Flash (independent, on-demand)

== Embedding Dimensions

Gemini Embedding 2 supports Matryoshka Representation Learning:

  • 3072: Full quality (default from API)
  • 1536: Used here for HNSW compatibility (pgvector 2000-dim limit)
  • 768: For constrained environments

== Rate Limiting

Redis-based sliding window rate limiter.
Configurable via GEMINI_EMBED_REQUESTS_PER_MINUTE (default: 300).

Examples:

Embed an image with metadata text

Embedding::Gemini.embed_image("https://cdn.example.com/photo.jpg",
  text: "Towel warmer, brushed nickel finish")

Embed text for semantic search

Embedding::Gemini.embed_text("radiant floor heating installation")

Embed a search query

Embedding::Gemini.embed_query("bathroom heating")

Defined Under Namespace

Classes: ApiError, ConfigurationError, Error, PermanentError, RateLimitError

Constant Summary collapse

BASE_URL =

URL for base.

'https://generativelanguage.googleapis.com'
API_VERSION =

Api version.

'v1beta'
EMBED_MODEL =

Embed model — sourced from the canonical registry
(config/initializers/ai_model_constants.rb).

AiModelConstants.id(:unified_embedding)
MODEL_NAME =

Model name (same model; kept as a distinct public constant for callers).

EMBED_MODEL
DEFAULT_DIMENSIONS =

Default dimensions.

1536
DEFAULT_TEXT_DIMENSIONS =

Default text dimensions.

1536
DEFAULT_VISUAL_DIMENSIONS =

Default visual dimensions.

1536
MAX_BATCH_SIZE =

Maximum items per batchEmbedContents request. Google caps batch size;
larger inputs are chunked into multiple requests transparently.

100
TIMEOUT =

Timeout.

120
RATE_LIMIT_KEY =

Key used for rate limit.

'gemini_embed:rate_limit'
REQUESTS_PER_MINUTE =

Requests per minute.

ENV.fetch('GEMINI_EMBED_REQUESTS_PER_MINUTE', 300).to_i
RATE_LIMIT_WINDOW =

seconds

60
MAX_RETRIES =

Maximum retries.

5
BASE_RETRY_DELAY =

Base retry delay.

2
MIME_TYPES =

Recognised mime types.

{
  '.jpg' => 'image/jpeg',
  '.jpeg' => 'image/jpeg',
  '.png' => 'image/png'
}.freeze
RETRYABLE_EXCEPTIONS =

Retryable exceptions.

[
  RateLimitError,
  Faraday::TimeoutError,
  Faraday::ConnectionFailed
].freeze

Class Method Summary collapse

Class Method Details

.available?Boolean

Check if the API is configured

Returns:

  • (Boolean)

    true if API key is configured



190
191
192
193
194
# File 'app/services/embedding/gemini.rb', line 190

def available?
  api_key.present?
rescue ConfigurationError
  false
end

.embed_image(image_url, text: nil, dimensions: DEFAULT_VISUAL_DIMENSIONS) ⇒ Array<Float>

Embed an image, optionally with accompanying text metadata.
Sends the image as inlineData (base64) with optional text parts.

Parameters:

  • image_url (String)

    URL of the image to embed

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

    Optional metadata text to embed alongside the image

  • dimensions (Integer) (defaults to: DEFAULT_VISUAL_DIMENSIONS)

    Output vector dimensions

Returns:

  • (Array<Float>)

    Embedding vector



117
118
119
120
121
122
123
# File 'app/services/embedding/gemini.rb', line 117

def embed_image(image_url, text: nil, dimensions: DEFAULT_VISUAL_DIMENSIONS)
  parts = []
  parts << { text: text } if text.present?
  parts << build_image_part(image_url)

  embed_content(parts, dimensions: dimensions)
end

.embed_image_file(path, text: nil, dimensions: DEFAULT_VISUAL_DIMENSIONS) ⇒ Array<Float>

Embed a local image file

Parameters:

  • path (String)

    Path to the local image file

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

    Optional metadata text

  • dimensions (Integer) (defaults to: DEFAULT_VISUAL_DIMENSIONS)

    Output vector dimensions

Returns:

  • (Array<Float>)

    Embedding vector

Raises:



131
132
133
134
135
136
137
138
139
# File 'app/services/embedding/gemini.rb', line 131

def embed_image_file(path, text: nil, dimensions: DEFAULT_VISUAL_DIMENSIONS)
  raise Error, "Image file not found: #{path}" unless File.exist?(path)

  parts = []
  parts << { text: text } if text.present?
  parts << build_file_image_part(path)

  embed_content(parts, dimensions: dimensions)
end

.embed_image_url(url, dimensions: DEFAULT_VISUAL_DIMENSIONS) ⇒ Object

Alias for embed_image



202
203
204
# File 'app/services/embedding/gemini.rb', line 202

def embed_image_url(url, dimensions: DEFAULT_VISUAL_DIMENSIONS)
  embed_image(url, dimensions: dimensions)
end

.embed_query(text, dimensions: DEFAULT_TEXT_DIMENSIONS) ⇒ Array<Float>

Embed text for a search query. (gemini-embedding-2 ignores task_type, so
this is currently equivalent to embed_text; kept as the query-side seam
for a future task-prefix optimization.)

Parameters:

  • text (String)

    Query text

  • dimensions (Integer) (defaults to: DEFAULT_TEXT_DIMENSIONS)

    Output vector dimensions

Returns:

  • (Array<Float>)

    Embedding vector



148
149
150
# File 'app/services/embedding/gemini.rb', line 148

def embed_query(text, dimensions: DEFAULT_TEXT_DIMENSIONS)
  embed_content([{ text: text }], dimensions: dimensions)
end

.embed_text(text, dimensions: DEFAULT_TEXT_DIMENSIONS) ⇒ Array<Float>

Embed text for storage/indexing

Parameters:

  • text (String)

    Text content to embed

  • dimensions (Integer) (defaults to: DEFAULT_TEXT_DIMENSIONS)

    Output vector dimensions

Returns:

  • (Array<Float>)

    Embedding vector



157
158
159
# File 'app/services/embedding/gemini.rb', line 157

def embed_text(text, dimensions: DEFAULT_TEXT_DIMENSIONS)
  embed_content([{ text: text }], dimensions: dimensions)
end

.embed_texts(texts, dimensions: DEFAULT_TEXT_DIMENSIONS) ⇒ Array<Array<Float>>

Embed many texts in batched requests via the batchEmbedContents endpoint.
One HTTP round-trip (and one rate-limit slot) per MAX_BATCH_SIZE items
instead of one per text — the recommended path for backfills.

Parameters:

  • texts (Array<String>)

    Text contents to embed

  • dimensions (Integer) (defaults to: DEFAULT_TEXT_DIMENSIONS)

    Output vector dimensions

Returns:

  • (Array<Array<Float>>)

    One embedding vector per input, in order



168
169
170
171
172
173
174
175
176
# File 'app/services/embedding/gemini.rb', line 168

def embed_texts(texts, dimensions: DEFAULT_TEXT_DIMENSIONS)
  texts = Array(texts)
  return [] if texts.empty?

  texts.each_slice(MAX_BATCH_SIZE).flat_map do |slice|
    parts_list = slice.map { |text| [{ text: text.to_s }] }
    batch_embed_content(parts_list, dimensions: dimensions)
  end
end

.embed_visual_query(text, dimensions: DEFAULT_VISUAL_DIMENSIONS) ⇒ Array<Float>

Embed a text query for visual search (cross-modal text → image)

Parameters:

  • text (String)

    Text description to search for

  • dimensions (Integer) (defaults to: DEFAULT_VISUAL_DIMENSIONS)

    Output vector dimensions

Returns:

  • (Array<Float>)

    Embedding vector



183
184
185
# File 'app/services/embedding/gemini.rb', line 183

def embed_visual_query(text, dimensions: DEFAULT_VISUAL_DIMENSIONS)
  embed_query(text, dimensions: dimensions)
end

.model_nameString

Returns Model name for database storage.

Returns:

  • (String)

    Model name for database storage



197
198
199
# File 'app/services/embedding/gemini.rb', line 197

def model_name
  MODEL_NAME
end