Skip to content

Commit 954d44d

Browse files
committed
refactor(store): rename PostgresHybridStore to HybridStore
- Rename class from PostgresHybridStore to HybridStore - The namespace already indicates it's Postgres-specific - Add postgres-hybrid.php RAG example demonstrating: * Different semantic ratios (0.0, 0.5, 1.0) * RRF (Reciprocal Rank Fusion) hybrid search * Full-text search with 'q' parameter * Per-query semanticRatio override
1 parent e33b2c2 commit 954d44d

File tree

3 files changed

+145
-19
lines changed

3 files changed

+145
-19
lines changed

examples/rag/postgres-hybrid.php

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
use Doctrine\DBAL\DriverManager;
13+
use Doctrine\DBAL\Tools\DsnParser;
14+
use Symfony\AI\Fixtures\Movies;
15+
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
16+
use Symfony\AI\Store\Bridge\Postgres\HybridStore;
17+
use Symfony\AI\Store\Document\Loader\InMemoryLoader;
18+
use Symfony\AI\Store\Document\Metadata;
19+
use Symfony\AI\Store\Document\TextDocument;
20+
use Symfony\AI\Store\Document\Vectorizer;
21+
use Symfony\AI\Store\Indexer;
22+
use Symfony\Component\Uid\Uuid;
23+
24+
require_once dirname(__DIR__).'/bootstrap.php';
25+
26+
echo "=== PostgreSQL Hybrid Search Demo ===\n\n";
27+
echo "This example demonstrates how to configure the semantic ratio to balance\n";
28+
echo "between semantic (vector) search and PostgreSQL Full-Text Search.\n\n";
29+
30+
// Initialize the hybrid store with balanced search (50/50)
31+
$connection = DriverManager::getConnection((new DsnParser())->parse(env('POSTGRES_URI')));
32+
$pdo = $connection->getNativeConnection();
33+
34+
if (!$pdo instanceof PDO) {
35+
throw new RuntimeException('Unable to get native PDO connection from Doctrine DBAL');
36+
}
37+
38+
$store = new HybridStore(
39+
connection: $pdo,
40+
tableName: 'hybrid_movies',
41+
semanticRatio: 0.5, // Balanced hybrid search by default
42+
);
43+
44+
// Create embeddings and documents
45+
$documents = [];
46+
foreach (Movies::all() as $i => $movie) {
47+
$documents[] = new TextDocument(
48+
id: Uuid::v4(),
49+
content: 'Title: '.$movie['title'].\PHP_EOL.'Director: '.$movie['director'].\PHP_EOL.'Description: '.$movie['description'],
50+
metadata: new Metadata(array_merge($movie, ['content' => 'Title: '.$movie['title'].\PHP_EOL.'Director: '.$movie['director'].\PHP_EOL.'Description: '.$movie['description']])),
51+
);
52+
}
53+
54+
// Initialize the table
55+
$store->setup();
56+
57+
// Create embeddings for documents
58+
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
59+
$vectorizer = new Vectorizer($platform, 'text-embedding-3-small', logger());
60+
$indexer = new Indexer(new InMemoryLoader($documents), $vectorizer, $store, logger: logger());
61+
$indexer->index($documents);
62+
63+
// Create a query embedding
64+
$queryText = 'futuristic technology and artificial intelligence';
65+
echo "Query: \"$queryText\"\n\n";
66+
$queryEmbedding = $vectorizer->vectorize($queryText);
67+
68+
// Test different semantic ratios to compare results
69+
$ratios = [
70+
['ratio' => 0.0, 'description' => '100% Full-text search (keyword matching)'],
71+
['ratio' => 0.5, 'description' => 'Balanced hybrid (RRF: 50% semantic + 50% FTS)'],
72+
['ratio' => 1.0, 'description' => '100% Semantic search (vector similarity)'],
73+
];
74+
75+
foreach ($ratios as $config) {
76+
echo "--- {$config['description']} ---\n";
77+
78+
// Override the semantic ratio for this specific query
79+
$results = $store->query($queryEmbedding, [
80+
'semanticRatio' => $config['ratio'],
81+
'q' => 'technology', // Full-text search keyword
82+
'limit' => 3,
83+
]);
84+
85+
echo "Top 3 results:\n";
86+
foreach ($results as $i => $result) {
87+
$metadata = $result->metadata->getArrayCopy();
88+
echo sprintf(
89+
" %d. %s (Score: %.4f)\n",
90+
$i + 1,
91+
$metadata['title'] ?? 'Unknown',
92+
$result->score ?? 0.0
93+
);
94+
}
95+
echo "\n";
96+
}
97+
98+
echo "--- Custom query with pure semantic search ---\n";
99+
echo "Query: Movies about space exploration\n";
100+
$spaceEmbedding = $vectorizer->vectorize('space exploration and cosmic adventures');
101+
$results = $store->query($spaceEmbedding, [
102+
'semanticRatio' => 1.0, // Pure semantic search
103+
'limit' => 3,
104+
]);
105+
106+
echo "Top 3 results:\n";
107+
foreach ($results as $i => $result) {
108+
$metadata = $result->metadata->getArrayCopy();
109+
echo sprintf(
110+
" %d. %s (Score: %.4f)\n",
111+
$i + 1,
112+
$metadata['title'] ?? 'Unknown',
113+
$result->score ?? 0.0
114+
);
115+
}
116+
echo "\n";
117+
118+
// Cleanup
119+
$store->drop();
120+
121+
echo "=== Summary ===\n";
122+
echo "- semanticRatio = 0.0: Best for exact keyword matches (PostgreSQL FTS)\n";
123+
echo "- semanticRatio = 0.5: Balanced approach using RRF (Reciprocal Rank Fusion)\n";
124+
echo "- semanticRatio = 1.0: Best for conceptual similarity searches (pgvector)\n";
125+
echo "\nYou can set the default ratio when instantiating the HybridStore,\n";
126+
echo "and override it per query using the 'semanticRatio' option.\n";

src/store/src/Bridge/Postgres/PostgresHybridStore.php renamed to src/store/src/Bridge/Postgres/HybridStore.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
*
3636
* @author Ahmed EBEN HASSINE <ahmedbhs123@gmail.com>
3737
*/
38-
final readonly class PostgresHybridStore implements ManagedStoreInterface, StoreInterface
38+
final readonly class HybridStore implements ManagedStoreInterface, StoreInterface
3939
{
4040
/**
4141
* @param string $vectorFieldName Name of the vector field

src/store/tests/Bridge/Postgres/PostgresHybridStoreTest.php renamed to src/store/tests/Bridge/Postgres/HybridStoreTest.php

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,21 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\AI\Platform\Vector\Vector;
16-
use Symfony\AI\Store\Bridge\Postgres\PostgresHybridStore;
16+
use Symfony\AI\Store\Bridge\Postgres\HybridStore;
1717
use Symfony\AI\Store\Document\Metadata;
1818
use Symfony\AI\Store\Document\VectorDocument;
1919
use Symfony\AI\Store\Exception\InvalidArgumentException;
2020
use Symfony\Component\Uid\Uuid;
2121

22-
final class PostgresHybridStoreTest extends TestCase
22+
final class HybridStoreTest extends TestCase
2323
{
2424
public function testConstructorValidatesSemanticRatio()
2525
{
2626
$this->expectException(InvalidArgumentException::class);
2727
$this->expectExceptionMessage('The semantic ratio must be between 0.0 and 1.0');
2828

2929
$pdo = $this->createMock(\PDO::class);
30-
new PostgresHybridStore($pdo, 'test_table', semanticRatio: 1.5);
30+
new HybridStore($pdo, 'test_table', semanticRatio: 1.5);
3131
}
3232

3333
public function testConstructorValidatesNegativeSemanticRatio()
@@ -36,13 +36,13 @@ public function testConstructorValidatesNegativeSemanticRatio()
3636
$this->expectExceptionMessage('The semantic ratio must be between 0.0 and 1.0');
3737

3838
$pdo = $this->createMock(\PDO::class);
39-
new PostgresHybridStore($pdo, 'test_table', semanticRatio: -0.5);
39+
new HybridStore($pdo, 'test_table', semanticRatio: -0.5);
4040
}
4141

4242
public function testSetupCreatesTableWithFullTextSearchSupport()
4343
{
4444
$pdo = $this->createMock(\PDO::class);
45-
$store = new PostgresHybridStore($pdo, 'hybrid_table');
45+
$store = new HybridStore($pdo, 'hybrid_table');
4646

4747
$pdo->expects($this->exactly(4))
4848
->method('exec')
@@ -75,7 +75,7 @@ public function testAddDocument()
7575
$pdo = $this->createMock(\PDO::class);
7676
$statement = $this->createMock(\PDOStatement::class);
7777

78-
$store = new PostgresHybridStore($pdo, 'hybrid_table');
78+
$store = new HybridStore($pdo, 'hybrid_table');
7979

8080
$expectedSql = 'INSERT INTO hybrid_table (id, metadata, content, embedding)
8181
VALUES (:id, :metadata, :content, :vector)
@@ -112,7 +112,7 @@ public function testPureVectorSearch()
112112
$pdo = $this->createMock(\PDO::class);
113113
$statement = $this->createMock(\PDOStatement::class);
114114

115-
$store = new PostgresHybridStore($pdo, 'hybrid_table', semanticRatio: 1.0);
115+
$store = new HybridStore($pdo, 'hybrid_table', semanticRatio: 1.0);
116116

117117
$expectedSql = 'SELECT id, embedding AS embedding, metadata, (embedding <-> :embedding) AS score
118118
FROM hybrid_table
@@ -157,7 +157,7 @@ public function testPureKeywordSearch()
157157
$pdo = $this->createMock(\PDO::class);
158158
$statement = $this->createMock(\PDOStatement::class);
159159

160-
$store = new PostgresHybridStore($pdo, 'hybrid_table', semanticRatio: 0.0);
160+
$store = new HybridStore($pdo, 'hybrid_table', semanticRatio: 0.0);
161161

162162
$expectedSql = "SELECT id, embedding AS embedding, metadata,
163163
(1.0 / (1.0 + ts_rank_cd(content_tsv, websearch_to_tsquery('simple', :query)))) AS score
@@ -204,7 +204,7 @@ public function testHybridSearchWithRRF()
204204
$pdo = $this->createMock(\PDO::class);
205205
$statement = $this->createMock(\PDOStatement::class);
206206

207-
$store = new PostgresHybridStore($pdo, 'hybrid_table', semanticRatio: 0.5, rrfK: 60);
207+
$store = new HybridStore($pdo, 'hybrid_table', semanticRatio: 0.5, rrfK: 60);
208208

209209
$pdo->expects($this->once())
210210
->method('prepare')
@@ -252,7 +252,7 @@ public function testQueryWithDefaultMaxScore()
252252
$pdo = $this->createMock(\PDO::class);
253253
$statement = $this->createMock(\PDOStatement::class);
254254

255-
$store = new PostgresHybridStore(
255+
$store = new HybridStore(
256256
$pdo,
257257
'hybrid_table',
258258
semanticRatio: 1.0,
@@ -291,7 +291,7 @@ public function testQueryWithMaxScoreOverride()
291291
$pdo = $this->createMock(\PDO::class);
292292
$statement = $this->createMock(\PDOStatement::class);
293293

294-
$store = new PostgresHybridStore(
294+
$store = new HybridStore(
295295
$pdo,
296296
'hybrid_table',
297297
semanticRatio: 1.0,
@@ -324,7 +324,7 @@ public function testQueryWithCustomLanguage()
324324
$pdo = $this->createMock(\PDO::class);
325325
$statement = $this->createMock(\PDOStatement::class);
326326

327-
$store = new PostgresHybridStore($pdo, 'hybrid_table', semanticRatio: 0.0, language: 'french');
327+
$store = new HybridStore($pdo, 'hybrid_table', semanticRatio: 0.0, language: 'french');
328328

329329
$pdo->expects($this->once())
330330
->method('prepare')
@@ -351,7 +351,7 @@ public function testQueryWithCustomRRFK()
351351
$pdo = $this->createMock(\PDO::class);
352352
$statement = $this->createMock(\PDOStatement::class);
353353

354-
$store = new PostgresHybridStore($pdo, 'hybrid_table', semanticRatio: 0.5, rrfK: 100);
354+
$store = new HybridStore($pdo, 'hybrid_table', semanticRatio: 0.5, rrfK: 100);
355355

356356
$pdo->expects($this->once())
357357
->method('prepare')
@@ -377,7 +377,7 @@ public function testQueryWithCustomRRFK()
377377
public function testQueryInvalidSemanticRatioInOptions()
378378
{
379379
$pdo = $this->createMock(\PDO::class);
380-
$store = new PostgresHybridStore($pdo, 'hybrid_table');
380+
$store = new HybridStore($pdo, 'hybrid_table');
381381

382382
$this->expectException(InvalidArgumentException::class);
383383
$this->expectExceptionMessage('The semantic ratio must be between 0.0 and 1.0');
@@ -388,7 +388,7 @@ public function testQueryInvalidSemanticRatioInOptions()
388388
public function testDrop()
389389
{
390390
$pdo = $this->createMock(\PDO::class);
391-
$store = new PostgresHybridStore($pdo, 'hybrid_table');
391+
$store = new HybridStore($pdo, 'hybrid_table');
392392

393393
$pdo->expects($this->once())
394394
->method('exec')
@@ -402,7 +402,7 @@ public function testQueryWithCustomLimit()
402402
$pdo = $this->createMock(\PDO::class);
403403
$statement = $this->createMock(\PDOStatement::class);
404404

405-
$store = new PostgresHybridStore($pdo, 'hybrid_table', semanticRatio: 1.0);
405+
$store = new HybridStore($pdo, 'hybrid_table', semanticRatio: 1.0);
406406

407407
$pdo->expects($this->once())
408408
->method('prepare')
@@ -429,7 +429,7 @@ public function testAddMultipleDocuments()
429429
$pdo = $this->createMock(\PDO::class);
430430
$statement = $this->createMock(\PDOStatement::class);
431431

432-
$store = new PostgresHybridStore($pdo, 'hybrid_table');
432+
$store = new HybridStore($pdo, 'hybrid_table');
433433

434434
$pdo->expects($this->once())
435435
->method('prepare')
@@ -469,7 +469,7 @@ public function testPureKeywordSearchReturnsEmptyWhenNoMatch()
469469
$pdo = $this->createMock(\PDO::class);
470470
$statement = $this->createMock(\PDOStatement::class);
471471

472-
$store = new PostgresHybridStore($pdo, 'hybrid_table', semanticRatio: 0.0);
472+
$store = new HybridStore($pdo, 'hybrid_table', semanticRatio: 0.0);
473473

474474
$pdo->expects($this->once())
475475
->method('prepare')

0 commit comments

Comments
 (0)