OpenSearchDemoController. You use Winter Boot for the application runtime and the Winter OpenSearch module from Winter Modules for search and analytics.
Prerequisites
You need PHP 8.5 or later with theswoole and pcntl extensions. You also need access to an OpenSearch cluster. The sample config points at https://localhost:9200 with basic auth — replace the host and credentials with your own cluster.
Project structure
The sample uses this layout:opensearch/
├── bin/
│ └── application.php # Application entry point
├── config/
│ ├── application.yml # Winter Boot application config
│ └── opensearch-config.yml # OpenSearch connections config
├── src/
│ ├── OpenSearchSampleApplication.php # Main application class
│ └── controller/
│ └── OpenSearchDemoController.php # OpenSearch operations controller
└── composer.json # Dependencies
Install dependencies
Require the framework and the modules package:composer require suvera/winter-boot suvera/winter-modules
Source files
Switch between the source files. Each tab shows the exact file from the sample.- OpenSearchSampleApplication.php
- OpenSearchDemoController.php
- bin/application.php
- composer.json
The single entry point. It points Winter Boot at the
config directory and at the sample namespace.<?php
namespace dev\example;
use dev\winterframework\stereotype\WinterBootApplication;
#[WinterBootApplication(
configDirectory: [__DIR__ . "/../config"],
scanNamespaces: [
['dev\\example', __DIR__ . '']
]
)]
class OpenSearchSampleApplication {
public static function main(): void {
$winterApp = new \dev\winterframework\core\app\WinterWebSwooleApplication();
$winterApp->run(self::class);
}
}
The controller. It autowires
OpenSearchTemplate and exposes one endpoint per operation — ping, create/delete index, index/get/update/delete document, search, count, bulk — plus a demo endpoint that runs them all in sequence.<?php
namespace dev\example\controller;
use dev\winterframework\opensearch\OpenSearchTemplate;
use dev\winterframework\stereotype\Autowired;
use dev\winterframework\stereotype\RestController;
use dev\winterframework\stereotype\web\DeleteMapping;
use dev\winterframework\stereotype\web\GetMapping;
use dev\winterframework\stereotype\web\PathVariable;
use dev\winterframework\stereotype\web\PostMapping;
use dev\winterframework\stereotype\web\PutMapping;
use dev\winterframework\stereotype\web\RequestBody;
use dev\winterframework\stereotype\web\RequestParam;
use Exception;
#[RestController]
class OpenSearchDemoController {
#[Autowired]
private OpenSearchTemplate $opensearch;
private function decodeJsonBody(?string $rawBody): array|null {
if ($rawBody === null || trim($rawBody) === '') {
return [];
}
$decoded = json_decode($rawBody, true);
if (!is_array($decoded)) {
return null;
}
return $decoded;
}
/**
* Check cluster connectivity and info
*/
#[GetMapping(path: "/opensearchdemo/ping")]
public function ping(): array {
try {
return [
'success' => true,
'ping' => $this->opensearch->ping(),
'info' => $this->opensearch->info()
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Create an index, optional JSON body with settings/mappings
*/
#[PutMapping(path: "/opensearchdemo/index/{indexName}")]
public function createIndex(
#[PathVariable] string $indexName,
#[RequestBody(required: false)] ?string $rawBody = null
): array {
try {
$body = $this->decodeJsonBody($rawBody);
if ($body === null) {
return ['success' => false, 'error' => 'Invalid JSON body'];
}
$params = ['index' => $indexName];
if (!empty($body)) {
$params['body'] = $body;
}
$result = $this->opensearch->indices()->create($params);
return [
'success' => true,
'message' => "Index {$indexName} created successfully",
'data' => $result
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Delete an index
*/
#[DeleteMapping(path: "/opensearchdemo/index/{indexName}")]
public function deleteIndex(#[PathVariable] string $indexName): array {
try {
$result = $this->opensearch->indices()->delete(['index' => $indexName]);
return [
'success' => true,
'message' => "Index {$indexName} deleted successfully",
'data' => $result
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Index a document, JSON document passed as request body
*/
#[PostMapping(path: "/opensearchdemo/doc")]
public function indexDocument(
#[RequestParam] string $index,
#[RequestParam(required: false)] ?string $id,
#[RequestBody] string $rawBody
): array {
try {
$doc = $this->decodeJsonBody($rawBody);
if ($doc === null) {
return ['success' => false, 'error' => 'Invalid JSON body'];
}
$params = ['index' => $index, 'body' => $doc];
if ($id !== null && $id !== '') {
$params['id'] = $id;
}
$result = $this->opensearch->index($params);
// Make the document immediately visible for subsequent reads
$this->opensearch->indices()->refresh(['index' => $index]);
return [
'success' => true,
'message' => "Document indexed successfully",
'id' => $result['_id'] ?? null,
'data' => $result
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Get a document by id
*/
#[GetMapping(path: "/opensearchdemo/doc")]
public function getDocument(
#[RequestParam] string $index,
#[RequestParam] string $id
): array {
try {
$result = $this->opensearch->get(['index' => $index, 'id' => $id]);
return [
'success' => true,
'id' => $result['_id'] ?? $id,
'source' => $result['_source'] ?? null,
'data' => $result
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Update a document, JSON partial doc passed as request body
*/
#[PostMapping(path: "/opensearchdemo/doc/update")]
public function updateDocument(
#[RequestParam] string $index,
#[RequestParam] string $id,
#[RequestBody] string $rawBody
): array {
try {
$doc = $this->decodeJsonBody($rawBody);
if ($doc === null) {
return ['success' => false, 'error' => 'Invalid JSON body'];
}
$result = $this->opensearch->update([
'index' => $index,
'id' => $id,
'body' => ['doc' => $doc]
]);
$this->opensearch->indices()->refresh(['index' => $index]);
return [
'success' => true,
'message' => "Document {$id} updated successfully",
'data' => $result
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Delete a document by id
*/
#[PostMapping(path: "/opensearchdemo/doc/delete")]
public function deleteDocument(
#[RequestParam] string $index,
#[RequestParam] string $id
): array {
try {
$result = $this->opensearch->delete(['index' => $index, 'id' => $id]);
$this->opensearch->indices()->refresh(['index' => $index]);
return [
'success' => true,
'message' => "Document {$id} deleted successfully from index {$index}",
'data' => $result
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Search documents with a match query
*/
#[GetMapping(path: "/opensearchdemo/search")]
public function search(
#[RequestParam] string $index,
#[RequestParam] string $field,
#[RequestParam] string $value,
#[RequestParam(required: false, defaultValue: '10')] ?int $size = 10
): array {
try {
$result = $this->opensearch->search([
'index' => $index,
'body' => [
'size' => $size,
'query' => [
'match' => [$field => $value]
]
]
]);
$hits = [];
foreach ($result['hits']['hits'] ?? [] as $hit) {
$hits[] = [
'id' => $hit['_id'] ?? null,
'score' => $hit['_score'] ?? null,
'source' => $hit['_source'] ?? null
];
}
$total = $result['hits']['total']['value'] ?? count($hits);
return [
'success' => true,
'index' => $index,
'total' => $total,
'hits' => $hits
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Count documents in an index
*/
#[GetMapping(path: "/opensearchdemo/count")]
public function count(#[RequestParam] string $index): array {
try {
$result = $this->opensearch->count(['index' => $index]);
return [
'success' => true,
'index' => $index,
'count' => $result['count'] ?? 0
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Bulk index documents, JSON array of documents passed as request body
*/
#[PostMapping(path: "/opensearchdemo/bulk")]
public function bulkIndex(
#[RequestParam] string $index,
#[RequestBody] string $rawBody
): array {
try {
$docs = $this->decodeJsonBody($rawBody);
if ($docs === null || array_is_list($docs) === false) {
return ['success' => false, 'error' => 'Body must be a JSON array of documents'];
}
$body = [];
foreach ($docs as $doc) {
$action = ['index' => ['_index' => $index]];
if (is_array($doc) && isset($doc['_id'])) {
$action['index']['_id'] = $doc['_id'];
unset($doc['_id']);
}
$body[] = $action;
$body[] = $doc;
}
$result = $this->opensearch->bulk(['body' => $body]);
$this->opensearch->indices()->refresh(['index' => $index]);
return [
'success' => true,
'message' => count($docs) . " documents bulk indexed",
'errors' => $result['errors'] ?? false,
'took' => $result['took'] ?? null
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Demo all operations together
*/
#[GetMapping(path: "/opensearchdemo/demo")]
public function demoAllOperations(): array {
try {
$index = 'demo-index-' . time();
// 1. Create index
$this->opensearch->indices()->create(['index' => $index]);
// 2. Index a document
$indexResult = $this->opensearch->index([
'index' => $index,
'id' => 'doc-1',
'body' => ['title' => 'Hello OpenSearch', 'views' => 10]
]);
// 3. Bulk index more documents
$this->opensearch->bulk(['body' => [
['index' => ['_index' => $index, '_id' => 'doc-2']],
['title' => 'Hello Winter Boot', 'views' => 20],
['index' => ['_index' => $index, '_id' => 'doc-3']],
['title' => 'Goodbye Winter Boot', 'views' => 5]
]]);
$this->opensearch->indices()->refresh(['index' => $index]);
// 4. Get document
$getResult = $this->opensearch->get(['index' => $index, 'id' => 'doc-1']);
// 5. Update document
$this->opensearch->update([
'index' => $index,
'id' => 'doc-1',
'body' => ['doc' => ['views' => 11]]
]);
$this->opensearch->indices()->refresh(['index' => $index]);
// 6. Search documents
$searchResult = $this->opensearch->search([
'index' => $index,
'body' => ['query' => ['match' => ['title' => 'Hello']]]
]);
// 7. Count documents
$countResult = $this->opensearch->count(['index' => $index]);
// 8. Delete a document
$this->opensearch->delete(['index' => $index, 'id' => 'doc-3']);
$this->opensearch->indices()->refresh(['index' => $index]);
return [
'success' => true,
'operations' => [
'createIndex' => "Created index: {$index}",
'indexDoc' => "Indexed doc-1: " . ($indexResult['result'] ?? 'ok'),
'bulk' => "Bulk indexed doc-2 and doc-3",
'get' => "Retrieved doc-1 title: "
. ($getResult['_source']['title'] ?? 'unknown'),
'update' => "Updated doc-1 views to 11",
'search' => "Found "
. ($searchResult['hits']['total']['value'] ?? 0)
. " docs matching 'Hello'",
'count' => "Index holds " . ($countResult['count'] ?? 0) . " docs",
'delete' => "Deleted doc-3"
],
'index' => $index
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
}
The launch script. It loads the Composer autoloader and starts the application.
#!/usr/bin/env php
<?php
use dev\example\OpenSearchSampleApplication;
require_once(dirname(__DIR__) . '/vendor/autoload.php');
OpenSearchSampleApplication::main();
The sample declares framework dependencies with PSR-4 autoloading for its own namespace.The runnable sample in
{
"name": "suvera/winter-boot-opensearch-sample",
"require": {
"ext-pcntl": "*",
"ext-swoole": "*",
"suvera/winter-boot": "@dev",
"suvera/winter-modules": "@dev"
},
"autoload": {
"psr-4": {
"dev\\example\\": "src/"
}
}
}
winter-boot-samples adds local path repositories for winter-boot and winter-modules so it resolves them from a sibling checkout. You do not need those entries when you install released packages from Packagist.Configuration
Switch between the two config files.application.yml enables the module, and opensearch-config.yml defines the primary connection with basic auth.
- application.yml
- opensearch-config.yml
The full sample file registers See Configuration for every
OpenSearchModule and sets the server and app identity:server:
port: 8080
address: 0.0.0.0
context-path: /
winter:
application:
name: OpenSearch Sample Application
id: opensearch-sample-app
version: 1.0.0
modules:
- module: dev\winterframework\opensearch\OpenSearchModule
enabled: true
configFile: opensearch-config.yml
application.yml key.Defines a Replace the host, username, and password with your own cluster credentials. The
primary connection with basic auth:opensearch:
- name: __default__
ssl_verification: true
retries: 3
- name: primary
hosts:
- https://localhost:9200
username: admin
password: StrongPass!2024
ssl_verification: true
retries: 3
__default__ entry supplies shared defaults. See the OpenSearch module for every connection property.Run the app
Start the app, then call the endpoints to check connectivity, index a document, and search it back. 1. Start the application:composer install
php bin/application.php
curl "http://localhost:8080/opensearchdemo/ping"
curl -X PUT "http://localhost:8080/opensearchdemo/index/my-test-index" \
-H "Content-Type: application/json" -d '{}'
curl -X POST "http://localhost:8080/opensearchdemo/doc?index=my-test-index&id=doc-1" \
-H "Content-Type: application/json" \
-d '{"title":"Hello OpenSearch","views":10}'
curl "http://localhost:8080/opensearchdemo/doc?index=my-test-index&id=doc-1"
curl "http://localhost:8080/opensearchdemo/search?index=my-test-index&field=title&value=Hello"
curl "http://localhost:8080/opensearchdemo/demo"
Next steps
- Read the OpenSearch module for the full
OpenSearchTemplateAPI and AWS SigV4 signing. - Browse all Libraries when you need Kafka, SQS, Redis, or Doctrine in the same app.