Commit e99df3bf authored by platyhouse's avatar platyhouse

# Elasticsearch 인덱스 조회 도구 추가

## 인덱스 조회 도구 구현

### 인덱스 목록 조회 도구
- ptyElasticGetIndexs: Elasticsearch 전체 인덱스 목록 조회 스크립트 추가
  - 인덱스별 상태, 문서 수, 용량, Health 상태 표시
  - 인덱스별 생성 시간 및 마지막 색인 시간 표시
  - 총 인덱스 수, 총 문서 수, 총 용량 요약 정보 제공
  - 설정 파일(~/.ptyElasticConfig.ini) 기반 접속 인증 지원

### 단일 인덱스 상세 조회 도구
- ptyElasticGetIndex: 특정 인덱스의 상세 정보 및 문서 조회 스크립트 추가
  - 인덱스 기본 정보(Health, Status, 샤드 수, 문서 수, 용량) 조회
  - 인덱스 설정(생성 날짜, UUID, 버전, 샤드 설정) 조회
  - 인덱스 통계(색인/검색 작업 수 및 소요 시간) 조회
  - 필드 매핑 정보(필드명 및 타입 목록) 조회
  - 검색어 기반 문서 조회 기능 (와일드카드 및 AND 연산 지원)
  - TOP 50 문서 샘플 조회 기능
  - 컬러 출력 지원(필드명, ID, URL 강조 표시)
parent 7e556677
#!/usr/bin/env php
<?php
/**
* ptyElasticGetIndexInfo
*
* 특정 Elasticsearch 인덱스의 상세 정보와 TOP 10 문서를 조회하는 도구
* 설정 파일: ~/.ptyElasticConfig.ini
*
* Usage: ./ptyElasticGetIndexInfo <index_name>
*/
// 커맨드 라인 인자 확인
if ($argc < 2) {
echo "Usage: $argv[0] <index_name> [search_term1] [search_term2] ...\n";
echo "Example: $argv[0] my_index\n";
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";
echo "\nNote: Multiple search terms are combined with AND operator\n";
exit(1);
}
$indexName = $argv[1];
// 2번째 인자부터 끝까지 모든 검색어를 AND로 연결
$searchTerms = array_slice($argv, 2);
if (!empty($searchTerms)) {
// 각 검색어를 괄호로 감싸고 AND로 연결
$searchQuery = '(' . implode(') AND (', $searchTerms) . ')';
} else {
$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) {
if ($bytes >= 1073741824) {
return number_format($bytes / 1073741824, 2) . ' GB';
} elseif ($bytes >= 1048576) {
return number_format($bytes / 1048576, 2) . ' MB';
} elseif ($bytes >= 1024) {
return number_format($bytes / 1024, 2) . ' KB';
} else {
return $bytes . ' bytes';
}
}
try {
echo "Elasticsearch 인덱스 정보 조회\n";
echo "Host: $host\n";
echo "Index: $indexName\n";
echo str_repeat("=", 100) . "\n\n";
// 1. 인덱스 기본 정보 조회
echo "[ 1. 기본 정보 ]\n";
echo str_repeat("-", 100) . "\n";
$catUrl = rtrim($host, '/') . '/_cat/indices/' . urlencode($indexName) . '?v&format=json&bytes=b';
$catResponse = callElasticAPI($catUrl, $user, $password);
$catData = json_decode($catResponse, true);
if (empty($catData)) {
throw new Exception("인덱스를 찾을 수 없습니다: $indexName");
}
$indexInfo = $catData[0];
printf("상태(Health): %s\n", $indexInfo['health'] ?? 'N/A');
printf("상태(Status): %s\n", $indexInfo['status'] ?? 'N/A');
printf("Primary 샤드: %s\n", $indexInfo['pri'] ?? 'N/A');
printf("Replica 샤드: %s\n", $indexInfo['rep'] ?? 'N/A');
printf("문서 수: %s\n", number_format($indexInfo['docs.count'] ?? 0));
printf("삭제된 문서 수: %s\n", number_format($indexInfo['docs.deleted'] ?? 0));
printf("저장 용량: %s\n", formatBytes($indexInfo['store.size'] ?? 0));
printf("Primary 용량: %s\n", formatBytes($indexInfo['pri.store.size'] ?? 0));
echo "\n";
// 2. 인덱스 설정 정보
echo "[ 2. 인덱스 설정 ]\n";
echo str_repeat("-", 100) . "\n";
$settingsUrl = rtrim($host, '/') . '/' . urlencode($indexName) . '/_settings';
$settingsResponse = callElasticAPI($settingsUrl, $user, $password);
$settingsData = json_decode($settingsResponse, true);
if (isset($settingsData[$indexName]['settings']['index'])) {
$settings = $settingsData[$indexName]['settings']['index'];
printf("생성 날짜: %s\n", $settings['creation_date'] ?? 'N/A');
if (isset($settings['creation_date'])) {
$timestamp = (int)(floatval($settings['creation_date']) / 1000);
printf("생성 날짜(변환): %s\n", date('Y-m-d H:i:s', $timestamp));
}
printf("UUID: %s\n", $settings['uuid'] ?? 'N/A');
printf("버전: %s\n", $settings['version']['created'] ?? 'N/A');
printf("샤드 수: %s\n", $settings['number_of_shards'] ?? 'N/A');
printf("Replica 수: %s\n", $settings['number_of_replicas'] ?? 'N/A');
}
echo "\n";
// 3. 인덱스 통계
echo "[ 3. 인덱스 통계 ]\n";
echo str_repeat("-", 100) . "\n";
$statsUrl = rtrim($host, '/') . '/' . urlencode($indexName) . '/_stats';
$statsResponse = callElasticAPI($statsUrl, $user, $password);
$statsData = json_decode($statsResponse, true);
if (isset($statsData['indices'][$indexName]['total'])) {
$total = $statsData['indices'][$indexName]['total'];
if (isset($total['docs'])) {
printf("총 문서 수: %s\n", number_format($total['docs']['count'] ?? 0));
printf("삭제된 문서: %s\n", number_format($total['docs']['deleted'] ?? 0));
}
if (isset($total['store'])) {
printf("총 저장 용량: %s\n", formatBytes($total['store']['size_in_bytes'] ?? 0));
}
if (isset($total['indexing'])) {
printf("색인 작업 수: %s\n", number_format($total['indexing']['index_total'] ?? 0));
printf("색인 소요 시간: %s ms\n", number_format($total['indexing']['index_time_in_millis'] ?? 0));
}
if (isset($total['search'])) {
printf("검색 작업 수: %s\n", number_format($total['search']['query_total'] ?? 0));
printf("검색 소요 시간: %s ms\n", number_format($total['search']['query_time_in_millis'] ?? 0));
}
}
echo "\n";
// 4. 매핑 정보 (필드 목록)
echo "[ 4. 필드 매핑 ]\n";
echo str_repeat("-", 100) . "\n";
$mappingUrl = rtrim($host, '/') . '/' . urlencode($indexName) . '/_mapping';
$mappingResponse = callElasticAPI($mappingUrl, $user, $password);
$mappingData = json_decode($mappingResponse, true);
if (isset($mappingData[$indexName]['mappings']['properties'])) {
$properties = $mappingData[$indexName]['mappings']['properties'];
printf("%-40s %-15s\n", "필드명", "타입");
echo str_repeat("-", 60) . "\n";
foreach ($properties as $fieldName => $fieldInfo) {
$type = $fieldInfo['type'] ?? 'object';
printf("%-40s %-15s\n", substr($fieldName, 0, 40), $type);
}
$totalFields = count($properties);
printf("\n총 필드 수: %d\n", $totalFields);
}
echo "\n";
// 5. TOP 50 문서 조회
if ($searchQuery) {
echo "[ 5. 검색 결과 TOP 50 (검색어: \"$searchQuery\") ]\n";
} else {
echo "[ 5. 샘플 문서 TOP 50 ]\n";
}
echo str_repeat("-", 100) . "\n";
$searchUrl = rtrim($host, '/') . '/' . urlencode($indexName) . '/_search';
// 검색 쿼리 생성
if ($searchQuery) {
// 검색어가 있으면 query_string 사용 (와일드카드 지원)
$query = [
'query_string' => [
'query' => $searchQuery,
'default_operator' => 'AND'
]
];
} else {
// 검색어가 없으면 match_all
$query = ['match_all' => (object)[]];
}
$searchBody = json_encode([
'size' => 50,
'query' => $query
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// 쿼리 표시
$cyanColor = "\033[1;36m"; // 밝은 청록색
$resetColor = "\033[0m";
echo "\n{$cyanColor}[사용된 Elasticsearch 쿼리]{$resetColor}\n";
echo str_repeat("-", 100) . "\n";
echo $searchBody . "\n";
echo str_repeat("-", 100) . "\n\n";
$searchResponse = callElasticAPI($searchUrl, $user, $password, 'POST', $searchBody);
$searchData = json_decode($searchResponse, true);
if (isset($searchData['hits']['hits']) && count($searchData['hits']['hits']) > 0) {
$hits = $searchData['hits']['hits'];
// 색상 코드 정의
$yellowColor = "\033[1;33m"; // 밝은 노란색
$greenColor = "\033[1;32m"; // 밝은 녹색
$resetColor = "\033[0m"; // 색상 리셋
foreach ($hits as $idx => $hit) {
$docId = $hit['_id'];
$docUrl = rtrim($host, '/') . '/' . urlencode($indexName) . '/_doc/' . urlencode($docId);
echo "\n문서 #" . ($idx + 1) . " (ID: {$greenColor}{$docId}{$resetColor}, URL: {$greenColor}{$docUrl}{$resetColor})\n";
echo str_repeat("-", 120) . "\n";
// 메타데이터 필드 먼저 표시
$metadataFields = ['_index', '_id', '_version', '_seq_no', '_primary_term', '_score'];
foreach ($metadataFields as $metaField) {
if (isset($hit[$metaField])) {
$value = $hit[$metaField];
echo " {$yellowColor}{$metaField}{$resetColor}: $value\n";
}
}
// 구분선
if (!empty($hit['_source'])) {
echo " " . str_repeat("-", 80) . "\n";
}
// _source 필드 표시
$source = $hit['_source'];
foreach ($source as $key => $value) {
$fullFieldName = "_source.$key";
if (is_array($value) || is_object($value)) {
echo " {$yellowColor}{$fullFieldName}{$resetColor}: " . json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
} else {
$valueStr = is_string($value) ? $value : json_encode($value);
echo " {$yellowColor}{$fullFieldName}{$resetColor}: $valueStr\n";
}
}
}
echo "\n총 조회된 문서 수: " . count($hits) . "\n";
echo "인덱스 전체 문서 수: " . number_format($searchData['hits']['total']['value'] ?? 0) . "\n";
} else {
echo "문서가 없습니다.\n";
}
echo "\n" . str_repeat("=", 100) . "\n";
echo "조회 완료\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
#!/usr/bin/env php
<?php
/**
* ptyElasticgetIndexs
*
* Elasticsearch 인덱스 정보를 조회하는 도구
* 설정 파일: ~/.ptyElasticConfig.ini
*/
// 설정 파일 경로
$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) {
$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) {
// 밀리초 타임스탬프인 경우 (13자리 숫자)
if (is_numeric($timestamp) && strlen($timestamp) >= 13) {
$seconds = intval($timestamp / 1000);
return date('Y-m-d H:i:s', $seconds);
}
// 초 타임스탬프인 경우 (10자리 숫자)
if (is_numeric($timestamp) && strlen($timestamp) == 10) {
return date('Y-m-d H:i:s', intval($timestamp));
}
// 이미 문자열 형식인 경우
if (is_string($timestamp)) {
return substr($timestamp, 0, 19);
}
return $timestamp;
}
// 인덱스의 마지막 문서 시간 조회
function getLastDocumentTime($host, $user, $password, $indexName) {
try {
$searchUrl = rtrim($host, '/') . '/' . urlencode($indexName) . '/_search';
$searchBody = json_encode([
'size' => 1,
'sort' => [
['_index' => ['order' => 'desc']]
],
'query' => ['match_all' => (object)[]]
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $searchUrl);
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) {
if (isset($source[$field])) {
return formatTimestamp($source[$field]);
}
}
}
return 'N/A';
} catch (Exception $e) {
return 'N/A';
}
}
try {
// 인덱스 정보 조회 (_cat/indices API 사용)
$url = rtrim($host, '/') . '/_cat/indices?v&format=json&bytes=mb&h=index,status,docs.count,store.size,health,pri,rep,creation.date.string';
echo "Elasticsearch 인덱스 정보 조회 중...\n";
echo "Host: $host\n";
echo str_repeat("=", 80) . "\n\n";
$response = callElasticAPI($url, $user, $password);
$indices = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("JSON 파싱 오류: " . json_last_error_msg());
}
if (empty($indices)) {
echo "인덱스가 없습니다.\n";
exit(0);
}
// 결과 정렬 (인덱스 이름순)
usort($indices, function($a, $b) {
return strcmp($a['index'], $b['index']);
});
// 헤더 출력
printf("%-40s %-10s %-12s %-12s %-10s %-10s %-20s %-20s\n",
"INDEX", "STATUS", "DOCS.COUNT", "STORE.SIZE", "HEALTH", "PRI/REP", "CREATED", "LAST INDEXED");
echo str_repeat("-", 165) . "\n";
// 각 인덱스 정보 출력
$totalDocs = 0;
$totalSize = 0;
foreach ($indices as $index) {
$indexName = $index['index'] ?? 'N/A';
$status = $index['status'] ?? 'N/A';
$docsCount = $index['docs.count'] ?? '0';
$storeSize = $index['store.size'] ?? '0';
$health = $index['health'] ?? 'N/A';
$pri = $index['pri'] ?? '0';
$rep = $index['rep'] ?? '0';
$creationDate = $index['creation.date.string'] ?? 'N/A';
// 마지막 색인 시간 조회
$lastIndexed = 'N/A';
if ($docsCount !== '0' && is_numeric($docsCount)) {
$lastIndexed = getLastDocumentTime($host, $user, $password, $indexName);
}
// 숫자 형식 정리
$docsCount = is_numeric($docsCount) ? number_format($docsCount) : $docsCount;
// 용량 형식 정리 (234mb -> 234 MB)
$storeSizeFormatted = is_numeric($storeSize) ? number_format($storeSize, 2) . ' MB' : $storeSize;
printf("%-40s %-10s %-12s %-12s %-10s %-10s %-20s %-20s\n",
substr($indexName, 0, 40),
$status,
$docsCount,
$storeSizeFormatted,
$health,
$pri . '/' . $rep,
substr($creationDate, 0, 20),
substr($lastIndexed, 0, 20)
);
// 총합 계산
if (is_numeric($index['docs.count'] ?? null)) {
$totalDocs += intval($index['docs.count']);
}
if (is_numeric($index['store.size'] ?? null)) {
$totalSize += floatval($index['store.size']);
}
}
// 요약 정보 출력
echo str_repeat("=", 165) . "\n";
printf("총 인덱스 수: %d\n", count($indices));
printf("총 문서 수: %s\n", number_format($totalDocs));
printf("총 용량: %.2f MB (%.2f GB)\n", $totalSize, $totalSize / 1024);
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
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