인덱서 (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.py의 SUPPORTED_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 create→semugpt-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 구조를 따른다:
parse_*_file(path) -> Generator[doc]— JSON 파싱 + 정규화create_index()—indices.exists확인 후 매핑 적용bulk_index(data_dir, batch_size, dry_run)— 파일 순회 + 배치 적재- (선택) 임베딩 —
_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)는 보존하여 재적재 비용 회피.- 임베딩 텍스트 truncation —
OpenAIEmbedder._truncate()가 8000 토큰 초과 시 자동 절단 (모델 max 8191). 절단된 부분은 검색에서 누락 — 긴 문서는 chunk 단위 분할 후 적재가 이상적이지만 미구현. tax-relations는 백엔드 미사용 — 3단 비교는tax-threeway인덱스를 통해 backendThreewayService가 직접 조회.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