Skip to content

Commit 1da1057

Browse files
cyppesixlive
andauthored
feat(gemini): add multimodal embedding support (#944)
Co-authored-by: TJ Miller <tj@themillers.co>
1 parent a9232d7 commit 1da1057

13 files changed

Lines changed: 594 additions & 72 deletions

docs/core-concepts/embeddings.md

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Embeddings
22

3-
Transform your content into powerful vector representations! Embeddings let you add semantic search, recommendation systems, and other advanced features to your applications - whether you're working with text or images.
3+
Transform your content into powerful vector representations! Embeddings let you add semantic search, recommendation systems, and other advanced features to your applications - whether you're working with text, images, audio, video, or documents.
44

55
## Quick Start
66

@@ -24,7 +24,7 @@ echo $response->usage->tokens;
2424

2525
## Generating multiple embeddings
2626

27-
You can generate multiple embeddings at once with all providers that support embeddings, other than Gemini:
27+
You can generate multiple embeddings at once with providers that support batch embeddings:
2828

2929
```php
3030
use Prism\Prism\Facades\Prism;
@@ -86,12 +86,12 @@ $response = Prism::embeddings()
8686
> [!NOTE]
8787
> Make sure your file exists and is readable. The generator will throw a helpful `PrismException` if there's any issue accessing the file.
8888
89-
## Image Embeddings
89+
## Multimodal Embeddings
9090

91-
Some providers support image embeddings, enabling powerful use cases like visual similarity search, cross-modal retrieval, and multimodal applications. Prism makes it easy to generate embeddings from images using the same fluent API.
91+
Some providers support multimodal embeddings, enabling powerful use cases like visual similarity search, cross-modal retrieval, and mixed media retrieval. Prism makes it easy to generate embeddings from images, audio, video, and documents using the same fluent API.
9292

9393
> [!IMPORTANT]
94-
> Image embeddings require a provider and model that supports image input (such as CLIP-based models or multimodal embedding models like BGE-VL). Check your provider's documentation to confirm image embedding support.
94+
> Multimodal embeddings require a provider and model that supports the input modalities you send. Check your provider's documentation to confirm support for images, audio, video, documents, and grouped content.
9595
9696
### Single Image
9797

@@ -131,25 +131,58 @@ foreach ($response->embeddings as $embedding) {
131131
}
132132
```
133133

134-
### Multimodal: Text + Image
134+
### Audio, Video, and Documents
135135

136-
Combine text and images for cross-modal search scenarios. This is particularly useful for applications like "find products similar to this image that match this description":
136+
```php
137+
use Prism\Prism\Facades\Prism;
138+
use Prism\Prism\ValueObjects\Media\Audio;
139+
use Prism\Prism\ValueObjects\Media\Document;
140+
use Prism\Prism\ValueObjects\Media\Video;
141+
142+
$response = Prism::embeddings()
143+
->using('provider', 'model')
144+
->fromAudio(Audio::fromLocalPath('/path/to/sample.mp3'))
145+
->fromVideo(Video::fromLocalPath('/path/to/sample.mp4'))
146+
->fromDocument(Document::fromLocalPath('/path/to/report.pdf'))
147+
->asEmbeddings();
148+
```
149+
150+
### Grouped Multimodal Content
151+
152+
Use `fromContent()` when you want a single embedding generated from multiple parts within the same content entry:
137153

138154
```php
139155
use Prism\Prism\Facades\Prism;
140156
use Prism\Prism\ValueObjects\Media\Image;
141157

142158
$response = Prism::embeddings()
143159
->using('provider', 'model')
144-
->fromInput('Find similar products in red')
145-
->fromImage(Image::fromBase64($productImage, 'image/png'))
160+
->fromContent([
161+
'Find similar products in red',
162+
Image::fromBase64($productImage, 'image/png'),
163+
])
164+
->asEmbeddings();
165+
```
166+
167+
Use `fromContents()` when you want multiple embeddings in a single request:
168+
169+
```php
170+
use Prism\Prism\Facades\Prism;
171+
use Prism\Prism\ValueObjects\Media\Image;
172+
173+
$response = Prism::embeddings()
174+
->using('provider', 'model')
175+
->fromContents([
176+
['The dog is cute'],
177+
[Image::fromLocalPath('/path/to/dog.png')],
178+
])
146179
->asEmbeddings();
147180
```
148181

149-
You can chain `fromImage()` and `fromInput()` in any order - Prism handles both gracefully.
182+
You can still chain `fromInput()` and `fromImage()` in any order. Each chained call creates a separate content entry.
150183

151184
> [!TIP]
152-
> The `Image` class supports multiple input sources: `fromLocalPath()`, `fromUrl()`, `fromBase64()`, `fromStoragePath()`, and `fromRawContent()`. See the [Images documentation](/input-modalities/images.html) for details.
185+
> Prism media value objects support multiple input sources. See the [Images documentation](/input-modalities/images.html) and related modality guides for details.
153186
154187
## Common Settings
155188

docs/providers/gemini.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,65 @@ $response = Prism::text()
395395

396396
You can customize your Gemini embeddings request with additional parameters using `->withProviderOptions()`.
397397

398+
### Gemini Embedding 2 Preview
399+
400+
Gemini's `gemini-embedding-2-preview` model supports text, images, audio, video, and PDF documents in a unified embedding space. Use Prism's content entry API to control whether you want a single aggregated embedding or multiple embeddings in one request.
401+
402+
### Single Modality Inputs
403+
404+
```php
405+
use Prism\Prism\Enums\Provider;
406+
use Prism\Prism\Facades\Prism;
407+
use Prism\Prism\ValueObjects\Media\Audio;
408+
use Prism\Prism\ValueObjects\Media\Document;
409+
use Prism\Prism\ValueObjects\Media\Image;
410+
use Prism\Prism\ValueObjects\Media\Video;
411+
412+
Prism::embeddings()
413+
->using(Provider::Gemini, 'gemini-embedding-2-preview')
414+
->fromImage(Image::fromLocalPath('/path/to/product.png'))
415+
->fromAudio(Audio::fromLocalPath('/path/to/example.mp3'))
416+
->fromVideo(Video::fromLocalPath('/path/to/example.mp4'))
417+
->fromDocument(Document::fromLocalPath('/path/to/report.pdf'))
418+
->asEmbeddings();
419+
```
420+
421+
### Aggregated Multimodal Embeddings
422+
423+
Use `fromContent()` to combine multiple parts into a single embedding:
424+
425+
```php
426+
use Prism\Prism\Enums\Provider;
427+
use Prism\Prism\Facades\Prism;
428+
use Prism\Prism\ValueObjects\Media\Image;
429+
430+
Prism::embeddings()
431+
->using(Provider::Gemini, 'gemini-embedding-2-preview')
432+
->fromContent([
433+
'An image of a dog',
434+
Image::fromLocalPath('/path/to/dog.png'),
435+
])
436+
->asEmbeddings();
437+
```
438+
439+
### Batch Embeddings
440+
441+
Use `fromContents()` to generate multiple embeddings in a single request:
442+
443+
```php
444+
use Prism\Prism\Enums\Provider;
445+
use Prism\Prism\Facades\Prism;
446+
use Prism\Prism\ValueObjects\Media\Image;
447+
448+
Prism::embeddings()
449+
->using(Provider::Gemini, 'gemini-embedding-2-preview')
450+
->fromContents([
451+
['The dog is cute'],
452+
[Image::fromLocalPath('/path/to/dog.png')],
453+
])
454+
->asEmbeddings();
455+
```
456+
398457
### Title
399458

400459
You can add a title to your embedding request. Only applicable when TaskType is `RETRIEVAL_DOCUMENT`

src/Embeddings/Content.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Prism\Prism\Embeddings;
6+
7+
use InvalidArgumentException;
8+
use Prism\Prism\ValueObjects\Media\Media;
9+
use Prism\Prism\ValueObjects\Media\Text;
10+
11+
readonly class Content
12+
{
13+
/** @var array<int, Media|Text> */
14+
private array $parts;
15+
16+
/**
17+
* @param array<int, Media|Text|string> $parts
18+
*/
19+
public function __construct(array $parts)
20+
{
21+
if ($parts === []) {
22+
throw new InvalidArgumentException('Embeddings content must contain at least one part.');
23+
}
24+
25+
$this->parts = array_map(
26+
static fn (Media|Text|string $part): Media|Text => is_string($part) ? new Text($part) : $part,
27+
$parts,
28+
);
29+
}
30+
31+
/**
32+
* @param array<int, Media|Text|string> $parts
33+
*/
34+
public static function make(array $parts): self
35+
{
36+
return new self($parts);
37+
}
38+
39+
/**
40+
* @return array<int, Media|Text>
41+
*/
42+
public function parts(): array
43+
{
44+
return $this->parts;
45+
}
46+
}

src/Embeddings/PendingRequest.php

Lines changed: 99 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,25 @@
99
use Prism\Prism\Concerns\ConfiguresProviders;
1010
use Prism\Prism\Concerns\HasProviderOptions;
1111
use Prism\Prism\Exceptions\PrismException;
12+
use Prism\Prism\ValueObjects\Media\Audio;
13+
use Prism\Prism\ValueObjects\Media\Document;
1214
use Prism\Prism\ValueObjects\Media\Image;
15+
use Prism\Prism\ValueObjects\Media\Media;
16+
use Prism\Prism\ValueObjects\Media\Text;
17+
use Prism\Prism\ValueObjects\Media\Video;
1318

1419
class PendingRequest
1520
{
1621
use ConfiguresClient;
1722
use ConfiguresProviders;
1823
use HasProviderOptions;
1924

20-
/** @var array<string> */
21-
protected array $inputs = [];
22-
23-
/** @var array<Image> */
24-
protected array $images = [];
25+
/** @var array<Content> */
26+
protected array $contents = [];
2527

2628
public function fromInput(string $input): self
2729
{
28-
$this->inputs[] = $input;
30+
$this->contents[] = Content::make([$input]);
2931

3032
return $this;
3133
}
@@ -35,7 +37,9 @@ public function fromInput(string $input): self
3537
*/
3638
public function fromArray(array $inputs): self
3739
{
38-
$this->inputs = array_merge($this->inputs, $inputs);
40+
foreach ($inputs as $input) {
41+
$this->fromInput($input);
42+
}
3943

4044
return $this;
4145
}
@@ -52,9 +56,7 @@ public function fromFile(string $path): self
5256
throw new PrismException(sprintf('%s contents could not be read', $path));
5357
}
5458

55-
$this->inputs[] = $contents;
56-
57-
return $this;
59+
return $this->fromInput($contents);
5860
}
5961

6062
/**
@@ -67,19 +69,98 @@ public function fromFile(string $path): self
6769
*/
6870
public function fromImage(Image $image): self
6971
{
70-
$this->images[] = $image;
72+
$this->contents[] = Content::make([$image]);
7173

7274
return $this;
7375
}
7476

7577
/**
76-
* Add multiple images for embedding generation.
77-
*
7878
* @param array<Image> $images
7979
*/
8080
public function fromImages(array $images): self
8181
{
82-
$this->images = array_merge($this->images, $images);
82+
foreach ($images as $image) {
83+
$this->fromImage($image);
84+
}
85+
86+
return $this;
87+
}
88+
89+
public function fromAudio(Audio $audio): self
90+
{
91+
$this->contents[] = Content::make([$audio]);
92+
93+
return $this;
94+
}
95+
96+
/**
97+
* @param array<Audio> $audios
98+
*/
99+
public function fromAudios(array $audios): self
100+
{
101+
foreach ($audios as $audio) {
102+
$this->fromAudio($audio);
103+
}
104+
105+
return $this;
106+
}
107+
108+
public function fromVideo(Video $video): self
109+
{
110+
$this->contents[] = Content::make([$video]);
111+
112+
return $this;
113+
}
114+
115+
/**
116+
* @param array<Video> $videos
117+
*/
118+
public function fromVideos(array $videos): self
119+
{
120+
foreach ($videos as $video) {
121+
$this->fromVideo($video);
122+
}
123+
124+
return $this;
125+
}
126+
127+
public function fromDocument(Document $document): self
128+
{
129+
$this->contents[] = Content::make([$document]);
130+
131+
return $this;
132+
}
133+
134+
/**
135+
* @param array<Document> $documents
136+
*/
137+
public function fromDocuments(array $documents): self
138+
{
139+
foreach ($documents as $document) {
140+
$this->fromDocument($document);
141+
}
142+
143+
return $this;
144+
}
145+
146+
/**
147+
* @param array<int, Media|Text|string> $parts
148+
*/
149+
public function fromContent(array $parts): self
150+
{
151+
$this->contents[] = Content::make($parts);
152+
153+
return $this;
154+
}
155+
156+
/**
157+
* @param array<int, array<int, Media|Text|string>> $contents
158+
*/
159+
public function fromContents(array $contents): self
160+
{
161+
foreach ($contents as $content) {
162+
$this->fromContent($content);
163+
}
83164

84165
return $this;
85166
}
@@ -94,8 +175,8 @@ public function generate(): Response
94175

95176
public function asEmbeddings(): Response
96177
{
97-
if ($this->inputs === [] && $this->images === []) {
98-
throw new PrismException('Embeddings input is required (text or images)');
178+
if ($this->contents === []) {
179+
throw new PrismException('Embeddings input is required (text, images, audio, video, documents, or content parts)');
99180
}
100181

101182
$request = $this->toRequest();
@@ -112,11 +193,10 @@ protected function toRequest(): Request
112193
return new Request(
113194
model: $this->model,
114195
providerKey: $this->providerKey(),
115-
inputs: $this->inputs,
116-
images: $this->images,
117196
clientOptions: $this->clientOptions,
118197
clientRetry: $this->clientRetry,
119-
providerOptions: $this->providerOptions
198+
providerOptions: $this->providerOptions,
199+
contents: $this->contents,
120200
);
121201
}
122202
}

0 commit comments

Comments
 (0)