> ## Documentation Index
> Fetch the complete documentation index at: https://suvera.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# OpenSearch Sample Application

> Build an end-to-end OpenSearch app with Winter Boot and the Winter OpenSearch module that indexes, searches, updates, and deletes documents.

Build a REST app that performs OpenSearch operations through an `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 the `swoole` 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:

```bash theme={null}
composer require suvera/winter-boot suvera/winter-modules
```

## Source files

Switch between the source files. Each tab shows the exact file from the sample.

<Tabs>
  <Tab title="OpenSearchSampleApplication.php">
    The single entry point. It points Winter Boot at the `config` directory and at the sample namespace.

    ```php theme={null}
    <?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);
        }
    }
    ```
  </Tab>

  <Tab title="OpenSearchDemoController.php">
    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 theme={null}
    <?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()
                ];
            }
        }
    }
    ```
  </Tab>

  <Tab title="bin/application.php">
    The launch script. It loads the Composer autoloader and starts the application.

    ```php theme={null}
    #!/usr/bin/env php
    <?php

    use dev\example\OpenSearchSampleApplication;

    require_once(dirname(__DIR__) . '/vendor/autoload.php');

    OpenSearchSampleApplication::main();
    ```
  </Tab>

  <Tab title="composer.json">
    The sample declares framework dependencies with PSR-4 autoloading for its own namespace.

    ```json theme={null}
    {
        "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/"
            }
        }
    }
    ```

    The runnable sample in `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.
  </Tab>
</Tabs>

## Configuration

Switch between the two config files. `application.yml` enables the module, and `opensearch-config.yml` defines the `primary` connection with basic auth.

<Tabs>
  <Tab title="application.yml">
    The full sample file registers `OpenSearchModule` and sets the server and app identity:

    ```yaml theme={null}
    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
    ```

    See [Configuration](/configuration) for every `application.yml` key.
  </Tab>

  <Tab title="opensearch-config.yml">
    Defines a `primary` connection with basic auth:

    ```yaml theme={null}
    opensearch:
        -   name: __default__
            ssl_verification: true
            retries: 3

        -   name: primary
            hosts:
                - https://localhost:9200
            username: admin
            password: StrongPass!2024
            ssl_verification: true
            retries: 3
    ```

    Replace the host, username, and password with your own cluster credentials. The `__default__` entry supplies shared defaults. See the [OpenSearch module](/modules/opensearch) for every connection property.
  </Tab>
</Tabs>

## Run the app

Start the app, then call the endpoints to check connectivity, index a document, and search it back.

**1. Start the application:**

```bash theme={null}
composer install
php bin/application.php
```

**2. Check cluster connectivity:**

```bash theme={null}
curl "http://localhost:8080/opensearchdemo/ping"
```

**3. Create an index:**

```bash theme={null}
curl -X PUT "http://localhost:8080/opensearchdemo/index/my-test-index" \
  -H "Content-Type: application/json" -d '{}'
```

**4. Index a document:**

```bash theme={null}
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}'
```

**5. Get the document:**

```bash theme={null}
curl "http://localhost:8080/opensearchdemo/doc?index=my-test-index&id=doc-1"
```

**6. Search documents:**

```bash theme={null}
curl "http://localhost:8080/opensearchdemo/search?index=my-test-index&field=title&value=Hello"
```

**7. Run the complete demo:**

```bash theme={null}
curl "http://localhost:8080/opensearchdemo/demo"
```

The demo creates its own index, runs create, index, bulk, get, update, search, count, and delete in sequence, and returns one status entry per operation.

## Next steps

* Read the [OpenSearch module](/modules/opensearch) for the full `OpenSearchTemplate` API and AWS SigV4 signing.
* Browse all [Libraries](/modules/overview) when you need Kafka, SQS, Redis, or Doctrine in the same app.
