Skip to content

Commit e538aec

Browse files
committed
Add ElasticSearch Store support
1 parent f2f71ae commit e538aec

File tree

14 files changed

+605
-0
lines changed

14 files changed

+605
-0
lines changed

examples/.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,5 +187,8 @@ REDIS_HOST=localhost
187187
# Manticore Search (store)
188188
MANTICORESEARCH_HOST=http://127.0.0.1:9308
189189

190+
# Elasticsearch (store)
191+
ELASTICSEARCH_ENDPOINT=http://127.0.0.1:9201
192+
190193
# OpenSearch (store)
191194
OPENSEARCH_ENDPOINT=http://127.0.0.1:9200

examples/compose.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,28 @@ services:
124124
- '7474:7474'
125125
- '7687:7687'
126126

127+
elasticsearch:
128+
image: docker.elastic.co/elasticsearch/elasticsearch:8.17.0
129+
environment:
130+
discovery.type: 'single-node'
131+
xpack.security.enabled: false
132+
ES_JAVA_OPTS: '-Xms512m -Xmx512m'
133+
ulimits:
134+
memlock:
135+
soft: -1
136+
hard: -1
137+
nofile:
138+
soft: 65536
139+
hard: 65536
140+
healthcheck:
141+
test: [ 'CMD', 'curl', "-f", "http://127.0.0.1:9201/_cluster/health" ]
142+
interval: 30s
143+
start_period: 120s
144+
timeout: 20s
145+
retries: 3
146+
ports:
147+
- '9201:9200'
148+
127149
opensearch:
128150
image: opensearchproject/opensearch
129151
environment:

examples/rag/elasticsearch.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
use Symfony\AI\Agent\Agent;
13+
use Symfony\AI\Agent\Bridge\SimilaritySearch\SimilaritySearch;
14+
use Symfony\AI\Agent\Toolbox\AgentProcessor;
15+
use Symfony\AI\Agent\Toolbox\Toolbox;
16+
use Symfony\AI\Fixtures\Movies;
17+
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
18+
use Symfony\AI\Platform\Message\Message;
19+
use Symfony\AI\Platform\Message\MessageBag;
20+
use Symfony\AI\Store\Bridge\Elasticsearch\Store;
21+
use Symfony\AI\Store\Document\Loader\InMemoryLoader;
22+
use Symfony\AI\Store\Document\Metadata;
23+
use Symfony\AI\Store\Document\TextDocument;
24+
use Symfony\AI\Store\Document\Vectorizer;
25+
use Symfony\AI\Store\Indexer;
26+
use Symfony\Component\Uid\Uuid;
27+
28+
require_once dirname(__DIR__).'/bootstrap.php';
29+
30+
// initialize the store
31+
$store = new Store(
32+
httpClient: http_client(),
33+
endpoint: env('ELASTICSEARCH_ENDPOINT'),
34+
indexName: 'movies',
35+
);
36+
37+
// create embeddings and documents
38+
$documents = [];
39+
foreach (Movies::all() as $i => $movie) {
40+
$documents[] = new TextDocument(
41+
id: Uuid::v4(),
42+
content: 'Title: '.$movie['title'].\PHP_EOL.'Director: '.$movie['director'].\PHP_EOL.'Description: '.$movie['description'],
43+
metadata: new Metadata($movie),
44+
);
45+
}
46+
47+
// initialize the index
48+
$store->setup();
49+
50+
// create embeddings for documents
51+
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
52+
$vectorizer = new Vectorizer($platform, 'text-embedding-3-small', logger());
53+
$indexer = new Indexer(new InMemoryLoader($documents), $vectorizer, $store, logger: logger());
54+
$indexer->index($documents);
55+
56+
$similaritySearch = new SimilaritySearch($vectorizer, $store);
57+
$toolbox = new Toolbox([$similaritySearch], logger: logger());
58+
$processor = new AgentProcessor($toolbox);
59+
$agent = new Agent($platform, 'gpt-4o-mini', [$processor], [$processor]);
60+
61+
$messages = new MessageBag(
62+
Message::forSystem('Please answer all user questions only using SimilaritySearch function.'),
63+
Message::ofUser('Which movie fits the theme of technology?')
64+
);
65+
$result = $agent->call($messages);
66+
67+
echo $result->getContent().\PHP_EOL;

src/ai-bundle/config/options.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,27 @@
752752
->end()
753753
->end()
754754
->end()
755+
->arrayNode('elasticsearch')
756+
->useAttributeAsKey('name')
757+
->arrayPrototype()
758+
->children()
759+
->stringNode('endpoint')->cannotBeEmpty()->end()
760+
->stringNode('index_name')->end()
761+
->stringNode('vectors_field')
762+
->defaultValue('_vectors')
763+
->end()
764+
->integerNode('dimensions')
765+
->defaultValue(1536)
766+
->end()
767+
->stringNode('similarity')
768+
->defaultValue('cosine')
769+
->end()
770+
->stringNode('http_client')
771+
->defaultValue('http_client')
772+
->end()
773+
->end()
774+
->end()
775+
->end()
755776
->arrayNode('opensearch')
756777
->useAttributeAsKey('name')
757778
->arrayPrototype()

src/ai-bundle/src/AiBundle.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
use Symfony\AI\Store\Bridge\ChromaDb\Store as ChromaDbStore;
8686
use Symfony\AI\Store\Bridge\ClickHouse\Store as ClickHouseStore;
8787
use Symfony\AI\Store\Bridge\Cloudflare\Store as CloudflareStore;
88+
use Symfony\AI\Store\Bridge\Elasticsearch\Store as ElasticsearchStore;
8889
use Symfony\AI\Store\Bridge\ManticoreSearch\Store as ManticoreSearchStore;
8990
use Symfony\AI\Store\Bridge\MariaDb\Store as MariaDbStore;
9091
use Symfony\AI\Store\Bridge\Meilisearch\Store as MeilisearchStore;
@@ -1379,6 +1380,33 @@ private function processStoreConfig(string $type, array $stores, ContainerBuilde
13791380
}
13801381
}
13811382

1383+
if ('elasticsearch' === $type) {
1384+
if (!ContainerBuilder::willBeAvailable('symfony/ai-elasticsearch-store', ElasticsearchStore::class, ['symfony/ai-bundle'])) {
1385+
throw new RuntimeException('Elasticsearch store configuration requires "symfony/ai-elasticsearch-store" package. Try running "composer require symfony/ai-elasticsearch-store".');
1386+
}
1387+
1388+
foreach ($stores as $name => $store) {
1389+
$definition = new Definition(ElasticsearchStore::class);
1390+
$definition
1391+
->setLazy(true)
1392+
->setArguments([
1393+
new Reference($store['http_client']),
1394+
$store['endpoint'],
1395+
$store['index_name'] ?? $name,
1396+
$store['vectors_field'],
1397+
$store['dimensions'],
1398+
$store['similarity'],
1399+
])
1400+
->addTag('proxy', ['interface' => StoreInterface::class])
1401+
->addTag('proxy', ['interface' => ManagedStoreInterface::class])
1402+
->addTag('ai.store');
1403+
1404+
$container->setDefinition('ai.store.'.$type.'.'.$name, $definition);
1405+
$container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $name);
1406+
$container->registerAliasForArgument('ai.store.'.$type.'.'.$name, StoreInterface::class, $type.'_'.$name);
1407+
}
1408+
}
1409+
13821410
if ('opensearch' === $type) {
13831411
if (!ContainerBuilder::willBeAvailable('symfony/ai-open-search-store', OpenSearchStore::class, ['symfony/ai-bundle'])) {
13841412
throw new RuntimeException('OpenSearch store configuration requires "symfony/ai-open-search-store" package. Try running "composer require symfony/ai-open-search-store".');

src/store/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ CHANGELOG
3838
- ChromaDB
3939
- ClickHouse
4040
- Cloudflare
41+
- Elasticsearch
4142
- Manticore Search
4243
- MariaDB
4344
- Meilisearch

src/store/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"chromadb",
1010
"clickhouse",
1111
"cloudflare",
12+
"elasticsearch",
1213
"mariadb",
1314
"meilisearch",
1415
"milvus",
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
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
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.

0 commit comments

Comments
 (0)