C++ Elasticsearch 완전 실전 가이드 | Elasticlient·REST API

C++ Elasticsearch 완전 실전 가이드 | Elasticlient·REST API

이 글의 핵심

C++에서 Elasticsearch 연동 종합: Elasticlient·libcurl REST API 설치부터 인덱싱·전문 검색·집계·벌크 인덱싱·스크롤 API·ILM까지. 실무 문제 시나리오, 완전한 예제 코드, 자주 발생하는 에러, 베스트 프랙티스, 프로덕션 패턴 900줄 분량.

들어가며: “C++에서 Elasticsearch 연동이 막막해요”

핵심 질문

"Elasticsearch 공식 C++ 클라이언트가 없는데 어떻게 연동하나요?"
"로그 100만 건을 인덱싱·검색하려면 어떤 API를 써야 하나요?"
"벌크 인덱싱 시 타임아웃·메모리 부족은 어떻게 해결하나요?"
"오래된 인덱스를 자동으로 정리하려면?"

이 글은 Elasticsearch C++ 연동의 종합 실전 가이드입니다. Elasticlient·REST API 선택 기준부터, 인덱싱·전문 검색·집계·벌크 인덱싱·스크롤 API·ILM(Index Lifecycle Management)까지 완전한 예제와 함께 다룹니다. 실무 문제 시나리오, 자주 발생하는 에러, 베스트 프랙티스, 프로덕션 패턴까지 900줄 분량으로 정리합니다.

이 글을 읽으면:

  • Elasticlient vs REST API 선택 기준을 알 수 있습니다.
  • 인덱싱·전문 검색·집계·벌크·스크롤·ILM을 완전한 코드로 구현할 수 있습니다.
  • Connection refused, mapper_parsing_exception, RequestTimeout 등 흔한 에러를 해결할 수 있습니다.
  • 프로덕션 환경에 맞는 패턴을 적용할 수 있습니다.

요구 환경: C++17 이상, Elasticsearch 7.x/8.x, Elasticlient 또는 libcurl + nlohmann/json


실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

문제 시나리오: 언제 무엇을 쓰나?

시나리오 1: 로그 검색이 10초 넘게 걸림

상황: 수백만 건 로그를 MySQL LIKE '%error%'로 검색
문제: 풀 테이블 스캔, 인덱스 무력, 10~30초 지연
결과: Elasticsearch 역인덱스 전문 검색으로 밀리초 단위 검색

시나리오 2: 전문 검색(Full-Text Search) 필요

상황: 제품 설명에서 "무선 이어폰 블루투스 노이즈캔슬링" 검색
문제: 단어 분리, 유사어 매칭, 점수 기반 정렬이 관계형 DB에서 어려움
결과: Elasticsearch match·match_phrase·bool 쿼리로 정확한 검색

시나리오 3: 실시간 집계·대시보드

상황: API 호출 수, 에러율, 평균 응답 시간을 1분 단위로 집계
문제: 매분마다 COUNT·AVG 쿼리로 DB 부하 급증
결과: date_histogram·terms 집계로 실시간 집계

시나리오 4: Connection refused 에러

상황: Elasticsearch 서버 주소 설정했는데 연결 실패
문제: localhost vs 127.0.0.1, Docker 네트워크, 방화벽 혼동
결과: curl로 연결 확인, 여러 노드 지정, 타임아웃 설정

시나리오 5: 벌크 인덱싱 시 타임아웃

상황: 수십만 건을 한 번에 인덱싱하려다 RequestTimeout·EsRejectedExecutionException
문제: 배치 크기 과다, 스레드 풀 포화
결과: 1000~5000건 배치, 재시도, refresh_interval 조정

시나리오 6: 수십만 건 검색 시 OOM

상황: 전체 결과를 vector에 담다 메모리 폭증
문제: size=100000으로 한 번에 조회
결과: 스크롤 API 또는 Search After로 스트리밍 조회

시나리오 7: 오래된 로그 인덱스 디스크 폭증

상황: 일별 인덱스가 수백 개 쌓여 디스크 부족
문제: 수동 삭제·압축 관리 부담
결과: ILM(Index Lifecycle Management)으로 hot→warm→delete 자동 전환

시나리오별 기술 선택

시나리오Elasticsearch 기능C++ 구현
느린 로그 검색역인덱스 전문 검색match·match_phrase 쿼리
복합 키워드 검색bool·multi_matchQuery DSL JSON
실시간 집계terms·date_histogramaggs 필드
대량 인덱싱_bulk API배치 1000~5000
대용량 검색Scroll·Search Afterscroll_id·search_after
인덱스 수명 관리ILMPUT _ilm/policy
flowchart TB
    subgraph 문제[실무 문제]
        P1[느린 검색] --> S1[전문 검색]
        P2[복합 검색] --> S2[Query DSL]
        P3[실시간 집계] --> S3[집계 API]
        P4[Connection refused] --> S4[연결·타임아웃]
        P5[벌크 타임아웃] --> S5[배치·재시도]
        P6[대용량 OOM] --> S6[스크롤]
        P7[인덱스 폭증] --> S7[ILM]
    end

목차

  1. 환경 설정
  2. Elasticlient 완전 예제
  3. REST API 완전 예제
  4. 인덱싱·전문 검색·집계
  5. 벌크 인덱싱
  6. 스크롤 API
  7. ILM (Index Lifecycle Management)
  8. 자주 발생하는 에러와 해결법
  9. 베스트 프랙티스
  10. 프로덕션 패턴
  11. 구현 체크리스트
  12. 정리

1. 환경 설정

필수 의존성

항목버전비고
C++C++14 이상C++17 권장
Elasticsearch7.x/8.xDocker 권장
Elasticlient-선택, cpr 의존
libcurl7.x+REST API용
nlohmann/json3.xJSON 파싱

Elasticsearch 서버 실행 (Docker)

# 단일 노드 (개발용)
docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 \
  -e "discovery.type=single-node" \
  -e "xpack.security.enabled=false" \
  -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
  docker.elastic.co/elasticsearch/elasticsearch:8.11.0

# 연결 확인
curl -X GET "localhost:9200/?pretty"

Elasticlient 설치 (선택)

# cpr 의존성 (Ubuntu/Debian)
sudo apt-get install libcurl4-openssl-dev

# Elasticlient 소스 빌드
git clone https://github.com/seznam/elasticlient.git
cd elasticlient
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build .
sudo cmake --build . --target install

libcurl + nlohmann/json 설치 (REST API용, 권장)

# Ubuntu/Debian
sudo apt-get install libcurl4-openssl-dev

# macOS (Homebrew)
brew install curl nlohmann-json

# vcpkg
vcpkg install curl nlohmann-json

CMakeLists.txt 기본 설정

REST API 직접 사용 시 (권장):

cmake_minimum_required(VERSION 3.16)
project(elasticsearch_demo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

find_package(CURL REQUIRED)
find_package(nlohmann_json REQUIRED)

add_executable(es_demo main.cpp)
target_link_libraries(es_demo PRIVATE CURL::libcurl nlohmann_json::nlohmann_json)

Elasticlient 사용 시:

cmake_minimum_required(VERSION 3.16)
project(elasticsearch_demo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

find_package(elasticlient REQUIRED)
find_package(cpr REQUIRED)

add_executable(es_demo main.cpp)
target_link_libraries(es_demo PRIVATE elasticlient::elasticlient cpr::cpr)

2. Elasticlient 완전 예제

2.1 최소 동작: 인덱싱·조회·삭제

// elasticlient_basic.cpp
#include <elasticlient/client.h>
#include <iostream>
#include <string>

int main() {
    try {
        elasticlient::Client client({"http://localhost:9200/"});

        // 1. 문서 인덱싱
        std::string doc = R"({"message": "Hello Elasticsearch!", "timestamp": "2024-01-15T10:00:00"})";
        cpr::Response indexResp = client.index("logs", "_doc", "1", doc);

        if (indexResp.status_code != 200 && indexResp.status_code != 201) {
            std::cerr << "인덱싱 실패: " << indexResp.status_code << " " << indexResp.text << std::endl;
            return 1;
        }
        std::cout << "인덱싱 완료: " << indexResp.text << std::endl;

        // 2. 문서 조회
        cpr::Response getResp = client.get("logs", "_doc", "1");
        if (getResp.status_code == 200) {
            std::cout << "조회 결과: " << getResp.text << std::endl;
        }

        // 3. 문서 삭제
        cpr::Response delResp = client.remove("logs", "_doc", "1");
        std::cout << "삭제 완료: " << delResp.status_code << std::endl;

    } catch (const std::exception& e) {
        std::cerr << "에러: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

코드 설명:

  • Client: Elasticsearch 클러스터 URL 목록 (여러 노드 지정 가능)
  • index(index, type, id, doc): 문서 인덱싱 (7.x 이후 type은 _doc 권장)
  • get·remove: 문서 조회·삭제
// elasticlient_search.cpp
#include <elasticlient/client.h>
#include <iostream>
#include <string>

int main() {
    elasticlient::Client client({"http://localhost:9200/"});

    // match 쿼리: message 필드에서 "error" 검색
    std::string query = R"({
        "query": {
            "match": {
                "message": "error"
            }
        },
        "size": 10,
        "_source": ["message", "level", "timestamp"]
    })";

    cpr::Response resp = client.search("logs", query);
    if (resp.status_code == 200) {
        std::cout << "검색 결과:\n" << resp.text << std::endl;
    } else {
        std::cerr << "검색 실패: " << resp.status_code << " " << resp.text << std::endl;
    }
    return 0;
}

2.3 벌크 인덱싱 (Bulk)

// elasticlient_bulk.cpp
#include <elasticlient/client.h>
#include <elasticlient/bulk.h>
#include <iostream>
#include <string>

int main() {
    elasticlient::Client client({"http://localhost:9200/"});
    elasticlient::Bulk bulk(client);

    for (int i = 1; i <= 100; ++i) {
        std::string doc = R"({"msg":"log)" + std::to_string(i) +
            R"(","level":")" + (i % 5 == 0 ? "error" : "info") + R"("})";
        bulk.index("logs", "_doc", std::to_string(i), doc);
    }

    cpr::Response resp = bulk.perform();
    if (resp.status_code == 200) {
        std::cout << "벌크 완료: " << resp.text << std::endl;
    } else {
        std::cerr << "벌크 실패: " << resp.status_code << " " << resp.text << std::endl;
    }
    return 0;
}

2.4 스크롤 API (Elasticlient Scroll)

// elasticlient_scroll.cpp
#include <elasticlient/client.h>
#include <elasticlient/scroll.h>
#include <iostream>
#include <string>

int main() {
    elasticlient::Client client({"http://localhost:9200/"});

    std::string query = R"({
        "query": {"match_all": {}},
        "size": 100
    })";

    elasticlient::Scroll scroll(client, "logs", query, "1m");
    size_t total = 0;

    while (scroll.hasNext()) {
        auto hits = scroll.next();
        for (const auto& hit : hits) {
            total++;
            // 문서 처리
        }
    }

    std::cout << "총 처리: " << total << "건\n";
    return 0;
}

3. REST API 완전 예제

Elasticlient 없이 libcurl과 nlohmann/json으로 REST API를 직접 호출합니다. 의존성 최소이며 모든 Elasticsearch API를 사용할 수 있습니다.

3.1 HTTP 헬퍼 클래스

// es_client.hpp
#pragma once
#include <curl/curl.h>
#include <string>

class EsClient {
public:
    EsClient(const std::string& base_url = "http://localhost:9200");
    ~EsClient();
    std::string get(const std::string& path);
    std::string put(const std::string& path, const std::string& body = "{}");
    std::string post(const std::string& path, const std::string& body = "{}");
    std::string del(const std::string& path);
private:
    static size_t write_cb(void* c, size_t s, size_t n, void* u) {
        size_t t = s * n;
        static_cast<std::string*>(u)->append(static_cast<char*>(c), t);
        return t;
    }
    std::string request(const std::string& method, const std::string& path, const std::string& body);
    std::string base_url_;
    CURL* curl_;
};

구현: request()에서 curl_easy_setopt로 URL·WRITEFUNCTION·CONNECTTIMEOUT(5)·TIMEOUT(30)·Content-Type: application/json 설정, HTTP 4xx/5xx 시 runtime_error throw.


4. 인덱싱·전문 검색·집계

4.1 인덱스 매핑 생성 및 문서 인덱싱

// rest_indexing.cpp
#include "es_client.hpp"
#include <iostream>
#include <nlohmann/json.hpp>

int main() {
    try {
        EsClient client("http://localhost:9200");

        // 1. 인덱스 매핑 생성
        std::string mapping = R"({
            "mappings": {
                "properties": {
                    "message": { "type": "text" },
                    "level": { "type": "keyword" },
                    "timestamp": { "type": "date" },
                    "userId": { "type": "keyword" }
                }
            }
        })";
        client.put("logs", mapping);

        // 2. 문서 인덱싱 (ID 지정)
        nlohmann::json doc = {
            {"message", "Application started"},
            {"level", "info"},
            {"timestamp", "2024-01-15T10:00:00Z"},
            {"userId", "user123"}
        };
        std::string indexResp = client.put("logs/_doc/1", doc.dump());
        std::cout << "인덱싱: " << indexResp << std::endl;

        // 3. 자동 ID 생성 (POST)
        std::string autoResp = client.post("logs/_doc", doc.dump());
        std::cout << "자동 ID: " << autoResp << std::endl;

    } catch (const std::exception& e) {
        std::cerr << "에러: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}
// rest_fulltext_search.cpp
#include "es_client.hpp"
#include <iostream>
#include <nlohmann/json.hpp>

int main() {
    try {
        EsClient client("http://localhost:9200");

        // match: 단어 분리 후 검색 (OR 기본)
        // match_phrase: 구문 검색 (순서 유지)
        // bool: must/should/must_not/filter로 복합 조건
        nlohmann::json match_query = {
            {"query", {
                {"match", {{"message", "error timeout"}}}
            }},
            {"size", 10},
            {"_source", {"message", "level", "timestamp"}}
        };

        std::string resp = client.post("logs/_search", match_query.dump());
        auto j = nlohmann::json::parse(resp);
        auto hits = j[hits][hits];
        std::cout << "총 " << j[hits][total][value] << "건\n";

        for (const auto& hit : hits) {
            std::cout << "  - " << hit[_source][message] << " ["
                      << hit[_source][level] << "]\n";
        }

    } catch (const std::exception& e) {
        std::cerr << "에러: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

4.3 집계 (Aggregation)

// rest_aggregation.cpp
#include "es_client.hpp"
#include <iostream>
#include <nlohmann/json.hpp>

int main() {
    try {
        EsClient client("http://localhost:9200");

        // terms: level별 문서 수. date_histogram: 1시간 단위. 중첩 aggs로 복합 집계 가능
        nlohmann::json terms_agg = {
            {"size", 0},
            {"aggs", {
                {"levels", {
                    {"terms", {{"field", "level"}}}
                }}
            }}
        };

        std::string resp = client.post("logs/_search", terms_agg.dump());
        auto j = nlohmann::json::parse(resp);

        std::cout << "level별 건수:\n";
        for (const auto& bucket : j[aggregations][levels][buckets]) {
            std::cout << "  " << bucket[key] << ": " << bucket[doc_count] << "\n";
        }

    } catch (const std::exception& e) {
        std::cerr << "에러: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

5. 벌크 인덱싱

5.1 기본 벌크 API

// rest_bulk.cpp
#include "es_client.hpp"
#include <iostream>
#include <sstream>
#include <nlohmann/json.hpp>

int main() {
    try {
        EsClient client("http://localhost:9200");

        // 벌크 형식: 액션\n문서\n액션\n문서...
        std::ostringstream bulk;
        for (int i = 1; i <= 1000; ++i) {
            nlohmann::json action = {
                {"index", {
                    {"_index", "logs"},
                    {"_id", std::to_string(i)}
                }}
            };
            nlohmann::json doc = {
                {"message", "Log entry " + std::to_string(i)},
                {"level", (i % 5 == 0 ? "error" : "info")},
                {"timestamp", "2024-01-15T10:00:00Z"}
            };
            bulk << action.dump() << "\n" << doc.dump() << "\n";
        }

        std::string resp = client.post("_bulk", bulk.str());
        auto j = nlohmann::json::parse(resp);

        if (j.contains("errors") && j[errors]) {
            std::cerr << "벌크 중 일부 실패\n";
            for (const auto& item : j[items]) {
                if (item.contains("index") && item[index].contains("error")) {
                    std::cerr << "  " << item[index][error][reason] << "\n";
                }
            }
        } else {
            std::cout << "벌크 인덱싱 완료: 1000건\n";
        }

    } catch (const std::exception& e) {
        std::cerr << "에러: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

5.2 대용량 배치 벌크 (재시도 포함)

// batch_size 2000, max_retries 3으로 배치별 _bulk 호출
// 실패 시 100*(retry+1)ms 지수 백오프 후 재시도
void bulk_index_batch(EsClient& client, const std::vector<nlohmann::json>& docs,
                      size_t batch_size = 2000, int max_retries = 3) {
    for (size_t offset = 0; offset < docs.size(); offset += batch_size) {
        std::ostringstream bulk;
        for (size_t i = offset; i < std::min(offset + batch_size, docs.size()); ++i) {
            bulk << R"({"index":{"_index":"logs","_id":")" << (i+1) << R"("}})" << "\n"
                 << docs[i].dump() << "\n";
        }
        for (int r = 0; r < max_retries; ++r) {
            try { client.post("_bulk", bulk.str()); break; }
            catch (const std::exception& e) {
                if (r == max_retries - 1) throw;
                std::this_thread::sleep_for(std::chrono::milliseconds(100 * (r + 1)));
            }
        }
    }
}

6. 스크롤 API

6.1 스크롤로 대용량 검색

// rest_scroll.cpp
#include "es_client.hpp"
#include <iostream>
#include <nlohmann/json.hpp>

int main() {
    try {
        EsClient client("http://localhost:9200");

        // 1. 스크롤 시작 (scroll=1m: 1분간 컨텍스트 유지)
        nlohmann::json search = {
            {"query", {{"match_all", {}}}},
            {"size", 100},
            {"sort", {{"_id", "asc"}}}
        };

        std::string resp = client.post("logs/_search?scroll=1m", search.dump());
        auto j = nlohmann::json::parse(resp);
        std::string scroll_id = j[_scroll_id];
        auto hits = j[hits][hits];

        size_t total = 0;
        while (!hits.empty()) {
            total += hits.size();
            for (const auto& hit : hits) {
                // 문서 처리 (예: 파일 저장, 변환)
                (void)hit;
            }

            // 2. 다음 스크롤
            nlohmann::json scroll_req = {{"scroll", "1m"}, {"scroll_id", scroll_id}};
            resp = client.post("_search/scroll", scroll_req.dump());
            j = nlohmann::json::parse(resp);
            scroll_id = j[_scroll_id];
            hits = j[hits][hits];
        }

        std::cout << "총 처리: " << total << "건\n";

    } catch (const std::exception& e) {
        std::cerr << "에러: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

6.2 Search After (실시간 페이지네이션)

// rest_search_after.cpp
#include "es_client.hpp"
#include <iostream>
#include <nlohmann/json.hpp>

int main() {
    try {
        EsClient client("http://localhost:9200");

        nlohmann::json query = {
            {"query", {{"match_all", {}}}},
            {"size", 100},
            {"sort", {{"timestamp", "asc"}, {"_id", "asc"}}}
        };

        // 첫 페이지
        std::string resp = client.post("logs/_search", query.dump());
        auto j = nlohmann::json::parse(resp);
        auto hits = j[hits][hits];

        while (!hits.empty()) {
            for (const auto& hit : hits) {
                std::cout << hit[_source][message] << "\n";
            }

            auto last = hits.back();
            if (!last.contains("sort")) break;

            query[search_after] = last[sort];
            resp = client.post("logs/_search", query.dump());
            j = nlohmann::json::parse(resp);
            hits = j[hits][hits];
        }

    } catch (const std::exception& e) {
        std::cerr << "에러: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

7. ILM (Index Lifecycle Management)

7.1 ILM 정책 생성

// rest_ilm.cpp
#include "es_client.hpp"
#include <iostream>
#include <nlohmann/json.hpp>

int main() {
    try {
        EsClient client("http://localhost:9200");

        // ILM 정책: hot → 7일 후 warm → 30일 후 delete
        nlohmann::json policy = {
            {"policy", {
                {"phases", {
                    {"hot", {
                        {"min_age", "0ms"},
                        {"actions", {
                            {"rollover", {
                                {"max_size", "50gb"},
                                {"max_age", "1d"}
                            }}
                        }}
                    }},
                    {"warm", {
                        {"min_age", "7d"},
                        {"actions", {
                            {"shrink", {"number_of_shards", 1}},
                            {"forcemerge", {"max_num_segments", 1}}
                        }}
                    }},
                    {"delete", {
                        {"min_age", "30d"},
                        {"actions", {{"delete", {}}}}
                    }}
                }}
            }}
        };

        client.put("_ilm/policy/logs_policy", policy.dump());
        std::cout << "ILM 정책 생성 완료\n";

        // 인덱스 템플릿에 ILM 연결
        nlohmann::json template_body = {
            {"index_patterns", {"logs-*"}},
            {"template", {
                {"settings", {
                    {"number_of_shards", 3},
                    {"number_of_replicas", 1},
                    {"index.lifecycle.name", "logs_policy"},
                    {"index.lifecycle.rollover_alias", "logs"}
                }}
            }}
        };

        client.put("_index_template/logs_template", template_body.dump());
        std::cout << "인덱스 템플릿 생성 완료\n";

    } catch (const std::exception& e) {
        std::cerr << "에러: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

7.2 롤오버 인덱스 생성 및 ILM 확인

롤오버용 별칭이 있는 첫 인덱스: PUT logs-000001 {"aliases":{"logs":{"is_write_index":true}}}
ILM 상태: GET _ilm/status, 인덱스별: GET logs-*/_ilm/explain

8. 자주 발생하는 에러와 해결법

에러 1: “Connection refused” / “Could not resolve host”

증상: Elasticsearch 연결 시도 시 실패.

원인: 서버 미실행, 잘못된 주소/포트, Docker 네트워크, 방화벽.

해결법:

# Elasticsearch 실행 확인
curl -X GET "localhost:9200/"

# Docker 컨테이너에서 호스트 접속 시
# URL: http://host.docker.internal:9200
// ✅ 여러 노드 지정 (고가용성)
EsClient client("http://node1:9200");

// ✅ 타임아웃 설정
curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT, 5L);
curl_easy_setopt(curl_, CURLOPT_TIMEOUT, 30L);

에러 2: “mapper_parsing_exception” / “400 Bad Request”

증상: 문서 인덱싱 시 400 응답.

원인: JSON 형식 오류, 필드 타입 불일치.

해결법:

// ❌ 잘못된 JSON
std::string bad = R"({"message": "test" "level": "info"})";  // 쉼표 누락

// ✅ nlohmann::json으로 안전하게 구성
nlohmann::json doc = {
    {"message", "test"},
    {"level", "info"},
    {"timestamp", "2024-01-15T10:00:00Z"}
};
std::string body = doc.dump();

에러 3: “index_not_found_exception”

증상: 검색/조회 시 인덱스 없음.

해결법:

// ✅ 인덱스 사전 생성
std::string mapping = R"({
    "mappings": {
        "properties": {
            "message": { "type": "text" },
            "level": { "type": "keyword" }
        }
    }
})";
client.put("logs", mapping);

에러 4: “RequestTimeout” / “EsRejectedExecutionException”

증상: 벌크 인덱싱 또는 대용량 검색 시 타임아웃.

해결법:

// ✅ 벌크 배치 크기 제한 (1000~5000 권장)
const size_t BATCH_SIZE = 1000;

// ✅ 타임아웃 증가
curl_easy_setopt(curl_, CURLOPT_TIMEOUT, 120L);

에러 5: “version_conflict_engine_exception”

증상: 동일 ID로 동시 인덱싱 시 충돌.

해결법:

// ✅ 자동 ID 사용
std::string resp = client.post("logs/_doc", doc);  // ID 없이 POST

에러 6: JSON 파싱 에러

증상: parse_error 예외. 해결: try-catch로 파싱, 실패 시 원본 응답 로깅.

에러 7: “security_exception” / 401 Unauthorized

증상: Elasticsearch 8.x 보안 활성화 시 인증 실패. 해결: curl_easy_setopt(curl_, CURLOPT_USERPWD, "elastic:password");


9. 베스트 프랙티스

9.1 벌크 배치 크기

// 배치 크기: 1000~5000 권장
// 너무 크면: 메모리·타임아웃
// 너무 작으면: HTTP 오버헤드
const size_t BATCH_SIZE = 2000;

9.2 연결 재사용

// ❌ 나쁜 예: 매 요청마다 새 연결
void handle_request() {
    EsClient client("http://localhost:9200");
    client.get("logs/_search");
}

// ✅ 좋은 예: EsClient 싱글톤 또는 풀
class SearchService {
    EsClient client_;
public:
    SearchService() : client_("http://localhost:9200") {}
    std::string search(const std::string& query) {
        return client_.post("logs/_search", query);
    }
};

9.3 _source 필드 제한

// 필요한 필드만 요청 (네트워크·파싱 부하 감소)
nlohmann::json query = {
    {"query", {{"match_all", {}}}},
    {"_source", {"message", "level"}},
    {"size", 100}
};

9.4 스크롤 vs Search After

방식용도메모리
Scroll대용량 일괄 처리서버에 스크롤 컨텍스트 유지
Search After페이지네이션, 실시간에 가까움stateless

10. 프로덕션 패턴

10.1 재시도 로직

std::string request_with_retry(const std::string& method, const std::string& path,
                               const std::string& body, int max_retries = 3) {
    for (int i = 0; i < max_retries; ++i) {
        try {
            if (method == "GET") return client_.get(path);
            if (method == "POST") return client_.post(path, body);
            if (method == "PUT") return client_.put(path, body);
        } catch (const std::exception& e) {
            if (i == max_retries - 1) throw;
            std::this_thread::sleep_for(std::chrono::milliseconds(100 * (i + 1)));
        }
    }
    throw std::runtime_error("재시도 실패");
}

10.2 헬스 체크

bool is_healthy() {
    try {
        std::string resp = client_.get("_cluster/health");
        auto j = nlohmann::json::parse(resp);
        std::string status = j[status];
        return status == "green" || status == "yellow";
    } catch (...) {
        return false;
    }
}

10.3 환경 변수 기반 설정

struct EsConfig {
    std::string url = "http://localhost:9200";
    int timeout_sec = 30;
    size_t bulk_size = 2000;
};

EsConfig load_config() {
    EsConfig c;
    if (const char* u = std::getenv("ELASTICSEARCH_URL")) c.url = u;
    if (const char* t = std::getenv("ES_TIMEOUT")) c.timeout_sec = std::stoi(t);
    return c;
}

10.4 인덱스 별칭 (Zero-Downtime 재인덱싱)

// POST _aliases: remove logs_v1, add logs_v2 to alias "logs"
client_.post("_aliases", R"({"actions":[{"remove":{"index":"logs_v1","alias":"logs"}},{"add":{"index":"logs_v2","alias":"logs"}}]})");

10.5 TLS/SSL (프로덕션)

EsClient client("https://elasticsearch.example.com:9200");
curl_easy_setopt(curl_, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt");

11. 구현 체크리스트

환경 설정

  • Elasticsearch 서버 실행 확인 (curl localhost:9200)
  • libcurl, nlohmann/json 설치 (또는 Elasticlient)
  • CMake/빌드 연동

REST API 클라이언트

  • Content-Type: application/json 헤더 설정
  • 타임아웃 설정 (connect, read)
  • HTTP 에러 코드 검사 (4xx, 5xx)
  • JSON 파싱 예외 처리

인덱싱

  • 인덱스 매핑 사전 정의 (필드 타입)
  • 벌크 배치 크기 1000~5000
  • 벌크 에러 시 개별 항목 실패 처리

검색

  • _source 필드 제한 (필요 시)
  • size 제한 (기본 10, 최대 10000)
  • 대용량 결과 시 스크롤 또는 Search After

ILM

  • 로그 인덱스에 ILM 정책 연결
  • 롤오버 조건 (max_size, max_age) 설정
  • delete 단계 min_age 설정

프로덕션

  • 연결 재사용 (EsClient 풀/싱글톤)
  • 헬스 체크 (/_cluster/health)
  • 환경 변수로 URL·타임아웃 외부화
  • TLS/SSL (HTTPS)

12. 정리

항목요약
공식 C++ 클라이언트없음. Elasticlient 또는 REST API 사용
REST APIlibcurl + nlohmann/json으로 모든 API 호출 가능
인덱싱PUT/POST, 벌크는 _bulk API
전문 검색match·match_phrase·bool 쿼리
집계aggs 필드로 terms·date_histogram 등
벌크배치 1000~5000, 재시도
스크롤scroll_id로 대용량 순차 조회
ILMhot→warm→delete 자동 전환
에러Connection refused, mapper_parsing, RequestTimeout
프로덕션재시도, 헬스 체크, 환경 변수, TLS

핵심 원칙:

  1. 벌크 사용: 단건 인덱싱 대신 _bulk API로 배치 처리
  2. 연결 재사용: 매 요청마다 새 클라이언트 생성 금지
  3. 에러 파싱: HTTP 상태·JSON error.reason 확인
  4. 대용량 검색: 스크롤 또는 Search After
  5. ILM 활용: 오래된 인덱스 자동 정리

자주 묻는 질문 (FAQ)

Q. Elasticlient와 REST API 중 어떤 것을 써야 하나요?

A. Elasticlient는 API가 단순하지만 유지보수가 느리고 cpr 의존이 있습니다. 새 프로젝트에서는 libcurl + nlohmann/json으로 REST API를 직접 호출하는 방식을 권장합니다.

Q. 대용량 로그 인덱싱 시 주의할 점은?

A. 벌크 배치 크기 1000~5000, 인덱스별 refresh_interval 조정, ILM으로 오래된 인덱스 정리 등을 고려하세요.

Q. 스크롤과 Search After의 차이는?

A. 스크롤은 대용량 일괄 처리에 적합하며 서버에 컨텍스트를 유지합니다. Search After는 stateless로 실시간 페이지네이션에 적합합니다.

한 줄 요약: Elasticsearch는 공식 C++ 클라이언트가 없지만, libcurl과 nlohmann/json으로 REST API를 호출하면 전문 검색·집계·벌크 인덱싱·스크롤·ILM을 C++에서 완전히 활용할 수 있습니다.

다음 글: C++ 시리즈 목차

이전 글: Elasticsearch 개요(#52-5)


참고 자료


관련 글

  • C++ Elasticsearch 완벽 가이드 | Elasticlient·REST API
  • C++ Elasticsearch 통합 | 전문 검색·집계·실시간 인덱싱 [#52-5]
  • C++ HTTP 클라이언트 완벽 가이드 | REST API 호출·연결 풀·타임아웃·프로덕션 패턴
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3