콘텐츠로 이동

Elasticsearch

RAG 검색의 1차 저장소. 14개 세무 인덱스(tax-*)에 법령·판례·예규·상담·회계기준 등이 적재되고, 백엔드는 BM25 텍스트 검색 + kNN 벡터 검색을 RRF(Reciprocal Rank Fusion)로 합쳐 hybrid 검색한다. 모든 인덱스가 동일한 분석기/벡터 매핑 컨벤션(Nori + dense_vector 3072 cosine)을 공유하여 source-agnostic하게 검색 로직을 통합한다.

인덱스 카탈로그

인덱스 적재 모듈 백엔드 Repository 임베딩
tax-laws LawIndexer SemugptLawElasticRepository
tax-precedents PrecedentIndexer (+ NTSPrecedentCollector) SemugptPrecedentElasticRepository
tax-counsel CounselIndexer SemugptCounselElasticRepository
tax-written-inquiry WrittenInquiryIndexer SemugptWrittenInquiryElasticRepository
tax-glossary GlossaryIndexer SemugptGlossaryElasticRepository
tax-enforcement EnforcementIndexer SemugptEnforcementElasticRepository
tax-oldnew OldAndNewIndexer SemugptOldAndNewElasticRepository
tax-basic-rules BasicRulesIndexer SemugptBasicRulesElasticRepository
tax-taxoffice TaxofficeIndexer SemugptTaxofficeElasticRepository
tax-tribunal TribunalIndexer SemugptTribunalElasticRepository
tax-supreme-court SupremeCourtIndexer SemugptSupremeCourtElasticRepository
tax-scourt ScourtIndexer SemugptScourtElasticRepository
tax-accounting AccountingIndexer SemugptAccountingElasticRepository
tax-threeway ThreeWayIndexer (백엔드 ThreewayService 직접 query)
tax-relations RelationIndexer 백엔드 미사용 (legacy)

총 적재 인덱스는 15개지만 백엔드 RAG가 사용하는 핵심 인덱스는 14개. 자세한 RAG 라우팅은 리포트 생성 참조.

사용자 여정

인덱스 생성 → 검색 흐름

sequenceDiagram
    autonumber
    participant CLI as semugpt-index
    participant ES as Elasticsearch
    participant API as Backend
    participant FE as 프론트엔드

    CLI->>ES: PUT /tax-laws (settings + mappings)
    Note over ES: nori_tokenizer, korean_text analyzer
content_vector: dense_vector(3072, cosine) CLI->>ES: POST /_bulk (documents + content_vector) ES-->>CLI: 적재 완료 Note over API: 사용자 질문 도착 API->>API: HyDE로 가상 답변 생성 → embed API->>ES: POST /tax-laws/_search
{ retriever: { rrf: { retrievers: [bm25, knn] } } } ES-->>API: 점수순 결과 (BM25 + kNN RRF 융합) API-->>FE: 답변 + 참고자료

Dev 환경 직접 조회

sequenceDiagram
    participant U as 운영자
    participant ES as Dev ES (localhost:9200 on Lightsail box)
    U->>U: printf 'elastic:uiti0701!' | base64
→ ZWxhc3RpYzp1aXRpMDcwMSE= U->>ES: curl -H 'Authorization: Basic ...' /_cat/indices ES-->>U: tax-laws / tax-precedents / ... 문서 수 U->>ES: curl ... /tax-laws/_search?q=양도소득세 ES-->>U: BM25 결과 (벡터 검색 없이)

백엔드 구현

계층 클래스 / 파일 역할
ES client Spring RestClient Bean 백엔드 측 ES 클라이언트, application-{prod,local}.ymlelastic.host/port/username/password 로드
Repository (인덱스별) SemugptXxxElasticRepository (apps/backend/.../persistence/) 인덱스마다 1:1. findByTextOnly, findByVectorOnly, findBatchById 등. RRF hybrid는 Law 인덱스에만 findSimilarLawListWithRRF()로 구현
RAG 검색 오케스트레이션 StreamingRagProcessor (apps/backend/.../service/) 인덱스 라우팅 + hybrid search + 정렬·필터. 리포트 생성 참조
Admin ElasticAdminRepository (apps/backend/.../persistence/ElasticAdminRepository.kt) 인덱스 상태/통계 조회 (admin console)
Indexer 측 client create_es_client (packages/data-pipeline/.../utils/elasticsearch.py) Python 파이프라인의 공통 ES 클라이언트

도메인 규칙

규칙 위치 값 / 설명
ES 버전 dev/prod 동일 8.17 (elasticsearch~=8.17.0 Python client) — RRF retriever API 지원
분석 플러그인 dev/prod 모두 설치 analysis-nori — 한국어 형태소 분석
단일 노드 운영 각 인덱스 settings number_of_shards: 1, number_of_replicas: 0. dev/prod 모두 single-node
Tokenizer 매핑 settings nori_tokenizer with decompound_mode: "mixed" — 복합어를 원형 + 분해형 모두 색인
Analyzer 매핑 settings korean_text = nori_tokenizer + lowercase + nori_readingform 필터
텍스트 필드 패턴 각 인덱서 매핑 { "type": "text", "analyzer": "korean_text", "copy_to": "_all_text" } — 다중 필드를 통합 검색용 단일 필드로 복사
벡터 필드 각 인덱서 매핑 content_vector: { type: "dense_vector", dims: 3072, index: true, similarity: "cosine" }
임베딩 모델 indexer + backend 동일 text-embedding-3-large (OpenAI). 백엔드 EmbeddingUtil.embed()와 인덱서가 동일 모델 사용 — 차원 미스매치 방지
Hybrid 검색 (RRF) SemugptLawElasticRepository.findSimilarLawListWithRRF() (Law 인덱스에만 구현됨) ES 8.x retriever.rrf API. BM25 + kNN을 rank_constant, window_size로 융합. 타 인덱스는 application layer에서 BM25 + kNN 결과 융합
BM25 매치 필드 repository별 query 보통 multi_matchtitle^2, content, _all_text
kNN 파라미터 repository별 k, num_candidates=k*2
벡터 검색 fallback findByVectorOnly cosineSimilarity(params.query_vector, 'content_vector') + 1.0 스크립트 점수 (script_score)
인덱스 명명 indexer INDEX_NAME 상수 tax-{source} (snake_case 대신 dash). 백엔드 indexName 상수와 1:1 일치
운영 단일 노드 + Docker Lightsail docker-compose.dev.yml dev/prod 모두 Lightsail box 안의 Docker 컨테이너로 운영 (RDS 분리 안 함). ES_JAVA_OPTS=-Xms1g -Xmx1g (Lightsail-only patch)

API 엔드포인트

해당 없음 — Elasticsearch는 내부 데이터 저장소이며 외부 API로 노출되지 않는다. 백엔드의 REST endpoint를 통해서만 접근. RAG 검색 API는 리포트 생성/conversations/** 참조.

ES 자체 API (dev 직접 조회용, 운영자 한정):

Method Path 설명
GET /_cat/indices?v 인덱스 목록 + 문서 수
GET /{index}/_count 단일 인덱스 문서 수
GET /{index}/_search?q=... BM25 검색
POST /{index}/_search DSL 쿼리 (RRF, kNN 등)
GET /{index}/_mapping 매핑 조회

데이터 모델

각 인덱스가 가진 공통 매핑 패턴 (구체 필드는 source별 추가):

erDiagram
    TAX_INDEX_COMMON {
        keyword type "문서 타입 (예: law_article, precedent)"
        text title "한국어 분석"
        text content "한국어 분석, copy_to _all_text"
        text _all_text "통합 검색 필드"
        dense_vector content_vector "3072 dims, cosine"
        keyword source "출처 도메인"
        keyword source_url
        date collected_at
    }
    TAX_LAWS {
        keyword law_id
        keyword law_name
        keyword law_type "현행 / 시행령 / 시행규칙"
        keyword article_key "7자 정규화 키 (A/B/C prefix)"
        integer article_no
        integer article_sub
        text article_title
        text content
        dense_vector content_vector
    }
    TAX_WRITTEN_INQUIRY {
        keyword doc_id
        keyword inquiry_number "정규화된 문서번호"
        integer inquiry_year
        keyword inquiry_tax_category
        integer inquiry_sequence
        text question "질의"
        text answer "회신"
    }
    TAX_THREEWAY {
        keyword law_article_key
        keyword decree_article_key
        keyword rules_article_key
        text law_content
        text decree_content
        text rules_content
    }
    TAX_INDEX_COMMON ||--o| TAX_LAWS : "tax-laws 추가 필드"
    TAX_INDEX_COMMON ||--o| TAX_WRITTEN_INQUIRY : "tax-written-inquiry 추가 필드"
    TAX_INDEX_COMMON ||--o| TAX_THREEWAY : "tax-threeway는 법-시행령-시행규칙 묶음"

전체 14개 인덱스의 상세 필드는 각 indexer 파일의 매핑 정의 (packages/data-pipeline/src/semugpt_pipeline/indexers/*_indexer.py) 또는 백엔드 Index DTO (apps/backend/src/main/kotlin/me/uiti/taxgpt/application/service/data/dto/Semugpt*Index.kt)에서 확인.

설정

항목 위치 비고
dev ES 호스트 application-local.yml elastic.host localhost:9200 (docker-compose.dev.yml의 컨테이너)
prod ES 호스트 application-prod.yml elastic.host 3.39.210.101:9200 (Lightsail prod, Docker로 운영)
ES username yaml elastic.username "elastic" (dev/prod 동일)
ES password yaml elastic.password "uiti0701!" (dev/prod 동일 — Memory: feedback_accepted_repo_secrets)
인덱서 측 URL ES_URL env 또는 --es-url URL 임베드 시 !%21로 인코딩
Docker compose (dev) docker-compose.dev.yml ES 8.17 + analysis-nori, UID 1000, single-node discovery
Lightsail-only patch /opt/semugpt-2026/docker-compose.dev.yml (커밋 안 됨) ES_JAVA_OPTS=-Xms1g -Xmx1g (4GB box 적응)
reindex remote whitelist Lightsail-only patch reindex.remote.whitelist=172.17.0.1:9202

알려진 이슈 / 개선 예정

  • 매핑 변경 → 재인덱싱 수동create_index()가 멱등이지만 기존 인덱스를 무시. 새 필드 추가나 분석기 변경 시 DELETE /{index}bulk 재실행 필요. Reindex API 자동화 미구현.
  • 단일 노드 / 단일 shard 운영 — 고가용성 없음. ES Docker 컨테이너가 죽으면 RAG 검색 전면 중단. Lightsail 박스 디스크 풀 시 자동 복구 안 됨 (CLAUDE.md "디스크 정리" 런북 참조).
  • tax-relations는 사용 안 함 — 3단 비교는 tax-threeway로 통합됨. RelationIndexer는 유지되지만 backend가 조회하지 않음 — 정리 후보.
  • ES password 평문 노출application-{local,prod}.yml"uiti0701!"이 평문. PR #167 등 일부 정리됐지만 (Memory: prod readiness), 완전한 secret 외부화는 미완. accepted blocker.
  • 임베딩 모델 변경 시 양쪽 동시 변경 필요 — Python 인덱서(text-embedding-3-large) ↔ 백엔드 EmbeddingUtil(text-embedding-3-large)가 같은 모델이어야 검색 정합성 유지. 한쪽만 바꾸면 차원 미스매치 또는 의미 어긋남 — Issue로 alarm 필요.
  • 분석기 변경 회귀 위험nori_readingform이 한자 → 한글 변환을 수행. 이 필터가 빠지면 한자 검색이 깨지는데 운영 중 분석기 교체는 위 매핑 변경 이슈와 결합되어 다운타임 발생.

관련 문서

  • 인덱서 (Indexers) — ES 인덱스를 채우는 Python 모듈
  • 임베딩content_vector 생성 + 백엔드 query 임베딩 정합성
  • 리포트 생성 — RAG 검색 (StreamingRagProcessor, RRF, 카테고리 백필, 4-tier 정렬)
  • packages/data-pipeline/CLAUDE.md (리포 안 직접 참조) — Phase 2 매핑 템플릿