Skip to content

Commit f53c8a6

Browse files
committed
[Store] Split all stores as dedicated packages
1 parent 714217e commit f53c8a6

File tree

11 files changed

+420
-4
lines changed

11 files changed

+420
-4
lines changed

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.git* export-ignore

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Please do not submit any Pull Requests here. They will be closed.
2+
---
3+
4+
Please submit your PR here instead:
5+
https://github.com/symfony/ai
6+
7+
This repository is what we call a "subtree split": a read-only subset of that main repository.
8+
We're looking forward to your PR there!

.github/close-pull-request.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Close Pull Request
2+
3+
on:
4+
pull_request_target:
5+
types: [opened]
6+
7+
jobs:
8+
run:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: superbrothers/close-pull-request@v3
12+
with:
13+
comment: |
14+
Thanks for your Pull Request! We love contributions.
15+
16+
However, you should instead open your PR on the main repository:
17+
https://github.com/symfony/ai
18+
19+
This repository is what we call a "subtree split": a read-only subset of that main repository.
20+
We're looking forward to your PR there!

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
4+
.phpunit.result.cache

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
0.1
5+
---
6+
7+
* Add the bridge

LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2025-present Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Cache Store
2+
===========
3+
4+
Provides Cache vector store integration for Symfony AI Store.
5+
6+
Resources
7+
---------
8+
9+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
10+
* [Report issues](https://github.com/symfony/ai/issues) and
11+
[send Pull Requests](https://github.com/symfony/ai/pulls)
12+
in the [main Symfony AI repository](https://github.com/symfony/ai)

Store.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
use Symfony\AI\Store\Document\Metadata;
1818
use Symfony\AI\Store\Document\VectorDocument;
1919
use Symfony\AI\Store\Exception\InvalidArgumentException;
20-
use Symfony\AI\Store\Exception\RuntimeException;
2120
use Symfony\AI\Store\ManagedStoreInterface;
2221
use Symfony\AI\Store\StoreInterface;
2322
use Symfony\Component\Uid\Uuid;
@@ -33,9 +32,6 @@ public function __construct(
3332
private readonly DistanceCalculator $distanceCalculator = new DistanceCalculator(),
3433
private readonly string $cacheKey = '_vectors',
3534
) {
36-
if (!interface_exists(CacheInterface::class)) {
37-
throw new RuntimeException('For using the Cache store as vector store, a symfony/contracts cache implementation is required. Try running "composer require symfony/cache" or another symfony/contracts compatible cache.');
38-
}
3935
}
4036

4137
public function setup(array $options = []): void

Tests/StoreTest.php

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Store\Bridge\Cache\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\AI\Platform\Vector\Vector;
16+
use Symfony\AI\Store\Bridge\Cache\Store;
17+
use Symfony\AI\Store\Distance\DistanceCalculator;
18+
use Symfony\AI\Store\Distance\DistanceStrategy;
19+
use Symfony\AI\Store\Document\Metadata;
20+
use Symfony\AI\Store\Document\VectorDocument;
21+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
22+
use Symfony\Component\Uid\Uuid;
23+
24+
final class StoreTest extends TestCase
25+
{
26+
public function testStoreCannotSetup()
27+
{
28+
$store = new Store(new ArrayAdapter());
29+
$store->setup();
30+
31+
$result = iterator_to_array($store->query(new Vector([0.0, 0.1, 0.6])));
32+
$this->assertCount(0, $result);
33+
}
34+
35+
public function testStoreCanDrop()
36+
{
37+
$store = new Store(new ArrayAdapter());
38+
$store->add(
39+
new VectorDocument(Uuid::v4(), new Vector([0.1, 0.1, 0.5])),
40+
new VectorDocument(Uuid::v4(), new Vector([0.7, -0.3, 0.0])),
41+
new VectorDocument(Uuid::v4(), new Vector([0.3, 0.7, 0.1])),
42+
);
43+
44+
$result = iterator_to_array($store->query(new Vector([0.0, 0.1, 0.6])));
45+
$this->assertCount(3, $result);
46+
47+
$store->drop();
48+
49+
$result = iterator_to_array($store->query(new Vector([0.0, 0.1, 0.6])));
50+
$this->assertCount(0, $result);
51+
}
52+
53+
public function testStoreCanSearchUsingCosineDistance()
54+
{
55+
$store = new Store(new ArrayAdapter());
56+
$store->add(
57+
new VectorDocument(Uuid::v4(), new Vector([0.1, 0.1, 0.5])),
58+
new VectorDocument(Uuid::v4(), new Vector([0.7, -0.3, 0.0])),
59+
new VectorDocument(Uuid::v4(), new Vector([0.3, 0.7, 0.1])),
60+
);
61+
62+
$result = iterator_to_array($store->query(new Vector([0.0, 0.1, 0.6])));
63+
$this->assertCount(3, $result);
64+
$this->assertSame([0.1, 0.1, 0.5], $result[0]->vector->getData());
65+
66+
$store->add(
67+
new VectorDocument(Uuid::v4(), new Vector([0.1, 0.1, 0.5])),
68+
new VectorDocument(Uuid::v4(), new Vector([0.7, -0.3, 0.0])),
69+
new VectorDocument(Uuid::v4(), new Vector([0.3, 0.7, 0.1])),
70+
);
71+
72+
$result = iterator_to_array($store->query(new Vector([0.0, 0.1, 0.6])));
73+
$this->assertCount(6, $result);
74+
$this->assertSame([0.1, 0.1, 0.5], $result[0]->vector->getData());
75+
}
76+
77+
public function testStoreCanSearchUsingCosineDistanceAndReturnCorrectOrder()
78+
{
79+
$store = new Store(new ArrayAdapter());
80+
$store->add(
81+
new VectorDocument(Uuid::v4(), new Vector([0.1, 0.1, 0.5])),
82+
new VectorDocument(Uuid::v4(), new Vector([0.7, -0.3, 0.0])),
83+
new VectorDocument(Uuid::v4(), new Vector([0.3, 0.7, 0.1])),
84+
new VectorDocument(Uuid::v4(), new Vector([0.3, 0.1, 0.6])),
85+
new VectorDocument(Uuid::v4(), new Vector([0.0, 0.1, 0.6])),
86+
);
87+
88+
$result = iterator_to_array($store->query(new Vector([0.0, 0.1, 0.6])));
89+
$this->assertCount(5, $result);
90+
$this->assertSame([0.0, 0.1, 0.6], $result[0]->vector->getData());
91+
$this->assertSame([0.1, 0.1, 0.5], $result[1]->vector->getData());
92+
$this->assertSame([0.3, 0.1, 0.6], $result[2]->vector->getData());
93+
$this->assertSame([0.3, 0.7, 0.1], $result[3]->vector->getData());
94+
$this->assertSame([0.7, -0.3, 0.0], $result[4]->vector->getData());
95+
}
96+
97+
public function testStoreCanSearchUsingCosineDistanceWithMaxItems()
98+
{
99+
$store = new Store(new ArrayAdapter());
100+
$store->add(
101+
new VectorDocument(Uuid::v4(), new Vector([0.1, 0.1, 0.5])),
102+
new VectorDocument(Uuid::v4(), new Vector([0.7, -0.3, 0.0])),
103+
new VectorDocument(Uuid::v4(), new Vector([0.3, 0.7, 0.1])),
104+
);
105+
106+
$this->assertCount(1, iterator_to_array($store->query(new Vector([0.0, 0.1, 0.6]), [
107+
'maxItems' => 1,
108+
])));
109+
}
110+
111+
public function testStoreCanSearchUsingAngularDistance()
112+
{
113+
$store = new Store(new ArrayAdapter(), new DistanceCalculator(DistanceStrategy::ANGULAR_DISTANCE));
114+
$store->add(
115+
new VectorDocument(Uuid::v4(), new Vector([1.0, 2.0, 3.0])),
116+
new VectorDocument(Uuid::v4(), new Vector([1.0, 5.0, 7.0])),
117+
);
118+
119+
$result = iterator_to_array($store->query(new Vector([1.2, 2.3, 3.4])));
120+
121+
$this->assertCount(2, $result);
122+
$this->assertSame([1.0, 2.0, 3.0], $result[0]->vector->getData());
123+
}
124+
125+
public function testStoreCanSearchUsingEuclideanDistance()
126+
{
127+
$store = new Store(new ArrayAdapter(), new DistanceCalculator(DistanceStrategy::EUCLIDEAN_DISTANCE));
128+
$store->add(
129+
new VectorDocument(Uuid::v4(), new Vector([1.0, 5.0, 7.0])),
130+
new VectorDocument(Uuid::v4(), new Vector([1.0, 2.0, 3.0])),
131+
);
132+
133+
$result = iterator_to_array($store->query(new Vector([1.2, 2.3, 3.4])));
134+
135+
$this->assertCount(2, $result);
136+
$this->assertSame([1.0, 2.0, 3.0], $result[0]->vector->getData());
137+
}
138+
139+
public function testStoreCanSearchUsingManhattanDistance()
140+
{
141+
$store = new Store(new ArrayAdapter(), new DistanceCalculator(DistanceStrategy::MANHATTAN_DISTANCE));
142+
$store->add(
143+
new VectorDocument(Uuid::v4(), new Vector([1.0, 2.0, 3.0])),
144+
new VectorDocument(Uuid::v4(), new Vector([1.0, 5.0, 7.0])),
145+
);
146+
147+
$result = iterator_to_array($store->query(new Vector([1.2, 2.3, 3.4])));
148+
149+
$this->assertCount(2, $result);
150+
$this->assertSame([1.0, 2.0, 3.0], $result[0]->vector->getData());
151+
}
152+
153+
public function testStoreCanSearchUsingChebyshevDistance()
154+
{
155+
$store = new Store(new ArrayAdapter(), new DistanceCalculator(DistanceStrategy::CHEBYSHEV_DISTANCE));
156+
$store->add(
157+
new VectorDocument(Uuid::v4(), new Vector([1.0, 2.0, 3.0])),
158+
new VectorDocument(Uuid::v4(), new Vector([1.0, 5.0, 7.0])),
159+
);
160+
161+
$result = iterator_to_array($store->query(new Vector([1.2, 2.3, 3.4])));
162+
163+
$this->assertCount(2, $result);
164+
$this->assertSame([1.0, 2.0, 3.0], $result[0]->vector->getData());
165+
}
166+
167+
public function testStoreCanSearchWithFilter()
168+
{
169+
$store = new Store(new ArrayAdapter());
170+
$store->add(
171+
new VectorDocument(Uuid::v4(), new Vector([0.1, 0.1, 0.5]), new Metadata(['category' => 'products', 'enabled' => true])),
172+
new VectorDocument(Uuid::v4(), new Vector([0.7, -0.3, 0.0]), new Metadata(['category' => 'articles', 'enabled' => true])),
173+
new VectorDocument(Uuid::v4(), new Vector([0.3, 0.7, 0.1]), new Metadata(['category' => 'products', 'enabled' => false])),
174+
);
175+
176+
$result = iterator_to_array($store->query(new Vector([0.0, 0.1, 0.6]), [
177+
'filter' => fn (VectorDocument $doc) => 'products' === $doc->metadata['category'],
178+
]));
179+
180+
$this->assertCount(2, $result);
181+
$this->assertSame('products', $result[0]->metadata['category']);
182+
$this->assertSame('products', $result[1]->metadata['category']);
183+
}
184+
185+
public function testStoreCanSearchWithFilterAndMaxItems()
186+
{
187+
$store = new Store(new ArrayAdapter());
188+
$store->add(
189+
new VectorDocument(Uuid::v4(), new Vector([0.1, 0.1, 0.5]), new Metadata(['category' => 'products'])),
190+
new VectorDocument(Uuid::v4(), new Vector([0.7, -0.3, 0.0]), new Metadata(['category' => 'articles'])),
191+
new VectorDocument(Uuid::v4(), new Vector([0.3, 0.7, 0.1]), new Metadata(['category' => 'products'])),
192+
new VectorDocument(Uuid::v4(), new Vector([0.0, 0.1, 0.6]), new Metadata(['category' => 'products'])),
193+
);
194+
195+
$result = iterator_to_array($store->query(new Vector([0.0, 0.1, 0.6]), [
196+
'filter' => fn (VectorDocument $doc) => 'products' === $doc->metadata['category'],
197+
'maxItems' => 2,
198+
]));
199+
200+
$this->assertCount(2, $result);
201+
$this->assertSame('products', $result[0]->metadata['category']);
202+
$this->assertSame('products', $result[1]->metadata['category']);
203+
}
204+
205+
public function testStoreCanSearchWithComplexFilter()
206+
{
207+
$store = new Store(new ArrayAdapter());
208+
$store->add(
209+
new VectorDocument(Uuid::v4(), new Vector([0.1, 0.1, 0.5]), new Metadata(['price' => 100, 'stock' => 5])),
210+
new VectorDocument(Uuid::v4(), new Vector([0.7, -0.3, 0.0]), new Metadata(['price' => 200, 'stock' => 0])),
211+
new VectorDocument(Uuid::v4(), new Vector([0.3, 0.7, 0.1]), new Metadata(['price' => 50, 'stock' => 10])),
212+
);
213+
214+
$result = iterator_to_array($store->query(new Vector([0.0, 0.1, 0.6]), [
215+
'filter' => fn (VectorDocument $doc) => $doc->metadata['price'] <= 150 && $doc->metadata['stock'] > 0,
216+
]));
217+
218+
$this->assertCount(2, $result);
219+
}
220+
221+
public function testStoreCanSearchWithNestedMetadataFilter()
222+
{
223+
$store = new Store(new ArrayAdapter());
224+
$store->add(
225+
new VectorDocument(Uuid::v4(), new Vector([0.1, 0.1, 0.5]), new Metadata(['options' => ['size' => 'S', 'color' => 'blue']])),
226+
new VectorDocument(Uuid::v4(), new Vector([0.7, -0.3, 0.0]), new Metadata(['options' => ['size' => 'M', 'color' => 'blue']])),
227+
new VectorDocument(Uuid::v4(), new Vector([0.3, 0.7, 0.1]), new Metadata(['options' => ['size' => 'S', 'color' => 'red']])),
228+
);
229+
230+
$result = iterator_to_array($store->query(new Vector([0.0, 0.1, 0.6]), [
231+
'filter' => fn (VectorDocument $doc) => 'S' === $doc->metadata['options']['size'],
232+
]));
233+
234+
$this->assertCount(2, $result);
235+
$this->assertSame('S', $result[0]->metadata['options']['size']);
236+
$this->assertSame('S', $result[1]->metadata['options']['size']);
237+
}
238+
239+
public function testStoreCanSearchWithInArrayFilter()
240+
{
241+
$store = new Store(new ArrayAdapter());
242+
$store->add(
243+
new VectorDocument(Uuid::v4(), new Vector([0.1, 0.1, 0.5]), new Metadata(['brand' => 'Nike'])),
244+
new VectorDocument(Uuid::v4(), new Vector([0.7, -0.3, 0.0]), new Metadata(['brand' => 'Adidas'])),
245+
new VectorDocument(Uuid::v4(), new Vector([0.3, 0.7, 0.1]), new Metadata(['brand' => 'Generic'])),
246+
);
247+
248+
$allowedBrands = ['Nike', 'Adidas', 'Puma'];
249+
$result = iterator_to_array($store->query(new Vector([0.0, 0.1, 0.6]), [
250+
'filter' => fn (VectorDocument $doc) => \in_array($doc->metadata['brand'] ?? '', $allowedBrands, true),
251+
]));
252+
253+
$this->assertCount(2, $result);
254+
}
255+
}

0 commit comments

Comments
 (0)