diff --git a/UPGRADE.md b/UPGRADE.md index 4409c125f3..1acae89b49 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,3 +1,15 @@ +UPGRADE FROM 0.5 to 0.6 +======================= + +Store +-------- + + * Add support for `ScopingHttpClient` in `AzureSearchStore` + * The `endpointUrl` parameter for `AzureSearchStore` has been removed + * The `apiKey` parameter for `AzureSearchStore` has been removed + * The `apiVersion` parameter for `AzureSearchStore` has been removed + * A `StoreFactory` has been introduced for `AzureSearchStore` + UPGRADE FROM 0.4 to 0.5 ======================= diff --git a/examples/rag/azuresearch.php b/examples/rag/azuresearch.php new file mode 100644 index 0000000000..6cd84540da --- /dev/null +++ b/examples/rag/azuresearch.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\AI\Agent\Agent; +use Symfony\AI\Agent\Bridge\SimilaritySearch\SimilaritySearch; +use Symfony\AI\Agent\Toolbox\AgentProcessor; +use Symfony\AI\Agent\Toolbox\Toolbox; +use Symfony\AI\Fixtures\Movies; +use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory; +use Symfony\AI\Platform\Message\Message; +use Symfony\AI\Platform\Message\MessageBag; +use Symfony\AI\Store\Bridge\AzureSearch\StoreFactory; +use Symfony\AI\Store\Document\Metadata; +use Symfony\AI\Store\Document\TextDocument; +use Symfony\AI\Store\Document\Vectorizer; +use Symfony\AI\Store\Indexer\DocumentIndexer; +use Symfony\AI\Store\Indexer\DocumentProcessor; +use Symfony\Component\Uid\Uuid; + +require_once dirname(__DIR__).'/bootstrap.php'; + +// initialize the store +$store = StoreFactory::create('movies'); + +// create embeddings and documents +$documents = []; +foreach (Movies::all() as $i => $movie) { + $documents[] = new TextDocument( + id: Uuid::v4(), + content: 'Title: '.$movie['title'].\PHP_EOL.'Director: '.$movie['director'].\PHP_EOL.'Description: '.$movie['description'], + metadata: new Metadata($movie), + ); +} + +// create embeddings for documents +$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client()); +$vectorizer = new Vectorizer($platform, 'text-embedding-3-small', logger()); +$indexer = new DocumentIndexer(new DocumentProcessor($vectorizer, $store, logger: logger())); +$indexer->index($documents); + +$similaritySearch = new SimilaritySearch($vectorizer, $store); +$toolbox = new Toolbox([$similaritySearch], logger: logger()); +$processor = new AgentProcessor($toolbox); +$agent = new Agent($platform, 'gpt-5-mini', [$processor], [$processor]); + +$messages = new MessageBag( + Message::forSystem('Please answer all user questions only using SimilaritySearch function.'), + Message::ofUser('Which movie fits the theme of the mafia?') +); +$result = $agent->call($messages); + +echo $result->getContent().\PHP_EOL; diff --git a/src/ai-bundle/CHANGELOG.md b/src/ai-bundle/CHANGELOG.md index 631e4205de..2ed8abb025 100644 --- a/src/ai-bundle/CHANGELOG.md +++ b/src/ai-bundle/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Add `TraceableAgent` * Add `TraceableStore` * Add `setup_options` configuration for PostgreSQL store to pass extra fields to `ai:store:setup` + * Add support for `ScopingHttpClient` usage in `AzureSearch` store 0.5 --- diff --git a/src/ai-bundle/config/store/azuresearch.php b/src/ai-bundle/config/store/azuresearch.php index 6fb0218847..faeca7d79a 100644 --- a/src/ai-bundle/config/store/azuresearch.php +++ b/src/ai-bundle/config/store/azuresearch.php @@ -17,10 +17,17 @@ ->useAttributeAsKey('name') ->arrayPrototype() ->children() - ->stringNode('endpoint')->isRequired()->end() - ->stringNode('api_key')->isRequired()->end() - ->stringNode('index_name')->isRequired()->end() - ->stringNode('api_version')->isRequired()->end() - ->stringNode('vector_field')->end() + ->stringNode('endpoint')->end() + ->stringNode('api_key')->end() + ->stringNode('api_version')->end() + ->stringNode('index_name') + ->info('The name of the store will be used if the "index_name" option is not set') + ->end() + ->stringNode('http_client') + ->defaultValue('http_client') + ->end() + ->stringNode('vector_field') + ->defaultValue('vector') + ->end() ->end() ->end(); diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index 47470213de..c97600e732 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -88,6 +88,7 @@ use Symfony\AI\Platform\PlatformInterface; use Symfony\AI\Platform\ResultConverterInterface; use Symfony\AI\Store\Bridge\AzureSearch\SearchStore as AzureSearchStore; +use Symfony\AI\Store\Bridge\AzureSearch\StoreFactory as AzureSearchStoreFactory; use Symfony\AI\Store\Bridge\Cache\Store as CacheStore; use Symfony\AI\Store\Bridge\ChromaDb\Store as ChromaDbStore; use Symfony\AI\Store\Bridge\ClickHouse\Store as ClickHouseStore; @@ -1278,22 +1279,17 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde } foreach ($stores as $name => $store) { - $arguments = [ - new Reference('http_client'), - $store['endpoint'], - $store['api_key'], - $store['index_name'], - $store['api_version'], - ]; - - if (\array_key_exists('vector_field', $store)) { - $arguments[5] = $store['vector_field']; - } - - $definition = new Definition(AzureSearchStore::class); - $definition + $definition = (new Definition(AzureSearchStore::class)) + ->setFactory(AzureSearchStoreFactory::class.'::create') ->setLazy(true) - ->setArguments($arguments) + ->setArguments([ + $store['index_name'] ?? $name, + $store['vector_field'], + $store['endpoint'] ?? null, + $store['api_key'] ?? null, + $store['api_version'] ?? null, + new Reference($store['http_client']), + ]) ->addTag('proxy', ['interface' => StoreInterface::class]) ->addTag('ai.store'); diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index c7db0efd3a..c3c165fe12 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -51,6 +51,7 @@ use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface; use Symfony\AI\Platform\PlatformInterface; use Symfony\AI\Store\Bridge\AzureSearch\SearchStore as AzureStore; +use Symfony\AI\Store\Bridge\AzureSearch\StoreFactory as AzureSearchStoreFactory; use Symfony\AI\Store\Bridge\Cache\Store as CacheStore; use Symfony\AI\Store\Bridge\ChromaDb\Store as ChromaDbStore; use Symfony\AI\Store\Bridge\ClickHouse\Store as ClickhouseStore; @@ -66,7 +67,7 @@ use Symfony\AI\Store\Bridge\Postgres\Distance as PostgresDistance; use Symfony\AI\Store\Bridge\Postgres\Store as PostgresStore; use Symfony\AI\Store\Bridge\Qdrant\Store as QdrantStore; -use Symfony\AI\Store\Bridge\Qdrant\StoreFactory; +use Symfony\AI\Store\Bridge\Qdrant\StoreFactory as QdrantStoreFactory; use Symfony\AI\Store\Bridge\Redis\Distance as RedisDistance; use Symfony\AI\Store\Bridge\Redis\Store as RedisStore; use Symfony\AI\Store\Bridge\Supabase\Store as SupabaseStore; @@ -485,16 +486,18 @@ public function testAzureStoreCanBeConfigured() $this->assertTrue($container->hasDefinition('ai.store.azuresearch.my_azuresearch_store')); $definition = $container->getDefinition('ai.store.azuresearch.my_azuresearch_store'); + $this->assertSame([AzureSearchStoreFactory::class, 'create'], $definition->getFactory()); $this->assertSame(AzureStore::class, $definition->getClass()); - $this->assertTrue($definition->isLazy()); - $this->assertCount(5, $definition->getArguments()); - $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); - $this->assertSame('http_client', (string) $definition->getArgument(0)); - $this->assertSame('https://mysearch.search.windows.net', $definition->getArgument(1)); - $this->assertSame('azure_search_key', $definition->getArgument(2)); - $this->assertSame('my-documents', $definition->getArgument(3)); + + $this->assertCount(6, $definition->getArguments()); + $this->assertSame('my-documents', (string) $definition->getArgument(0)); + $this->assertSame('vector', $definition->getArgument(1)); + $this->assertSame('https://mysearch.search.windows.net', $definition->getArgument(2)); + $this->assertSame('azure_search_key', $definition->getArgument(3)); $this->assertSame('2023-11-01', $definition->getArgument(4)); + $this->assertInstanceOf(Reference::class, $definition->getArgument(5)); + $this->assertSame('http_client', (string) $definition->getArgument(5)); $this->assertTrue($definition->hasTag('proxy')); $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); @@ -504,10 +507,8 @@ public function testAzureStoreCanBeConfigured() $this->assertTrue($container->hasAlias(StoreInterface::class.' $myAzuresearchStore')); $this->assertTrue($container->hasAlias(StoreInterface::class.' $azuresearchMyAzuresearchStore')); $this->assertTrue($container->hasAlias(StoreInterface::class)); - } - public function testAzureStoreCanBeConfiguredWithCustomVectorField() - { + // Custom vector field $container = $this->buildContainer([ 'ai' => [ 'store' => [ @@ -527,17 +528,56 @@ public function testAzureStoreCanBeConfiguredWithCustomVectorField() $this->assertTrue($container->hasDefinition('ai.store.azuresearch.my_azuresearch_store')); $definition = $container->getDefinition('ai.store.azuresearch.my_azuresearch_store'); + $this->assertSame([AzureSearchStoreFactory::class, 'create'], $definition->getFactory()); $this->assertSame(AzureStore::class, $definition->getClass()); - $this->assertTrue($definition->isLazy()); + $this->assertCount(6, $definition->getArguments()); - $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); - $this->assertSame('http_client', (string) $definition->getArgument(0)); - $this->assertSame('https://mysearch.search.windows.net', $definition->getArgument(1)); - $this->assertSame('azure_search_key', $definition->getArgument(2)); - $this->assertSame('my-documents', $definition->getArgument(3)); + $this->assertSame('my-documents', (string) $definition->getArgument(0)); + $this->assertSame('foo', $definition->getArgument(1)); + $this->assertSame('https://mysearch.search.windows.net', $definition->getArgument(2)); + $this->assertSame('azure_search_key', $definition->getArgument(3)); $this->assertSame('2023-11-01', $definition->getArgument(4)); - $this->assertSame('foo', $definition->getArgument(5)); + $this->assertInstanceOf(Reference::class, $definition->getArgument(5)); + $this->assertSame('http_client', (string) $definition->getArgument(5)); + + $this->assertTrue($definition->hasTag('proxy')); + $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); + $this->assertTrue($definition->hasTag('ai.store')); + + $this->assertTrue($container->hasAlias('.'.StoreInterface::class.' $my_azuresearch_store')); + $this->assertTrue($container->hasAlias(StoreInterface::class.' $myAzuresearchStore')); + $this->assertTrue($container->hasAlias(StoreInterface::class.' $azuresearchMyAzuresearchStore')); + $this->assertTrue($container->hasAlias(StoreInterface::class)); + + // Scoped HttpClient + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'azuresearch' => [ + 'my_azuresearch_store' => [ + 'http_client' => 'scoped_http_client', + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.azuresearch.my_azuresearch_store')); + + $definition = $container->getDefinition('ai.store.azuresearch.my_azuresearch_store'); + $this->assertSame([AzureSearchStoreFactory::class, 'create'], $definition->getFactory()); + $this->assertSame(AzureStore::class, $definition->getClass()); + $this->assertTrue($definition->isLazy()); + + $this->assertCount(6, $definition->getArguments()); + $this->assertSame('my_azuresearch_store', (string) $definition->getArgument(0)); + $this->assertSame('vector', $definition->getArgument(1)); + $this->assertNull($definition->getArgument(2)); + $this->assertNull($definition->getArgument(3)); + $this->assertNull($definition->getArgument(4)); + $this->assertInstanceOf(Reference::class, $definition->getArgument(5)); + $this->assertSame('scoped_http_client', (string) $definition->getArgument(5)); $this->assertTrue($definition->hasTag('proxy')); $this->assertSame([['interface' => StoreInterface::class]], $definition->getTag('proxy')); @@ -2678,7 +2718,7 @@ public function testQdrantStoreCanBeConfigured() $this->assertTrue($container->hasDefinition('ai.store.qdrant.my_qdrant_store')); $definition = $container->getDefinition('ai.store.qdrant.my_qdrant_store'); - $this->assertSame([StoreFactory::class, 'create'], $definition->getFactory()); + $this->assertSame([QdrantStoreFactory::class, 'create'], $definition->getFactory()); $this->assertSame(QdrantStore::class, $definition->getClass()); $this->assertTrue($definition->isLazy()); @@ -2722,7 +2762,7 @@ public function testQdrantStoreCanBeConfigured() $this->assertTrue($container->hasDefinition('ai.store.qdrant.my_qdrant_store')); $definition = $container->getDefinition('ai.store.qdrant.my_qdrant_store'); - $this->assertSame([StoreFactory::class, 'create'], $definition->getFactory()); + $this->assertSame([QdrantStoreFactory::class, 'create'], $definition->getFactory()); $this->assertSame(QdrantStore::class, $definition->getClass()); $this->assertTrue($definition->isLazy()); @@ -2767,7 +2807,7 @@ public function testQdrantStoreCanBeConfigured() $this->assertTrue($container->hasDefinition('ai.store.qdrant.my_qdrant_store')); $definition = $container->getDefinition('ai.store.qdrant.my_qdrant_store'); - $this->assertSame([StoreFactory::class, 'create'], $definition->getFactory()); + $this->assertSame([QdrantStoreFactory::class, 'create'], $definition->getFactory()); $this->assertSame(QdrantStore::class, $definition->getClass()); $this->assertTrue($definition->isLazy()); @@ -2813,7 +2853,7 @@ public function testQdrantStoreCanBeConfigured() $this->assertTrue($container->hasDefinition('ai.store.qdrant.my_qdrant_store')); $definition = $container->getDefinition('ai.store.qdrant.my_qdrant_store'); - $this->assertSame([StoreFactory::class, 'create'], $definition->getFactory()); + $this->assertSame([QdrantStoreFactory::class, 'create'], $definition->getFactory()); $this->assertSame(QdrantStore::class, $definition->getClass()); $this->assertTrue($definition->isLazy()); @@ -2856,7 +2896,7 @@ public function testQdrantStoreCanBeConfigured() $this->assertTrue($container->hasDefinition('ai.store.qdrant.my_qdrant_store')); $definition = $container->getDefinition('ai.store.qdrant.my_qdrant_store'); - $this->assertSame([StoreFactory::class, 'create'], $definition->getFactory()); + $this->assertSame([QdrantStoreFactory::class, 'create'], $definition->getFactory()); $this->assertSame(QdrantStore::class, $definition->getClass()); $this->assertTrue($definition->isLazy()); @@ -7920,6 +7960,9 @@ private function getFullConfig(): array 'api_version' => '2023-11-01', 'vector_field' => 'contentVector', ], + 'my_azuresearch_store_with_scoped_http_client' => [ + 'http_client' => 'scoped_http_client', + ], ], 'cache' => [ 'my_cache_store' => [ diff --git a/src/store/src/Bridge/AzureSearch/CHANGELOG.md b/src/store/src/Bridge/AzureSearch/CHANGELOG.md index f5a86f07ca..7321f11eee 100644 --- a/src/store/src/Bridge/AzureSearch/CHANGELOG.md +++ b/src/store/src/Bridge/AzureSearch/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +0.6 +--- + + * [BC BREAK] Add support for `ScopingHttpClient` in `SearchStore` + * [BC BREAK] The `endpointUrl` parameter for `SearchStore` has been removed + * [BC BREAK] The `apiKey` parameter for `SearchStore` has been removed + * [BC BREAK] The `apiVersion` parameter for `SearchStore` has been removed + 0.4 --- diff --git a/src/store/src/Bridge/AzureSearch/SearchStore.php b/src/store/src/Bridge/AzureSearch/SearchStore.php index 93f69deeed..871024edb7 100644 --- a/src/store/src/Bridge/AzureSearch/SearchStore.php +++ b/src/store/src/Bridge/AzureSearch/SearchStore.php @@ -31,10 +31,7 @@ final class SearchStore implements StoreInterface */ public function __construct( private readonly HttpClientInterface $httpClient, - private readonly string $endpointUrl, - #[\SensitiveParameter] private readonly string $apiKey, private readonly string $indexName, - private readonly string $apiVersion, private readonly string $vectorFieldName = 'vector', ) { } @@ -46,7 +43,10 @@ public function add(VectorDocument|array $documents): void } $this->request('index', [ - 'value' => array_map([$this, 'convertToIndexableArray'], $documents), + 'value' => array_map(fn (VectorDocument $document): array => array_merge([ + 'id' => $document->getId(), + $this->vectorFieldName => $document->getVector()->getData(), + ], $document->getMetadata()->getArrayCopy()), $documents), ]); } @@ -83,7 +83,16 @@ public function query(QueryInterface $query, array $options = []): iterable $vector = $query->getVector(); $result = $this->request('search', [ - 'vectorQueries' => [$this->buildVectorQuery($vector)], + 'vectorQueries' => [ + [ + 'kind' => 'vector', + 'vector' => $vector->getData(), + 'exhaustive' => true, + 'fields' => $this->vectorFieldName, + 'weight' => 0.5, + 'k' => 5, + ], + ], ]); foreach ($result['value'] as $item) { @@ -98,29 +107,13 @@ public function query(QueryInterface $query, array $options = []): iterable */ private function request(string $endpoint, array $payload): array { - $url = \sprintf('%s/indexes/%s/docs/%s', $this->endpointUrl, $this->indexName, $endpoint); - $result = $this->httpClient->request('POST', $url, [ - 'headers' => [ - 'api-key' => $this->apiKey, - ], - 'query' => ['api-version' => $this->apiVersion], + $result = $this->httpClient->request('POST', \sprintf('indexes/%s/docs/%s', $this->indexName, $endpoint), [ 'json' => $payload, ]); return $result->toArray(); } - /** - * @return array - */ - private function convertToIndexableArray(VectorDocument $document): array - { - return array_merge([ - 'id' => $document->getId(), - $this->vectorFieldName => $document->getVector()->getData(), - ], $document->getMetadata()->getArrayCopy()); - } - /** * @param array $data */ @@ -134,26 +127,4 @@ private function convertToVectorDocument(array $data): VectorDocument metadata: new Metadata($data), ); } - - /** - * @return array{ - * kind: 'vector', - * vector: float[], - * exhaustive: true, - * fields: non-empty-string, - * weight: float, - * k: int, - * } - */ - private function buildVectorQuery(Vector $vector): array - { - return [ - 'kind' => 'vector', - 'vector' => $vector->getData(), - 'exhaustive' => true, - 'fields' => $this->vectorFieldName, - 'weight' => 0.5, - 'k' => 5, - ]; - } } diff --git a/src/store/src/Bridge/AzureSearch/StoreFactory.php b/src/store/src/Bridge/AzureSearch/StoreFactory.php new file mode 100644 index 0000000000..e28233cd14 --- /dev/null +++ b/src/store/src/Bridge/AzureSearch/StoreFactory.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Store\Bridge\AzureSearch; + +use Symfony\AI\Store\StoreInterface; +use Symfony\Component\HttpClient\HttpClient; +use Symfony\Component\HttpClient\ScopingHttpClient; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Guillaume Loulier + */ +final class StoreFactory +{ + public static function create( + string $indexName, + string $vectorField = 'vector', + ?string $endpoint = null, + #[\SensitiveParameter] ?string $apiKey = null, + ?string $apiVersion = null, + ?HttpClientInterface $httpClient = null, + ): StoreInterface { + if (null !== $endpoint) { + $defaultOptions = []; + if (null !== $apiKey) { + $defaultOptions['headers']['api-key'] = $apiKey; + } + + if (null !== $apiVersion) { + $defaultOptions['query']['api-version'] = $apiVersion; + } + + $httpClient = ScopingHttpClient::forBaseUri($httpClient ?? HttpClient::create(), $endpoint, $defaultOptions); + } + + return new SearchStore($httpClient, $indexName, $vectorField); + } +} diff --git a/src/store/src/Bridge/AzureSearch/Tests/SearchStoreTest.php b/src/store/src/Bridge/AzureSearch/Tests/SearchStoreTest.php index 29d9cfe4d1..c7e963f457 100644 --- a/src/store/src/Bridge/AzureSearch/Tests/SearchStoreTest.php +++ b/src/store/src/Bridge/AzureSearch/Tests/SearchStoreTest.php @@ -37,13 +37,7 @@ public function testAddDocumentsSuccessfully() ]), ]); - $store = new SearchStore( - $httpClient, - 'https://test.search.windows.net', - 'test-api-key', - 'test-index', - '2023-11-01', - ); + $store = new SearchStore($httpClient, 'test-index'); $uuid = Uuid::v4(); $document = new VectorDocument($uuid, new Vector([0.1, 0.2, 0.3])); @@ -58,12 +52,10 @@ public function testAddDocumentsWithMetadata() $httpClient = new MockHttpClient([ function (string $method, string $url, array $options): JsonMockResponse { $this->assertSame('POST', $method); - $this->assertSame('https://test.search.windows.net/indexes/test-index/docs/index?api-version=2023-11-01', $url); + $this->assertSame('https://test.search.windows.net/indexes/test-index/docs/index', $url); // Check normalized headers as Symfony HTTP client might lowercase them $this->assertArrayHasKey('normalized_headers', $options); $this->assertIsArray($options['normalized_headers']); - $this->assertArrayHasKey('api-key', $options['normalized_headers']); - $this->assertSame(['api-key: test-api-key'], $options['normalized_headers']['api-key']); $this->assertArrayHasKey('body', $options); $this->assertIsString($options['body']); @@ -85,15 +77,9 @@ function (string $method, string $url, array $options): JsonMockResponse { 'http_code' => 200, ]); }, - ]); + ], 'https://test.search.windows.net/'); - $store = new SearchStore( - $httpClient, - 'https://test.search.windows.net', - 'test-api-key', - 'test-index', - '2023-11-01', - ); + $store = new SearchStore($httpClient, 'test-index'); $uuid = Uuid::v4(); $document = new VectorDocument($uuid, new Vector([0.1, 0.2, 0.3]), new Metadata(['title' => 'Test Document'])); @@ -116,13 +102,7 @@ public function testAddDocumentsFailure() ]), ]); - $store = new SearchStore( - $httpClient, - 'https://test.search.windows.net', - 'test-api-key', - 'test-index', - '2023-11-01', - ); + $store = new SearchStore($httpClient, 'test-index'); $uuid = Uuid::v4(); $document = new VectorDocument($uuid, new Vector([0.1, 0.2, 0.3])); @@ -160,13 +140,7 @@ public function testQueryReturnsDocuments() ]), ]); - $store = new SearchStore( - $httpClient, - 'https://test.search.windows.net', - 'test-api-key', - 'test-index', - '2023-11-01', - ); + $store = new SearchStore($httpClient, 'test-index'); $results = iterator_to_array($store->query(new VectorQuery(new Vector([0.1, 0.2, 0.3])))); @@ -208,14 +182,7 @@ function (string $method, string $url, array $options): JsonMockResponse { }, ]); - $store = new SearchStore( - $httpClient, - 'https://test.search.windows.net', - 'test-api-key', - 'test-index', - '2023-11-01', - 'custom_vector_field', - ); + $store = new SearchStore($httpClient, 'test-index', 'custom_vector_field'); $results = iterator_to_array($store->query(new VectorQuery(new Vector([0.1, 0.2, 0.3])))); @@ -236,13 +203,7 @@ public function testQueryFailure() ]), ]); - $store = new SearchStore( - $httpClient, - 'https://test.search.windows.net', - 'test-api-key', - 'test-index', - '2023-11-01', - ); + $store = new SearchStore($httpClient, 'test-index'); $this->expectException(ClientException::class); $this->expectExceptionMessage('HTTP 400 returned'); @@ -270,13 +231,7 @@ public function testQueryWithNullVector() ]), ]); - $store = new SearchStore( - $httpClient, - 'https://test.search.windows.net', - 'test-api-key', - 'test-index', - '2023-11-01', - ); + $store = new SearchStore($httpClient, 'test-index'); $results = iterator_to_array($store->query(new VectorQuery(new Vector([0.1, 0.2, 0.3])))); @@ -290,7 +245,7 @@ public function testRemoveWithSingleId() $httpClient = new MockHttpClient([ function (string $method, string $url, array $options): JsonMockResponse { $this->assertSame('POST', $method); - $this->assertSame('https://test.search.windows.net/indexes/test-index/docs/index?api-version=2023-11-01', $url); + $this->assertSame('https://test.search.windows.net/indexes/test-index/docs/index', $url); $this->assertArrayHasKey('body', $options); $this->assertIsString($options['body']); @@ -312,15 +267,9 @@ function (string $method, string $url, array $options): JsonMockResponse { 'http_code' => 200, ]); }, - ]); + ], 'https://test.search.windows.net/'); - $store = new SearchStore( - $httpClient, - 'https://test.search.windows.net', - 'test-api-key', - 'test-index', - '2023-11-01', - ); + $store = new SearchStore($httpClient, 'test-index'); $store->remove('doc1'); @@ -332,7 +281,7 @@ public function testRemoveWithMultipleIds() $httpClient = new MockHttpClient([ function (string $method, string $url, array $options): JsonMockResponse { $this->assertSame('POST', $method); - $this->assertSame('https://test.search.windows.net/indexes/test-index/docs/index?api-version=2023-11-01', $url); + $this->assertSame('https://test.search.windows.net/indexes/test-index/docs/index', $url); $this->assertArrayHasKey('body', $options); $this->assertIsString($options['body']); @@ -367,15 +316,9 @@ function (string $method, string $url, array $options): JsonMockResponse { 'http_code' => 200, ]); }, - ]); + ], 'https://test.search.windows.net/'); - $store = new SearchStore( - $httpClient, - 'https://test.search.windows.net', - 'test-api-key', - 'test-index', - '2023-11-01', - ); + $store = new SearchStore($httpClient, 'test-index'); $store->remove(['doc1', 'doc2', 'doc3']); @@ -386,13 +329,7 @@ public function testRemoveWithEmptyArray() { $httpClient = new MockHttpClient([]); - $store = new SearchStore( - $httpClient, - 'https://test.search.windows.net', - 'test-api-key', - 'test-index', - '2023-11-01', - ); + $store = new SearchStore($httpClient, 'test-index'); $store->remove([]); @@ -412,13 +349,7 @@ public function testRemoveFailure() ]), ]); - $store = new SearchStore( - $httpClient, - 'https://test.search.windows.net', - 'test-api-key', - 'test-index', - '2023-11-01', - ); + $store = new SearchStore($httpClient, 'test-index'); $this->expectException(ClientException::class); $this->expectExceptionMessage('HTTP 404 returned'); @@ -429,7 +360,7 @@ public function testRemoveFailure() public function testStoreSupportsVectorQuery() { - $store = new SearchStore(new MockHttpClient(), 'https://test.search.windows.net', 'test-key', 'test-index', '2023-11-01'); + $store = new SearchStore(new MockHttpClient(), 'test-index'); $this->assertTrue($store->supports(VectorQuery::class)); } } diff --git a/src/store/src/Bridge/AzureSearch/Tests/StoreFactoryTest.php b/src/store/src/Bridge/AzureSearch/Tests/StoreFactoryTest.php new file mode 100644 index 0000000000..2af826a3b6 --- /dev/null +++ b/src/store/src/Bridge/AzureSearch/Tests/StoreFactoryTest.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Store\Bridge\AzureSearch\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\AI\Store\Bridge\AzureSearch\SearchStore; +use Symfony\AI\Store\Bridge\AzureSearch\StoreFactory; +use Symfony\Component\HttpClient\HttpClient; +use Symfony\Component\HttpClient\ScopingHttpClient; + +final class StoreFactoryTest extends TestCase +{ + public function testStoreCanBeCreated() + { + $store = StoreFactory::create('foo', endpoint: 'https://test.search.windows.net/', apiKey: 'foo', apiVersion: '2023-11-01', httpClient: HttpClient::create()); + $this->assertInstanceOf(SearchStore::class, $store); + } + + public function testStoreCanBeCreatedWithScopedHttpClient() + { + $store = StoreFactory::create('foo', httpClient: ScopingHttpClient::forBaseUri(HttpClient::create(), 'https://test.search.windows.net/', [ + 'headers' => [ + 'api-key' => 'foo', + ], + 'query' => [ + 'api-version' => '2023-11-01', + ], + ])); + + $this->assertInstanceOf(SearchStore::class, $store); + } +}