diff --git a/src/Contracts/VectorClient.php b/src/Contracts/VectorClient.php index 8a6a81f..626c7ff 100644 --- a/src/Contracts/VectorClient.php +++ b/src/Contracts/VectorClient.php @@ -51,6 +51,10 @@ public function setPayload(string $collection, array $ids, array $payload, bool */ public function count(string $collection, ?array $filter = null): int; + public function createPayloadIndex(string $collection, string $fieldName, ?string $fieldSchema = null): UpsertResult; + + public function deletePayloadIndex(string $collection, string $fieldName): UpsertResult; + /** * @param array|null $ids * @param array|null $filter diff --git a/src/Qdrant.php b/src/Qdrant.php index a7520f2..298bed9 100644 --- a/src/Qdrant.php +++ b/src/Qdrant.php @@ -14,6 +14,8 @@ use TheShit\Vector\Requests\Collections\DeleteCollectionRequest; use TheShit\Vector\Requests\Collections\GetCollectionRequest; use TheShit\Vector\Requests\Points\CountPointsRequest; +use TheShit\Vector\Requests\Points\CreatePayloadIndexRequest; +use TheShit\Vector\Requests\Points\DeletePayloadIndexRequest; use TheShit\Vector\Requests\Points\DeletePointsRequest; use TheShit\Vector\Requests\Points\GetPointsRequest; use TheShit\Vector\Requests\Points\ScrollPointsRequest; @@ -137,6 +139,22 @@ public function count(string $collection, ?array $filter = null): int return (int) $response->json('result.count'); } + public function createPayloadIndex(string $collection, string $fieldName, ?string $fieldSchema = null): UpsertResult + { + $response = $this->connector->send(new CreatePayloadIndexRequest($collection, $fieldName, $fieldSchema)); + $response->throw(); + + return UpsertResult::fromArray($response->json('result')); + } + + public function deletePayloadIndex(string $collection, string $fieldName): UpsertResult + { + $response = $this->connector->send(new DeletePayloadIndexRequest($collection, $fieldName)); + $response->throw(); + + return UpsertResult::fromArray($response->json('result')); + } + /** * @param array|null $ids * @param array|null $filter diff --git a/src/Requests/Points/CreatePayloadIndexRequest.php b/src/Requests/Points/CreatePayloadIndexRequest.php new file mode 100644 index 0000000..13c8268 --- /dev/null +++ b/src/Requests/Points/CreatePayloadIndexRequest.php @@ -0,0 +1,51 @@ +collection.'/index'; + } + + /** + * @return array + */ + protected function defaultQuery(): array + { + return ['wait' => $this->wait ? 'true' : 'false']; + } + + /** + * @return array + */ + protected function defaultBody(): array + { + $body = ['field_name' => $this->fieldName]; + + if ($this->fieldSchema !== null) { + $body['field_schema'] = $this->fieldSchema; + } + + return $body; + } +} diff --git a/src/Requests/Points/DeletePayloadIndexRequest.php b/src/Requests/Points/DeletePayloadIndexRequest.php new file mode 100644 index 0000000..03f930a --- /dev/null +++ b/src/Requests/Points/DeletePayloadIndexRequest.php @@ -0,0 +1,32 @@ +collection.'/index/'.$this->fieldName; + } + + /** + * @return array + */ + protected function defaultQuery(): array + { + return ['wait' => $this->wait ? 'true' : 'false']; + } +} diff --git a/tests/Contract/OpenApiSchemaTest.php b/tests/Contract/OpenApiSchemaTest.php index dc3dda0..ce27a13 100644 --- a/tests/Contract/OpenApiSchemaTest.php +++ b/tests/Contract/OpenApiSchemaTest.php @@ -95,6 +95,13 @@ function specRequired(array $schema): array ->and($fields)->toContain('exact'); }); + it('CreateFieldIndex requires field_name', function (): void { + $schema = getRequestSchema(loadSpec(), 'put', '/collections/{collection_name}/index'); + + expect(specFields($schema))->toContain('field_name') + ->and(specRequired($schema))->toContain('field_name'); + }); + it('SetPayload requires payload field', function (): void { $schema = getRequestSchema(loadSpec(), 'post', '/collections/{collection_name}/points/payload'); @@ -152,7 +159,8 @@ function specRequired(array $schema): array ->and($paths)->toContain('/collections/{collection_name}/points/scroll') ->and($paths)->toContain('/collections/{collection_name}/points/delete') ->and($paths)->toContain('/collections/{collection_name}/points/count') - ->and($paths)->toContain('/collections/{collection_name}/points/payload'); + ->and($paths)->toContain('/collections/{collection_name}/points/payload') + ->and($paths)->toContain('/collections/{collection_name}/index'); }); it('our HTTP methods match', function (): void { @@ -167,6 +175,8 @@ function specRequired(array $schema): array ->and($spec['paths']['/collections/{collection_name}/points/scroll'])->toHaveKey('post') ->and($spec['paths']['/collections/{collection_name}/points/delete'])->toHaveKey('post') ->and($spec['paths']['/collections/{collection_name}/points/count'])->toHaveKey('post') - ->and($spec['paths']['/collections/{collection_name}/points/payload'])->toHaveKey('post'); + ->and($spec['paths']['/collections/{collection_name}/points/payload'])->toHaveKey('post') + ->and($spec['paths']['/collections/{collection_name}/index'])->toHaveKey('put') + ->and($spec['paths']['/collections/{collection_name}/index/{field_name}'])->toHaveKey('delete'); }); }); diff --git a/tests/Feature/QdrantTest.php b/tests/Feature/QdrantTest.php index 0f3619b..ed82532 100644 --- a/tests/Feature/QdrantTest.php +++ b/tests/Feature/QdrantTest.php @@ -15,6 +15,8 @@ use TheShit\Vector\Requests\Collections\DeleteCollectionRequest; use TheShit\Vector\Requests\Collections\GetCollectionRequest; use TheShit\Vector\Requests\Points\CountPointsRequest; +use TheShit\Vector\Requests\Points\CreatePayloadIndexRequest; +use TheShit\Vector\Requests\Points\DeletePayloadIndexRequest; use TheShit\Vector\Requests\Points\DeletePointsRequest; use TheShit\Vector\Requests\Points\GetPointsRequest; use TheShit\Vector\Requests\Points\ScrollPointsRequest; @@ -165,6 +167,40 @@ function makeClient(MockClient $mock): Qdrant }); }); +describe('Qdrant::createPayloadIndex', function (): void { + it('creates an index and returns result', function (): void { + $mock = new MockClient([ + CreatePayloadIndexRequest::class => MockResponse::make([ + 'result' => ['status' => 'completed', 'operation_id' => 20], + 'status' => 'ok', + ]), + ]); + + $result = makeClient($mock)->createPayloadIndex('coll', 'artist', 'keyword'); + + expect($result)->toBeInstanceOf(UpsertResult::class) + ->and($result->completed())->toBeTrue(); + $mock->assertSent(CreatePayloadIndexRequest::class); + }); +}); + +describe('Qdrant::deletePayloadIndex', function (): void { + it('deletes an index and returns result', function (): void { + $mock = new MockClient([ + DeletePayloadIndexRequest::class => MockResponse::make([ + 'result' => ['status' => 'completed', 'operation_id' => 21], + 'status' => 'ok', + ]), + ]); + + $result = makeClient($mock)->deletePayloadIndex('coll', 'artist'); + + expect($result)->toBeInstanceOf(UpsertResult::class) + ->and($result->completed())->toBeTrue(); + $mock->assertSent(DeletePayloadIndexRequest::class); + }); +}); + describe('Qdrant::upsert', function (): void { it('upserts raw array points', function (): void { $mock = new MockClient([ diff --git a/tests/Unit/RequestTest.php b/tests/Unit/RequestTest.php index c97ff07..150bff2 100644 --- a/tests/Unit/RequestTest.php +++ b/tests/Unit/RequestTest.php @@ -6,6 +6,8 @@ use TheShit\Vector\Requests\Collections\DeleteCollectionRequest; use TheShit\Vector\Requests\Collections\GetCollectionRequest; use TheShit\Vector\Requests\Points\CountPointsRequest; +use TheShit\Vector\Requests\Points\CreatePayloadIndexRequest; +use TheShit\Vector\Requests\Points\DeletePayloadIndexRequest; use TheShit\Vector\Requests\Points\DeletePointsRequest; use TheShit\Vector\Requests\Points\GetPointsRequest; use TheShit\Vector\Requests\Points\ScrollPointsRequest; @@ -113,6 +115,50 @@ }); }); +describe('CreatePayloadIndexRequest', function (): void { + it('resolves endpoint', function (): void { + $request = new CreatePayloadIndexRequest('coll', 'artist'); + + expect($request->resolveEndpoint())->toBe('/collections/coll/index'); + }); + + it('builds body with field name only', function (): void { + $request = new CreatePayloadIndexRequest('coll', 'artist'); + $body = invade($request)->defaultBody(); + + expect($body)->toBe(['field_name' => 'artist']); + }); + + it('includes field schema when provided', function (): void { + $request = new CreatePayloadIndexRequest('coll', 'artist', 'keyword'); + $body = invade($request)->defaultBody(); + + expect($body)->toBe(['field_name' => 'artist', 'field_schema' => 'keyword']); + }); + + it('sets wait query param', function (): void { + $request = new CreatePayloadIndexRequest('coll', 'artist', wait: false); + $query = invade($request)->defaultQuery(); + + expect($query['wait'])->toBe('false'); + }); +}); + +describe('DeletePayloadIndexRequest', function (): void { + it('resolves endpoint with field name', function (): void { + $request = new DeletePayloadIndexRequest('coll', 'artist'); + + expect($request->resolveEndpoint())->toBe('/collections/coll/index/artist'); + }); + + it('sets wait query param', function (): void { + $request = new DeletePayloadIndexRequest('coll', 'artist', wait: true); + $query = invade($request)->defaultQuery(); + + expect($query['wait'])->toBe('true'); + }); +}); + describe('DeleteCollectionRequest', function (): void { it('resolves endpoint', function (): void { $request = new DeleteCollectionRequest('old_collection');