Skip to content

refactor: separate chatbot query index loading - #129

Merged
Yu-JeSeung merged 3 commits into
mainfrom
refactor/chatbot-search-index-loading
Jun 11, 2026
Merged

refactor: separate chatbot query index loading#129
Yu-JeSeung merged 3 commits into
mainfrom
refactor/chatbot-search-index-loading

Conversation

@Yu-JeSeung

@Yu-JeSeung Yu-JeSeung commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🎯 배경

  • 검색 인덱스 로딩/초기화 코드가 query_index.py 안에 함께 있어 검색 로직과 런타임 리소스 준비 책임이 섞여 있었습니다.
  • RAG 검색 로직은 유지하면서 환경변수 경로 해석과 아티팩트 로딩 책임을 분리해 유지보수성을 높입니다.

🔍 주요 내용

  • LLM/sub_model/query_index_loader.py를 추가해 검색 인덱스 경로 해석, 키워드 로딩, 임베딩 정규화, BM25 로딩/fallback, 임베딩 모델 초기화를 담당하도록 분리했습니다.
  • query_index.py는 기존 전역 변수와 hybrid_search(), build_answer() 인터페이스를 유지하면서 loader에서 준비된 리소스를 사용하도록 변경했습니다.
  • 구조 변경에 맞춰 docs/PLANS.md의 검색 및 인덱싱 설명을 갱신했습니다.

변경 요약(1~3줄)

검색 인덱스 로딩/초기화 책임을 LLM/sub_model/query_index_loader.py로 분리해 모듈화와 유지보수성을 개선했습니다. hybrid_search()와 build_answer() 인터페이스는 그대로 두고, 로더가 경로 해석·임베딩·BM25·모델 초기화를 담당합니다.

주요 변경점

  • query_index_loader.py 신규 추가: QueryIndexPaths / QueryIndexResources 데이터클래스와 env_path, load_embeddings, load_bm25, load_query_index_resources 등 로딩 로직 통합
  • query_index.py 리팩토링: 경로/로딩 유틸 및 임베딩 정규화 로직 제거, load_query_index_resources() 결과를 전역 변수로 할당하도록 변경
  • BM25 폴백 흐름 명확화: 토큰 코퍼스 유무에 따라 gz 토큰 로드 또는 search_df 기반 토크나이즈로 BM25 생성
  • 임베딩 로드 시 0-노름 보정 및 행별 L2 정규화 적용
  • SentenceTransformer 모델 초기화는 로더에서 수행 (기본 모델: intfloat/multilingual-e5-base)
  • docs/PLANS.md 업데이트: 검색·인덱싱 구조와 loader 역할 반영
  • 보안 패치: pickle 역직렬화 취약점 관련 수정 커밋 포함

주의/리스크

  • 모듈 임포트 시 리소스(임베딩, BM25, 모델 등)가 로드되므로 환경변수와 파일 경로가 정확해야 함
  • BM25가 pickle 대신 토크나이즈/코퍼스 재생성 경로로 대체되면서 검색 결과·성능이 달라질 수 있음
  • SentenceTransformer 초기화로 모듈 로드 시간이 길어질 수 있음

다음 액션

  • 배포 전 환경변수 및 아티팩트 경로 검증
  • BM25 폴백 경로와 기존 방식 간 검색 결과/성능 비교 테스트
  • hybrid_search/build_answer 통합 동작 및 로드 시간 영향 검증

@Yu-JeSeung Yu-JeSeung self-assigned this Jun 9, 2026
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 62d6bbf5-b013-4d26-90b3-2592a9dd8039

📥 Commits

Reviewing files that changed from the base of the PR and between 3010cf0 and b5a3a74.

📒 Files selected for processing (1)
  • LLM/sub_model/query_index_loader.py
💤 Files with no reviewable changes (1)
  • LLM/sub_model/query_index_loader.py

📝 Walkthrough

Walkthrough

Query Index 리소스 로딩 로직을 dedicated loader 모듈로 분리했습니다. 새로운 query_index_loader.py는 환경변수 해석, 경로 구성, 임베딩/BM25 로딩, 모델 초기화를 통합하며, 기존 query_index.py는 이제 loader를 호출하여 초기화합니다.

Changes

Query Index Resource Loader 추출 및 리팩토링

Layer / File(s) Summary
Loader 데이터 계약 및 초기 설정
LLM/sub_model/query_index_loader.py
QueryIndexPathsQueryIndexResources 데이터 클래스를 정의하고, 모듈 임포트·프로젝트 루트 경로·기본 모델 이름 상수를 선언합니다.
경로 및 파일 로딩 유틸리티
LLM/sub_model/query_index_loader.py
env_path()로 환경변수 경로를 절대화하고 load_list_from_txt()로 텍스트 목록을 읽습니다. load_query_index_paths()로 여러 경로를 QueryIndexPaths로 조립합니다.
임베딩 및 BM25 로딩
LLM/sub_model/query_index_loader.py
load_embeddings().npy를 float32로 로드해 행별 L2 정규화를 수행합니다. load_bm25()tok_path 존재 여부에 따라 gz 토큰 코퍼스를 로드하거나 런타임 토크나이징으로 BM25Okapi를 생성합니다.
통합 리소스 로더
LLM/sub_model/query_index_loader.py
load_query_index_resources()가 parquet search_df를 읽고 스키마를 정규화한 뒤 토크나이저·연락처 키워드·임베딩·BM25를 로드하고 SentenceTransformer를 초기화하여 QueryIndexResources를 반환합니다.
query_index.py 로더 적용
LLM/sub_model/query_index.py
임포트를 단순화하고 load_query_index_resources를 임포트한 뒤, 기존의 환경변수 기반 경로 처리 및 모델·임베딩·BM25 초기화 로직을 _index_resources 호출 결과의 필드 할당으로 교체했습니다.
문서 업데이트
docs/PLANS.md
LLM/sub_model/ 구성에 query_index_loader.py를 추가하고 검색 인덱싱 섹션에 로더의 책임을 명시했습니다.

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive 설명에서 배경과 주요 내용을 포함하고 있으나 템플릿에서 필수인 '관련 이슈' 섹션이 누락되어 있습니다. PR 설명에 '## 관련 이슈' 섹션을 추가하여 이슈 번호(Closes #XXX)를 명시해 주시기 바랍니다.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 PR의 주요 변경사항을 명확하게 요약하고 있으며, 검색 인덱스 로딩 로직 분리라는 핵심 목표를 정확하게 반영합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/chatbot-search-index-loading

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
LLM/sub_model/query_index.py (1)

445-463: 🏗️ Heavy lift

리소스 로딩 트리거도 import 시점에서 분리하는 편이 낫습니다.

지금 구조도 query_index.py를 import하는 순간 parquet/embedding/BM25/model이 전부 로드됩니다. 이번 PR 목적이 로딩 책임 분리라면, load_query_index_resources() 호출 자체는 lifecycle 같은 startup 경로나 initialize_query_index()로 옮겨서 import 비용과 초기화 타이밍을 분리하는 쪽이 더 일관됩니다.

예시 방향
-_index_resources = load_query_index_resources()
-DATA_JSON        = _index_resources.paths.data_json
-ART_DIR          = _index_resources.paths.art_dir
-SEARCH_DF_PATH   = _index_resources.paths.search_df_path
-EMB_PATH         = _index_resources.paths.emb_path
-BM25_PATH        = _index_resources.paths.bm25_path
-TOK_PATH         = _index_resources.paths.tok_path
-CONTACTS_CSV     = _index_resources.paths.contacts_csv
-META_PATH        = _index_resources.paths.meta_path
-CONTACT_KWS      = _index_resources.contact_kws
-embeddings       = _index_resources.embeddings
-
-model_name   = _index_resources.model_name
-model        = _index_resources.model
-search_df    = _index_resources.search_df
-tokenize_kor = _index_resources.tokenizer
-bm25         = _index_resources.bm25
+_index_resources = None
+
+def initialize_query_index(resources=None):
+    global _index_resources, DATA_JSON, ART_DIR, SEARCH_DF_PATH, EMB_PATH, BM25_PATH
+    global TOK_PATH, CONTACTS_CSV, META_PATH, CONTACT_KWS, embeddings
+    global model_name, model, search_df, tokenize_kor, bm25
+
+    _index_resources = resources or load_query_index_resources()
+    DATA_JSON = _index_resources.paths.data_json
+    ART_DIR = _index_resources.paths.art_dir
+    SEARCH_DF_PATH = _index_resources.paths.search_df_path
+    EMB_PATH = _index_resources.paths.emb_path
+    BM25_PATH = _index_resources.paths.bm25_path
+    TOK_PATH = _index_resources.paths.tok_path
+    CONTACTS_CSV = _index_resources.paths.contacts_csv
+    META_PATH = _index_resources.paths.meta_path
+    CONTACT_KWS = _index_resources.contact_kws
+    embeddings = _index_resources.embeddings
+    model_name = _index_resources.model_name
+    model = _index_resources.model
+    search_df = _index_resources.search_df
+    tokenize_kor = _index_resources.tokenizer
+    bm25 = _index_resources.bm25
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@LLM/sub_model/query_index.py` around lines 445 - 463, Currently
load_query_index_resources() is called at import time causing heavy startup
costs; refactor by removing the top-level call and instead add an
initialize_query_index() function that calls load_query_index_resources() and
assigns its result to a module-level _index_resources (or returns it) and
populates DATA_JSON, ART_DIR, SEARCH_DF_PATH, EMB_PATH, BM25_PATH, TOK_PATH,
CONTACTS_CSV, META_PATH, CONTACT_KWS, embeddings, model_name, model, search_df,
tokenize_kor, bm25, and UNIT_TOK_RE as needed; change the existing module-level
assignments that reference _index_resources to either lazy getters or to None
until initialize_query_index() runs, and update any consumers to call
initialize_query_index() during application startup/lifecycle to ensure
resources are loaded only at initialization time and that functions using these
symbols verify initialization and raise a clear error if called before
initialization.
docs/PLANS.md (1)

92-93: ⚡ Quick win

검색 및 인덱싱 핵심 파일 목록에도 loader를 같이 올려 주세요.

현재 스냅샷과 설명 문단에는 query_index_loader.py가 반영됐는데, 바로 위 핵심 파일 목록에는 아직 빠져 있어서 문서 안에서 구조 설명이 살짝 엇갈립니다.

문서 정리 예시
 ### 검색 및 인덱싱

 - `LLM/sub_model/query_index.py`
+- `LLM/sub_model/query_index_loader.py`
 - `LLM/sub_model/schedule_index.py`
 - `LLM/sub_model/index_utils.py`
 - `LLM/sub_model/build_index.py`

Also applies to: 181-182

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/PLANS.md` around lines 92 - 93, The core file list in the "검색 및 인덱싱"
section is missing the loader entry; add "query_index_loader.py   # 검색 인덱스 환경 경로
해석 및 아티팩트 로딩" to the 핵심 파일 목록 alongside "schedule_index.py" and likewise add the
same loader entry where the same list appears again (the other occurrence
mentioned around lines 181-182) so the snapshot/description and the core-file
list stay consistent with the documented files.
LLM/sub_model/query_index_loader.py (2)

116-116: ⚡ Quick win

SentenceTransformer 모델 다운로드 동작을 문서화하세요.

SentenceTransformer(model_name)은 모델이 캐시되지 않은 경우 자동으로 다운로드합니다. 첫 실행 시 네트워크 오류나 긴 대기 시간이 발생할 수 있습니다.

사용자 경험 향상을 위해:

  1. 모델 다운로드 동작을 문서화하거나
  2. 다운로드 진행 상황 로깅을 추가하거나
  3. 네트워크 오류에 대한 명시적 오류 처리를 추가하는 것을 권장합니다.
📝 로깅 추가 제안
+import logging
+
+logger = logging.getLogger(__name__)
+
 def load_query_index_resources(
     paths: QueryIndexPaths | None = None,
     model_name: str = DEFAULT_MODEL_NAME,
 ) -> QueryIndexResources:
     paths = paths or load_query_index_paths()
     search_df = normalize_search_df_schema(pd.read_parquet(paths.search_df_path))
     tokenizer = get_tokenizer()
 
+    logger.info(f"Loading SentenceTransformer model: {model_name}")
+    try:
+        model = SentenceTransformer(model_name)
+    except Exception as e:
+        logger.error(f"Failed to load model {model_name}: {e}")
+        raise
+
     return QueryIndexResources(
         paths=paths,
         contact_kws=load_list_from_txt(paths.contact_kws_path),
         embeddings=load_embeddings(paths.emb_path),
         search_df=search_df,
         tokenizer=tokenizer,
         bm25=load_bm25(paths.bm25_path, paths.tok_path, search_df, tokenizer),
         model_name=model_name,
-        model=SentenceTransformer(model_name),
+        model=model,
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@LLM/sub_model/query_index_loader.py` at line 116, The call to
SentenceTransformer(model_name) will auto-download models on first run; update
the code around where model=SentenceTransformer(model_name) is created (the
model initialization block) to (1) document this behavior in a comment or
docstring, and (2) wrap the constructor in a try/except that logs download
start/completion and any network errors (use your processLogger or similar) and
either retry or surface a clear, actionable error; ensure logs include
model_name and exception details so users know a long download or network
failure occurred.

48-56: ⚡ Quick win

경로 존재 여부 검증 추가를 고려하세요.

env_path 함수가 환경 변수를 절대 경로로 변환하지만, 파일이나 디렉토리가 실제로 존재하는지 검증하지 않습니다. 모든 경로가 읽기 전용 아티팩트를 가리키므로, 존재하지 않는 경로는 나중에 load_embeddings, pd.read_parquet 등에서 불명확한 오류를 발생시킬 수 있습니다.

경로 존재 여부를 검증하면 더 명확한 오류 메시지로 빠르게 실패할 수 있습니다.

♻️ 경로 존재 검증 추가 제안
 def env_path(name: str) -> Path:
     val = os.getenv(name, "").strip()
     if not val:
         raise FileNotFoundError(f"{name} environment empty.")
     s = os.path.expandvars(val)
     p = Path(s)
     if not p.is_absolute():
         p = ROOT_DIR / p
-    return p.resolve()
+    p = p.resolve()
+    if not p.exists():
+        raise FileNotFoundError(f"{name}={p} does not exist.")
+    return p
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@LLM/sub_model/query_index_loader.py` around lines 48 - 56, The env_path
function should validate that the resolved path actually exists to fail early
with a clear error; after computing p = Path(...).resolve() (in env_path) add a
check using p.exists() and raise FileNotFoundError containing both the
environment variable name and the resolved path when it doesn't exist
(optionally also use p.is_file()/p.is_dir() if callers expect a specific type),
so callers like load_embeddings or pd.read_parquet get an explicit, actionable
error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@LLM/sub_model/query_index_loader.py`:
- Around line 86-88: The code currently uses pickle.load on BM25_PATH (inside
the path.exists() branch) which is unsafe for untrusted files; replace this by
either (a) removing pickle-based deserialization and always rebuilding the BM25
index from the tokenized corpus/search DataFrame (call the BM25 construction
routine you have where the index is built from the corpus/tokenizer), or (b) if
you must load a prebuilt artifact, add integrity verification (e.g., compare a
stored checksum/signature before loading) and switch to a safer serialization
strategy (store reconstructable source data like JSON/csv or a reproducible
tokenized corpus rather than raw pickle). Update the code that currently calls
pickle.load(path) and any code paths referencing BM25_PATH to implement the
rebuild-or-verify-and-load flow.

---

Nitpick comments:
In `@docs/PLANS.md`:
- Around line 92-93: The core file list in the "검색 및 인덱싱" section is missing the
loader entry; add "query_index_loader.py   # 검색 인덱스 환경 경로 해석 및 아티팩트 로딩" to the
핵심 파일 목록 alongside "schedule_index.py" and likewise add the same loader entry
where the same list appears again (the other occurrence mentioned around lines
181-182) so the snapshot/description and the core-file list stay consistent with
the documented files.

In `@LLM/sub_model/query_index_loader.py`:
- Line 116: The call to SentenceTransformer(model_name) will auto-download
models on first run; update the code around where
model=SentenceTransformer(model_name) is created (the model initialization
block) to (1) document this behavior in a comment or docstring, and (2) wrap the
constructor in a try/except that logs download start/completion and any network
errors (use your processLogger or similar) and either retry or surface a clear,
actionable error; ensure logs include model_name and exception details so users
know a long download or network failure occurred.
- Around line 48-56: The env_path function should validate that the resolved
path actually exists to fail early with a clear error; after computing p =
Path(...).resolve() (in env_path) add a check using p.exists() and raise
FileNotFoundError containing both the environment variable name and the resolved
path when it doesn't exist (optionally also use p.is_file()/p.is_dir() if
callers expect a specific type), so callers like load_embeddings or
pd.read_parquet get an explicit, actionable error.

In `@LLM/sub_model/query_index.py`:
- Around line 445-463: Currently load_query_index_resources() is called at
import time causing heavy startup costs; refactor by removing the top-level call
and instead add an initialize_query_index() function that calls
load_query_index_resources() and assigns its result to a module-level
_index_resources (or returns it) and populates DATA_JSON, ART_DIR,
SEARCH_DF_PATH, EMB_PATH, BM25_PATH, TOK_PATH, CONTACTS_CSV, META_PATH,
CONTACT_KWS, embeddings, model_name, model, search_df, tokenize_kor, bm25, and
UNIT_TOK_RE as needed; change the existing module-level assignments that
reference _index_resources to either lazy getters or to None until
initialize_query_index() runs, and update any consumers to call
initialize_query_index() during application startup/lifecycle to ensure
resources are loaded only at initialization time and that functions using these
symbols verify initialization and raise a clear error if called before
initialization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 28a75191-dcbf-427c-8a22-69cc9c7df88e

📥 Commits

Reviewing files that changed from the base of the PR and between 9d23ece and 152f1ee.

📒 Files selected for processing (3)
  • LLM/sub_model/query_index.py
  • LLM/sub_model/query_index_loader.py
  • docs/PLANS.md

Comment thread LLM/sub_model/query_index_loader.py Outdated
@Yu-JeSeung Yu-JeSeung added the fix label Jun 10, 2026
@Yu-JeSeung
Yu-JeSeung merged commit a700baa into main Jun 11, 2026
3 checks passed
@Yu-JeSeung
Yu-JeSeung deleted the refactor/chatbot-search-index-loading branch June 11, 2026 03:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant