From bd6f62dd688cbf5975e1e1832da614f0ff9965f7 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 6 Nov 2025 17:48:39 +0530 Subject: [PATCH] updated realtime, create endpoint , resources, registers --- .env | 3 +- .github/workflows/tests.yml | 1 + app/config/collections/vectordb.php | 1 - app/init/registers.php | 7 +- app/init/resources.php | 7 +- docker-compose.yml | 114 +++++------ ollama.dockerfile | 8 +- src/Appwrite/Event/Event.php | 4 +- src/Appwrite/Messaging/Adapter/Realtime.php | 2 + .../Http/VectorDB/Collections/Create.php | 36 ++-- .../Collections/Documents/Text/Create.php | 4 +- .../Databases/Services/Registry/VectorDB.php | 7 +- src/Appwrite/Utopia/Response.php | 6 +- .../Realtime/RealtimeCustomClientTest.php | 188 ++++++++++++++++++ 14 files changed, 291 insertions(+), 97 deletions(-) diff --git a/.env b/.env index 3eef0189d0..ae597d5bc7 100644 --- a/.env +++ b/.env @@ -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= diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1d98ee5952..4c26dc0c97 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -172,6 +172,7 @@ jobs: Databases/Legacy, Databases/TablesDB, Databases/DocumentsDB, + Databases/VectorDB, Functions, FunctionsSchedule, GraphQL, diff --git a/app/config/collections/vectordb.php b/app/config/collections/vectordb.php index c7d87b3509..6a8a26aabb 100644 --- a/app/config/collections/vectordb.php +++ b/app/config/collections/vectordb.php @@ -106,7 +106,6 @@ return [ '$id' => ID::custom('embeddings'), 'type' => Database::VAR_VECTOR, 'required' => true, - 'size' => 128, 'signed' => false, 'array' => false, 'filters' => [], diff --git a/app/init/registers.php b/app/init/registers.php index fd21b5ce81..cb8a3bb9ed 100644 --- a/app/init/registers.php +++ b/app/init/registers.php @@ -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); diff --git a/app/init/resources.php b/app/init/resources.php index adf6ea9620..18d37c3a37 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -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']); diff --git a/docker-compose.yml b/docker-compose.yml index 4e8b58086e..3d09f324e1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: \ No newline at end of file + _APP_EMBEDDING_MODELS: \ No newline at end of file diff --git a/ollama.dockerfile b/ollama.dockerfile index 1d4d29a22b..fc538ea4d2 100644 --- a/ollama.dockerfile +++ b/ollama.dockerfile @@ -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 & \ diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index 73957c1155..b72b70cf2d 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -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; } diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index 95c3c79155..30e5942185 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -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}"; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorDB/Collections/Create.php index b319c28c4a..e1c9998323 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorDB/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorDB/Collections/Create.php @@ -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 diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorDB/Collections/Documents/Text/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorDB/Collections/Documents/Text/Create.php index 1896e91b8d..4bc8281656 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorDB/Collections/Documents/Text/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorDB/Collections/Documents/Text/Create.php @@ -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), ]); diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/VectorDB.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/VectorDB.php index f05e339ac0..ec071d4e6f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/VectorDB.php +++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/VectorDB.php @@ -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()); } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index cb14aae7da..6f78c70c0f 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -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()) diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 50430122f8..1121d99302 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -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(); + } }