콘텐츠로 이동

인덱서 (Indexers)

수집기가 저장한 JSONL/JSON을 읽어 Elasticsearch 인덱스에 bulk 적재하는 Python 모듈. 각 indexer는 source별 ES 매핑(필드 타입, Nori 분석기, content_vector dims=3072)을 정의하고, OpenAIEmbedder로 임베딩 벡터를 생성한 뒤 elasticsearch.helpers.bulk로 일괄 적재한다. 모두 create_es_client 공통 팩토리를 사용하여 ES 인증(URL 임베드 / env vars)을 일관되게 처리한다.

인덱서 카탈로그

Indexer 클래스 ES 인덱스 (INDEX_NAME) CLI 키 임베딩 데이터 디렉토리
LawIndexer tax-laws laws data/laws
RelationIndexer tax-relations relations ❌ (ID 매핑만) data/three_way
PrecedentIndexer tax-precedents precedents data/precedents
EnforcementIndexer tax-enforcement enforcement data/enforcement
GlossaryIndexer tax-glossary glossary data/glossary
CounselIndexer tax-counsel counsel data/counsel
OldAndNewIndexer tax-oldnew old-and-new data/old_and_new
ThreeWayIndexer tax-threeway threeway data/three_way
WrittenInquiryIndexer tax-written-inquiry written-inquiry data/written_inquiry
BasicRulesIndexer tax-basic-rules basic-rules data/basic_rules
TaxofficeIndexer tax-taxoffice taxoffice data/taxoffice
TribunalIndexer tax-tribunal tribunal data/tribunal
SupremeCourtIndexer tax-supreme-court supreme-court data/supreme_court
AccountingIndexer tax-accounting accounting data/accounting
ScourtIndexer tax-scourt scourt data/scourt

전체 등록은 cli/index.pySUPPORTED_INDEXES, indexer_map, index_config 세 곳에서 일치해야 한다. relations는 3단 비교의 ID 매핑 전용(content 없음)이라 임베딩 미지원. 백엔드가 검색에 사용하는 14개 핵심 인덱스는 tax-relations를 제외한 모두에 해당한다.

사용자 여정

인덱스 생성 + 벌크 적재

sequenceDiagram
    autonumber
    participant U as 운영자
    participant CLI as semugpt-index
    participant IDX as XxxIndexer
    participant EMB as OpenAIEmbedder
    participant CACHE as SQLite 임베딩 캐시
    participant ES as Elasticsearch

    U->>CLI: semugpt-index create --index laws --es-url ...
    CLI->>IDX: indexer.create_index()
    IDX->>ES: PUT /tax-laws (settings + mappings)
    ES-->>IDX: 200 created

    U->>CLI: semugpt-index bulk --index laws --embed --es-url ...
    CLI->>IDX: indexer.bulk_index(data_dir, batch_size=100)
    IDX->>IDX: parse_*_file() 제너레이터로 doc 생성
    loop 배치당 100건
        IDX->>CACHE: SHA256(text) 키로 캐시 hit 확인
        alt 캐시 hit
            CACHE-->>IDX: 기존 벡터 반환
        else 캐시 miss
            IDX->>EMB: get_embeddings_batch(miss_texts)
            EMB-->>IDX: 3072-dim vectors
            IDX->>CACHE: put_many(hash, vector)
        end
        IDX->>ES: bulk API (100건)
        ES-->>IDX: 적재 결과
    end
    IDX-->>U: 통계 (indexed, errors)

Dry-run / 부분 재인덱싱

sequenceDiagram
    participant U
    participant CLI

    U->>CLI: semugpt-index bulk --index laws --dry-run
    CLI->>CLI: 문서 생성만 수행 (ES 호출 없음, 임베딩도 없음)
    CLI-->>U: "Would index N documents"

    U->>CLI: semugpt-index bulk --index laws --batch-size 50
    Note over CLI: 배치 크기 조정 (메모리/속도 트레이드오프)

별도의 "재인덱싱" 명령은 없다. 매핑 변경 후 재구축이 필요하면 운영자가 curl -X DELETE /{index}semugpt-index createsemugpt-index bulk --embed 순으로 수동 진행. 임베딩 캐시 덕분에 2회차는 비용 추가 없음.

백엔드 구현

계층 클래스 / 파일 역할
ES client factory create_es_client (utils/elasticsearch.py) URL 임베드된 credential(http://user:pass@host:port) 우선, 그 다음 explicit args, 마지막으로 ES_USERNAME/ES_PASSWORD (또는 SEMUGPT_ES_USERNAME/SEMUGPT_ES_PASSWORD) env var
Indexer (공통 패턴) XxxIndexer (indexers/{source}_indexer.py) INDEX_NAME 상수, parse_*_file() 제너레이터, create_index() 매핑 정의, bulk_index() adopt elasticsearch.helpers.bulk
Embedder OpenAIEmbedder (embeddings/openai_embedder.py) 임베딩 생성 + SQLite 캐시. 자세한 동작은 임베딩 페이지
CLI cli/index.py create / bulk 명령, --embed 시 OpenAIEmbedder 주입
Relation 적재 RelationIndexer.bulk_index() (relation_indexer.py) 3단 비교 매핑 적재 (content 없음, ID만)

각 인덱서는 동일한 4-stage 구조를 따른다:

  1. parse_*_file(path) -> Generator[doc] — JSON 파싱 + 정규화
  2. create_index()indices.exists 확인 후 매핑 적용
  3. bulk_index(data_dir, batch_size, dry_run) — 파일 순회 + 배치 적재
  4. (선택) 임베딩 — _embedding_text 임시 필드를 batch로 묶어 OpenAI 호출 후 content_vector에 주입

도메인 규칙

규칙 위치 값 / 설명
임베딩 차원 indexer별 매핑 + EMBEDDING_DIMS (module-level in written_inquiry_indexer.py:23) 3072 (text-embedding-3-large) — 모든 인덱서 동일
유사도 함수 각 indexer의 content_vector 필드 similarity: "cosine" — 모든 인덱서 동일
Nori 분석기 각 indexer의 settings nori_tokenizer (decompound_mode=mixed) + lowercase + nori_readingform
단일 노드 셋업 각 indexer의 settings number_of_shards: 1, number_of_replicas: 0 — 운영 ES도 single-node
_all_text 필드 매핑에 copy_to: "_all_text" 지정 다중 필드를 검색용 단일 필드로 통합
기본 배치 크기 bulk_index(batch_size=100) CLI --batch-size로 override
임베딩 텍스트 조립 indexer별 parse_*_file() 보통 title + "\n" + content_embedding_text 임시 필드에 담아 bulk 전 batch 임베딩
Dry-run CLI --dry-run ES 호출도 임베딩도 스킵 — 문서 생성 결과만 미리보기
ES 인증 — URL 임베드 권장 create_es_client http://elastic:uiti0701%21@host:9200 형식. !%21로 URL-encode (shell 이슈 회피)
ES 인증 — env var ES_USERNAME / ES_PASSWORD 또는 SEMUGPT_ES_USERNAME / SEMUGPT_ES_PASSWORD URL에 credential 없으면 env에서 로드
기본 ES URL create_es_client ES_URL env var → 없으면 http://localhost:9200
_id 컨벤션 indexer별 상이 예: LawIndexer{law_name}_{article_key}, WrittenInquiryIndexer는 정규화된 inquiry_number

API 엔드포인트

해당 없음 — Indexer는 ES bulk API를 호출하는 클라이언트. CLI 명령은 CLI 레퍼런스 참조.

ES bulk endpoint (참고): POST /_bulk, source 별 인덱스명은 위 카탈로그 참조.

데이터 모델

erDiagram
    INPUT_JSONL {
        string id "수집기가 생성한 식별자"
        string title "제목 (인덱서가 정규화)"
        string content "본문"
        string source_url
        string crawled_at
    }
    INDEXER_DOC {
        string _id "ES 문서 ID (인덱서가 재조합)"
        string title
        string content
        string content_vector "OpenAI 3072-dim"
        string _all_text "모든 텍스트 필드 copy_to"
        string source
        string source_url
        string collected_at
        string _embedding_text "임시 필드, ES 적재 전 제거"
    }
    ES_INDEX {
        string index_name "예: tax-laws"
        string mappings "Nori + dense_vector(3072, cosine)"
        string settings "1 shard / 0 replica"
    }
    INPUT_JSONL ||--o| INDEXER_DOC : "parse_*_file() 변환"
    INDEXER_DOC }o--|| ES_INDEX : "helpers.bulk() 적재"

source별 추가 필드(예: law_id, article_key, law_type, tax_type_code, inquiry_year 등)는 각 인덱서의 매핑에서 keyword / integer 등으로 정의되어 backend 필터링에 사용된다.

설정

항목 위치 비고
ES URL CLI --es-url / env ES_URL dev: SSH tunnel + http://elastic:uiti0701%21@localhost:9200 (Lightsail box 내부). 과거 GCP VM 34.50.1.57:9200은 폐기됨. URL 형식 (CLAUDE.md "Agent Notes")
ES 인증 (env) ES_USERNAME, ES_PASSWORD, SEMUGPT_ES_USERNAME, SEMUGPT_ES_PASSWORD URL 임베드 우선, 그 다음 env
OpenAI API key env OPENAI_API_KEY 또는 CLI --api-key --embed 사용 시 필수
임베딩 모델 CLI --model 기본 text-embedding-3-large (3072 dims). 모델 변경 시 매핑의 dims도 함께 수정 필요
임베딩 캐시 위치 OpenAIEmbedder(cache_path="data/embedding_cache.json") 실제로는 .db 확장자로 SQLite 저장
배치 크기 CLI --batch-size 기본 100, 메모리 부족 시 50으로
Dry-run CLI --dry-run 문서 변환만 (ES + 임베딩 호출 없음)

알려진 이슈 / 개선 예정

  • 매핑 변경 시 수동 재인덱싱 필요create_index()는 이미 존재하면 skip만 한다. 매핑 마이그레이션 (예: 새 필드 추가, 분석기 변경) 명령이 없어서 curl -X DELETE 후 재생성 → 재적재 수동 절차. 운영 인덱스에 위험.
  • text-embedding-3-large 비용--embed 옵션은 OpenAI 호출 비용을 발생시킨다. CLI 출력에 경고 메시지 있지만, 캐시 miss인 경우 대량 호출 가능. 초기 적재 후 캐시(data/embedding_cache.db)는 보존하여 재적재 비용 회피.
  • 임베딩 텍스트 truncationOpenAIEmbedder._truncate()가 8000 토큰 초과 시 자동 절단 (모델 max 8191). 절단된 부분은 검색에서 누락 — 긴 문서는 chunk 단위 분할 후 적재가 이상적이지만 미구현.
  • tax-relations는 백엔드 미사용 — 3단 비교는 tax-threeway 인덱스를 통해 backend ThreewayService가 직접 조회. tax-relations(ID-only)는 RelationIndexer가 채우지만 실제 검색 라우팅에 등장하지 않음. 향후 정리 후보.
  • bulk 실패 시 부분 적재helpers.bulk가 실패해도 일부 문서는 이미 적재된 상태. 멱등성은 _id 기반이라 재실행은 안전하나, 실패 원인 진단을 위한 별도 dead-letter queue 없음.
  • dev ES password 노출 위험application-local.yml 및 indexer CLI 예제에 uiti0701! 평문 존재. dev 한정이라 영향 작지만 prod 자격증명은 별도로 관리 필요.

관련 문서

  • Elasticsearch — 14개 인덱스 매핑/분석기 상세
  • 임베딩OpenAIEmbedder 동작, SQLite 캐시, 모델 정합성
  • CLI 레퍼런스semugpt-index create / bulk 옵션 상세
  • packages/data-pipeline/CLAUDE.md (리포 안 직접 참조) — 새 인덱서 추가 시 백엔드 Repository까지 포함된 8단계
  • 리포트 생성 — 적재된 인덱스를 검색하는 StreamingRagProcessor