From e2d01ac57a62ccc097492bc30894a2cd99ccbc55 Mon Sep 17 00:00:00 2001 From: Ahmed EBEN HASSINE Date: Tue, 14 Oct 2025 21:02:04 +0200 Subject: [PATCH 1/9] [Store] Add configurable semantic ratio to Meilisearch Store This adds a semanticRatio parameter to control the balance between keyword-based (BM25) and semantic (vector) search in hybrid queries. - Add semanticRatio constructor parameter (default: 1.0 for BC) - Allow per-query override via options array - Add validation (0.0-1.0 range) - Add support for 'q' parameter in query options for BM25 text queries - Add comprehensive tests covering all scenarios Use cases: - 0.0 = Pure keyword search (IDs, codes, exact terms) - 0.5 = Balanced hybrid search (general use) - 1.0 = Pure semantic search (conceptual similarity) Backward compatible - default behavior unchanged. --- src/store/src/Bridge/Meilisearch/Store.php | 21 ++- .../tests/Bridge/Meilisearch/StoreTest.php | 146 ++++++++++++++++++ 2 files changed, 165 insertions(+), 2 deletions(-) diff --git a/src/store/src/Bridge/Meilisearch/Store.php b/src/store/src/Bridge/Meilisearch/Store.php index 7aed6ce16..80db42a31 100644 --- a/src/store/src/Bridge/Meilisearch/Store.php +++ b/src/store/src/Bridge/Meilisearch/Store.php @@ -28,7 +28,11 @@ { /** * @param string $embedder The name of the embedder where vectors are stored - * @param string $vectorFieldName The name of the field int the index that contains the vector + * @param string $vectorFieldName The name of the field in the index that contains the vector + * @param float $semanticRatio The ratio between semantic (vector) and keyword (BM25) search (0.0 to 1.0) + * - 0.0 = 100% keyword search (BM25) + * - 0.5 = balanced hybrid search + * - 1.0 = 100% semantic search (vector only) */ public function __construct( private HttpClientInterface $httpClient, @@ -38,7 +42,11 @@ public function __construct( private string $embedder = 'default', private string $vectorFieldName = '_vectors', private int $embeddingsDimension = 1536, + private float $semanticRatio = 1.0, ) { + if ($semanticRatio < 0.0 || $semanticRatio > 1.0) { + throw new InvalidArgumentException(\sprintf('The semantic ratio must be between 0.0 and 1.0, "%s" given.', $semanticRatio)); + } } public function setup(array $options = []): void @@ -71,13 +79,22 @@ public function add(VectorDocument ...$documents): void public function query(Vector $vector, array $options = []): array { + $semanticRatio = $options['semanticRatio'] ?? $this->semanticRatio; + + if ($semanticRatio < 0.0 || $semanticRatio > 1.0) { + throw new InvalidArgumentException(\sprintf('The semantic ratio must be between 0.0 and 1.0, "%s" given.', $semanticRatio)); + } + + $queryText = $options['q'] ?? ''; + $result = $this->request('POST', \sprintf('indexes/%s/search', $this->indexName), [ + 'q' => $queryText, 'vector' => $vector->getData(), 'showRankingScore' => true, 'retrieveVectors' => true, 'hybrid' => [ 'embedder' => $this->embedder, - 'semanticRatio' => 1.0, + 'semanticRatio' => $semanticRatio, ], ]); diff --git a/src/store/tests/Bridge/Meilisearch/StoreTest.php b/src/store/tests/Bridge/Meilisearch/StoreTest.php index e06f01ef2..9afb0a63b 100644 --- a/src/store/tests/Bridge/Meilisearch/StoreTest.php +++ b/src/store/tests/Bridge/Meilisearch/StoreTest.php @@ -15,9 +15,11 @@ use Symfony\AI\Platform\Vector\Vector; use Symfony\AI\Store\Bridge\Meilisearch\Store; use Symfony\AI\Store\Document\VectorDocument; +use Symfony\AI\Store\Exception\InvalidArgumentException; use Symfony\Component\HttpClient\Exception\ClientException; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\JsonMockResponse; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\Uid\Uuid; final class StoreTest extends TestCase @@ -275,4 +277,148 @@ public function testMetadataWithoutIDRankingandVector() $this->assertSame($expected, $vectors[0]->metadata->getArrayCopy()); } + + public function testConstructorWithValidSemanticRatio() + { + $httpClient = new MockHttpClient(); + + $store = new Store($httpClient, 'http://localhost:7700', 'key', 'index', semanticRatio: 0.5); + + $this->assertInstanceOf(Store::class, $store); + } + + public function testConstructorThrowsExceptionForInvalidSemanticRatio() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The semantic ratio must be between 0.0 and 1.0'); + + $httpClient = new MockHttpClient(); + new Store($httpClient, 'http://localhost:7700', 'key', 'index', semanticRatio: 1.5); + } + + public function testConstructorThrowsExceptionForNegativeSemanticRatio() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The semantic ratio must be between 0.0 and 1.0'); + + $httpClient = new MockHttpClient(); + new Store($httpClient, 'http://localhost:7700', 'key', 'index', semanticRatio: -0.1); + } + + public function testQueryUsesDefaultSemanticRatio() + { + $responses = [ + new MockResponse(json_encode([ + 'hits' => [ + [ + 'id' => '550e8400-e29b-41d4-a716-446655440000', + '_vectors' => [ + 'default' => [ + 'embeddings' => [0.1, 0.2, 0.3], + ], + ], + '_rankingScore' => 0.95, + 'content' => 'Test document', + ], + ], + ])), + ]; + + $httpClient = new MockHttpClient($responses); + $store = new Store($httpClient, 'http://localhost:7700', 'key', 'index', semanticRatio: 0.7); + + $vector = new Vector([0.1, 0.2, 0.3]); + $store->query($vector); + + $request = $httpClient->getRequestsCount() > 0 ? $responses[0]->getRequestOptions() : null; + $this->assertNotNull($request); + + $body = json_decode($request['body'], true); + $this->assertSame(0.7, $body['hybrid']['semanticRatio']); + } + + public function testQueryCanOverrideSemanticRatio() + { + $responses = [ + new MockResponse(json_encode([ + 'hits' => [], + ])), + ]; + + $httpClient = new MockHttpClient($responses); + $store = new Store($httpClient, 'http://localhost:7700', 'key', 'index', semanticRatio: 0.5); + + $vector = new Vector([0.1, 0.2, 0.3]); + $store->query($vector, ['semanticRatio' => 0.2]); + + $request = $responses[0]->getRequestOptions(); + $body = json_decode($request['body'], true); + + $this->assertSame(0.2, $body['hybrid']['semanticRatio']); + } + + public function testQueryThrowsExceptionForInvalidSemanticRatioOption() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The semantic ratio must be between 0.0 and 1.0'); + + $httpClient = new MockHttpClient(); + $store = new Store($httpClient, 'http://localhost:7700', 'key', 'index'); + + $vector = new Vector([0.1, 0.2, 0.3]); + $store->query($vector, ['semanticRatio' => 2.0]); + } + + public function testQueryWithPureKeywordSearch() + { + $responses = [ + new MockResponse(json_encode([ + 'hits' => [ + [ + 'id' => '550e8400-e29b-41d4-a716-446655440000', + '_vectors' => [ + 'default' => [ + 'embeddings' => [0.1, 0.2, 0.3], + ], + ], + '_rankingScore' => 0.85, + 'title' => 'Symfony Framework', + ], + ], + ])), + ]; + + $httpClient = new MockHttpClient($responses); + $store = new Store($httpClient, 'http://localhost:7700', 'key', 'index'); + + $vector = new Vector([0.1, 0.2, 0.3]); + $results = $store->query($vector, ['semanticRatio' => 0.0]); + + $this->assertCount(1, $results); + $this->assertInstanceOf(VectorDocument::class, $results[0]); + + $request = $responses[0]->getRequestOptions(); + $body = json_decode($request['body'], true); + $this->assertSame(0.0, $body['hybrid']['semanticRatio']); + } + + public function testQueryWithBalancedHybridSearch() + { + $responses = [ + new MockResponse(json_encode([ + 'hits' => [], + ])), + ]; + + $httpClient = new MockHttpClient($responses); + $store = new Store($httpClient, 'http://localhost:7700', 'key', 'index', semanticRatio: 0.5); + + $vector = new Vector([0.1, 0.2, 0.3]); + $store->query($vector); + + $request = $responses[0]->getRequestOptions(); + $body = json_decode($request['body'], true); + + $this->assertSame(0.5, $body['hybrid']['semanticRatio']); + } } From 3066ce593e95a32a1422386186e1d007d6496e79 Mon Sep 17 00:00:00 2001 From: Ahmed EBEN HASSINE Date: Wed, 15 Oct 2025 20:21:19 +0200 Subject: [PATCH 2/9] feat(ai-bundle): add semantic_ratio to meilisearch store config --- examples/rag/meilisearch.php | 2 ++ src/ai-bundle/config/options.php | 8 ++++++ src/ai-bundle/src/AiBundle.php | 4 +++ .../DependencyInjection/AiBundleTest.php | 25 +++++++++++++++++++ 4 files changed, 39 insertions(+) diff --git a/examples/rag/meilisearch.php b/examples/rag/meilisearch.php index 364317651..b56edc12c 100644 --- a/examples/rag/meilisearch.php +++ b/examples/rag/meilisearch.php @@ -33,6 +33,8 @@ endpointUrl: env('MEILISEARCH_HOST'), apiKey: env('MEILISEARCH_API_KEY'), indexName: 'movies', + // Optional: configure hybrid search ratio (0.0 = keyword, 1.0 = semantic) + // semanticRatio: 0.5, // 50/50 hybrid search ); // create embeddings and documents diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index 74322d79c..db2c409b7 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -525,6 +525,14 @@ ->stringNode('embedder')->end() ->stringNode('vector_field')->end() ->integerNode('dimensions')->end() + ->floatNode('semantic_ratio') + ->info('The ratio between semantic (vector) and keyword (BM25) search (0.0 to 1.0). Default: 1.0 (100% semantic)') + ->defaultValue(1.0) + ->validate() + ->ifTrue(fn ($v) => $v < 0.0 || $v > 1.0) + ->thenInvalid('The semantic ratio must be between 0.0 and 1.0.') + ->end() + ->end() ->end() ->end() ->end() diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index 71e948f46..59ae66e3a 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -919,6 +919,10 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde $arguments[6] = $store['dimensions']; } + if (\array_key_exists('semantic_ratio', $store)) { + $arguments[7] = $store['semantic_ratio']; + } + $definition = new Definition(MeilisearchStore::class); $definition ->addTag('ai.store') diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index 87f42f4f9..834ef1687 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -2904,6 +2904,7 @@ private function getFullConfig(): array 'embedder' => 'default', 'vector_field' => '_vectors', 'dimensions' => 768, + 'semantic_ratio' => 0.5, ], ], 'memory' => [ @@ -3060,4 +3061,28 @@ private function getFullConfig(): array ], ]; } + + #[TestDox('Meilisearch store with custom semantic_ratio can be configured')] + public function testMeilisearchStoreWithCustomSemanticRatioCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'meilisearch' => [ + 'test_store' => [ + 'endpoint' => 'http://127.0.0.1:7700', + 'api_key' => 'test_key', + 'index_name' => 'test_index', + 'semantic_ratio' => 0.5, + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.meilisearch.test_store')); + $definition = $container->getDefinition('ai.store.meilisearch.test_store'); + $arguments = $definition->getArguments(); + $this->assertSame(0.5, $arguments[7]); + } } From 8b498f0eec3ceeb7e6320d5f4f14ea231439ffc5 Mon Sep 17 00:00:00 2001 From: Ahmed EBEN HASSINE Date: Wed, 15 Oct 2025 20:32:07 +0200 Subject: [PATCH 3/9] docs: update changelogs for meilisearch semantic_ratio --- src/ai-bundle/CHANGELOG.md | 1 + src/store/CHANGELOG.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/ai-bundle/CHANGELOG.md b/src/ai-bundle/CHANGELOG.md index f790ab27b..eb85ae83e 100644 --- a/src/ai-bundle/CHANGELOG.md +++ b/src/ai-bundle/CHANGELOG.md @@ -21,6 +21,7 @@ CHANGELOG - Platform credentials (API keys, endpoints) - Model configurations per agent - Vector store configurations + - Meilisearch `semantic_ratio` option for configuring hybrid search balance * Add dependency injection integration: - Autoconfiguration for tools and processors - Service aliases for default agent and platform diff --git a/src/store/CHANGELOG.md b/src/store/CHANGELOG.md index e337546bd..074497a18 100644 --- a/src/store/CHANGELOG.md +++ b/src/store/CHANGELOG.md @@ -59,5 +59,9 @@ CHANGELOG - Minimum score filtering - Result limiting - Distance/similarity scoring + * Add Meilisearch hybrid search support: + - Configurable `semanticRatio` parameter to control the balance between semantic (vector) and keyword (BM25) search + - Default ratio of 1.0 (100% semantic search) for backward compatibility + - Per-query override support via query options * Add custom exception hierarchy with `ExceptionInterface` * Add support for specific exceptions for invalid arguments and runtime errors From 65275c27bb99cffb7575e6c6078f3918b38ba50f Mon Sep 17 00:00:00 2001 From: Ahmed EBEN HASSINE Date: Wed, 15 Oct 2025 20:34:57 +0200 Subject: [PATCH 4/9] style(ai-bundle): apply php-cs-fixer on AiBundleTest.php --- .../DependencyInjection/AiBundleTest.php | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index 834ef1687..36d5f4b45 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -2741,6 +2741,30 @@ public function testVectorizerModelBooleanOptionsArePreserved() $this->assertSame('text-embedding-3-small?normalize=false&cache=true&nested%5Bbool%5D=false', $vectorizerDefinition->getArgument(1)); } + #[TestDox('Meilisearch store with custom semantic_ratio can be configured')] + public function testMeilisearchStoreWithCustomSemanticRatioCanBeConfigured() + { + $container = $this->buildContainer([ + 'ai' => [ + 'store' => [ + 'meilisearch' => [ + 'test_store' => [ + 'endpoint' => 'http://127.0.0.1:7700', + 'api_key' => 'test_key', + 'index_name' => 'test_index', + 'semantic_ratio' => 0.5, + ], + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.store.meilisearch.test_store')); + $definition = $container->getDefinition('ai.store.meilisearch.test_store'); + $arguments = $definition->getArguments(); + $this->assertSame(0.5, $arguments[7]); + } + private function buildContainer(array $configuration): ContainerBuilder { $container = new ContainerBuilder(); @@ -3061,28 +3085,4 @@ private function getFullConfig(): array ], ]; } - - #[TestDox('Meilisearch store with custom semantic_ratio can be configured')] - public function testMeilisearchStoreWithCustomSemanticRatioCanBeConfigured() - { - $container = $this->buildContainer([ - 'ai' => [ - 'store' => [ - 'meilisearch' => [ - 'test_store' => [ - 'endpoint' => 'http://127.0.0.1:7700', - 'api_key' => 'test_key', - 'index_name' => 'test_index', - 'semantic_ratio' => 0.5, - ], - ], - ], - ], - ]); - - $this->assertTrue($container->hasDefinition('ai.store.meilisearch.test_store')); - $definition = $container->getDefinition('ai.store.meilisearch.test_store'); - $arguments = $definition->getArguments(); - $this->assertSame(0.5, $arguments[7]); - } } From 25b4785fff15789f0283204b5645478383ecae88 Mon Sep 17 00:00:00 2001 From: Ahmed EBEN HASSINE Date: Wed, 15 Oct 2025 20:35:29 +0200 Subject: [PATCH 5/9] docs: align wording for meilisearch hybrid search --- src/ai-bundle/config/options.php | 2 +- src/store/CHANGELOG.md | 5 +---- src/store/src/Bridge/Meilisearch/Store.php | 4 ++-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index db2c409b7..c2229d37b 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -526,7 +526,7 @@ ->stringNode('vector_field')->end() ->integerNode('dimensions')->end() ->floatNode('semantic_ratio') - ->info('The ratio between semantic (vector) and keyword (BM25) search (0.0 to 1.0). Default: 1.0 (100% semantic)') + ->info('The ratio between semantic (vector) and full-text search (0.0 to 1.0). Default: 1.0 (100% semantic)') ->defaultValue(1.0) ->validate() ->ifTrue(fn ($v) => $v < 0.0 || $v > 1.0) diff --git a/src/store/CHANGELOG.md b/src/store/CHANGELOG.md index 074497a18..c18e69156 100644 --- a/src/store/CHANGELOG.md +++ b/src/store/CHANGELOG.md @@ -59,9 +59,6 @@ CHANGELOG - Minimum score filtering - Result limiting - Distance/similarity scoring - * Add Meilisearch hybrid search support: - - Configurable `semanticRatio` parameter to control the balance between semantic (vector) and keyword (BM25) search - - Default ratio of 1.0 (100% semantic search) for backward compatibility - - Per-query override support via query options + * Add Meilisearch hybrid search support with a configurable `semanticRatio` parameter to control the balance between semantic (vector) and full-text search. * Add custom exception hierarchy with `ExceptionInterface` * Add support for specific exceptions for invalid arguments and runtime errors diff --git a/src/store/src/Bridge/Meilisearch/Store.php b/src/store/src/Bridge/Meilisearch/Store.php index 80db42a31..9ce7049cf 100644 --- a/src/store/src/Bridge/Meilisearch/Store.php +++ b/src/store/src/Bridge/Meilisearch/Store.php @@ -29,8 +29,8 @@ /** * @param string $embedder The name of the embedder where vectors are stored * @param string $vectorFieldName The name of the field in the index that contains the vector - * @param float $semanticRatio The ratio between semantic (vector) and keyword (BM25) search (0.0 to 1.0) - * - 0.0 = 100% keyword search (BM25) + * @param float $semanticRatio The ratio between semantic (vector) and full-text search (0.0 to 1.0) + * - 0.0 = 100% full-text search * - 0.5 = balanced hybrid search * - 1.0 = 100% semantic search (vector only) */ From 6b01bddfd501614b867ded586b6cff14c04d8c58 Mon Sep 17 00:00:00 2001 From: Ahmed EBEN HASSINE Date: Thu, 16 Oct 2025 15:04:41 +0200 Subject: [PATCH 6/9] refactor: use static closures and inline query parameter --- src/ai-bundle/config/options.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index c2229d37b..2b42f4127 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -529,7 +529,7 @@ ->info('The ratio between semantic (vector) and full-text search (0.0 to 1.0). Default: 1.0 (100% semantic)') ->defaultValue(1.0) ->validate() - ->ifTrue(fn ($v) => $v < 0.0 || $v > 1.0) + ->ifTrue(static fn ($v) => $v < 0.0 || $v > 1.0) ->thenInvalid('The semantic ratio must be between 0.0 and 1.0.') ->end() ->end() From 686b6d7ebecbb6cfa075319533085379aaf1ac97 Mon Sep 17 00:00:00 2001 From: Ahmed EBEN HASSINE Date: Thu, 16 Oct 2025 15:05:00 +0200 Subject: [PATCH 7/9] optimize: remove intermediate variable in Meilisearch request --- src/store/src/Bridge/Meilisearch/Store.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/store/src/Bridge/Meilisearch/Store.php b/src/store/src/Bridge/Meilisearch/Store.php index 9ce7049cf..da4ec99d2 100644 --- a/src/store/src/Bridge/Meilisearch/Store.php +++ b/src/store/src/Bridge/Meilisearch/Store.php @@ -85,10 +85,8 @@ public function query(Vector $vector, array $options = []): array throw new InvalidArgumentException(\sprintf('The semantic ratio must be between 0.0 and 1.0, "%s" given.', $semanticRatio)); } - $queryText = $options['q'] ?? ''; - $result = $this->request('POST', \sprintf('indexes/%s/search', $this->indexName), [ - 'q' => $queryText, + 'q' => $options['q'] ?? '', 'vector' => $vector->getData(), 'showRankingScore' => true, 'retrieveVectors' => true, From 898d2ce29c3146f7201117ee7ca80d844be050e9 Mon Sep 17 00:00:00 2001 From: Ahmed EBEN HASSINE Date: Fri, 17 Oct 2025 09:40:02 +0200 Subject: [PATCH 8/9] refactor(ai-bundle): simplify semantic_ratio validation - Use min/max constraints instead of custom validation callback - Remove redundant comments from meilisearch example - Clean up changelog entry --- examples/rag/meilisearch.php | 2 -- src/ai-bundle/CHANGELOG.md | 1 - src/ai-bundle/config/options.php | 6 ++---- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/examples/rag/meilisearch.php b/examples/rag/meilisearch.php index b56edc12c..364317651 100644 --- a/examples/rag/meilisearch.php +++ b/examples/rag/meilisearch.php @@ -33,8 +33,6 @@ endpointUrl: env('MEILISEARCH_HOST'), apiKey: env('MEILISEARCH_API_KEY'), indexName: 'movies', - // Optional: configure hybrid search ratio (0.0 = keyword, 1.0 = semantic) - // semanticRatio: 0.5, // 50/50 hybrid search ); // create embeddings and documents diff --git a/src/ai-bundle/CHANGELOG.md b/src/ai-bundle/CHANGELOG.md index eb85ae83e..f790ab27b 100644 --- a/src/ai-bundle/CHANGELOG.md +++ b/src/ai-bundle/CHANGELOG.md @@ -21,7 +21,6 @@ CHANGELOG - Platform credentials (API keys, endpoints) - Model configurations per agent - Vector store configurations - - Meilisearch `semantic_ratio` option for configuring hybrid search balance * Add dependency injection integration: - Autoconfiguration for tools and processors - Service aliases for default agent and platform diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index 2b42f4127..9c95cbaac 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -528,10 +528,8 @@ ->floatNode('semantic_ratio') ->info('The ratio between semantic (vector) and full-text search (0.0 to 1.0). Default: 1.0 (100% semantic)') ->defaultValue(1.0) - ->validate() - ->ifTrue(static fn ($v) => $v < 0.0 || $v > 1.0) - ->thenInvalid('The semantic ratio must be between 0.0 and 1.0.') - ->end() + ->min(0.0) + ->max(1.0) ->end() ->end() ->end() From 72d529a89d774f69b01ae530db742c9bb60e0bf2 Mon Sep 17 00:00:00 2001 From: Ahmed EBEN HASSINE Date: Thu, 30 Oct 2025 15:14:15 +0100 Subject: [PATCH 9/9] docs: add meilisearch hybrid search example Demonstrates the configurable semanticRatio parameter by comparing results across different ratio values (0.0, 0.5, 1.0) and showing how to override the ratio per query. --- examples/rag/meilisearch-hybrid.php | 117 ++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 examples/rag/meilisearch-hybrid.php diff --git a/examples/rag/meilisearch-hybrid.php b/examples/rag/meilisearch-hybrid.php new file mode 100644 index 000000000..cb8346b66 --- /dev/null +++ b/examples/rag/meilisearch-hybrid.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\AI\Fixtures\Movies; +use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory; +use Symfony\AI\Store\Bridge\Meilisearch\Store; +use Symfony\AI\Store\Document\Loader\InMemoryLoader; +use Symfony\AI\Store\Document\Metadata; +use Symfony\AI\Store\Document\TextDocument; +use Symfony\AI\Store\Document\Vectorizer; +use Symfony\AI\Store\Indexer; +use Symfony\Component\Uid\Uuid; + +require_once dirname(__DIR__).'/bootstrap.php'; + +echo "=== Meilisearch Hybrid Search Demo ===\n\n"; +echo "This example demonstrates how to configure the semantic ratio to balance\n"; +echo "between semantic (vector) search and full-text search in Meilisearch.\n\n"; + +// Initialize the store with a balanced hybrid search (50/50) +$store = new Store( + httpClient: http_client(), + endpointUrl: env('MEILISEARCH_HOST'), + apiKey: env('MEILISEARCH_API_KEY'), + indexName: 'movies_hybrid', + semanticRatio: 0.5, // Balanced hybrid search by default +); + +// 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), + ); +} + +// Initialize the index +$store->setup(); + +// Create embeddings for documents +$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client()); +$vectorizer = new Vectorizer($platform, 'text-embedding-3-small', logger()); +$indexer = new Indexer(new InMemoryLoader($documents), $vectorizer, $store, logger: logger()); +$indexer->index($documents); + +// Create a query embedding +$queryText = 'futuristic technology and artificial intelligence'; +echo "Query: \"$queryText\"\n\n"; +$queryEmbedding = $vectorizer->vectorize($queryText); + +// Test different semantic ratios to compare results +$ratios = [ + ['ratio' => 0.0, 'description' => '100% Full-text search (keyword matching)'], + ['ratio' => 0.5, 'description' => 'Balanced hybrid (50% semantic + 50% full-text)'], + ['ratio' => 1.0, 'description' => '100% Semantic search (vector similarity)'], +]; + +foreach ($ratios as $config) { + echo "--- {$config['description']} ---\n"; + + // Override the semantic ratio for this specific query + $results = $store->query($queryEmbedding, [ + 'semanticRatio' => $config['ratio'], + 'q' => 'technology', // Full-text search keyword + ]); + + echo "Top 3 results:\n"; + foreach (array_slice($results, 0, 3) as $i => $result) { + $metadata = $result->metadata->getArrayCopy(); + echo sprintf( + " %d. %s (Score: %.4f)\n", + $i + 1, + $metadata['title'] ?? 'Unknown', + $result->score ?? 0.0 + ); + } + echo "\n"; +} + +echo "--- Custom query with pure semantic search ---\n"; +echo "Query: Movies about space exploration\n"; +$spaceEmbedding = $vectorizer->vectorize('space exploration and cosmic adventures'); +$results = $store->query($spaceEmbedding, [ + 'semanticRatio' => 1.0, // Pure semantic search +]); + +echo "Top 3 results:\n"; +foreach (array_slice($results, 0, 3) as $i => $result) { + $metadata = $result->metadata->getArrayCopy(); + echo sprintf( + " %d. %s (Score: %.4f)\n", + $i + 1, + $metadata['title'] ?? 'Unknown', + $result->score ?? 0.0 + ); +} +echo "\n"; + +// Cleanup +$store->drop(); + +echo "=== Summary ===\n"; +echo "- semanticRatio = 0.0: Best for exact keyword matches\n"; +echo "- semanticRatio = 0.5: Balanced approach combining both methods\n"; +echo "- semanticRatio = 1.0: Best for conceptual similarity searches\n"; +echo "\nYou can set the default ratio when instantiating the Store,\n"; +echo "and override it per query using the 'semanticRatio' option.\n";