본문 바로가기
WIKI 기술 지식 베이스

PHP client

원문 보기 위키 갱신

출처: 문서

본문

PHP 클라이언트

OpenSearch PHP 클라이언트는 OpenSearch 클러스터와 더 안전하고 쉽게 상호작용할 수 있는 방법을 제공해요. 브라우저에서 OpenSearch를 사용하다가 데이터가 외부에 노출될 위험을 감수하는 대신, 클러스터로의 요청 전송을 알아서 처리해 주는 OpenSearch 클라이언트를 만들 수 있어요. 클라이언트에는 클러스터에서 다양한 작업을 수행하고 표준 응답 본문을 반환할 수 있게 해 주는 API 라이브러리가 들어 있어요.

이 시작 가이드에서는 OpenSearch에 연결하고, 문서를 인덱싱하고, 쿼리를 실행하는 방법을 설명해요. 클라이언트 소스 코드는 opensearch-php 저장소를 참고하세요.

PHP 클라이언트 설치

클라이언트는 PHP 8.2 이상이 필요해요. 프로젝트에 클라이언트를 추가하려면 Composer를 사용해 설치하세요:

composer require opensearch-project/opensearch-php

{% include copy.html %}

특정 버전의 클라이언트를 설치하려면 다음 명령어를 실행하세요:

composer require opensearch-project/opensearch-php:<version>

{% include copy.html %}

클라이언트는 PSR-18을 구현하는 어떤 HTTP 클라이언트로든 요청을 보낼 수 있어서, HTTP 클라이언트도 함께 설치해야 해요. Guzzle을 사용하려면 다음 명령어를 실행하세요:

composer require guzzlehttp/guzzle

{% include copy.html %}

Symfony HTTP client를 사용하려면 다음 명령어를 실행하세요:

composer require symfony/http-client

{% include copy.html %}

그다음 코드에서 composer의 autoload 파일을 require 하세요:

require __DIR__ . '/vendor/autoload.php';

{% include copy.html %}

OpenSearch에 연결하기

GuzzleClientFactory나 SymfonyClientFactory를 사용해 클라이언트를 생성하세요. base_uri 옵션은 필수예요. 팩토리는 다른 모든 옵션을 내부 HTTP 클라이언트에 전달해요. 다음 코드는 Security 플러그인이 활성화되지 않은 클러스터에 연결해요:

$client = (new \OpenSearch\GuzzleClientFactory())->create([
    'base_uri' => 'http://localhost:9200',
]);

{% include copy.html %}

Security 플러그인이 활성화된 클러스터에 연결하려면 자격 증명과 TLS 옵션을 제공하세요:

$client = (new \OpenSearch\GuzzleClientFactory())->create([
    'base_uri' => 'https://localhost:9200',
    // Only for demo purposes. Don't specify your credentials in code.
    'auth' => ['admin', '<custom-admin-password>'],
    'verify' => false, // Disables TLS certificate verification. Use only for local development.
]);

{% include copy.html %}

Symfony HTTP 클라이언트도 동등한 옵션을 받아들여요:

$client = (new \OpenSearch\SymfonyClientFactory())->create([
    'base_uri' => 'https://localhost:9200',
    // Only for demo purposes. Don't specify your credentials in code.
    'auth_basic' => ['admin', '<custom-admin-password>'],
    'verify_peer' => false, // Disables TLS certificate verification. Use only for local development.
]);

{% include copy.html %}

지원되는 PSR 클라이언트에 대한 자세한 내용은 클라이언트 팩토리를 참고하세요. 기본 인증에 대한 자세한 내용은 PSR 클라이언트를 사용한 기본 인증을 참고하세요.

Amazon OpenSearch Service에 연결하기

AWS Identity and Access Management(IAM) 자격 증명으로 요청에 서명하려면 AWS SDK for PHP를 설치하세요:

composer require aws/aws-sdk-php

{% include copy.html %}

다음 예제에서는 엔드포인트를 여러분의 도메인 엔드포인트로 바꾸세요. 도메인 엔드포인트는 Amazon OpenSearch Service 콘솔의 도메인 세부 정보 페이지에 나열되어 있어요.

그다음 클라이언트를 생성할 때 auth_aws 옵션을 전달하세요:

$client = (new \OpenSearch\GuzzleClientFactory())->create([
    'base_uri' => 'https://search-<domain-name>-<id>.us-east-1.es.amazonaws.com',
    'auth_aws' => [
        'region' => 'us-east-1',
        'service' => 'es',
    ],
]);

{% include copy.html %}

이 예제에서는 credentials를 지정하지 않기 때문에, AWS SDK for PHP가 기본 자격 증명 공급자 체인을 사용해 자격 증명을 확인해요. 이 체인은 환경 변수, 공유 AWS 설정 파일과 자격 증명 파일, 그리고 코드가 실행되는 Amazon EC2 인스턴스나 컨테이너의 IAM 역할을 확인해요.

자격 증명을 명시적으로 전달하려면 credentials 옵션을 추가하세요. session_token은 임시 자격 증명을 사용할 때에만 지정하세요:

$client = (new \OpenSearch\GuzzleClientFactory())->create([
    'base_uri' => 'https://search-<domain-name>-<id>.us-east-1.es.amazonaws.com',
    'auth_aws' => [
        'region' => 'us-east-1',
        'service' => 'es',
        'credentials' => [
            'access_key' => getenv('AWS_ACCESS_KEY_ID'),
            'secret_key' => getenv('AWS_SECRET_ACCESS_KEY'),
            'session_token' => getenv('AWS_SESSION_TOKEN'),
        ],
    ],
]);

{% include copy.html %}

자세한 내용은 PSR 클라이언트를 사용한 IAM 인증을 참고하세요.

Amazon OpenSearch Serverless에 연결하기

다음 예제에서는 엔드포인트를 여러분의 컬렉션 엔드포인트로 바꾸세요. 컬렉션 엔드포인트는 Amazon OpenSearch Service 콘솔의 컬렉션 세부 정보 페이지에 나열되어 있어요.

Amazon OpenSearch Serverless에 연결하려면 service를 aoss로 설정하고 컬렉션 엔드포인트를 지정하세요. 다음 예제는 인덱스가 존재하는지 확인해요:

$client = (new \OpenSearch\GuzzleClientFactory())->create([
    'base_uri' => 'https://<collection-id>.us-east-1.aoss.amazonaws.com',
    'auth_aws' => [
        'region' => 'us-east-1',
        'service' => 'aoss',
    ],
]);

$exists = $client->indices()->exists(['index' => 'students']);
echo $exists ? 'Index exists' : 'Index does not exist', PHP_EOL;

{% include copy.html %}

Amazon OpenSearch Serverless는 OpenSearch API 작업 중 일부만 지원하며, 이 페이지의 예제에서 사용하는 refresh 파라미터는 지원하지 않아요. 자세한 내용은 Amazon OpenSearch Serverless에서 지원되는 작업 및 플러그인을 참고하세요. {: .note}

인덱스 생성하기

다음 예제는 프라이머리 샤드 하나와 레플리카 하나를 가진 인덱스를 생성해요. gradDate 필드를 yyyy-MM-dd 형식의 date로 명시적으로 매핑하고 있어요. 문서를 인덱싱하면 나머지 문서 필드는 OpenSearch가 동적으로 매핑해요:

$index = 'students';

$client->indices()->create([
    'index' => $index,
    'body' => [
        'settings' => [
            'index' => [
                'number_of_shards' => 1,
                'number_of_replicas' => 1,
            ],
        ],
        'mappings' => [
            'properties' => [
                'gradDate' => ['type' => 'date', 'format' => 'yyyy-MM-dd'],
            ],
        ],
    ],
]);

{% include copy.html %}

문서 인덱싱하기

다음 코드를 사용해 문서를 인덱싱하세요. 문서를 즉시 검색할 수 있게 하려면 refresh를 true로 설정하세요:

$response = $client->index([
    'index' => $index,
    'id' => '1',
    'body' => [
        'firstName' => 'John',
        'lastName' => 'Doe',
        'gpa' => 3.89,
        'gradDate' => '2022-05-15',
    ],
    'refresh' => true,
]);

{% include copy.html %}

ID가 아직 존재하지 않을 때만 문서를 생성하려면 index() 대신 create()를 사용하세요. 이미 존재하는 ID로 create() 요청을 보내면 409 응답이 반환돼요.

벌크 인덱싱

다음 코드를 사용하면 하나의 요청으로 여러 문서를 인덱싱할 수 있어요. 요청 본문은 액션 라인과 그 액션이 적용될 문서가 번갈아 나타나는 구조예요:

$response = $client->bulk([
    'body' => [
        ['index' => ['_index' => $index, '_id' => '2']],
        ['firstName' => 'Paulo', 'lastName' => 'Santos', 'gpa' => 3.93, 'gradDate' => '2021-05-20'],
        ['index' => ['_index' => $index, '_id' => '3']],
        ['firstName' => 'Shirley', 'lastName' => 'Rodriguez', 'gpa' => 3.91, 'gradDate' => '2019-05-10'],
    ],
    'refresh' => true,
]);

{% include copy.html %}

벌크 요청은 개별 액션이 실패해도 예외를 던지지 않기 때문에, 액션별 결과는 응답의 errors 필드와 items 배열에서 확인해야 해요.

문서 검색하기

다음 코드를 사용해 인덱스의 모든 문서를 검색하세요:

$response = $client->search([
    'index' => $index,
]);

foreach ($response['hits']['hits'] as $hit) {
    echo json_encode($hit['_source']) . "\n";
}

{% include copy.html %}

range 쿼리를 사용해 검색해 보세요:

$response = $client->search([
    'index' => $index,
    'body' => [
        'query' => [
            'range' => [
                'gradDate' => [
                    'gte' => '2019-01-01',
                    'lte' => '2019-12-31',
                ],
            ],
        ],
    ],
]);

{% include copy.html %}

쿼리를 SQL로 작성하려면 sql() 네임스페이스를 사용하세요. 응답에는 컬럼을 설명하는 schema 배열과, 일치하는 행을 담은 datarows 배열이 들어 있어요:

$response = $client->sql()->query([
    'body' => [
        'query' => "SELECT firstName, lastName, gpa FROM $index WHERE gradDate BETWEEN '2019-01-01' AND '2019-12-31'",
    ],
]);

{% include copy.html %}

결과 페이징하기

결과를 페이징하려면 from과 size 파라미터를 사용하세요. 다음 예제는 학생들을 졸업일 기준으로 정렬한 다음 결과를 한 번에 두 개씩 가져와요. 첫 번째 요청은 결과의 첫 페이지를 반환하고, 두 번째 요청은 그다음 페이지를 반환해요:

foreach ([0, 2] as $from) {
    $response = $client->search([
        'index' => $index,
        'body' => [
            'from' => $from,
            'size' => 2,
            'sort' => [['gradDate' => 'asc']],
        ],
    ]);

    foreach ($response['hits']['hits'] as $hit) {
        echo json_encode($hit['_source']) . "\n";
    }
}

{% include copy.html %}

from과 size 파라미터는 결과의 처음 몇 페이지를 조회할 때 잘 동작해요. 많은 양의 결과를 페이징하려면 point in time을 사용한 페이징에서 설명하는 것처럼 point in time을 search_after와 함께 사용하세요.

point in time을 사용한 페이징

많은 양의 결과를 페이징하거나 인덱스의 고정된 뷰를 훑어 보려면 point in time(PIT)을 search_after와 함께 사용하세요. PIT를 생성한 다음 검색 본문에 PIT의 ID를 전달하고, 마지막 히트의 sort 값을 다음 페이지의 search_after 값으로 사용하세요:

$response = $client->createPit([
    'index' => $index,
    'keep_alive' => '10m',
]);
$pitId = $response['pit_id'];

// Get the first page of results.
$response = $client->search([
    'body' => [
        'pit' => ['id' => $pitId, 'keep_alive' => '10m'],
        'size' => 2,
        'sort' => [['gradDate' => 'asc']],
    ],
]);
$last = end($response['hits']['hits']);

// Get the next page of results.
$response = $client->search([
    'body' => [
        'pit' => ['id' => $pitId, 'keep_alive' => '10m'],
        'search_after' => $last['sort'],
        'size' => 2,
        'sort' => [['gradDate' => 'asc']],
    ],
]);

// Delete the point in time.
$client->deletePit([
    'body' => ['pit_id' => [$pitId]],
]);

{% include copy.html %}

문서 업데이트하기

변경할 필드를 doc 객체로 감싸서 문서를 업데이트하세요:

$response = $client->update([
    'index' => $index,
    'id' => '1',
    'body' => [
        'doc' => [
            'gpa' => 3.92,
        ],
    ],
]);

{% include copy.html %}

문서 삭제하기

다음 코드를 사용해 문서를 삭제하세요:

$response = $client->delete([
    'index' => $index,
    'id' => '3',
    'refresh' => true,
]);

{% include copy.html %}

쿼리와 일치하는 모든 문서를 삭제하려면 deleteByQuery()를 사용하세요:

$response = $client->deleteByQuery([
    'index' => $index,
    'body' => [
        'query' => [
            'range' => [
                'gradDate' => [
                    'gte' => '2021-01-01',
                    'lte' => '2021-12-31',
                ],
            ],
        ],
    ],
]);

{% include copy.html %}

인덱스 삭제하기

다음 코드를 사용해 인덱스를 삭제하세요:

$response = $client->indices()->delete([
    'index' => $index,
]);

{% include copy.html %}

샘플 프로그램

이 샘플 프로그램은 앞선 섹션의 코드들을 하나로 합친 것이에요. Security 플러그인이 활성화된 클러스터에 연결해요. Security 플러그인이 없는 클러스터에 연결하려면 // Without security 주석이 붙은 줄을 바꾸세요.

이 샘플 프로그램은 테스트 용도로만 사용하세요. 자체 서명 인증서를 사용하는 클러스터에 연결할 수 있도록 코드에 자격 증명을 직접 지정하고 인증서 검증을 비활성화했어요. 프로덕션 환경에서는 자격 증명을 안전한 위치에서 불러오고 클러스터의 인증서를 검증하세요. {: .warning}

다음 샘플 프로그램은 클라이언트를 생성하고, 인덱스를 만들고, 문서를 개별적으로 그리고 벌크로 인덱싱한 다음, 문서를 검색하고, 업데이트하고, 삭제한 후 마지막에 인덱스를 삭제해요:

<?php

require __DIR__ . '/vendor/autoload.php';

$client = (new \OpenSearch\GuzzleClientFactory())->create([
    'base_uri' => 'https://localhost:9200', // Without security, use http://localhost:9200
    'auth' => ['admin', '<custom-admin-password>'], // Without security, remove this line
    'verify' => false, // Without security, remove this line
]);

try {
    // Create the index
    $index = 'students';
    echo "Creating index......\n";
    $response = $client->indices()->create([
        'index' => $index,
        'body' => [
            'settings' => [
                'index' => [
                    'number_of_shards' => 1,
                    'number_of_replicas' => 1,
                ],
            ],
            'mappings' => [
                'properties' => [
                    'gradDate' => ['type' => 'date', 'format' => 'yyyy-MM-dd'],
                ],
            ],
        ],
    ]);
    echo "Index created: {$response['index']}\n";

    // Index a document
    echo "\nIndexing one student......\n";
    $response = $client->index([
        'index' => $index,
        'id' => '1',
        'body' => [
            'firstName' => 'John',
            'lastName' => 'Doe',
            'gpa' => 3.89,
            'gradDate' => '2022-05-15',
        ],
        'refresh' => true,
    ]);
    echo "Result: {$response['result']}, id: {$response['_id']}, version: {$response['_version']}\n";

    // Bulk index documents
    echo "\nIndexing many students......\n";
    $response = $client->bulk([
        'body' => [
            ['index' => ['_index' => $index, '_id' => '2']],
            ['firstName' => 'Paulo', 'lastName' => 'Santos', 'gpa' => 3.93, 'gradDate' => '2021-05-20'],
            ['index' => ['_index' => $index, '_id' => '3']],
            ['firstName' => 'Shirley', 'lastName' => 'Rodriguez', 'gpa' => 3.91, 'gradDate' => '2019-05-10'],
        ],
        'refresh' => true,
    ]);
    echo 'Errors: ' . var_export($response['errors'], true) . "\n";
    foreach ($response['items'] as $item) {
        $action = array_key_first($item);
        echo "  {$item[$action]['result']} id: {$item[$action]['_id']}\n";
    }

    // Search for all students
    echo "\nSearching for all students......\n";
    $response = $client->search([
        'index' => $index,
        'body' => [
            'from' => 0,
            'size' => 2,
            'sort' => [['gradDate' => 'asc']],
        ],
    ]);
    echo "Total hits: {$response['hits']['total']['value']}\n";
    echo "Page 1:\n";
    foreach ($response['hits']['hits'] as $hit) {
        echo '  ' . json_encode($hit['_source']) . "\n";
    }

    $response = $client->search([
        'index' => $index,
        'body' => [
            'from' => 2,
            'size' => 2,
            'sort' => [['gradDate' => 'asc']],
        ],
    ]);
    echo "Page 2:\n";
    foreach ($response['hits']['hits'] as $hit) {
        echo '  ' . json_encode($hit['_source']) . "\n";
    }

    // Search for students who graduated in 2019
    echo "\nSearching for students who graduated in 2019......\n";
    $response = $client->search([
        'index' => $index,
        'body' => [
            'query' => [
                'range' => [
                    'gradDate' => [
                        'gte' => '2019-01-01',
                        'lte' => '2019-12-31',
                    ],
                ],
            ],
        ],
    ]);
    echo "Total hits: {$response['hits']['total']['value']}\n";
    foreach ($response['hits']['hits'] as $hit) {
        echo '  ' . json_encode($hit['_source']) . "\n";
    }

    // Update a document
    echo "\nUpdating a student's GPA......\n";
    $response = $client->update([
        'index' => $index,
        'id' => '1',
        'body' => [
            'doc' => [
                'gpa' => 3.92,
            ],
        ],
    ]);
    echo "Result: {$response['result']}, version: {$response['_version']}\n";

    // Get the updated document
    $response = $client->get([
        'index' => $index,
        'id' => '1',
    ]);
    echo 'Updated document: ' . json_encode($response['_source']) . "\n";

    // Delete a document
    echo "\nDeleting a student......\n";
    $response = $client->delete([
        'index' => $index,
        'id' => '3',
        'refresh' => true,
    ]);
    echo "Result: {$response['result']}\n";

    // Delete the index
    echo "\nDeleting the index......\n";
    $response = $client->indices()->delete([
        'index' => $index,
    ]);
    echo 'Acknowledged: ' . var_export($response['acknowledged'], true) . "\n";
} catch (\OpenSearch\Exception\HttpExceptionInterface $e) {
    echo 'OpenSearch returned an error: ' . $e->getMessage() . "\n";
}

{% include copy.html %}

이 프로그램을 실행하면 다음과 같은 출력이 나와요:

Creating index......
Index created: students

Indexing one student......
Result: created, id: 1, version: 1

Indexing many students......
Errors: false
  created id: 2
  created id: 3

Searching for all students......
Total hits: 3
Page 1:
  {"firstName":"Shirley","lastName":"Rodriguez","gpa":3.91,"gradDate":"2019-05-10"}
  {"firstName":"Paulo","lastName":"Santos","gpa":3.93,"gradDate":"2021-05-20"}
Page 2:
  {"firstName":"John","lastName":"Doe","gpa":3.89,"gradDate":"2022-05-15"}

Searching for students who graduated in 2019......
Total hits: 1
  {"firstName":"Shirley","lastName":"Rodriguez","gpa":3.91,"gradDate":"2019-05-10"}

Updating a student's GPA......
Result: updated, version: 2
Updated document: {"firstName":"John","lastName":"Doe","gpa":3.92,"gradDate":"2022-05-15"}

Deleting a student......
Result: deleted

Deleting the index......
Acknowledged: true

관련 문서

더 알아보기 (Learn more)