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
/** /**
* ptyElasticGetIndexInfo * ptyElasticGetIndex
* *
* 특정 Elasticsearch 인덱스의 상세 정보와 TOP 10 문서를 조회하는 도구 * 특정 Elasticsearch 인덱스의 상세 정보와 문서를 조회하는 도구
* 설정 파일: ~/.ptyElasticConfig.ini * 설정 파일: ~/.ptyElasticConfig.ini
* *
* Usage: ./ptyElasticGetIndexInfo <index_name> * Usage: ./ptyElasticGetIndex <index_name> [search_term] [--elastic=섹션명]
*/ */
// 커맨드 라인 인자 확인 namespace platyFramework;
if ($argc < 2) {
echo "Usage: $argv[0] <index_name> [search_term1] [search_term2] ...\n"; require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';
echo "Example: $argv[0] my_index\n"; require_once __DIR__ . '/ptyLibrary_PHP/elastic/ptyElasticConfig.php';
echo "Example: $argv[0] my_index \"제3장*\"\n";
echo "Example: $argv[0] my_index \"전기통신\" \"제11조\"\n"; // 인자 파싱
echo "Example: $argv[0] my_index \"keyword AND another\"\n"; $parsed = ptyCliOptionParser::parse($argv);
echo "\nNote: Multiple search terms are combined with AND operator\n"; $positionalArgs = $parsed['positional'];
exit(1); $options = $parsed['options'];
$elasticSection = $options['elastic'] ?? 'default';
$limit = isset($options['limit']) ? (int)$options['limit'] : 50;
$verbose = isset($options['verbose']);
// 도움말 또는 인덱스명 확인
if (empty($positionalArgs) || isset($options['help'])) {
echo "사용법: {$argv[0]} <index_name> [search_term1] [search_term2] ... [옵션]\n";
echo "\n";
echo "옵션:\n";
echo " --limit=N 결과 문서 수 (기본값: 50)\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 --limit=5\n";
echo " {$argv[0]} my_index \"제3장*\"\n";
echo " {$argv[0]} my_index \"전기통신\" \"제11조\"\n";
echo " {$argv[0]} my_index \"keyword AND another\"\n";
echo " {$argv[0]} my_index --elastic=production --limit=10\n";
echo "\n";
echo "Note: Multiple search terms are combined with AND operator\n";
echo "\n";
echo "설정 파일: ~/.ptyElasticConfig.ini\n";
echo ptyElasticConfig::getConfigExample() . "\n";
exit(isset($options['help']) ? 0 : 1);
} }
$indexName = $argv[1]; $indexName = $positionalArgs[0];
// 2번째 인자부터 끝까지 모든 검색어를 AND로 연결 // 2번째 인자부터 끝까지 검색어로 사용
$searchTerms = array_slice($argv, 2); $searchTerms = array_slice($positionalArgs, 1);
if (!empty($searchTerms)) { if (!empty($searchTerms)) {
// 각 검색어를 괄호로 감싸고 AND로 연결
$searchQuery = '(' . implode(') AND (', $searchTerms) . ')'; $searchQuery = '(' . implode(') AND (', $searchTerms) . ')';
} else { } else {
$searchQuery = null; $searchQuery = null;
} }
// 설정 파일 경로
$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'])) {
echo "Error: 설정 파일에 [elastic] 섹션이 없습니다.\n";
exit(1);
}
$elasticConfig = $config['elastic'];
$host = $elasticConfig['host'] ?? '';
$user = $elasticConfig['user'] ?? '';
$password = $elasticConfig['password'] ?? '';
if (empty($host)) {
echo "Error: host 설정이 필요합니다.\n";
exit(1);
}
// Elasticsearch API 호출
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;
}
// JSON을 보기 좋게 출력
function printJson($data, $indent = 0) {
$prefix = str_repeat(" ", $indent);
if (is_array($data)) {
foreach ($data as $key => $value) {
if (is_array($value) || is_object($value)) {
echo $prefix . "$key:\n";
printJson($value, $indent + 1);
} else {
echo $prefix . "$key: $value\n";
}
}
} elseif (is_object($data)) {
foreach ($data as $key => $value) {
if (is_array($value) || is_object($value)) {
echo $prefix . "$key:\n";
printJson($value, $indent + 1);
} else {
echo $prefix . "$key: $value\n";
}
}
}
}
// 바이트를 읽기 쉬운 형식으로 변환 // 바이트를 읽기 쉬운 형식으로 변환
function formatBytes($bytes) { function formatBytes($bytes) {
if ($bytes >= 1073741824) { if ($bytes >= 1073741824) {
...@@ -139,8 +71,15 @@ function formatBytes($bytes) { ...@@ -139,8 +71,15 @@ function formatBytes($bytes) {
} }
try { try {
// Elasticsearch 연결
$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 "Index: $indexName\n"; echo "Index: $indexName\n";
echo str_repeat("=", 100) . "\n\n"; echo str_repeat("=", 100) . "\n\n";
...@@ -148,12 +87,10 @@ try { ...@@ -148,12 +87,10 @@ try {
echo "[ 1. 기본 정보 ]\n"; echo "[ 1. 기본 정보 ]\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];
...@@ -171,9 +108,7 @@ try { ...@@ -171,9 +108,7 @@ try {
echo "[ 2. 인덱스 설정 ]\n"; echo "[ 2. 인덱스 설정 ]\n";
echo str_repeat("-", 100) . "\n"; echo str_repeat("-", 100) . "\n";
$settingsUrl = rtrim($host, '/') . '/' . urlencode($indexName) . '/_settings'; $settingsData = $elastic->get(urlencode($indexName) . '/_settings');
$settingsResponse = callElasticAPI($settingsUrl, $user, $password);
$settingsData = json_decode($settingsResponse, true);
if (isset($settingsData[$indexName]['settings']['index'])) { if (isset($settingsData[$indexName]['settings']['index'])) {
$settings = $settingsData[$indexName]['settings']['index']; $settings = $settingsData[$indexName]['settings']['index'];
...@@ -193,9 +128,7 @@ try { ...@@ -193,9 +128,7 @@ try {
echo "[ 3. 인덱스 통계 ]\n"; echo "[ 3. 인덱스 통계 ]\n";
echo str_repeat("-", 100) . "\n"; echo str_repeat("-", 100) . "\n";
$statsUrl = rtrim($host, '/') . '/' . urlencode($indexName) . '/_stats'; $statsData = $elastic->get(urlencode($indexName) . '/_stats');
$statsResponse = callElasticAPI($statsUrl, $user, $password);
$statsData = json_decode($statsResponse, true);
if (isset($statsData['indices'][$indexName]['total'])) { if (isset($statsData['indices'][$indexName]['total'])) {
$total = $statsData['indices'][$indexName]['total']; $total = $statsData['indices'][$indexName]['total'];
...@@ -225,9 +158,7 @@ try { ...@@ -225,9 +158,7 @@ try {
echo "[ 4. 필드 매핑 ]\n"; echo "[ 4. 필드 매핑 ]\n";
echo str_repeat("-", 100) . "\n"; echo str_repeat("-", 100) . "\n";
$mappingUrl = rtrim($host, '/') . '/' . urlencode($indexName) . '/_mapping'; $mappingData = $elastic->get(urlencode($indexName) . '/_mapping');
$mappingResponse = callElasticAPI($mappingUrl, $user, $password);
$mappingData = json_decode($mappingResponse, true);
if (isset($mappingData[$indexName]['mappings']['properties'])) { if (isset($mappingData[$indexName]['mappings']['properties'])) {
$properties = $mappingData[$indexName]['mappings']['properties']; $properties = $mappingData[$indexName]['mappings']['properties'];
...@@ -244,19 +175,16 @@ try { ...@@ -244,19 +175,16 @@ try {
} }
echo "\n"; echo "\n";
// 5. TOP 50 문서 조회 // 5. TOP N 문서 조회
if ($searchQuery) { if ($searchQuery) {
echo "[ 5. 검색 결과 TOP 50 (검색어: \"$searchQuery\") ]\n"; echo "[ 5. 검색 결과 TOP {$limit} (검색어: \"$searchQuery\") ]\n";
} else { } else {
echo "[ 5. 샘플 문서 TOP 50 ]\n"; echo "[ 5. 샘플 문서 TOP {$limit} ]\n";
} }
echo str_repeat("-", 100) . "\n"; echo str_repeat("-", 100) . "\n";
$searchUrl = rtrim($host, '/') . '/' . urlencode($indexName) . '/_search';
// 검색 쿼리 생성 // 검색 쿼리 생성
if ($searchQuery) { if ($searchQuery) {
// 검색어가 있으면 query_string 사용 (와일드카드 지원)
$query = [ $query = [
'query_string' => [ 'query_string' => [
'query' => $searchQuery, 'query' => $searchQuery,
...@@ -264,37 +192,34 @@ try { ...@@ -264,37 +192,34 @@ try {
] ]
]; ];
} else { } else {
// 검색어가 없으면 match_all
$query = ['match_all' => (object)[]]; $query = ['match_all' => (object)[]];
} }
$searchBody = json_encode([ $searchBody = [
'size' => 50, 'size' => $limit,
'query' => $query 'query' => $query
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ];
// 쿼리 표시 // 쿼리 표시
$cyanColor = "\033[1;36m"; // 밝은 청록색 $cyanColor = "\033[1;36m";
$resetColor = "\033[0m"; $resetColor = "\033[0m";
echo "\n{$cyanColor}[사용된 Elasticsearch 쿼리]{$resetColor}\n"; echo "\n{$cyanColor}[사용된 Elasticsearch 쿼리]{$resetColor}\n";
echo str_repeat("-", 100) . "\n"; echo str_repeat("-", 100) . "\n";
echo $searchBody . "\n"; echo json_encode($searchBody, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
echo str_repeat("-", 100) . "\n\n"; echo str_repeat("-", 100) . "\n\n";
$searchResponse = callElasticAPI($searchUrl, $user, $password, 'POST', $searchBody); $searchData = $elastic->search(urlencode($indexName) . '/_search', $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'];
// 색상 코드 정의 $yellowColor = "\033[1;33m";
$yellowColor = "\033[1;33m"; // 밝은 노란색 $greenColor = "\033[1;32m";
$greenColor = "\033[1;32m"; // 밝은 녹색 $resetColor = "\033[0m";
$resetColor = "\033[0m"; // 색상 리셋
foreach ($hits as $idx => $hit) { foreach ($hits as $idx => $hit) {
$docId = $hit['_id']; $docId = $hit['_id'];
$docUrl = rtrim($host, '/') . '/' . urlencode($indexName) . '/_doc/' . urlencode($docId); $docUrl = rtrim($config['host'], '/') . '/' . urlencode($indexName) . '/_doc/' . urlencode($docId);
echo "\n문서 #" . ($idx + 1) . " (ID: {$greenColor}{$docId}{$resetColor}, URL: {$greenColor}{$docUrl}{$resetColor})\n"; echo "\n문서 #" . ($idx + 1) . " (ID: {$greenColor}{$docId}{$resetColor}, URL: {$greenColor}{$docUrl}{$resetColor})\n";
echo str_repeat("-", 120) . "\n"; echo str_repeat("-", 120) . "\n";
...@@ -336,7 +261,7 @@ try { ...@@ -336,7 +261,7 @@ try {
echo "\n" . str_repeat("=", 100) . "\n"; echo "\n" . str_repeat("=", 100) . "\n";
echo "조회 완료\n"; echo "조회 완료\n";
} catch (Exception $e) { } catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n"; echo "Error: " . $e->getMessage() . "\n";
exit(1); exit(1);
} }
#!/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);
} }
#!/usr/bin/env php
<?php
/**
* MySQL 테이블을 Elasticsearch에 업로드하는 스크립트
*
* 사용법:
* ./ptyElasticUploadFromMysql --table=테이블명 --index=인덱스명 [옵션]
*
* 설정 파일:
* - MySQL: ~/.ptyMysqlConfig.ini
* - Elasticsearch: ~/.ptyElasticConfig.ini
*
* 예시:
* ./ptyElasticUploadFromMysql --table=new_law_items --index=law_items --batch=500 --recreate
* ./ptyElasticUploadFromMysql --table=users --index=users --mysql-section=production --elastic-section=production
*/
namespace platyFramework;
require_once __DIR__ . "/ptyLibrary_PHP/cli/ptyCliLog.php";
require_once __DIR__ . "/ptyLibrary_PHP/elastic/Elastic.php";
/**
* INI 설정 파일 로더
*/
class ConfigLoader
{
private static function getConfigPath($filename)
{
return $_SERVER['HOME'] . '/' . $filename;
}
/**
* MySQL 설정 로드
*/
public static function loadMysqlConfig($section = 'default')
{
$configPath = self::getConfigPath('.ptyMysqlConfig.ini');
if (!file_exists($configPath)) {
throw new \Exception("MySQL 설정 파일을 찾을 수 없습니다: {$configPath}");
}
$config = parse_ini_file($configPath, true);
if ($config === false) {
throw new \Exception("MySQL 설정 파일을 파싱할 수 없습니다: {$configPath}");
}
if (!isset($config[$section])) {
throw new \Exception("MySQL 설정에서 [{$section}] 섹션을 찾을 수 없습니다. 사용 가능한 섹션: " . implode(', ', array_keys($config)));
}
$sectionConfig = $config[$section];
// 필수 필드 검증
$required = ['host', 'username', 'password', 'database'];
foreach ($required as $field) {
if (!isset($sectionConfig[$field]) || empty($sectionConfig[$field])) {
throw new \Exception("MySQL 설정 [{$section}] 섹션에 필수 필드 '{$field}'가 없습니다.");
}
}
return [
'host' => $sectionConfig['host'],
'username' => $sectionConfig['username'],
'password' => trim($sectionConfig['password'], '"\''),
'database' => $sectionConfig['database'],
'charset' => $sectionConfig['charset'] ?? 'utf8mb4',
];
}
/**
* Elasticsearch 설정 로드
*/
public static function loadElasticConfig($section = 'default')
{
$configPath = self::getConfigPath('.ptyElasticConfig.ini');
if (!file_exists($configPath)) {
throw new \Exception("Elasticsearch 설정 파일을 찾을 수 없습니다: {$configPath}");
}
$config = parse_ini_file($configPath, true);
if ($config === false) {
throw new \Exception("Elasticsearch 설정 파일을 파싱할 수 없습니다: {$configPath}");
}
if (!isset($config[$section])) {
throw new \Exception("Elasticsearch 설정에서 [{$section}] 섹션을 찾을 수 없습니다. 사용 가능한 섹션: " . implode(', ', array_keys($config)));
}
$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,
];
}
}
error_reporting(E_ALL & ~E_DEPRECATED);
// 메모리 제한 증가 (대용량 데이터 처리를 위해)
ini_set('memory_limit', '2048M');
/**
* MySQL to Elasticsearch 업로더 클래스
*/
class MySQLToElastic
{
private $pdo;
private $elastic;
private $log;
private $tableName;
private $indexName;
private $batchSize;
private $primaryKey;
private $mysqlConfig;
private $elasticConfig;
private $sampleDocIds = [];
public function __construct($tableName, $indexName, $batchSize = 1000, $mysqlConfig = null, $elasticConfig = null)
{
$this->tableName = $tableName;
$this->indexName = $indexName;
$this->batchSize = $batchSize;
$this->primaryKey = 'id'; // 기본값, 필요시 변경 가능
$this->mysqlConfig = $mysqlConfig;
$this->elasticConfig = $elasticConfig;
$this->log = new ptyCliLog(prefix: "MySQL→ES", color: ptyCliLog::COLOR_CYAN);
// MySQL 연결
$this->initMySQLConnection();
// Elasticsearch 연결
$this->initElasticConnection();
$this->log->info("초기화 완료");
$this->log->info("테이블: {$this->tableName}");
$this->log->info("인덱스: {$this->indexName}");
$this->log->info("배치 크기: {$this->batchSize}");
$this->log->info("_id 생성 규칙: serviceName, serviceId 컬럼 있으면 {serviceName}_{serviceId}_{primaryKey}, 없으면 {primaryKey}");
}
/**
* MySQL 연결 초기화
*/
private function initMySQLConnection()
{
try {
$dsn = "mysql:host={$this->mysqlConfig['host']};dbname={$this->mysqlConfig['database']};charset={$this->mysqlConfig['charset']}";
$this->pdo = new \PDO(
$dsn,
$this->mysqlConfig['username'],
$this->mysqlConfig['password'],
[
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
\PDO::ATTR_EMULATE_PREPARES => false,
\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false, // 메모리 절약을 위해 버퍼링 비활성화
]
);
$this->log->success("MySQL 연결 성공: {$this->mysqlConfig['host']}/{$this->mysqlConfig['database']}");
} catch(\PDOException $e) {
$this->log->error("MySQL 연결 실패: " . $e->getMessage());
exit(1);
}
}
/**
* Elasticsearch 연결 초기화
*/
private function initElasticConnection()
{
$this->elastic = new Elastic(
$this->elasticConfig['apiKey'],
$this->elasticConfig['host'],
null,
$this->elasticConfig['user'],
$this->elasticConfig['password']
);
$authMethod = !empty($this->elasticConfig['apiKey']) ? 'apiKey' : 'user/password';
$this->log->success("Elasticsearch 연결 성공: {$this->elasticConfig['host']} ({$authMethod})");
}
/**
* 테이블 구조를 읽어서 Elasticsearch 매핑 생성
*/
public function getTableStructure()
{
$this->log->info("테이블 구조 조회 중...");
$stmt = $this->pdo->query("DESCRIBE `{$this->tableName}`");
$columns = $stmt->fetchAll();
$mapping = [];
foreach ($columns as $column) {
$field = $column['Field'];
$type = $column['Type'];
$key = $column['Key'];
// Primary Key 저장
if ($key === 'PRI') {
$this->primaryKey = $field;
}
// MySQL 타입을 Elasticsearch 타입으로 변환
$esType = $this->convertMySQLTypeToElastic($type);
$mapping[$field] = ['type' => $esType];
// text 타입인 경우 keyword 필드도 추가 (정렬, 집계용)
if ($esType === 'text') {
$mapping[$field]['fields'] = [
'keyword' => [
'type' => 'keyword',
'ignore_above' => 256
]
];
}
}
$this->log->success("테이블 구조 조회 완료 (컬럼 수: " . count($columns) . ")");
return $mapping;
}
/**
* MySQL 타입을 Elasticsearch 타입으로 변환
*/
private function convertMySQLTypeToElastic($mysqlType)
{
$mysqlType = strtolower($mysqlType);
// 정수형
if (preg_match('/^(tinyint|smallint|mediumint|int|bigint)/', $mysqlType)) {
if (preg_match('/bigint/', $mysqlType)) {
return 'long';
}
return 'integer';
}
// 실수형
if (preg_match('/^(float|double|decimal)/', $mysqlType)) {
return 'float';
}
// 날짜/시간
if (preg_match('/^(datetime|timestamp)/', $mysqlType)) {
return 'date';
}
if (preg_match('/^date/', $mysqlType)) {
return 'date';
}
// Boolean
if (preg_match('/^tinyint\(1\)/', $mysqlType)) {
return 'boolean';
}
// TEXT, LONGTEXT 등
if (preg_match('/text/', $mysqlType)) {
return 'text';
}
// VARCHAR, CHAR 등 - 길이에 따라 keyword 또는 text
if (preg_match('/^(varchar|char)\((\d+)\)/', $mysqlType, $matches)) {
$length = (int)$matches[2];
// 짧은 문자열은 keyword, 긴 문자열은 text
return $length <= 255 ? 'keyword' : 'text';
}
// ENUM
if (preg_match('/^enum/', $mysqlType)) {
return 'keyword';
}
// JSON
if (preg_match('/^json/', $mysqlType)) {
return 'object';
}
// 기본값은 keyword
return 'keyword';
}
/**
* Elasticsearch 인덱스 생성
*/
public function createIndex($recreate = false)
{
$this->log->info("인덱스 생성 중: {$this->indexName}");
// 인덱스가 이미 존재하는지 확인
$exists = $this->elastic->indexExists($this->indexName);
if ($exists) {
if ($recreate) {
$this->log->warning("기존 인덱스 삭제 중...");
$this->elastic->deleteIndex($this->indexName);
$this->log->success("기존 인덱스 삭제 완료");
} else {
$this->log->warning("인덱스가 이미 존재합니다. 매핑 업데이트를 시도합니다.");
return;
}
}
// 테이블 구조 조회
$mapping = $this->getTableStructure();
// 인덱스 생성 요청 데이터
$indexData = [
'settings' => [
'number_of_shards' => 1,
'number_of_replicas' => 1,
'analysis' => [
'analyzer' => [
'korean' => [
'type' => 'custom',
'tokenizer' => 'standard',
'filter' => ['lowercase']
]
]
]
],
'mappings' => [
'properties' => $mapping
]
];
try {
$response = $this->elastic->put($this->indexName, $indexData);
$this->log->success("인덱스 생성 완료");
} catch (\Exception $e) {
$this->log->error("인덱스 생성 실패: " . $e->getMessage());
throw $e;
}
}
/**
* 테이블의 전체 레코드 수 조회
*/
public function getTotalCount()
{
$stmt = $this->pdo->query("SELECT COUNT(*) as cnt FROM `{$this->tableName}`");
$result = $stmt->fetch();
return (int)$result['cnt'];
}
/**
* 데이터 업로드
*/
public function uploadData($whereClause = null)
{
$this->log->info("=== 데이터 업로드 시작 ===");
// 전체 레코드 수 조회
$totalCount = $this->getTotalCount();
$this->log->info("전체 레코드 수: " . number_format($totalCount));
if ($totalCount === 0) {
$this->log->warning("업로드할 데이터가 없습니다.");
return;
}
$offset = 0;
$successCount = 0;
$errorCount = 0;
$startTime = microtime(true);
while ($offset < $totalCount) {
$sql = "SELECT * FROM `{$this->tableName}`";
if ($whereClause) {
$sql .= " WHERE {$whereClause}";
}
$sql .= " LIMIT {$this->batchSize} OFFSET {$offset}";
$stmt = $this->pdo->query($sql);
$rows = $stmt->fetchAll();
if (empty($rows)) {
break;
}
$batchCount = count($rows);
$progress = min(100, round(($offset + $batchCount) / $totalCount * 100, 2));
$this->log->info("배치 처리 중... [{$offset}-" . ($offset + $batchCount) . "] / {$totalCount} ({$progress}%)");
// Bulk API를 위한 데이터 준비
try {
$batchDocIds = $this->bulkInsert($rows);
$successCount += $batchCount;
// 배치 성공 시 첫/마지막 URL 출력
if (!empty($batchDocIds)) {
$baseUrl = rtrim($this->elasticConfig['host'], '/');
$firstId = reset($batchDocIds);
$lastId = end($batchDocIds);
$this->log->success(" 처음: {$baseUrl}/{$this->indexName}/_doc/{$firstId}");
$this->log->success(" 마지막: {$baseUrl}/{$this->indexName}/_doc/{$lastId}");
}
} catch (\Exception $e) {
$this->log->error("배치 업로드 실패: " . $e->getMessage());
$errorCount += $batchCount;
}
$offset += $batchCount;
// 메모리 해제 및 가비지 컬렉션
unset($rows);
unset($stmt);
gc_collect_cycles();
// 진행 상황 요약 (메모리 사용량 포함)
$elapsed = round(microtime(true) - $startTime, 2);
$speed = $elapsed > 0 ? round($successCount / $elapsed, 2) : 0;
$memoryUsage = round(memory_get_usage() / 1024 / 1024, 2);
$memoryPeak = round(memory_get_peak_usage() / 1024 / 1024, 2);
$this->log->info("진행: {$successCount}건 성공, {$errorCount}건 실패, 속도: {$speed}건/초, 메모리: {$memoryUsage}MB / Peak: {$memoryPeak}MB");
}
$totalTime = round(microtime(true) - $startTime, 2);
$this->log->success("=== 데이터 업로드 완료 ===");
$this->log->success("총 {$successCount}건 성공, {$errorCount}건 실패");
$this->log->success("소요 시간: {$totalTime}초");
// 예시 URL 출력
if (!empty($this->sampleDocIds)) {
$this->log->info("");
$this->log->info("예시 URL:");
$baseUrl = rtrim($this->elasticConfig['host'], '/');
foreach ($this->sampleDocIds as $docId) {
$this->log->info(" {$baseUrl}/{$this->indexName}/_doc/{$docId}");
}
}
}
/**
* Bulk API를 사용한 배치 삽입
*/
private function bulkInsert($rows)
{
$bulkData = '';
$batchDocIds = []; // 이 배치의 문서 ID들
foreach ($rows as $row) {
// 문서 ID 생성
// serviceName과 serviceId 필드가 있으면 조합하여 ID 생성
$docId = null;
if (isset($row['serviceName']) && isset($row['serviceId'])) {
// serviceName_serviceId_primaryKey 형태로 생성
$serviceName = $row['serviceName'];
$serviceId = $row['serviceId'];
$primaryValue = $row[$this->primaryKey] ?? '';
$docId = "{$serviceName}_{$serviceId}_{$primaryValue}";
} else {
// 기본: primary key 사용
$docId = $row[$this->primaryKey] ?? null;
}
// 배치 문서 ID 수집
if ($docId !== null) {
$batchDocIds[] = $docId;
}
// 샘플 문서 ID 수집 (최대 5개)
if (count($this->sampleDocIds) < 5 && $docId !== null) {
$this->sampleDocIds[] = $docId;
}
// 날짜 형식 변환 (MySQL datetime -> Elasticsearch date)
foreach ($row as $key => $value) {
if ($value !== null && preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $value)) {
// MySQL datetime을 ISO 8601 형식으로 변환
$row[$key] = str_replace(' ', 'T', $value);
}
}
// index 액션
$action = [
'index' => [
'_index' => $this->indexName,
'_id' => $docId
]
];
$bulkData .= json_encode($action, JSON_UNESCAPED_UNICODE) . "\n";
$bulkData .= json_encode($row, JSON_UNESCAPED_UNICODE) . "\n";
// 메모리 절약을 위해 사용한 변수 즉시 해제
unset($action, $row);
}
// Bulk API 호출
$ch = curl_init();
$url = rtrim($this->elasticConfig['host'], '/') . '/_bulk';
// 인증 헤더 설정
$headers = [
'Content-Type: application/x-ndjson',
];
if (!empty($this->elasticConfig['apiKey'])) {
$headers[] = 'Authorization: ApiKey ' . $this->elasticConfig['apiKey'];
} elseif (!empty($this->elasticConfig['user']) && !empty($this->elasticConfig['password'])) {
$headers[] = 'Authorization: Basic ' . base64_encode($this->elasticConfig['user'] . ':' . $this->elasticConfig['password']);
}
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $bulkData,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 400) {
$decodedResponse = json_decode($response, true);
// 상세 에러 메시지 생성
$errorMessage = "HTTP {$httpCode}";
if ($httpCode == 413) {
$errorMessage = "HTTP 413 Request Entity Too Large - 배치 크기가 너무 큽니다. --batch 값을 줄여보세요.";
} elseif (isset($decodedResponse['error']['reason'])) {
$errorMessage = "HTTP {$httpCode}: " . $decodedResponse['error']['reason'];
} elseif (isset($decodedResponse['error']['type'])) {
$errorMessage = "HTTP {$httpCode}: " . $decodedResponse['error']['type'];
}
// 실패한 배치의 처음과 끝 URL 출력
$baseUrl = rtrim($this->elasticConfig['host'], '/');
if (!empty($batchDocIds)) {
$firstId = reset($batchDocIds);
$lastId = end($batchDocIds);
$this->log->error(" 실패 범위:");
$this->log->error(" 처음: {$baseUrl}/{$this->indexName}/_doc/{$firstId}");
$this->log->error(" 마지막: {$baseUrl}/{$this->indexName}/_doc/{$lastId}");
}
throw new \Exception($errorMessage);
}
$result = json_decode($response, true);
// 메모리 해제
unset($response, $bulkData);
// 오류 체크
if (isset($result['errors']) && $result['errors'] === true) {
$errorItems = array_filter($result['items'], function($item) {
return isset($item['index']['error']);
});
if (!empty($errorItems)) {
$this->log->warning("일부 문서 업로드 실패: " . count($errorItems) . "건");
}
unset($errorItems);
}
unset($result);
return $batchDocIds;
}
/**
* Primary Key 설정
*/
public function setPrimaryKey($key)
{
$this->primaryKey = $key;
$this->log->info("Primary Key 설정: {$key}");
}
}
// ==========================================
// 메인 실행 코드
// ==========================================
$log = new ptyCliLog(prefix: "메인", color: ptyCliLog::COLOR_GREEN);
// 커맨드 라인 인자 파싱 (위치 인자와 옵션 분리)
$positionalArgs = [];
$optionArgs = [];
foreach ($argv as $i => $arg) {
if ($i === 0) continue; // 스크립트 이름 제외
if (strpos($arg, '--') === 0) {
$optionArgs[] = $arg;
} else {
$positionalArgs[] = $arg;
}
}
// 옵션 파싱
$options = [];
foreach ($optionArgs as $arg) {
if (preg_match('/^--([^=]+)=(.*)$/', $arg, $matches)) {
$options[$matches[1]] = $matches[2];
} elseif (preg_match('/^--([^=]+)$/', $arg, $matches)) {
$options[$matches[1]] = true;
}
}
$tableName = $positionalArgs[0] ?? null;
$indexName = $positionalArgs[1] ?? null;
if (!$tableName || !$indexName || isset($options['help'])) {
echo "\n";
echo "사용법: ./ptyElasticUploadFromMysql <테이블명> <인덱스명> [옵션]\n";
echo "\n";
echo "설정 파일:\n";
echo " MySQL: ~/.ptyMysqlConfig.ini\n";
echo " Elasticsearch: ~/.ptyElasticConfig.ini\n";
echo "\n";
echo "필수 인자:\n";
echo " 테이블명 MySQL 테이블 이름 (첫번째 인자)\n";
echo " 인덱스명 Elasticsearch 인덱스 이름 (두번째 인자)\n";
echo "\n";
echo "선택 인자:\n";
echo " --mysql=섹션명 MySQL INI 섹션 (기본값: default)\n";
echo " --elastic=섹션명 Elasticsearch INI 섹션 (기본값: default)\n";
echo " --batch=N 배치 크기 (기본값: 100)\n";
echo " --recreate 기존 인덱스 삭제 후 재생성\n";
echo " --primary=필드명 Primary Key 필드명 (기본값: id)\n";
echo " --where='조건' WHERE 절 추가 (예: --where='enabled=1')\n";
echo " --help 도움말 출력\n";
echo "\n";
echo "INI 파일 형식:\n";
echo " ~/.ptyMysqlConfig.ini:\n";
echo " [default]\n";
echo " host=localhost\n";
echo " username=root\n";
echo " password=\"your_password\"\n";
echo " database=your_db\n";
echo " charset=utf8mb4\n";
echo "\n";
echo " ~/.ptyElasticConfig.ini:\n";
echo " [default]\n";
echo " host=https://localhost:9200\n";
echo " apiKey=your_api_key # apiKey 또는 user/password 중 하나 사용\n";
echo " # user=elastic\n";
echo " # password=\"your_password\"\n";
echo "\n";
echo "예시:\n";
echo " ./ptyElasticUploadFromMysql new_law_items law_items\n";
echo " ./ptyElasticUploadFromMysql users users --mysql=production --elastic=production\n";
echo " ./ptyElasticUploadFromMysql new_law_items law_items --batch=500 --recreate\n";
echo "\n";
exit(isset($options['help']) ? 0 : 1);
}
$batchSize = isset($options['batch']) ? (int)$options['batch'] : 100;
$recreate = isset($options['recreate']);
$primaryKey = $options['primary'] ?? 'id';
$whereClause = $options['where'] ?? null;
$mysqlSection = $options['mysql'] ?? 'default';
$elasticSection = $options['elastic'] ?? 'default';
$log->info("=== MySQL to Elasticsearch 업로드 시작 ===");
// 설정 로드
try {
$log->info("MySQL 설정 로드 중... (섹션: {$mysqlSection})");
$mysqlConfig = ConfigLoader::loadMysqlConfig($mysqlSection);
$log->success("MySQL 설정 로드 완료: {$mysqlConfig['host']}/{$mysqlConfig['database']}");
$log->info("Elasticsearch 설정 로드 중... (섹션: {$elasticSection})");
$elasticConfig = ConfigLoader::loadElasticConfig($elasticSection);
$log->success("Elasticsearch 설정 로드 완료: {$elasticConfig['host']}");
} catch (\Exception $e) {
$log->error($e->getMessage());
exit(1);
}
$log->info("MySQL 테이블: {$tableName}");
$log->info("Elasticsearch 인덱스: {$indexName}");
// 업로더 생성
$uploader = new MySQLToElastic($tableName, $indexName, $batchSize, $mysqlConfig, $elasticConfig);
// Primary Key 설정
if ($primaryKey !== 'id') {
$uploader->setPrimaryKey($primaryKey);
}
// 인덱스 생성
$uploader->createIndex($recreate);
// 데이터 업로드
$uploader->uploadData($whereClause);
$log->success("=== 모든 작업 완료 ===");
?>
#!/bin/bash
# ptyGitSubtree - Git Subtree 관리 통합 스크립트
# 사용법: ptyGitSubtree [명령] [옵션]
# 색상 정의
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
NC='\033[0m' # No Color
BOLD='\033[1m'
DIM='\033[2m'
# 현재 등록된 subtree 목록 표시
show_subtrees() {
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════════════════════════${NC}"
echo -e "${BOLD}${CYAN} 현재 Git Subtree 정보${NC}"
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════════════════════════${NC}"
echo ""
# git remote 목록 가져오기
local remotes=$(git remote 2>/dev/null)
if [ -z "$remotes" ]; then
echo -e " ${YELLOW}등록된 remote가 없습니다.${NC}"
echo ""
return
fi
local found_subtree=false
# 각 remote에 대해 subtree인지 확인
for remote in $remotes; do
# origin은 일반적으로 subtree가 아님
if [ "$remote" = "origin" ]; then
continue
fi
local url=$(git remote get-url "$remote" 2>/dev/null)
local folder="$remote"
# 해당 폴더가 존재하고 subtree 커밋 이력이 있는지 확인
if [ -d "$folder" ]; then
local squash_commit=$(git log --oneline --grep="git-subtree-dir: $folder" -1 2>/dev/null)
local split_commit=$(git log --oneline --all --grep="Squashed '$folder/'" -1 2>/dev/null)
if [ -n "$squash_commit" ] || [ -n "$split_commit" ]; then
found_subtree=true
echo -e "${BOLD}${YELLOW}[$folder]${NC}"
echo -e " ${GREEN}Remote:${NC} $remote"
echo -e " ${GREEN}URL:${NC} $url"
# 폴더 정보
local file_count=$(find "$folder" -type f 2>/dev/null | wc -l | tr -d ' ')
local folder_size=$(du -sh "$folder" 2>/dev/null | cut -f1)
echo -e " ${GREEN}폴더:${NC} $folder/ (${file_count}개 파일, ${folder_size})"
# 마지막 subtree 작업 커밋
local last_subtree_commit=$(git log --oneline --grep="git-subtree-dir: $folder" -1 2>/dev/null || \
git log --oneline --all --grep="Squashed '$folder/'" -1 2>/dev/null)
if [ -n "$last_subtree_commit" ]; then
echo -e " ${GREEN}최근 동기화:${NC} $last_subtree_commit"
fi
# 마지막 커밋 날짜
local last_date=$(git log --format="%ar" --grep="git-subtree-dir: $folder" -1 2>/dev/null || \
git log --format="%ar" --all --grep="Squashed '$folder/'" -1 2>/dev/null)
if [ -n "$last_date" ]; then
echo -e " ${GREEN}동기화 시점:${NC} ${DIM}$last_date${NC}"
fi
echo ""
fi
fi
done
# subtree 커밋 로그에서 추가 정보 찾기 (remote가 없는 경우 대비)
local subtree_dirs=$(git log --oneline --all 2>/dev/null | grep -oE "Squashed '[^']+/'" | sed "s/Squashed '//g" | sed "s/\/'//g" | sort -u)
for dir in $subtree_dirs; do
# 이미 표시된 것은 건너뛰기
if git remote get-url "$dir" > /dev/null 2>&1; then
continue
fi
if [ -d "$dir" ]; then
found_subtree=true
echo -e "${BOLD}${YELLOW}[$dir]${NC} ${RED}(remote 미등록)${NC}"
local file_count=$(find "$dir" -type f 2>/dev/null | wc -l | tr -d ' ')
local folder_size=$(du -sh "$dir" 2>/dev/null | cut -f1)
echo -e " ${GREEN}폴더:${NC} $dir/ (${file_count}개 파일, ${folder_size})"
local last_commit=$(git log --oneline --all --grep="Squashed '$dir/'" -1 2>/dev/null)
if [ -n "$last_commit" ]; then
echo -e " ${GREEN}최근 동기화:${NC} $last_commit"
fi
echo -e " ${YELLOW}힌트:${NC} ${DIM}remote 등록: git remote add $dir <URL>${NC}"
echo ""
fi
done
if [ "$found_subtree" = false ]; then
echo -e " ${YELLOW}등록된 subtree가 없습니다.${NC}"
echo ""
echo -e " ${DIM}새 subtree 추가:${NC}"
echo -e " ${CYAN}ptyGitSubtree add <폴더명> <remote주소> [브랜치명]${NC}"
fi
}
# subtree 히스토리 표시
show_history() {
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════════════════════════${NC}"
echo -e "${BOLD}${CYAN} Git Subtree 히스토리${NC}"
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════════════════════════${NC}"
echo ""
# 모든 subtree 관련 커밋
git log --oneline --all --grep="git-subtree" --decorate -20 2>/dev/null
if [ $? -ne 0 ] || [ -z "$(git log --oneline --all --grep="git-subtree" -1 2>/dev/null)" ]; then
# 대안: Squashed 커밋 찾기
git log --oneline --all --grep="Squashed '" --decorate -20 2>/dev/null
fi
echo ""
}
# subtree 추가
do_add() {
local folder_name="$1"
local remote_url="$2"
local branch="${3:-main}"
if [ -z "$folder_name" ] || [ -z "$remote_url" ]; then
echo -e "${RED}사용법: ptyGitSubtree add <폴더명> <remote주소> [브랜치명]${NC}"
echo -e "${DIM}예시: ptyGitSubtree add ptyLibrary_PHP https://gitlab.platyhouse.com/platyhouse/ptyLibrary_PHP${NC}"
return 1
fi
# 폴더가 이미 존재하는지 확인
if [ -d "$folder_name" ]; then
echo -e "${RED}오류: '$folder_name' 폴더가 이미 존재합니다.${NC}"
return 1
fi
# remote 등록 (이미 있으면 URL 업데이트)
if git remote get-url "$folder_name" > /dev/null 2>&1; then
git remote set-url "$folder_name" "$remote_url"
echo -e "${GREEN}Remote '$folder_name' URL 업데이트됨${NC}"
else
git remote add "$folder_name" "$remote_url"
echo -e "${GREEN}Remote '$folder_name' 등록됨${NC}"
fi
echo ""
echo -e "${CYAN}Subtree 추가 중...${NC}"
echo -e " ${GREEN}폴더:${NC} $folder_name"
echo -e " ${GREEN}Remote:${NC} $folder_name ($remote_url)"
echo -e " ${GREEN}브랜치:${NC} $branch"
echo ""
git subtree add --prefix="$folder_name" "$folder_name" "$branch" --squash
if [ $? -eq 0 ]; then
echo ""
echo -e "${GREEN}완료: '$folder_name' subtree가 추가되었습니다.${NC}"
else
echo ""
echo -e "${RED}오류: subtree 추가에 실패했습니다.${NC}"
# 실패 시 remote 제거
git remote remove "$folder_name" 2>/dev/null
return 1
fi
}
# subtree pull
do_pull() {
local folder_name="$1"
local branch="${2:-main}"
if [ -z "$folder_name" ]; then
echo -e "${RED}사용법: ptyGitSubtree pull <폴더명> [브랜치명]${NC}"
echo -e "${DIM}예시: ptyGitSubtree pull ptyLibrary_PHP${NC}"
return 1
fi
# 폴더가 존재하는지 확인
if [ ! -d "$folder_name" ]; then
echo -e "${RED}오류: '$folder_name' 폴더가 존재하지 않습니다.${NC}"
return 1
fi
# remote가 등록되어 있는지 확인
if ! git remote get-url "$folder_name" > /dev/null 2>&1; then
echo -e "${RED}오류: '$folder_name' remote가 등록되어 있지 않습니다.${NC}"
echo -e "${DIM}먼저 ptyGitSubtree add로 subtree를 추가하거나, git remote add로 remote를 등록하세요.${NC}"
return 1
fi
echo -e "${CYAN}Subtree pull 중...${NC}"
echo -e " ${GREEN}폴더:${NC} $folder_name"
echo -e " ${GREEN}브랜치:${NC} $branch"
echo ""
git subtree pull --prefix="$folder_name" "$folder_name" "$branch" --squash
if [ $? -eq 0 ]; then
echo ""
echo -e "${GREEN}완료: '$folder_name' subtree가 업데이트되었습니다.${NC}"
else
echo ""
echo -e "${RED}오류: subtree pull에 실패했습니다.${NC}"
return 1
fi
}
# subtree push
do_push() {
local folder_name="$1"
local branch="${2:-main}"
if [ -z "$folder_name" ]; then
echo -e "${RED}사용법: ptyGitSubtree push <폴더명> [브랜치명]${NC}"
echo -e "${DIM}예시: ptyGitSubtree push ptyLibrary_PHP${NC}"
return 1
fi
# 폴더가 존재하는지 확인
if [ ! -d "$folder_name" ]; then
echo -e "${RED}오류: '$folder_name' 폴더가 존재하지 않습니다.${NC}"
return 1
fi
# remote가 등록되어 있는지 확인
if ! git remote get-url "$folder_name" > /dev/null 2>&1; then
echo -e "${RED}오류: '$folder_name' remote가 등록되어 있지 않습니다.${NC}"
echo -e "${DIM}먼저 ptyGitSubtree add로 subtree를 추가하거나, git remote add로 remote를 등록하세요.${NC}"
return 1
fi
echo -e "${CYAN}Subtree push 중...${NC}"
echo -e " ${GREEN}폴더:${NC} $folder_name"
echo -e " ${GREEN}브랜치:${NC} $branch"
echo ""
git subtree push --prefix="$folder_name" "$folder_name" "$branch"
if [ $? -eq 0 ]; then
echo ""
echo -e "${GREEN}완료: '$folder_name' subtree가 push되었습니다.${NC}"
else
echo ""
echo -e "${RED}오류: subtree push에 실패했습니다.${NC}"
return 1
fi
}
# remote 등록 (폴더가 이미 존재하는 경우)
do_register() {
local folder_name="$1"
local remote_url="$2"
if [ -z "$folder_name" ] || [ -z "$remote_url" ]; then
echo -e "${RED}사용법: ptyGitSubtree register <폴더명> <remote주소>${NC}"
echo -e "${DIM}이미 존재하는 subtree 폴더에 remote를 등록합니다.${NC}"
return 1
fi
# 폴더가 존재하는지 확인
if [ ! -d "$folder_name" ]; then
echo -e "${RED}오류: '$folder_name' 폴더가 존재하지 않습니다.${NC}"
echo -e "${DIM}새 subtree를 추가하려면 'ptyGitSubtree add'를 사용하세요.${NC}"
return 1
fi
# remote 등록 (이미 있으면 URL 업데이트)
if git remote get-url "$folder_name" > /dev/null 2>&1; then
git remote set-url "$folder_name" "$remote_url"
echo -e "${GREEN}Remote '$folder_name' URL이 업데이트되었습니다.${NC}"
else
git remote add "$folder_name" "$remote_url"
echo -e "${GREEN}Remote '$folder_name'가 등록되었습니다.${NC}"
fi
echo -e " ${GREEN}URL:${NC} $remote_url"
}
# remote 삭제
do_unregister() {
local folder_name="$1"
if [ -z "$folder_name" ]; then
echo -e "${RED}사용법: ptyGitSubtree unregister <폴더명>${NC}"
echo -e "${DIM}subtree의 remote 등록을 해제합니다. (폴더는 삭제되지 않음)${NC}"
return 1
fi
if git remote get-url "$folder_name" > /dev/null 2>&1; then
git remote remove "$folder_name"
echo -e "${GREEN}Remote '$folder_name'가 삭제되었습니다.${NC}"
echo -e "${DIM}폴더 '$folder_name/'는 그대로 유지됩니다.${NC}"
else
echo -e "${YELLOW}Remote '$folder_name'가 등록되어 있지 않습니다.${NC}"
fi
}
# 도움말
show_help() {
echo -e "${BOLD}${CYAN}ptyGitSubtree${NC} - Git Subtree 관리 도구"
echo ""
echo -e "${BOLD}사용법:${NC}"
echo " ptyGitSubtree 현재 subtree 정보 표시"
echo " ptyGitSubtree list 현재 subtree 정보 표시"
echo " ptyGitSubtree history subtree 관련 커밋 히스토리"
echo ""
echo " ptyGitSubtree add <폴더> <URL> [브랜치] 새 subtree 추가"
echo " ptyGitSubtree pull <폴더> [브랜치] 원격에서 변경사항 가져오기"
echo " ptyGitSubtree push <폴더> [브랜치] 원격으로 변경사항 보내기"
echo ""
echo " ptyGitSubtree register <폴더> <URL> 기존 폴더에 remote 등록"
echo " ptyGitSubtree unregister <폴더> remote 등록 해제"
echo ""
echo -e "${BOLD}예시:${NC}"
echo " ptyGitSubtree add ptyLibrary_PHP https://gitlab.platyhouse.com/platyhouse/ptyLibrary_PHP"
echo " ptyGitSubtree pull ptyLibrary_PHP main"
echo " ptyGitSubtree push ptyLibrary_PHP"
echo ""
echo -e "${BOLD}관련 명령어:${NC}"
echo " ptyGitSubtreeAdd subtree 추가 전용 스크립트"
echo " ptyGitSubtreePull subtree pull 전용 스크립트"
echo " ptyGitSubtreePush subtree push 전용 스크립트"
}
# 메인 로직
case "$1" in
"list"|"-l"|"ls")
show_subtrees
;;
"history"|"log")
show_history
;;
"add"|"-a")
do_add "$2" "$3" "$4"
;;
"pull"|"-p")
do_pull "$2" "$3"
;;
"push"|"-P")
do_push "$2" "$3"
;;
"register"|"reg")
do_register "$2" "$3"
;;
"unregister"|"unreg")
do_unregister "$2"
;;
"help"|"-h"|"--help")
show_help
;;
"")
show_subtrees
show_help
;;
*)
echo -e "${RED}알 수 없는 명령: $1${NC}"
echo ""
show_help
exit 1
;;
esac
echo ""
<?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