updated realtime, create endpoint , resources, registers

This commit is contained in:
ArnabChatterjee20k
2025-11-06 17:48:39 +05:30
parent d2275c05fb
commit bd6f62dd68
14 changed files with 291 additions and 97 deletions
+2 -1
View File
@@ -48,7 +48,8 @@ _APP_DB_PORT_DOCUMENTSDB=27017
_APP_DB_ADAPTER_VECTORDB=postgresql
_APP_DB_HOST_VECTORDB=postgresql
_APP_DB_PORT_VECTORDB=5432
OLLAMA_MODELS=embeddinggemma
_APP_EMBEDDING_MODELS=embeddinggemma
_APP_EMBEDDING_ENDPOINT='http://ollama:11434/api/embed'
_APP_STORAGE_DEVICE=Local
_APP_STORAGE_S3_ACCESS_KEY=
_APP_STORAGE_S3_SECRET=
+1
View File
@@ -172,6 +172,7 @@ jobs:
Databases/Legacy,
Databases/TablesDB,
Databases/DocumentsDB,
Databases/VectorDB,
Functions,
FunctionsSchedule,
GraphQL,
-1
View File
@@ -106,7 +106,6 @@ return [
'$id' => ID::custom('embeddings'),
'type' => Database::VAR_VECTOR,
'required' => true,
'size' => 128,
'signed' => false,
'array' => false,
'filters' => [],
+5 -2
View File
@@ -119,7 +119,7 @@ $register->set('pools', function () {
'pass' => System::getEnv('_APP_DB_PASS', ''),
'path' => System::getEnv('_APP_DB_SCHEMA', ''),
]);
$fallbackForVECTORDB = 'db_main=' . AppwriteURL::unparse([
$fallbackForVectorDB = 'db_main=' . AppwriteURL::unparse([
'scheme' => System::getEnv('_APP_DB_ADAPTER_VECTORDB', 'postgresql'),
'host' => System::getEnv('_APP_DB_HOST_VECTORDB', 'postgresql'),
'port' => System::getEnv('_APP_DB_PORT_VECTORDB', '5432'),
@@ -156,7 +156,7 @@ $register->set('pools', function () {
],
'vectordb' => [
'type' => 'database',
'dsns' => System::getEnv('_APP_CONNECTIONS_DATABASE_VECTORDB', $fallbackForVECTORDB),
'dsns' => System::getEnv('_APP_CONNECTIONS_DATABASE_VECTORDB', $fallbackForVectorDB),
'multiple' => true,
'schemes' => ['postgresql'],
],
@@ -400,6 +400,9 @@ $register->set('smtp', function () {
$register->set('geodb', function () {
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2024-09.mmdb');
});
$register->set('embeddingAgent', function () {
return System::getEnv('_APP_EMBEDDING_ENDPOINT', 'http://ollama:11434/api/embed');
});
$register->set('passwordsDictionary', function () {
$content = \file_get_contents(__DIR__ . '/../assets/security/10k-common-passwords');
$content = explode("\n", $content);
+3 -4
View File
@@ -1170,9 +1170,8 @@ App::setResource('transactionState', function (Database $dbForProject, callable
return new TransactionState($dbForProject, $getDatabasesDB);
}, ['dbForProject', 'getDatabasesDB']);
App::setResource('embeddingAgent', function () {
// TODO: ollama endpoint should be taken from env in cloud(for autoscaling)
App::setResource('embeddingAgent', function ($register) {
$adapter = new Ollama();
$adapter->setEndpoint('http://ollama:11434/api/embed');
$adapter->setEndpoint($register->get('embeddingAgent'));
return new Agent($adapter);
});
}, ['register']);
+58 -56
View File
@@ -276,6 +276,7 @@ services:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- ./vendor:/usr/src/code/vendor
depends_on:
- ${_APP_DB_HOST:-mongodb}
- postgresql
@@ -751,6 +752,7 @@ services:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- ./tests:/usr/src/code/tests
- ./vendor:/usr/src/code/vendor
depends_on:
- ${_APP_DB_HOST:-mongodb}
- postgresql
@@ -1125,55 +1127,55 @@ services:
- MARIADB_AUTO_UPGRADE=1
command: "mysqld --innodb-flush-method=fsync"
# mongodb:
# image: mongo:8.0.10
# container_name: appwrite-mongodb
# <<: *x-logging
# networks:
# - appwrite
# volumes:
# - appwrite-mongodb:/data/db
# - appwrite-mongodb-keyfile:/data/keyfile
# - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
# - ./mongo-entrypoint.sh:/mongo-entrypoint.sh:ro
# ports:
# - "27017:27017"
# environment:
# - MONGO_INITDB_ROOT_USERNAME=root
# - MONGO_INITDB_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
# - MONGO_INITDB_DATABASE=${_APP_DB_SCHEMA}
# - MONGO_INITDB_USERNAME=${_APP_DB_USER}
# - MONGO_INITDB_PASSWORD=${_APP_DB_PASS}
# entrypoint: ["/bin/bash", "/mongo-entrypoint.sh"]
# healthcheck:
# test: |
# bash -c "
# if mongosh -u root -p ${_APP_DB_ROOT_PASS} --authenticationDatabase admin --quiet --eval 'rs.status().ok' 2>/dev/null | grep -q 1; then
# exit 0
# else
# mongosh -u root -p ${_APP_DB_ROOT_PASS} --authenticationDatabase admin --quiet --eval \"
# rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'appwrite-mongodb:27017'}]})
# \" 2>/dev/null || exit 1
# fi
# "
# interval: 10s
# timeout: 10s
# retries: 10
# start_period: 30s
mongodb:
image: mongo:8.0.10
container_name: appwrite-mongodb
<<: *x-logging
networks:
- appwrite
volumes:
- appwrite-mongodb:/data/db
- appwrite-mongodb-keyfile:/data/keyfile
- ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
- ./mongo-entrypoint.sh:/mongo-entrypoint.sh:ro
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
- MONGO_INITDB_DATABASE=${_APP_DB_SCHEMA}
- MONGO_INITDB_USERNAME=${_APP_DB_USER}
- MONGO_INITDB_PASSWORD=${_APP_DB_PASS}
entrypoint: ["/bin/bash", "/mongo-entrypoint.sh"]
healthcheck:
test: |
bash -c "
if mongosh -u root -p ${_APP_DB_ROOT_PASS} --authenticationDatabase admin --quiet --eval 'rs.status().ok' 2>/dev/null | grep -q 1; then
exit 0
else
mongosh -u root -p ${_APP_DB_ROOT_PASS} --authenticationDatabase admin --quiet --eval \"
rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'appwrite-mongodb:27017'}]})
\" 2>/dev/null || exit 1
fi
"
interval: 10s
timeout: 10s
retries: 10
start_period: 30s
# appwrite-mongo-express:
# image: mongo-express
# container_name: appwrite-mongo-express
# networks:
# - appwrite
# ports:
# - "8082:8081"
# environment:
# ME_CONFIG_MONGODB_URL: "mongodb://root:${_APP_DB_ROOT_PASS}@appwrite-mongodb:27017/?replicaSet=rs0&directConnection=true"
# ME_CONFIG_BASICAUTH_USERNAME: ${_APP_DB_USER}
# ME_CONFIG_BASICAUTH_PASSWORD: ${_APP_DB_PASS}
# depends_on:
# - mongodb
appwrite-mongo-express:
image: mongo-express
container_name: appwrite-mongo-express
networks:
- appwrite
ports:
- "8082:8081"
environment:
ME_CONFIG_MONGODB_URL: "mongodb://root:${_APP_DB_ROOT_PASS}@appwrite-mongodb:27017/?replicaSet=rs0&directConnection=true"
ME_CONFIG_BASICAUTH_USERNAME: ${_APP_DB_USER}
ME_CONFIG_BASICAUTH_PASSWORD: ${_APP_DB_PASS}
depends_on:
- mongodb
postgresql:
@@ -1198,17 +1200,17 @@ services:
build:
context: .
dockerfile: ollama.dockerfile
args:
MODELS: ${_APP_EMBEDDING_MODELS:-embeddinggemma}
# duration to keep model in memory
OLLAMA_KEEP_ALIVE: 24h
container_name: ollama
ports:
- "11434:11434"
restart: unless-stopped
# persistent for caching models across restarts and preloading
volumes:
- ollama_models:/root/.ollama
environment:
- MODELS=${OLLAMA_MODELS:-embeddinggemma}
# duration to keep model in memory
- OLLAMA_KEEP_ALIVE=24h
- _APP_EMBEDDING_MODELS:/root/.ollama
networks:
- appwrite
@@ -1310,8 +1312,8 @@ networks:
volumes:
appwrite-mariadb:
# appwrite-mongodb:
# appwrite-mongodb-keyfile:
appwrite-mongodb:
appwrite-mongodb-keyfile:
appwrite-postgresql:
appwrite-redis:
appwrite-cache:
@@ -1322,4 +1324,4 @@ volumes:
appwrite-sites:
appwrite-builds:
appwrite-config:
ollama_models:
_APP_EMBEDDING_MODELS:
+5 -3
View File
@@ -1,8 +1,10 @@
FROM ollama/ollama:latest
FROM ollama/ollama:0.12.7
# Preload specific models
ENV MODELS="embeddinggemma"
ENV OLLAMA_KEEP_ALIVE=24h
ARG MODELS
# needed to set in the environment
ARG OLLAMA_KEEP_ALIVE
ENV OLLAMA_KEEP_ALIVE=${OLLAMA_KEEP_ALIVE:-24h}
# Pre-pull models at build time for Docker layer caching
RUN ollama serve & \
+3 -1
View File
@@ -684,8 +684,10 @@ class Event
];
break;
case 'documentsdb':
case 'vectordb':
// sending the type itself(eg: documentsdb, vectordb)
$eventMap = [
'databases' => 'documentsdb'
'databases' => $database->getAttribute('type')
];
break;
}
@@ -299,6 +299,7 @@ class Realtime extends MessagingAdapter
case 'databases':
case 'tablesdb':
case 'documentsdb':
case 'vectordb':
$resource = $parts[4] ?? '';
if (in_array($resource, ['columns', 'attributes', 'indexes'])) {
$channels[] = 'console';
@@ -434,6 +435,7 @@ class Realtime extends MessagingAdapter
break;
case 'documentsdb':
case 'vectordb':
$channels[] = 'documents';
$channels[] = "{$basePrefix}.{$databaseId}.collections.{$resourceId}.documents";
$channels[] = "{$basePrefix}.{$databaseId}.collections.{$resourceId}.documents.{$payloadId}";
@@ -140,22 +140,11 @@ class Create extends CollectionAction
attributes:$attributes,
indexes:$indexes
);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (IndexException) {
throw new Exception($this->getInvalidIndexException());
} catch (LimitException) {
throw new Exception($this->getLimitException());
}
// Create attribute metadata documents in the attributes table
// This is necessary so that indexes can find the attributes when they're created
foreach ($collections['defaultAttributes'] as $attributeConfig) {
$key = \is_string($attributeConfig['$id']) ? $attributeConfig['$id'] : (string)$attributeConfig['$id'];
$size = $key === 'embeddings' ? $dimensions : ($attributeConfig['size'] ?? 0);
try {
$attributeDoc = new Document([
// Create attribute metadata documents in the attributes table
// This is necessary so that indexes can find the attributes when they're created
$attributeDocs = array_map(function ($attributeConfig) use ($database, $collection, $databaseId, $collectionId, $dimensions) {
$key = \is_string($attributeConfig['$id']) ? $attributeConfig['$id'] : (string) $attributeConfig['$id'];
return new Document([
'$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key),
'key' => $key,
'databaseInternalId' => $database->getSequence(),
@@ -164,7 +153,7 @@ class Create extends CollectionAction
'collectionId' => $collectionId,
'type' => $attributeConfig['type'],
'status' => 'available',
'size' => $size,
'size' => $dimensions,
'required' => $attributeConfig['required'] ?? false,
'signed' => $attributeConfig['signed'] ?? false,
'default' => $attributeConfig['default'] ?? null,
@@ -174,11 +163,14 @@ class Create extends CollectionAction
'filters' => $attributeConfig['filters'] ?? [],
'options' => $attributeConfig['options'] ?? [],
]);
$dbForProject->createDocument('attributes', $attributeDoc);
} catch (DuplicateException) {
// Attribute already exists, skip
}
}, $collections['defaultAttributes']);
$dbForProject->createDocuments('attributes', $attributeDocs);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (IndexException) {
throw new Exception($this->getInvalidIndexException());
} catch (LimitException) {
throw new Exception($this->getLimitException());
}
$queueForEvents
@@ -25,7 +25,7 @@ class Create extends CreateDocumentAction
{
public static function getName(): string
{
return 'createVectorDBDocument';
return 'createTextEmbedding';
}
protected function getResponseModel(): string
@@ -144,7 +144,7 @@ class Create extends CreateDocumentAction
}
$list = new Document([
'embeddings' => array_map(fn($d) => $d, $results),
'embeddings' => array_map(fn ($d) => $d, $results),
'total' => \count($results),
]);
@@ -10,6 +10,7 @@ use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Bulk
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Create as CreateDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Delete as DeleteDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Get as GetDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Text\Create as CreateTextEmbeddings;
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Update as UpdateDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Upsert as UpsertDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\XList as ListDocuments;
@@ -29,7 +30,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorDB\Update as UpdateVectorData
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Usage\Get as GetVectorDatabaseUsage;
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Usage\XList as ListVectorDatabaseUsage;
use Appwrite\Platform\Modules\Databases\Http\VectorDB\XList as ListVectorDatabases;
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Text\Create as CreateTextEmbeddings;
use Utopia\Platform\Service;
class VectorDB extends Base
@@ -87,7 +87,8 @@ class VectorDB extends Base
$service->addAction(DeleteDocuments::getName(), new DeleteDocuments());
}
private function registerEmbeddingActions(Service $service):void{
$service->addAction(CreateTextEmbeddings::getName(),new CreateTextEmbeddings());
private function registerEmbeddingActions(Service $service): void
{
$service->addAction(CreateTextEmbeddings::getName(), new CreateTextEmbeddings());
}
}
+4 -2
View File
@@ -63,6 +63,7 @@ use Appwrite\Utopia\Response\Model\DetectionFramework;
use Appwrite\Utopia\Response\Model\DetectionRuntime;
use Appwrite\Utopia\Response\Model\DevKey;
use Appwrite\Utopia\Response\Model\Document as ModelDocument;
use Appwrite\Utopia\Response\Model\Embedding;
use Appwrite\Utopia\Response\Model\Error;
use Appwrite\Utopia\Response\Model\ErrorDev;
use Appwrite\Utopia\Response\Model\Execution;
@@ -144,6 +145,7 @@ use Appwrite\Utopia\Response\Model\UsageUsers;
use Appwrite\Utopia\Response\Model\User;
use Appwrite\Utopia\Response\Model\Variable;
use Appwrite\Utopia\Response\Model\VcsContent;
use Appwrite\Utopia\Response\Model\VectorDBCollection;
use Appwrite\Utopia\Response\Model\Webhook;
use Exception;
use JsonException;
@@ -500,10 +502,10 @@ class Response extends SwooleResponse
->setModel(new BaseList('Embedding list', self::MODEL_EMBEDDING_LIST, 'embeddings', self::MODEL_EMBEDDING))
// Entities
->setModel(new Database())
->setModel(new \Appwrite\Utopia\Response\Model\Embedding())
->setModel(new Embedding())
// Collection API Models
->setModel(new Collection())
->setModel(new \Appwrite\Utopia\Response\Model\VectorDBCollection())
->setModel(new VectorDBCollection())
->setModel(new Attribute())
->setModel(new AttributeList())
->setModel(new AttributeString())
@@ -4299,4 +4299,192 @@ class RealtimeCustomClientTest extends Scope
$client->close();
}
public function testChannelVectorDB()
{
$user = $this->getUser();
$session = $user['session'] ?? '';
$projectId = $this->getProject()['$id'];
$client = $this->getWebsocket(['documents', 'collections'], [
'origin' => 'http://localhost',
'cookie' => 'a_session_' . $projectId . '=' . $session
]);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('connected', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertCount(2, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains('collections', $response['data']['channels']);
$this->assertNotEmpty($response['data']['user']);
$this->assertEquals($user['$id'], $response['data']['user']['$id']);
// Create VectorDB database
$database = $this->client->call(Client::METHOD_POST, '/vectordb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'Actors VDB',
]);
$databaseId = $database['body']['$id'];
// Create collection in VectorDB
$actors = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'Actors',
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'documentSecurity' => true,
'dimensions' => 3,
]);
$actorsId = $actors['body']['$id'];
// Create document in VectorDB
$document = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $actorsId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => ID::unique(),
'data' => [
'embeddings' => [1.0, 0.0, 0.0],
'metadata' => ['name' => 'Chris Evans']
],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$response = json_decode($client->receive(), true);
$documentId = $document['body']['$id'];
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
// vectordb channels should include 3 items like documentsdb
$this->assertCount(3, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains('vectordb.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $documentId, $response['data']['channels']);
$this->assertContains('vectordb.' . $databaseId . '.collections.' . $actorsId . '.documents', $response['data']['channels']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertIsArray($response['data']['payload']['embeddings']);
$this->assertCount(3, $response['data']['payload']['embeddings']);
$this->assertEquals('Chris Evans', $response['data']['payload']['metadata']['name']);
// Update document
$this->client->call(Client::METHOD_PATCH, '/vectordb/' . $databaseId . '/collections/' . $actorsId . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'embeddings' => [0.0, 1.0, 0.0],
'metadata' => ['name' => 'Chris Evans 2']
],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']);
$this->assertContains('vectordb.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $documentId, $response['data']['channels']);
$this->assertContains('vectordb.' . $databaseId . '.collections.' . $actorsId . '.documents', $response['data']['channels']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertIsArray($response['data']['payload']['embeddings']);
$this->assertEquals('Chris Evans 2', $response['data']['payload']['metadata']['name']);
// Delete document
$this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId . '/collections/' . $actorsId . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']);
$this->assertContains('vectordb.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $documentId, $response['data']['channels']);
$this->assertContains('vectordb.' . $databaseId . '.collections.' . $actorsId . '.documents', $response['data']['channels']);
// Bulk create two documents
$this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$actorsId}/documents", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documents' => [
[
'embeddings' => [1.0, 0.0, 0.0],
'metadata' => ['name' => 'Robert Downey Jr.'],
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
],
[
'embeddings' => [0.0, 1.0, 0.0],
'metadata' => ['name' => 'Scarlett Johansson'],
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]
],
]);
// Receive first bulk document event
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']);
$this->assertContains('vectordb.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $response['data']['payload']['$id'] . '.create', $response['data']['events']);
$this->assertContains('vectordb.*.collections.*.documents.*.create', $response['data']['events']);
$this->assertContains('vectordb.' . $databaseId . '.collections.*.documents.*.create', $response['data']['events']);
$this->assertContains('vectordb.*.collections.' . $actorsId . '.documents.*.create', $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertIsArray($response['data']['payload']);
// Receive second bulk document event
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']);
$this->assertContains('vectordb.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $response['data']['payload']['$id'] . '.create', $response['data']['events']);
$client->close();
}
}