lochmueller / seal-ai
SEAL Search AI - Vector based search based on EXT:seal
Package info
github.com/lochmueller/seal_ai
Type:typo3-cms-extension
pkg:composer/lochmueller/seal-ai
Requires
- php: ^8.3
- lochmueller/seal: ~2.0.3
- symfony/ai-platform: ^0.14
- symfony/ai-store: ^0.14
- typo3/cms-core: ^13.4 || ^14.3
Requires (Dev)
- b13/aim: ^0.5
- friendsofphp/php-cs-fixer: ^3.84
- mcp/sdk: ^0.8
- phpstan/phpstan: ^2.1
- symfony/ai-ai-ml-api-platform: ^0.14
- symfony/ai-albert-platform: ^0.14
- symfony/ai-anthropic-platform: ^0.14
- symfony/ai-azure-platform: ^0.14
- symfony/ai-azure-search-store: ^0.14
- symfony/ai-bedrock-platform: ^0.14
- symfony/ai-cache-message-store: ^0.14
- symfony/ai-cache-platform: ^0.14
- symfony/ai-cache-store: ^0.14
- symfony/ai-cartesia-platform: ^0.14
- symfony/ai-cerebras-platform: ^0.14
- symfony/ai-chroma-db-store: ^0.14
- symfony/ai-click-house-store: ^0.14
- symfony/ai-cloudflare-message-store: ^0.14
- symfony/ai-cloudflare-store: ^0.14
- symfony/ai-cohere-platform: ^0.14
- symfony/ai-decart-platform: ^0.14
- symfony/ai-deep-seek-platform: ^0.14
- symfony/ai-docker-model-runner-platform: ^0.14
- symfony/ai-doctrine-message-store: ^0.14
- symfony/ai-eden-ai-platform: ^0.14
- symfony/ai-elasticsearch-store: ^0.14
- symfony/ai-eleven-labs-platform: ^0.14
- symfony/ai-failover-platform: ^0.14
- symfony/ai-gemini-platform: ^0.14
- symfony/ai-generic-platform: ^0.14
- symfony/ai-hugging-face-platform: ^0.14
- symfony/ai-lm-studio-platform: ^0.14
- symfony/ai-manticore-search-store: ^0.14
- symfony/ai-maria-db-store: ^0.14
- symfony/ai-meilisearch-message-store: ^0.14
- symfony/ai-meilisearch-store: ^0.14
- symfony/ai-meta-platform: ^0.14
- symfony/ai-milvus-store: ^0.14
- symfony/ai-mistral-platform: ^0.14
- symfony/ai-mongo-db-message-store: ^0.14
- symfony/ai-mongo-db-store: ^0.14
- symfony/ai-neo4j-store: ^0.14
- symfony/ai-ollama-platform: ^0.14
- symfony/ai-open-ai-platform: ^0.14
- symfony/ai-open-responses-platform: ^0.14
- symfony/ai-open-router-platform: ^0.14
- symfony/ai-open-search-store: ^0.14
- symfony/ai-perplexity-platform: ^0.14
- symfony/ai-pogocache-message-store: ^0.14
- symfony/ai-postgres-store: ^0.14
- symfony/ai-qdrant-store: ^0.14
- symfony/ai-redis-message-store: ^0.14
- symfony/ai-redis-store: ^0.14
- symfony/ai-replicate-platform: ^0.14
- symfony/ai-s3vectors-store: ^0.14
- symfony/ai-scaleway-platform: ^0.14
- symfony/ai-session-message-store: ^0.14
- symfony/ai-sqlite-store: ^0.14
- symfony/ai-supabase-store: ^0.14
- symfony/ai-surreal-db-message-store: ^0.14
- symfony/ai-surreal-db-store: ^0.14
- symfony/ai-transformers-php-platform: ^0.14
- symfony/ai-typesense-store: ^0.14
- symfony/ai-vektor-store: ^0.14
- symfony/ai-vertex-ai-platform: ^0.14
- symfony/ai-voyage-platform: ^0.14
- symfony/ai-weaviate-store: ^0.14
- typo3/testing-framework: ^8.0 || ^9.3
Suggests
Provides
None
Conflicts
None
Replaces
None
README
AI Vector search integration for EXT:seal based on symfony/ai.
Enables semantic (vector-based) search in TYPO3 by bridging the SEAL search abstraction with the Symfony AI platform and store components. Documents are automatically vectorized via configurable embedding models and stored in a vector database of your choice.
This extension was funded by the TYPO3 Association: community ideas I / community idea II & first blogpost / second blogpost
Requirements
Installation
composer require lochmueller/seal-ai
Additionally, install the symfony/ai packages for your chosen platform and store (see Supported Platforms and Supported Stores).
Configuration
- Set the SEAL search adapter to
ai://in your site configuration. - Configure the two DSN fields added to the TYPO3 site configuration module:
- AI Platform DSN (
sealAiPlatformDsn) — connection to the embedding/AI provider - AI Store DSN (
sealAiStoreDsn) — connection to the vector store
- AI Platform DSN (
The platform DSN must include a model query parameter specifying the embedding model:
openai://YOUR_API_KEY@default?model=text-embedding-3-small
DSN Format
scheme://user@host:port?param1=value1¶m2=value2
scheme— provider identifier (e.g.openai,qdrant)user— API key (where applicable)host/port— server address- Query parameters — provider-specific options
Supported Platforms
Each platform requires its own composer package. Install only what you need.
| Scheme | Package | Example DSN |
|---|---|---|
openai |
symfony/ai-open-ai-platform |
openai://api-key@default?model=text-embedding-3-small |
anthropic |
symfony/ai-anthropic-platform |
anthropic://api-key@default |
gemini |
symfony/ai-gemini-platform |
gemini://api-key@default |
openrouter |
symfony/ai-open-router-platform |
openrouter://api-key@default?model=gemini-embedding-001 |
vertex |
symfony/ai-vertex-ai-platform |
vertex://location/project-id?api_key=key |
bedrock |
symfony/ai-bedrock-platform |
bedrock://default |
mistral |
symfony/ai-mistral-platform |
mistral://api-key@default |
ollama |
symfony/ai-ollama-platform |
ollama://localhost:11434 |
huggingface |
symfony/ai-hugging-face-platform |
huggingface://api-key@default?provider=hf_inference |
replicate |
symfony/ai-replicate-platform |
replicate://api-key@default |
lmstudio |
symfony/ai-lm-studio-platform |
lmstudio://localhost:1234 |
deepseek |
symfony/ai-deep-seek-platform |
deepseek://api-key@default |
voyage |
symfony/ai-voyage-platform |
voyage://api-key@default |
cohere |
symfony/ai-cohere-platform |
cohere://api-key@default |
albert |
symfony/ai-albert-platform |
albert://api-key@host |
cartesia |
symfony/ai-cartesia-platform |
cartesia://api-key@default?version=v1 |
elevenlabs |
symfony/ai-eleven-labs-platform |
elevenlabs://api-key@host |
perplexity |
symfony/ai-perplexity-platform |
perplexity://api-key@default |
scaleway |
symfony/ai-scaleway-platform |
scaleway://api-key@default |
cerebras |
symfony/ai-cerebras-platform |
cerebras://api-key@default |
edenai |
symfony/ai-eden-ai-platform |
edenai://api-key@default?model=openai/text-embedding-3-small |
decart |
symfony/ai-decart-platform |
decart://api-key@host |
aimlapi |
symfony/ai-ai-ml-api-platform |
aimlapi://api-key@host |
docker |
symfony/ai-docker-model-runner-platform |
docker://localhost:12434 |
transformers |
symfony/ai-transformers-php-platform |
transformers:// |
generic |
symfony/ai-generic-platform |
generic://host?api_key=key |
azure-openai |
symfony/ai-azure-platform |
azure-openai://api-key@host?deployment=dep&api_version=ver |
azure-meta |
symfony/ai-azure-platform |
azure-meta://api-key@host |
openresponses |
symfony/ai-open-responses-platform |
openresponses://api-key@host?path=/v1/responses |
failover |
symfony/ai-failover-platform |
failover://default?platforms[]=...&platforms[]=...&model=... |
Failover platform
The failover scheme wraps several platforms, which are tried in the given order until one succeeds.
Each fallback is a nested platform DSN in the platforms[] query parameter. URL-encode nested DSNs that
contain their own ? / &. The model parameter belongs to the outer DSN and is used for all platforms.
failover://default?platforms[]=openai://sk-key@default&platforms[]=ollama://localhost:11434&model=text-embedding-3-small
All platforms have to provide the same embedding model (or at least one with identical dimensions), otherwise the stored vectors are not comparable.
AiM integration (EXT:aim)
If b13/aim is installed (composer require b13/aim), embeddings and search summaries
(SGE) can be routed through the AiM proxy instead of a directly configured platform. Requests then go through the AiM
middleware pipeline (request logging, cost tracking, budgets, fallback) and providers / API keys are managed in the
AiM backend module.
ai://aim
ai://aim?model=openai:text-embedding-3-small&dimensions=768
model— AiM provider notation for the embedding requests (optional, defaults to the AiM default embedding provider)dimensions— requested vector dimensions (optional)
With an ai://aim platform DSN, sealAiChatModel is an AiM provider notation as well (e.g. anthropic:claude-sonnet-4).
Use default to use the AiM default conversation provider. Leave it empty to disable SGE.
AiM does not provide a vector store, so sealAiStoreDsn has to be configured as usual.
Pin a concrete embedding model (
provider:model). If AiM reroutes embedding requests to another model, the vector dimensions may change and the stored vectors are no longer comparable.
Embedding cache
Embeddings of indexed content are cached in the TYPO3 caching framework (cache seal_ai_embedding). The cache key is a
SHA-256 hash of the normalized text (Unicode NFC, collapsed whitespace, without control / zero-width characters) plus
the platform scheme, model and dimensions. Unchanged content is not sent to the AI platform again, so a full
re-index only costs tokens for new or changed documents. Search queries are not cached.
- Enabled by default, can be disabled per site (AI Embedding Cache,
sealAiEmbeddingCache). - Database backend with 30 days lifetime by default. Run "Analyze Database Structure" once to create the cache tables.
Backend and lifetime can be changed via
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['seal_ai_embedding']. - The cache is in its own group
seal_ai, so "Flush frontend caches" keeps the embeddings. Flush it withvendor/bin/typo3 cache:flush --group seal_ai. Expired entries are removed by the scheduler task "Caching framework garbage collection". - A changed model or dimension results in new cache keys automatically. Entries are tagged with
seal_ai_config_<fingerprint>.
MCP server (mcp/sdk)
If mcp/sdk is installed (composer require mcp/sdk), every site can
provide a read-only MCP server, so AI agents (Claude, ChatGPT, Cursor, ...) can
use the website search. The server uses the SEAL engine of the site, so it works with every SEAL adapter (not only ai://).
- Set a token in the site configuration (MCP Server Token,
sealAiMcpToken). Without a token the server is disabled. Use%env(SEAL_AI_MCP_TOKEN)%to keep the token out of theconfig.yaml. - Connect the client to
<site/language base>/seal/mcp(Streamable HTTP) with the headerAuthorization: Bearer <token>.
{
"mcpServers": {
"my-website": {
"type": "http",
"url": "https://www.example.com/seal/mcp",
"headers": { "Authorization": "Bearer YOUR_TOKEN" }
}
}
}
Tools (all read-only, bound to the site and language of the requested URL):
search—query, optionallimit(max. 50) andoffset. Returns ID, title, URI, a short snippet and the score.retrieve—idfrom a search result. Returns the full document (title, URI, content, tags, ...).
Vector stores provide no lookup by ID. Therefore documents of search results are cached for 24h (cache
seal_ai_mcp) andretrieveworks for documents that were returned by a previoussearch.
Supported Stores
| Scheme | Package | Example DSN |
|---|---|---|
mariadb |
symfony/ai-maria-db-store |
mariadb://default?tableName=my_table |
postgres |
symfony/ai-postgres-store |
postgres://default?tableName=my_table |
qdrant |
symfony/ai-qdrant-store |
qdrant://api-key@host:6333?collectionName=col |
elasticsearch |
symfony/ai-elasticsearch-store |
elasticsearch://host:9200?indexName=idx |
opensearch |
symfony/ai-open-search-store |
opensearch://host:9200?indexName=idx |
meilisearch |
symfony/ai-meilisearch-store |
meilisearch://api-key@host:7700?indexName=idx |
milvus |
symfony/ai-milvus-store |
milvus://api-key@host:19530?database=db&collection=col |
redis |
symfony/ai-redis-store |
redis://host:6379?indexName=idx |
weaviate |
symfony/ai-weaviate-store |
weaviate://api-key@host:8080?collection=col |
typesense |
symfony/ai-typesense-store |
typesense://api-key@host:8108?collection=col |
neo4j |
symfony/ai-neo4j-store |
neo4j://user:pass@host:7474?databaseName=neo4j&vectorIndexName=idx |
cloudflare |
symfony/ai-cloudflare-store |
cloudflare://api-key@default?accountId=acc&index=idx |
mongodb |
symfony/ai-mongo-db-store |
mongodb://host:27017?databaseName=db&collectionName=col&indexName=idx |
surrealdb |
symfony/ai-surreal-db-store |
surrealdb://user:pass@host:8000?namespace=ns&database=db |
manticore |
symfony/ai-manticore-search-store |
manticore://host:9308?table=tbl |
clickhouse |
symfony/ai-click-house-store |
clickhouse://host:8123?databaseName=db&tableName=tbl |
vektor |
symfony/ai-vektor-store |
vektor://default/path?dimensions=1536 |
s3vectors |
symfony/ai-s3vectors-store |
s3vectors://region@default?vectorBucketName=bucket&indexName=idx |
chroma |
symfony/ai-chroma-db-store |
chroma://api-key@host:8000?collectionName=col&tenant=t&database=db |
cache |
symfony/ai-cache-store |
cache://default/path?key=my_key&strategy=cosine |
sqlite |
symfony/ai-sqlite-store |
sqlite://default/var/seal-ai.sqlite?tableName=tbl |
azure-search |
symfony/ai-azure-search-store |
azure-search://api-key@service.search.windows.net?indexName=idx |
supabase |
symfony/ai-supabase-store |
supabase://api-key@project.supabase.co?table=documents |
For
mariadbandpostgresstores, the extension reuses the existing TYPO3 database connection automatically.
The
cachestore keeps all vectors in a filesystem cache pool and is meant for small indexes, local development and testing. It defaults tovar/cache/data/seal-ai.
The
sqlitestore needs no additional service. The path is relative to the project root and defaults tovar/seal-ai.sqlite. By default the similarity is calculated in PHP. Withvec=1(anddimensions,distance) the sqlite-vec extension is used, which has to be loaded into PDO.
The
azure-searchandsupabasestores can not create their schema. Create the index (Azure AI Search) or the table and match function (Supabase, see thesymfony/ai-supabase-storedocumentation) before indexing. Dropping the index in SEAL only removes the documents. Further options:vectorFieldandapiVersion(Azure AI Search),vectorFieldName,dimensionsandfunctionName(Supabase).
Extending via Events
Both factories support the event:// DSN scheme, allowing you to provide custom platform or store implementations via PSR-14 event listeners.
Lochmueller\SealAi\Event\CreatePlatformEvent— dispatched forevent://platform DSNLochmueller\SealAi\Event\CreateStoreEvent— dispatched forevent://store DSN
Example event listener:
use Lochmueller\SealAi\Event\CreatePlatformEvent; final class CustomPlatformListener { public function __invoke(CreatePlatformEvent $event): void { $dsn = $event->getDsn(); // Build your custom platform... $event->setPlatform($myCustomPlatform); } }
Modifying the search
Lochmueller\SealAi\Event\SealAiSearchEvent is dispatched on every search, before the search term is vectorized
(embedding request) and the store is queried. Listeners can
- read the SEAL search (
getSearch()), - change the documents that are vectorized, e.g. rewrite the search term (
getDocuments()/setDocuments()), - skip the AI search and return an own result (
setResult()) or an empty result (cancel()).
Rate limiting (TYPO3 v14)
Every search, that is not answered by the embedding cache, is a (paid) request to the AI platform. With the rate limiter factory of TYPO3 v14, the event can be used to limit the searches, e.g. 100 searches per IP in a sliding window:
use Lochmueller\SealAi\Event\SealAiSearchEvent; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Attribute\AsEventListener; use TYPO3\CMS\Core\RateLimiter\RateLimiterFactoryInterface; #[AsEventListener('my-extension/seal-ai-search-rate-limiter')] final readonly class SealAiSearchRateLimiter { public function __construct( private RateLimiterFactoryInterface $rateLimiterFactory, ) {} public function __invoke(SealAiSearchEvent $event): void { $request = $GLOBALS['TYPO3_REQUEST'] ?? null; if (!$request instanceof ServerRequestInterface) { return; // CLI, scheduler: no limit } $limiter = $this->rateLimiterFactory->createRequestBasedLimiter($request, [ 'id' => 'seal-ai-search', 'policy' => 'sliding_window', 'limit' => 100, 'interval' => '1 hour', ]); if (!$limiter->consume()->isAccepted()) { $event->cancel(); } } }
The limiter state is stored via the TYPO3 caching framework. The configuration can be overridden per instance via
$GLOBALS['TYPO3_CONF_VARS']['SYS']['rateLimiter']['seal-ai-search'].
Architecture
The extension implements the CmsIg\Seal\Adapter\AdapterInterface with these components:
- AiAdapterFactory — registered as SEAL adapter factory for the
ai://scheme, initializes the bridge from the current site configuration - AiBridge — central service that holds the initialized platform, store, and vectorizer
- AiSchemaManager — manages index creation/deletion in the vector store
- AiIndexer — vectorizes documents and stores them; handles save, delete, and bulk operations
- AiSearcher — performs semantic search by vectorizing the query and querying the store
Development
# Install dependencies composer install # Fix code style (PHP-CS-Fixer, PER-CS 3.0) composer code-fix # Static analysis (PHPStan level 8) composer code-check # Run unit tests composer code-test # Tests with coverage (requires Xdebug) composer code-test-coverage
Comparing available symfony/ai packages
diff --color \ <(grep -E 'symfony\/ai-[a-z0-9-]+-(store|platform)' composer.json) \ <(curl -s https://raw.githubusercontent.com/symfony/ai-bundle/refs/heads/main/composer.json \ | sed -nE '/"symfony\/ai-[a-z0-9-]+-(store|platform)":/s/^ //p')
Updating available symfony/ai packages
jq --argjson updates "$(curl -sL https://raw.githubusercontent.com/symfony/ai-bundle/main/composer.json | jq '."require-dev" | with_entries(select(.key | test("^symfony/ai-.*-(store|platform)$")))')" '."require-dev" |= ((. // {}) + $updates | to_entries | sort_by(.key) | from_entries)' composer.json | sponge composer.json
