Commit b17c8324 authored by platyhouse's avatar platyhouse

# Elasticsearch CLI 도구 모음 리팩토링 및 IDE 설정 추가

## IDE 및 프로젝트 설정

### JetBrains IDE 설정 추가
- .gitignore: JetBrains IDE 자동 생성 파일 제외 규칙 추가
- .idea/.gitignore: IDE 기본 무시 파일 설정
- .idea/modules.xml: 프로젝트 모듈 구성
- .idea/vcs.xml: Git VCS 매핑 설정
- .idea/php.xml: PHP 8.1 언어 레벨 및 코드 분석 도구 설정
- .idea/pty_centos.git.iml: 소스 폴더 및 네임스페이스 매핑 (platyFramework)
- .idea/inspectionProfiles/Project_Default.xml: JSHint 검사 활성화
- .idea/jsLinters/jshint.xml: JSHint 린터 설정

### Claude Code 가이드 추가
- CLAUDE.md: ptyElastic* 스크립트 작성 규칙, 라이브러리 구조, 설정 파일 형식 문서화

## Elasticsearch CLI 스크립트 리팩토링

### 공통 라이브러리 추가
- ptyLibrary_PHP/cli/ptyCliOptionParser.php: CLI 인자 파서 (positional/options 분리)
- ptyLibrary_PHP/elastic/ptyElasticConfig.php: Elasticsearch 설정 로더 및 연결 관리

### 인덱스 조회 도구 개선
- ptyElasticGetIndex → ptyElasticGetIndex.php 리네임:
  - platyFramework 네임스페이스 적용
  - ptyCliOptionParser, ptyElasticConfig 사용으로 코드 간소화
  - --elastic, --verbose, --limit, --help 옵션 추가
  - 직접 cURL 호출 대신 Elastic 클라이언트 사용

### 인덱스 목록 도구 재작성
- ptyElasticGetIndexs 삭제 및 ptyElasticGetIndexs.php 신규 작성:
  - 기존 220줄 → 168줄로 간소화
  - 공통 라이브러리 사용으로 중복 코드 제거
  - 동일한 옵션 체계 적용

### 인덱스 초기화 도구 개선
- ptyElasticTruncateIndex → ptyElasticTruncateIndex.php 리네임:
  - 공통 패턴 적용 (네임스페이스, 옵션 파서, 설정 로더)
  - 안전 확인 프롬프트 유지

### MySQL 데이터 업로드 도구 추가
- ptyElasticUploadFromMysql.php: MySQL 테이블 데이터를 Elasticsearch에 벌크 업로드하는 도구

## 라이브러리 개선

### Elastic.php 클라이언트 개선
- ptyLibrary_PHP/elastic/Elastic.php:
  - setDebug() 메서드 추가로 상세 로그 제어 가능
  - 디버그 모드에서만 요청/응답 로그 출력

## Git 서브트리 관리

### 서브트리 관리 스크립트 추가
- ptyGitSubtree: ptyLibrary_PHP 서브트리 push/pull 자동화 스크립트
parent ff757275
# Covers JetBrains IDEs: IntelliJ, GoLand, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
.idea/sonarlint.xml # see https://community.sonarsource.com/t/is-the-file-idea-idea-idea-sonarlint-xml-intended-to-be-under-source-control/121119
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based HTTP Client
.idea/httpRequests
http-client.private.env.json
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# Apifox Helper cache
.idea/.cache/.Apifox_Helper
.idea/ApifoxUploaderProjectSetting.xml
# Github Copilot persisted session migrations, see: https://github.com/microsoft/copilot-intellij-feedback/issues/712#issuecomment-3322062215
.idea/**/copilot.data.migration.*.xml
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="JSHint" enabled="true" level="ERROR" enabled_by_default="true" />
</profile>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JSHintConfiguration" version="2.13.6" use-config-file="true" use-custom-config-file="true" custom-config-file-path="$PROJECT_DIR$/ptyLibrary_PHP/ptycommon/resources/uploader-master/.jshintrc">
<option bitwise="true" />
<option browser="true" />
<option curly="true" />
<option eqeqeq="true" />
<option forin="true" />
<option maxerr="50" />
<option noarg="true" />
<option noempty="true" />
<option nonew="true" />
<option strict="true" />
<option undef="true" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/pty_centos.git.iml" filepath="$PROJECT_DIR$/.idea/pty_centos.git.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.1">
<option name="suggestChangeDefaultLanguageLevel" value="false" />
</component>
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/ptyLibrary_PHP/aivectorembedding" isTestSource="false" packagePrefix="platyFramework" />
<sourceFolder url="file://$MODULE_DIR$/ptyLibrary_PHP/cli" isTestSource="false" packagePrefix="platyFramework" />
<sourceFolder url="file://$MODULE_DIR$/ptyLibrary_PHP/crawling" isTestSource="false" packagePrefix="platyFramework" />
<sourceFolder url="file://$MODULE_DIR$/ptyLibrary_PHP/elastic" isTestSource="false" packagePrefix="platyFramework" />
<sourceFolder url="file://$MODULE_DIR$/ptyLibrary_PHP/external" isTestSource="false" packagePrefix="PhpOffice" />
<sourceFolder url="file://$MODULE_DIR$/ptyLibrary_PHP/external/PhpSpreadsheet2/src" isTestSource="false" packagePrefix="PhpOffice" />
<sourceFolder url="file://$MODULE_DIR$/ptyLibrary_PHP/external/PhpSpreadsheet2/tests" isTestSource="true" packagePrefix="PhpOffice" />
<sourceFolder url="file://$MODULE_DIR$/ptyLibrary_PHP/external/html2text" isTestSource="false" packagePrefix="platyFramework" />
<sourceFolder url="file://$MODULE_DIR$/ptyLibrary_PHP/external/pagination" isTestSource="false" packagePrefix="platyFramework" />
<sourceFolder url="file://$MODULE_DIR$/ptyLibrary_PHP/media" isTestSource="false" packagePrefix="platyFramework" />
<sourceFolder url="file://$MODULE_DIR$/ptyLibrary_PHP/ptycommon" isTestSource="false" packagePrefix="platyFramework" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
\ No newline at end of file
# CLAUDE.md
이 파일은 Claude Code가 이 저장소의 코드 작업 시 참고하는 가이드입니다.
## 프로젝트 개요
pty_centos.git은 Elasticsearch, MySQL 등 다양한 서비스를 다루는 CLI 도구 모음입니다.
## 네임스페이스
모든 PHP 코드는 `platyFramework` 네임스페이스를 사용합니다.
## ptyElastic* 스크립트 작성 규칙
새로운 Elasticsearch CLI 스크립트 생성 시 다음 패턴을 따릅니다:
### 필수 구조
```php
#!/usr/bin/env php
<?php
/**
* 스크립트 설명
*
* 설정 파일: ~/.ptyElasticConfig.ini
*
* Usage: ./ptyElasticXXX <args> [options]
*/
namespace platyFramework;
require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';
require_once __DIR__ . '/ptyLibrary_PHP/elastic/ptyElasticConfig.php';
// 인자 파싱
$parsed = ptyCliOptionParser::parse($argv);
$positionalArgs = $parsed['positional'];
$options = $parsed['options'];
$elasticSection = $options['elastic'] ?? 'default';
$verbose = isset($options['verbose']);
// 도움말 또는 필수 인자 확인
if (/* 필수 인자 누락 */ || isset($options['help'])) {
echo "사용법: {$argv[0]} <필수인자> [옵션]\n";
echo "\n";
echo "옵션:\n";
echo " --elastic=섹션명 INI 파일 섹션 (기본값: default)\n";
echo " --verbose 상세 로그 출력\n";
echo " --help 도움말 출력\n";
echo "\n";
// ... 추가 도움말 ...
echo "설정 파일: ~/.ptyElasticConfig.ini\n";
echo ptyElasticConfig::getConfigExample() . "\n";
exit(isset($options['help']) ? 0 : 1);
}
try {
// Elasticsearch 연결
$connection = ptyElasticConfig::connect($elasticSection);
$elastic = $connection['client'];
$elastic->setDebug($verbose); // --verbose 옵션에 따라 로그 제어
$config = $connection['config'];
$authMethod = $connection['authMethod'];
// ... 비즈니스 로직 ...
} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
```
### 필수 옵션
| 옵션 | 설명 |
|------|------|
| `--elastic=섹션명` | INI 파일 섹션 지정 (기본값: default) |
| `--verbose` | 상세 로그 출력 (기본: 비활성화) |
| `--help` | 도움말 출력 |
### 주요 클래스/함수
- `ptyCliOptionParser::parse($argv)` - CLI 인자 파싱 (positional, options 분리)
- `ptyElasticConfig::connect($section)` - Elasticsearch 연결 및 클라이언트 반환
- `ptyElasticConfig::getConfigExample()` - 설정 파일 예시 문자열 반환
- `$elastic->setDebug($verbose)` - 상세 로그 활성화/비활성화
### Exit 코드
- `0`: 정상 종료 (--help 포함)
- `1`: 에러 또는 필수 인자 누락
## ptyLibrary_PHP 구조
```
ptyLibrary_PHP/
├── cli/
│ ├── ptyCliOptionParser.php # CLI 옵션 파서
│ ├── ptyCliLog.php # 컬러 로깅
│ └── ptyCliColor.php # ANSI 색상 코드
├── elastic/
│ ├── ptyElasticConfig.php # Elasticsearch 설정 로더
│ └── Elastic.php # Elasticsearch 클라이언트
└── ...
```
## 설정 파일
### ~/.ptyElasticConfig.ini
```ini
[default]
host=https://localhost:9200
apiKey=your_api_key
# 또는 user/password 방식
# user=elastic
# password="your_password"
[production]
host=https://prod-elastic:9200
apiKey=production_api_key
```
### ~/.ptyMysqlConfig.ini
```ini
[default]
host=localhost
username=root
password="your_password"
database=your_db
charset=utf8mb4
```
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
/** /**
* ptyElasticgetIndexs * ptyElasticGetIndexs
* *
* Elasticsearch 인덱스 정보를 조회하는 도구 * Elasticsearch 인덱스 목록을 조회하는 도구
* 설정 파일: ~/.ptyElasticConfig.ini * 설정 파일: ~/.ptyElasticConfig.ini
*
* Usage: ./ptyElasticGetIndexs [--elastic=섹션명]
*/ */
// 설정 파일 경로 namespace platyFramework;
$configFile = getenv('HOME') . '/.ptyElasticConfig.ini';
require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';
// 설정 파일 확인 require_once __DIR__ . '/ptyLibrary_PHP/elastic/ptyElasticConfig.php';
if (!file_exists($configFile)) {
echo "Error: 설정 파일을 찾을 수 없습니다: $configFile\n"; // 인자 파싱
echo "\n설정 파일 예시:\n"; $parsed = ptyCliOptionParser::parse($argv);
echo "[elastic]\n"; $options = $parsed['options'];
echo "host=https://localhost:9200\n"; $elasticSection = $options['elastic'] ?? 'default';
echo "user=elastic\n"; $verbose = isset($options['verbose']);
echo "password=yourpassword\n";
exit(1); // 도움말 요청시
} if (isset($options['help'])) {
echo "사용법: {$argv[0]} [옵션]\n";
// 설정 파일 읽기 echo "\n";
$config = parse_ini_file($configFile, true); echo "Elasticsearch 인덱스 목록을 조회합니다.\n";
echo "\n";
if (!isset($config['elastic'])) { echo "옵션:\n";
echo "Error: 설정 파일에 [elastic] 섹션이 없습니다.\n"; echo " --elastic=섹션명 INI 파일 섹션 (기본값: default)\n";
exit(1); echo " --verbose 상세 로그 출력\n";
} echo " --help 도움말 출력\n";
echo "\n";
$elasticConfig = $config['elastic']; echo "예시:\n";
$host = $elasticConfig['host'] ?? ''; echo " {$argv[0]}\n";
$user = $elasticConfig['user'] ?? ''; echo " {$argv[0]} --elastic=production\n";
$password = $elasticConfig['password'] ?? ''; echo " {$argv[0]} --verbose\n";
echo "\n";
if (empty($host)) { echo "설정 파일: ~/.ptyElasticConfig.ini\n";
echo "Error: host 설정이 필요합니다.\n"; echo ptyElasticConfig::getConfigExample() . "\n";
exit(1); exit(0);
}
// Elasticsearch API 호출
function callElasticAPI($url, $user, $password) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
if (!empty($user) && !empty($password)) {
curl_setopt($ch, CURLOPT_USERPWD, "$user:$password");
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
$error = curl_error($ch);
curl_close($ch);
throw new Exception("cURL Error: $error");
}
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception("HTTP Error: $httpCode - $response");
}
return $response;
} }
// 타임스탬프 형식 변환 // 타임스탬프 형식 변환
function formatTimestamp($timestamp) { function formatTimestamp($timestamp) {
// 밀리초 타임스탬프인 경우 (13자리 숫자)
if (is_numeric($timestamp) && strlen($timestamp) >= 13) { if (is_numeric($timestamp) && strlen($timestamp) >= 13) {
$seconds = intval($timestamp / 1000); $seconds = intval($timestamp / 1000);
return date('Y-m-d H:i:s', $seconds); return date('Y-m-d H:i:s', $seconds);
} }
// 초 타임스탬프인 경우 (10자리 숫자)
if (is_numeric($timestamp) && strlen($timestamp) == 10) { if (is_numeric($timestamp) && strlen($timestamp) == 10) {
return date('Y-m-d H:i:s', intval($timestamp)); return date('Y-m-d H:i:s', intval($timestamp));
} }
// 이미 문자열 형식인 경우
if (is_string($timestamp)) { if (is_string($timestamp)) {
return substr($timestamp, 0, 19); return substr($timestamp, 0, 19);
} }
...@@ -89,37 +57,16 @@ function formatTimestamp($timestamp) { ...@@ -89,37 +57,16 @@ function formatTimestamp($timestamp) {
} }
// 인덱스의 마지막 문서 시간 조회 // 인덱스의 마지막 문서 시간 조회
function getLastDocumentTime($host, $user, $password, $indexName) { function getLastDocumentTime($elastic, $indexName) {
try { try {
$searchUrl = rtrim($host, '/') . '/' . urlencode($indexName) . '/_search'; $response = $elastic->search($indexName . '/_search', [
$searchBody = json_encode([
'size' => 1, 'size' => 1,
'sort' => [ 'sort' => [['_index' => ['order' => 'desc']]],
['_index' => ['order' => 'desc']]
],
'query' => ['match_all' => (object)[]] 'query' => ['match_all' => (object)[]]
]); ]);
$ch = curl_init(); if (isset($response['hits']['hits'][0]['_source'])) {
curl_setopt($ch, CURLOPT_URL, $searchUrl); $source = $response['hits']['hits'][0]['_source'];
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, $searchBody);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
if (!empty($user) && !empty($password)) {
curl_setopt($ch, CURLOPT_USERPWD, "$user:$password");
}
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
if (isset($data['hits']['hits'][0]['_source'])) {
$source = $data['hits']['hits'][0]['_source'];
// 일반적인 타임스탬프 필드 찾기
foreach (['@timestamp', 'timestamp', 'created_at', 'updated_at', 'date', 'createdAt', 'updatedAt'] as $field) { foreach (['@timestamp', 'timestamp', 'created_at', 'updated_at', 'date', 'createdAt', 'updatedAt'] as $field) {
if (isset($source[$field])) { if (isset($source[$field])) {
return formatTimestamp($source[$field]); return formatTimestamp($source[$field]);
...@@ -127,33 +74,34 @@ function getLastDocumentTime($host, $user, $password, $indexName) { ...@@ -127,33 +74,34 @@ function getLastDocumentTime($host, $user, $password, $indexName) {
} }
} }
return 'N/A'; return 'N/A';
} catch (Exception $e) { } catch (\Exception $e) {
return 'N/A'; return 'N/A';
} }
} }
try { try {
// 인덱스 정보 조회 (_cat/indices API 사용) // Elasticsearch 연결
$url = rtrim($host, '/') . '/_cat/indices?v&format=json&bytes=mb&h=index,status,docs.count,store.size,health,pri,rep,creation.date.string'; $connection = ptyElasticConfig::connect($elasticSection);
$elastic = $connection['client'];
$elastic->setDebug($verbose);
$config = $connection['config'];
$authMethod = $connection['authMethod'];
echo "Elasticsearch 인덱스 정보 조회 중...\n"; echo "Elasticsearch 인덱스 정보 조회 중...\n";
echo "Host: $host\n"; echo "Host: {$config['host']} ({$authMethod})\n";
echo "섹션: {$elasticSection}\n";
echo str_repeat("=", 80) . "\n\n"; echo str_repeat("=", 80) . "\n\n";
$response = callElasticAPI($url, $user, $password); // 인덱스 정보 조회
$indices = json_decode($response, true); $response = $elastic->get('_cat/indices?v&format=json&bytes=mb&h=index,status,docs.count,store.size,health,pri,rep,creation.date.string');
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("JSON 파싱 오류: " . json_last_error_msg());
}
if (empty($indices)) { if (empty($response)) {
echo "인덱스가 없습니다.\n"; echo "인덱스가 없습니다.\n";
exit(0); exit(0);
} }
// 결과 정렬 (인덱스 이름순) // 결과 정렬 (인덱스 이름순)
usort($indices, function($a, $b) { usort($response, function($a, $b) {
return strcmp($a['index'], $b['index']); return strcmp($a['index'], $b['index']);
}); });
...@@ -166,7 +114,7 @@ try { ...@@ -166,7 +114,7 @@ try {
$totalDocs = 0; $totalDocs = 0;
$totalSize = 0; $totalSize = 0;
foreach ($indices as $index) { foreach ($response as $index) {
$indexName = $index['index'] ?? 'N/A'; $indexName = $index['index'] ?? 'N/A';
$status = $index['status'] ?? 'N/A'; $status = $index['status'] ?? 'N/A';
$docsCount = $index['docs.count'] ?? '0'; $docsCount = $index['docs.count'] ?? '0';
...@@ -179,19 +127,19 @@ try { ...@@ -179,19 +127,19 @@ try {
// 마지막 색인 시간 조회 // 마지막 색인 시간 조회
$lastIndexed = 'N/A'; $lastIndexed = 'N/A';
if ($docsCount !== '0' && is_numeric($docsCount)) { if ($docsCount !== '0' && is_numeric($docsCount)) {
$lastIndexed = getLastDocumentTime($host, $user, $password, $indexName); $lastIndexed = getLastDocumentTime($elastic, $indexName);
} }
// 숫자 형식 정리 // 숫자 형식 정리
$docsCount = is_numeric($docsCount) ? number_format($docsCount) : $docsCount; $docsCountFormatted = is_numeric($docsCount) ? number_format($docsCount) : $docsCount;
// 용량 형식 정리 (234mb -> 234 MB) // 용량 형식 정리
$storeSizeFormatted = is_numeric($storeSize) ? number_format($storeSize, 2) . ' MB' : $storeSize; $storeSizeFormatted = is_numeric($storeSize) ? number_format($storeSize, 2) . ' MB' : $storeSize;
printf("%-40s %-10s %-12s %-12s %-10s %-10s %-20s %-20s\n", printf("%-40s %-10s %-12s %-12s %-10s %-10s %-20s %-20s\n",
substr($indexName, 0, 40), substr($indexName, 0, 40),
$status, $status,
$docsCount, $docsCountFormatted,
$storeSizeFormatted, $storeSizeFormatted,
$health, $health,
$pri . '/' . $rep, $pri . '/' . $rep,
...@@ -200,21 +148,21 @@ try { ...@@ -200,21 +148,21 @@ try {
); );
// 총합 계산 // 총합 계산
if (is_numeric($index['docs.count'] ?? null)) { if (is_numeric($docsCount)) {
$totalDocs += intval($index['docs.count']); $totalDocs += intval($docsCount);
} }
if (is_numeric($index['store.size'] ?? null)) { if (is_numeric($storeSize)) {
$totalSize += floatval($index['store.size']); $totalSize += floatval($storeSize);
} }
} }
// 요약 정보 출력 // 요약 정보 출력
echo str_repeat("=", 165) . "\n"; echo str_repeat("=", 165) . "\n";
printf("총 인덱스 수: %d\n", count($indices)); printf("총 인덱스 수: %d\n", count($response));
printf("총 문서 수: %s\n", number_format($totalDocs)); printf("총 문서 수: %s\n", number_format($totalDocs));
printf("총 용량: %.2f MB (%.2f GB)\n", $totalSize, $totalSize / 1024); printf("총 용량: %.2f MB (%.2f GB)\n", $totalSize, $totalSize / 1024);
} catch (Exception $e) { } catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n"; echo "Error: " . $e->getMessage() . "\n";
exit(1); exit(1);
} }
...@@ -6,87 +6,42 @@ ...@@ -6,87 +6,42 @@
* Elasticsearch 인덱스의 모든 문서를 삭제하는 도구 * Elasticsearch 인덱스의 모든 문서를 삭제하는 도구
* 설정 파일: ~/.ptyElasticConfig.ini * 설정 파일: ~/.ptyElasticConfig.ini
* *
* Usage: ./ptyElasticTruncateIndex <index_name> * Usage: ./ptyElasticTruncateIndex <index_name> [--elastic=섹션명]
*/ */
// 커맨드 라인 인자 확인 namespace platyFramework;
if ($argc < 2) {
echo "Usage: $argv[0] <index_name>\n";
echo "Example: $argv[0] my_index\n";
echo "\nWarning: This will DELETE ALL DOCUMENTS in the index!\n";
exit(1);
}
$indexName = $argv[1];
// 설정 파일 경로
$configFile = getenv('HOME') . '/.ptyElasticConfig.ini';
// 설정 파일 확인
if (!file_exists($configFile)) {
echo "Error: 설정 파일을 찾을 수 없습니다: $configFile\n";
echo "\n설정 파일 예시:\n";
echo "[elastic]\n";
echo "host=https://localhost:9200\n";
echo "user=elastic\n";
echo "password=yourpassword\n";
exit(1);
}
// 설정 파일 읽기
$config = parse_ini_file($configFile, true);
if (!isset($config['elastic'])) { require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';
echo "Error: 설정 파일에 [elastic] 섹션이 없습니다.\n"; require_once __DIR__ . '/ptyLibrary_PHP/elastic/ptyElasticConfig.php';
exit(1);
}
$elasticConfig = $config['elastic']; // 인자 파싱
$host = $elasticConfig['host'] ?? ''; $parsed = ptyCliOptionParser::parse($argv);
$user = $elasticConfig['user'] ?? ''; $positionalArgs = $parsed['positional'];
$password = $elasticConfig['password'] ?? ''; $options = $parsed['options'];
$elasticSection = $options['elastic'] ?? 'default';
$verbose = isset($options['verbose']);
if (empty($host)) { // 도움말 또는 인덱스명 확인
echo "Error: host 설정이 필요합니다.\n"; if (empty($positionalArgs) || isset($options['help'])) {
exit(1); echo "사용법: {$argv[0]} <index_name> [--elastic=섹션명]\n";
echo "\n";
echo "옵션:\n";
echo " --elastic=섹션명 INI 파일 섹션 (기본값: default)\n";
echo " --verbose 상세 로그 출력\n";
echo " --help 도움말 출력\n";
echo "\n";
echo "예시:\n";
echo " {$argv[0]} my_index\n";
echo " {$argv[0]} my_index --elastic=production\n";
echo "\n";
echo "Warning: This will DELETE ALL DOCUMENTS in the index!\n";
echo "\n";
echo "설정 파일: ~/.ptyElasticConfig.ini\n";
echo ptyElasticConfig::getConfigExample() . "\n";
exit(isset($options['help']) ? 0 : 1);
} }
// Elasticsearch API 호출 $indexName = $positionalArgs[0];
function callElasticAPI($url, $user, $password, $method = 'GET', $body = null) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
}
if (!empty($user) && !empty($password)) {
curl_setopt($ch, CURLOPT_USERPWD, "$user:$password");
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
$error = curl_error($ch);
curl_close($ch);
throw new Exception("cURL Error: $error");
}
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception("HTTP Error: $httpCode - $response");
}
return $response;
}
// 바이트를 읽기 쉬운 형식으로 변환 // 바이트를 읽기 쉬운 형식으로 변환
function formatBytes($bytes) { function formatBytes($bytes) {
...@@ -103,14 +58,21 @@ function formatBytes($bytes) { ...@@ -103,14 +58,21 @@ function formatBytes($bytes) {
try { try {
// 색상 코드 정의 // 색상 코드 정의
$redColor = "\033[1;31m"; // 밝은 빨간색 $redColor = "\033[1;31m";
$yellowColor = "\033[1;33m"; // 밝은 노란색 $yellowColor = "\033[1;33m";
$greenColor = "\033[1;32m"; // 밝은 녹색 $greenColor = "\033[1;32m";
$cyanColor = "\033[1;36m"; // 밝은 청록색 $cyanColor = "\033[1;36m";
$resetColor = "\033[0m"; $resetColor = "\033[0m";
// Elasticsearch 연결
$connection = ptyElasticConfig::connect($elasticSection);
$elastic = $connection['client'];
$elastic->setDebug($verbose);
$config = $connection['config'];
$authMethod = $connection['authMethod'];
echo "{$redColor}WARNING: Elasticsearch 인덱스 전체 삭제{$resetColor}\n"; echo "{$redColor}WARNING: Elasticsearch 인덱스 전체 삭제{$resetColor}\n";
echo "Host: $host\n"; echo "Host: {$config['host']} ({$authMethod})\n";
echo "Index: {$yellowColor}$indexName{$resetColor}\n"; echo "Index: {$yellowColor}$indexName{$resetColor}\n";
echo str_repeat("=", 100) . "\n\n"; echo str_repeat("=", 100) . "\n\n";
...@@ -118,12 +80,10 @@ try { ...@@ -118,12 +80,10 @@ try {
echo "{$cyanColor}[ 인덱스 정보 ]{$resetColor}\n"; echo "{$cyanColor}[ 인덱스 정보 ]{$resetColor}\n";
echo str_repeat("-", 100) . "\n"; echo str_repeat("-", 100) . "\n";
$catUrl = rtrim($host, '/') . '/_cat/indices/' . urlencode($indexName) . '?v&format=json&bytes=b'; $catData = $elastic->get('_cat/indices/' . urlencode($indexName) . '?v&format=json&bytes=b');
$catResponse = callElasticAPI($catUrl, $user, $password);
$catData = json_decode($catResponse, true);
if (empty($catData)) { if (empty($catData)) {
throw new Exception("인덱스를 찾을 수 없습니다: $indexName"); throw new \Exception("인덱스를 찾을 수 없습니다: $indexName");
} }
$indexInfo = $catData[0]; $indexInfo = $catData[0];
...@@ -137,15 +97,11 @@ try { ...@@ -137,15 +97,11 @@ try {
echo "{$cyanColor}[ 샘플 문서 10개 ]{$resetColor}\n"; echo "{$cyanColor}[ 샘플 문서 10개 ]{$resetColor}\n";
echo str_repeat("-", 100) . "\n"; echo str_repeat("-", 100) . "\n";
$searchUrl = rtrim($host, '/') . '/' . urlencode($indexName) . '/_search'; $searchData = $elastic->search(urlencode($indexName) . '/_search', [
$searchBody = json_encode([
'size' => 10, 'size' => 10,
'query' => ['match_all' => (object)[]] 'query' => ['match_all' => (object)[]]
]); ]);
$searchResponse = callElasticAPI($searchUrl, $user, $password, 'POST', $searchBody);
$searchData = json_decode($searchResponse, true);
if (isset($searchData['hits']['hits']) && count($searchData['hits']['hits']) > 0) { if (isset($searchData['hits']['hits']) && count($searchData['hits']['hits']) > 0) {
$hits = $searchData['hits']['hits']; $hits = $searchData['hits']['hits'];
...@@ -207,30 +163,28 @@ try { ...@@ -207,30 +163,28 @@ try {
// 4. 모든 문서 삭제 // 4. 모든 문서 삭제
echo "\n{$yellowColor}삭제 중...{$resetColor}\n"; echo "\n{$yellowColor}삭제 중...{$resetColor}\n";
$deleteUrl = rtrim($host, '/') . '/' . urlencode($indexName) . '/_delete_by_query?conflicts=proceed'; $deleteData = $elastic->post(urlencode($indexName) . '/_delete_by_query?conflicts=proceed', [
$deleteBody = json_encode([
'query' => ['match_all' => (object)[]] 'query' => ['match_all' => (object)[]]
]); ]);
$deleteResponse = callElasticAPI($deleteUrl, $user, $password, 'POST', $deleteBody);
$deleteData = json_decode($deleteResponse, true);
// 5. 결과 표시 // 5. 결과 표시
echo "\n" . str_repeat("=", 100) . "\n"; echo "\n" . str_repeat("=", 100) . "\n";
echo "{$greenColor}삭제 완료!{$resetColor}\n"; echo "{$greenColor}삭제 완료!{$resetColor}\n";
echo str_repeat("=", 100) . "\n"; echo str_repeat("=", 100) . "\n";
printf("삭제된 문서 수: {$greenColor}%s{$resetColor}\n", number_format($deleteData['deleted'] ?? 0)); printf("삭제된 문서 수: {$greenColor}%s{$resetColor}\n", number_format($deleteData['deleted'] ?? 0));
printf("실패한 문서 수: %s\n", number_format($deleteData['failures'] ?? 0)); printf("실패한 문서 수: %s\n", number_format(count($deleteData['failures'] ?? [])));
printf("소요 시간: %s ms\n", number_format($deleteData['took'] ?? 0)); printf("소요 시간: %s ms\n", number_format($deleteData['took'] ?? 0));
if (isset($deleteData['failures']) && $deleteData['failures'] > 0) { if (!empty($deleteData['failures'])) {
echo "\n{$redColor}일부 문서 삭제에 실패했습니다. 로그를 확인하세요.{$resetColor}\n"; echo "\n{$redColor}일부 문서 삭제에 실패했습니다. 로그를 확인하세요.{$resetColor}\n";
} }
echo "\n"; echo "\n";
} catch (Exception $e) { } catch (\Exception $e) {
$redColor = "\033[1;31m";
$resetColor = "\033[0m";
echo "{$redColor}Error: " . $e->getMessage() . "{$resetColor}\n"; echo "{$redColor}Error: " . $e->getMessage() . "{$resetColor}\n";
exit(1); exit(1);
} }
This diff is collapsed.
This diff is collapsed.
<?php
/**
* ptyCliOptionParser
*
* 커맨드라인 옵션 파서
*/
namespace platyFramework;
class ptyCliOptionParser
{
/**
* argv에서 위치 인자와 옵션을 분리
*
* @param array $argv
* @return array [positional => [], options => []]
*/
public static function parse($argv)
{
$positionalArgs = [];
$options = [];
foreach ($argv as $i => $arg) {
if ($i === 0) continue; // 스크립트 이름 제외
if (strpos($arg, '--') === 0) {
if (preg_match('/^--([^=]+)=(.*)$/', $arg, $matches)) {
$options[$matches[1]] = $matches[2];
} elseif (preg_match('/^--([^=]+)$/', $arg, $matches)) {
$options[$matches[1]] = true;
}
} else {
$positionalArgs[] = $arg;
}
}
return [
'positional' => $positionalArgs,
'options' => $options,
];
}
}
...@@ -9,12 +9,21 @@ class Elastic ...@@ -9,12 +9,21 @@ class Elastic
private $apiKey; private $apiKey;
private $baseUrl; private $baseUrl;
private $cliLog; private $cliLog;
private $user;
private $password;
public function __construct($apiKey = null, $baseUrl = null, $cliLog = null) public function __construct($apiKey = null, $baseUrl = null, $cliLog = null, $user = null, $password = null)
{ {
$this->apiKey = $apiKey ?? 'RWxCT2tvMEJyTUd0VTMwSDlleW06VHBNbmpmZE9TenkteThCMW5FOC1YUQ=='; $this->apiKey = $apiKey;
$this->baseUrl = $baseUrl ?? 'https://elastic:9200'; $this->baseUrl = $baseUrl ?? 'https://elastic:9200';
$this->cliLog = $cliLog ?: new ptyCliLog(true); $this->cliLog = $cliLog ?: new ptyCliLog(true);
$this->user = $user;
$this->password = $password;
// apiKey도 없고 user/password도 없으면 기본 apiKey 사용
if (empty($this->apiKey) && empty($this->user)) {
$this->apiKey = 'RWxCT2tvMEJyTUd0VTMwSDlleW06VHBNbmpmZE9TenkteThCMW5FOC1YUQ==';
}
} }
public function search(string $path, array $data) public function search(string $path, array $data)
...@@ -156,8 +165,10 @@ class Elastic ...@@ -156,8 +165,10 @@ class Elastic
$ch = curl_init(); $ch = curl_init();
$headers = ['Content-Type: application/json']; $headers = ['Content-Type: application/json'];
if ($this->apiKey) { if (!empty($this->apiKey)) {
$headers[] = 'Authorization: ApiKey ' . $this->apiKey; $headers[] = 'Authorization: ApiKey ' . $this->apiKey;
} elseif (!empty($this->user) && !empty($this->password)) {
$headers[] = 'Authorization: Basic ' . base64_encode($this->user . ':' . $this->password);
} }
curl_setopt_array($ch, [ curl_setopt_array($ch, [
......
<?php
/**
* ptyElasticConfig
*
* Elasticsearch 공통 설정 로더
* 설정 파일: ~/.ptyElasticConfig.ini
*
* 지원 인증 방식:
* - apiKey: API Key 인증
* - user/password: Basic 인증
*/
namespace platyFramework;
require_once __DIR__ . "/../cli/ptyCliLog.php";
require_once __DIR__ . "/Elastic.php";
/**
* Elasticsearch 설정 로더 클래스
*/
class ptyElasticConfig
{
private static $configPath = null;
/**
* 설정 파일 경로 반환
*/
public static function getConfigPath()
{
if (self::$configPath === null) {
self::$configPath = getenv('HOME') . '/.ptyElasticConfig.ini';
}
return self::$configPath;
}
/**
* 설정 파일에서 섹션 목록 조회
*/
public static function getSections()
{
$configPath = self::getConfigPath();
if (!file_exists($configPath)) {
return [];
}
$config = parse_ini_file($configPath, true);
return $config ? array_keys($config) : [];
}
/**
* Elasticsearch 설정 로드
*
* @param string $section INI 파일 섹션명 (기본값: default)
* @return array 설정 배열 [host, apiKey, user, password]
* @throws \Exception 설정 파일이나 섹션이 없을 경우
*/
public static function load($section = 'default')
{
$configPath = self::getConfigPath();
if (!file_exists($configPath)) {
throw new \Exception("Elasticsearch 설정 파일을 찾을 수 없습니다: {$configPath}\n\n" . self::getConfigExample());
}
$config = parse_ini_file($configPath, true);
if ($config === false) {
throw new \Exception("Elasticsearch 설정 파일을 파싱할 수 없습니다: {$configPath}");
}
if (!isset($config[$section])) {
$availableSections = implode(', ', array_keys($config));
throw new \Exception("Elasticsearch 설정에서 [{$section}] 섹션을 찾을 수 없습니다.\n사용 가능한 섹션: {$availableSections}");
}
$sectionConfig = $config[$section];
// 필수 필드 검증
if (!isset($sectionConfig['host']) || empty($sectionConfig['host'])) {
throw new \Exception("Elasticsearch 설정 [{$section}] 섹션에 필수 필드 'host'가 없습니다.");
}
return [
'host' => $sectionConfig['host'],
'apiKey' => isset($sectionConfig['apiKey']) ? trim($sectionConfig['apiKey'], '"\'') : null,
'user' => $sectionConfig['user'] ?? null,
'password' => isset($sectionConfig['password']) ? trim($sectionConfig['password'], '"\'') : null,
];
}
/**
* Elastic 클래스 인스턴스 생성
*
* @param string $section INI 파일 섹션명 (기본값: default)
* @return Elastic
*/
public static function createClient($section = 'default')
{
$config = self::load($section);
return new Elastic(
$config['apiKey'],
$config['host'],
null,
$config['user'],
$config['password']
);
}
/**
* 설정 정보와 함께 Elastic 클라이언트 반환
*
* @param string $section INI 파일 섹션명 (기본값: default)
* @return array [client => Elastic, config => array]
*/
public static function connect($section = 'default')
{
$config = self::load($section);
$client = new Elastic(
$config['apiKey'],
$config['host'],
null,
$config['user'],
$config['password']
);
$authMethod = !empty($config['apiKey']) ? 'apiKey' : 'user/password';
return [
'client' => $client,
'config' => $config,
'authMethod' => $authMethod,
];
}
/**
* 인증 헤더 생성 (curl용)
*
* @param array $config 설정 배열
* @return array HTTP 헤더 배열
*/
public static function getAuthHeaders($config)
{
$headers = ['Content-Type: application/json'];
if (!empty($config['apiKey'])) {
$headers[] = 'Authorization: ApiKey ' . $config['apiKey'];
} elseif (!empty($config['user']) && !empty($config['password'])) {
$headers[] = 'Authorization: Basic ' . base64_encode($config['user'] . ':' . $config['password']);
}
return $headers;
}
/**
* 설정 파일 예시 반환
*/
public static function getConfigExample()
{
return <<<EOT
설정 파일 예시 (~/.ptyElasticConfig.ini):
[default]
host=https://localhost:9200
apiKey=your_api_key
# 또는 user/password 방식
# [default]
# host=https://localhost:9200
# user=elastic
# password="your_password"
EOT;
}
/**
* 사용법 출력
*/
public static function printUsage($scriptName, $extraArgs = '', $extraOptions = '')
{
$sections = self::getSections();
$sectionsStr = empty($sections) ? '(설정 파일 없음)' : implode(', ', $sections);
echo "\n";
echo "사용법: {$scriptName} {$extraArgs}[--elastic=섹션명]\n";
echo "\n";
echo "설정 파일: ~/.ptyElasticConfig.ini\n";
echo "사용 가능한 섹션: {$sectionsStr}\n";
echo "\n";
if ($extraOptions) {
echo "옵션:\n";
echo $extraOptions;
echo "\n";
}
echo " --elastic=섹션명 INI 파일 섹션 (기본값: default)\n";
echo "\n";
echo self::getConfigExample();
echo "\n";
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment