## 배경
1차 Phase 2.2 eval에서 발견: 23개 쿼리가 순차 호출되지만 각 request의
background analyzer task는 모두 동시에 MLX에 요청 날림 → MLX single-inference
서버 queue 폭발 → 22개가 15초 timeout. cache 채워지지 않음.
## 수정
### query_analyzer.py
- LLM_CONCURRENCY = 1 상수 추가
- _LLM_SEMAPHORE: lazy init asyncio.Semaphore (event loop 바인딩)
- analyze() 내부: semaphore → timeout(실제 LLM 호출만) 이중 래핑
semaphore 대기 시간이 timeout에 포함되지 않도록 주의
### run_eval.py
- --analyze true|false 파라미터 추가 (Phase 2.1+ 측정용)
- call_search / evaluate 시그니처에 analyze 전달
## 기대 효과
- prewarm/background/동기 호출 모두 1개씩 순차 MLX 호출
- 23개 대기 시 최악 230초 소요, 단 모두 성공해서 cache 채움
- MLX 서버 부하 안정
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## 변경 사항
### app/services/search/retrieval_service.py
- **_QUERY_EMBED_CACHE**: 모듈 레벨 LRU (maxsize=500, TTL=24h)
- sha256(text|bge-m3) 키. fixed query 재호출 시 vector_ms 절반 감소.
- **_get_query_embedding(client, text)**: cache-first helper. 기존 search_vector()도 이를 사용하도록 교체.
- **search_vector_multilingual(session, normalized_queries, limit)**: 신규
- normalized_queries 각 언어별 embedding 병렬 생성 (cache hit 활용)
- 각 embedding에 대해 docs+chunks hybrid retrieval 병렬
- weight 기반 score 누적 merge (lang_weight 이미 1.0 정규화)
- match_reason에 "ml_ko+en" 등 언어 병합 표시
- 호출 조건 문서화 — cache hit + analyzer_tier=analyzed 시에만
### app/api/search.py
- use_multilingual 결정 로직:
- analyzer_cache_hit == True
- analyzer_tier == "analyzed" (confidence >= 0.85)
- normalized_queries >= 2 (다언어 버전 실제 존재)
- 위 3조건 모두 만족할 때만 search_vector_multilingual 호출
- 그 외 모든 경로 (cache miss, low conf, single lang)는 기존 search_vector 그대로 사용 (회귀 0 보장)
- notes에 `multilingual langs=[ko, en, ...]` 기록
## 기대 효과
- crosslingual_ko_en NDCG 0.53 → 0.65+ (Phase 2 목표)
- 기존 경로 완전 불변 → 회귀 0
- Phase 2.1 async 구조와 결합해 "cache hit일 때만 활성" 조건 준수
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
기본 logging.getLogger()는 WARNING 레벨이라 prewarm/analyze 진행 로그가
stdout/파일 어디에도 안 찍혔음. setup_logger("query_analyzer")로 교체하면
logs/query_analyzer.log + stdout 둘 다 INFO 레벨 출력.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1.2-G hybrid retrieval 측정 결과 Recall 0.66 정체 + 진단:
직접 nl 쿼리 시도 결과 일부 정답 doc(3854, 3981, 3982, 3920, 3921)이
top-100에도 못 들어옴. doc은 corpus + chunks + embedding 모두 정상.
진짜 원인: 자연어 query ↔ 법령 조항 의미 거리 + 짧은 본문 embedding signal 약함.
- query: '유해화학물질을 다루는 회사가 지켜야 할 안전 의무'
- 본문: '화학물질관리법 제4장 유해화학물질 영업자'
- bge-m3 입장: chunk text만으로는 같은 의미인지 못 알아봄
해결: chunks embedding 입력에 doc.title + section_title 포함.
- before: embed(c['text'])
- after: embed('[제목] {title}\n[섹션] {section}\n[본문] {text}')
기대 효과:
- 짧은 조항 문서 매칭 회복 (3920/3921 등 300자대)
- 자연어 query → 법령 조항 의미 매칭 개선
- Recall 0.66 → 0.72~0.78
영향: chunks embedding 차원/구조 변경 X — 입력 텍스트 prefix만 다름.
재인덱싱 1회로 모든 chunks 재생성 필요.
영어/외국 법령(ai_domain Foreign_Law 등)은 '제N조' 패턴이 없어 split 결과가
1개 element만 나옴 → 서문 chunk(첫 1500자)만 생성되고 본문 대부분 손실.
발견: doc 3759 (Industrial Safety, 93KB 영어) → 1개 chunk만 생성.
수정: parts split 결과가 1개 이하면 _chunk_sliding fallback 호출.
한국어 법령(제N조 패턴 있음)은 기존 분할 로직 그대로 작동.
Phase 1.2-D smoke test에서 발견. 재인덱싱 전 fix 필수.
Phase 1.2-B 평가셋 결과 recall 0.788 → 0.750 회귀.
원인: trigram default threshold 0.3이 multi-token 쿼리에서 너무 엄격.
예: '이란 미국 전쟁 글로벌 반응' 같은 5단어 한국어 뉴스 쿼리는
title/ai_summary trigram 매칭이 거의 안 됨.
해결: search_text 시작 시 set_limit(0.15) 호출.
- trigram 매칭 더 관대 (recall ↑)
- precision은 ORDER BY similarity 가중 합산이 보정
- p95 latency 169ms 여유 충분 (목표 500ms)
SQLAlchemy text() + asyncpg dialect에서 trigram operator 위치의 %%는
unescape 안 되어 'text %% unknown' 에러 발생. 단일 %로 변경.
ILIKE의 string literal 안의 %%는 PostgreSQL에서 두 wildcard로 동작했으나,
operator 위치는 escape 처리 경로가 다름.
logging.getLogger("search")만 사용하면 uvicorn 기본 설정에서 INFO가
stdout에 안 나옴. 기존 core.utils.setup_logger 패턴 사용:
- logs/search.log 파일 핸들러
- stdout 콘솔 핸들러
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vector-only 매치(match_reason == 'vector')에서 raw 코사인 0.43이
0.6으로 잘못 amplify되어 low_confidence threshold(0.5)를 못 넘기던 문제.
- vector-only 분기: amplify 제거, _cosine_to_confidence로 일관 환산
- _cosine_to_confidence: bge-m3 코사인 분포 (무관 텍스트 ~0.4) 반영
- 코사인 0.55 = threshold 경계(0.50), 0.45 미만은 명확히 low
smoke test 결과 zzzqxywvkpqxnj1234 같은 무의미 쿼리(top cosine 0.43)가
low_confidence로 잡히지 않던 문제 해결.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- PAPER_NAMES 매핑으로 'Le Monde', 'Der Spiegel' 등 정확 분리
- NewsSourceResponse datetime 타입 수정
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- summarize_worker: 요약만 생성 (분류 안 함)
- queue_consumer: summarize stage 추가 (batch 3)
- news_collector: summarize + embed 큐 등록
- process_stage enum에 'summarize' 추가
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- /news 전용 페이지: 신문사 필터, 읽지않음 필터, 시간순 리스트, 미리보기
- 뉴스 분류 격리: ai_domain='News', classify 제거, embed만 등록
- is_read: 클릭 시 자동 읽음, 전체 읽음 API
- documents 목록에서 뉴스 제외 (source_channel != 'news')
- nav에 뉴스 링크 추가
- GET /api/news/articles, POST /api/news/mark-all-read
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- config.yaml: embedding model → bge-m3
- document.py: Vector(768) → Vector(1024)
- embed_worker.py: 모델 버전 업데이트
- migration 011: 벡터 컬럼 재생성 (기존 임베딩 초기화)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
국가법령 XML은 <편>/<장> 태그가 아닌 <조문단위 조문키="xxxx000">에
"제X장 ..." 형태로 장 구분자가 포함됨. 이를 파싱하여 분할.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 모니터링 법령 12개 → 26개 (산업안전/건설/위험물/소방/전기/가스/근로/환경)
- lawSearch.do로 검색, lawService.do로 본문 조회
- 대형 법령 편/장 단위 분할 저장 (fallback: 편→장→전체)
- 저장 경로: PKM/Inbox/ (AI 자동 분류 연계)
- 변경 감지 시 user_note에 이력 자동 기록
- CalDAV + SMTP 알림
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ILIKE '%' → '%%' (SQLAlchemy text() 파라미터 충돌 해결)
- e.isComposing 체크로 한글 조합 완료 전 Enter 무시
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- tree API: domain 경로를 파싱하여 계층 구조로 반환
(Industrial_Safety → Practice → Patrol_Inspection)
- Sidebar: 재귀 snippet으로 N단계 트리 렌더링
- domain 필터: prefix 매칭 (상위 클릭 시 하위 전부 포함)
- 사이드바 너비: 260px → 320px
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- config.yaml: 6개 domain × 3단계 taxonomy + 13개 document_types 정의
- classify.txt: 영문 프롬프트, taxonomy 경로 기반 분류 + 분류 규칙 주입
- classify_worker: taxonomy 검증, confidence 기반 분류, document_type 저장
- migration 008: document_type, importance, ai_confidence 컬럼
- API: DocumentResponse에 document_type, importance, ai_confidence 추가
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- original_path/format/hash + conversion_status 필드 추가 (migration 007)
- extract_worker: 텍스트 추출 후 xlsx→ods, docx→odt 등 ODF 변환
- 변환본은 .derived/{doc_id}.ods 에 저장
- 원본 메타 보존 (original_path/format/hash)
- file_watcher: .derived/ .preview/ 디렉토리 제외
- DocumentViewer: ODF 포맷이면 편집 버튼 자동 표시
- edit_url 있으면 "편집", 없으면 "Synology Drive에서 열기"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>