Nori 형태소 분석기 Stop Filter 문제 — "안녕" 0건과 "안녕하세" 노이즈 해결
Lucene Nori 분석기에서 "안녕" 검색이 0건이 되는 IC 필터링 문제와, "안녕하세" 검색 시 "하세" 관련 문서만 나오는 형태소 분석 한계를 분석합니다. IC 제거 + title_ngram dis_max + PrefixQuery 폴백 3단계 해결을 적용하고, 자동완성 title_raw fallback까지 포함합니다.
검색 결과가 없습니다
제목, 태그, 카테고리로 검색
Lucene Nori 분석기에서 "안녕" 검색이 0건이 되는 IC 필터링 문제와, "안녕하세" 검색 시 "하세" 관련 문서만 나오는 형태소 분석 한계를 분석합니다. IC 제거 + title_ngram dis_max + PrefixQuery 폴백 3단계 해결을 적용하고, 자동완성 title_raw fallback까지 포함합니다.
나무위키+한국어 위키백과+영어 위키백과+뉴스+웹텍스트+C4 한국어 코퍼스 1,215만 건 검색 엔진 프로젝트를 2개월간 26편의 기술 블로그로 기록하고 총정리합니다. MySQL LIKE 5,000ms 타임아웃에서 시작하여 임베디드 Lucene + Nori 한국어 형태소 분석으로 전환하고, Caffeine+Redis 2계층 캐시(82% 히트율), MySQL Replication R/W 분리, Nginx 스케일아웃(에러율 13.25%→0%), Debezium+Kafka CDC, Redis 3노드 Consistent Hashing까지 분산 아키텍처를 완성합니다. 검색 품질은 동의어 확장, 오타 교정, UnifiedHighlighter snippet, LTR(NDCG +4.8%p), 카테고리 28개 자동 분류, Aho-Corasick 금칙어 필터링으로 고도화하고, RAG(Gemini SSE 스트리밍)로 AI 검색 요약을 제공합니다. 자동완성 시스템 설계(CQRS + MapReduce + CDC)의 이론과 실제 구현의 매핑, 26편 전체 시리즈 링크, 핵심 수치 총정리를 포함합니다.
Lucene BM25 검색 결과 Top-5 문서를 LLM 컨텍스트에 주입하는 RAG(Retrieval-Augmented Generation) 파이프라인을 구축합니다. Spring AI 2.0 + Gemini 2.0 Flash로 SSE 스트리밍 답변을 생성하고, 인라인 출처 배지를 파싱하여 게시글 링크로 연결합니다. 할루시네이션 방지(문서 기반 답변 제한 + 인용 강제), AI 요약 트리거 조건(네비게이션 의도 스킵), Redis Token Bucket rate limiting(10 RPM 전역), 동일 쿼리 캐싱(TTL 30분, LLM 비용 40-60% 절감), Grafana 7패널 대시보드(RPM, 응답시간, 토큰, 피드백, 비용 추정)까지 포함합니다. BM25가 이 프로젝트에서 Dense Retrieval보다 적합한 근거와, Hybrid Retrieval 전환 로드맵도 정리합니다.
커뮤니티 검색 서비스의 운영 안전장치를 구축합니다. 16,090개 금칙어를 Aho-Corasick O(N+Z) 알고리즘으로 탐지하여 자동완성 결과에서 유해 검색어를 필터링하고, 블라인드 게시글을 Lucene Occur.MUST_NOT으로 검색에서 제외합니다. 영어 금칙어의 Scunthorpe 문제(단어 경계 매칭), Negative Caching(빈 결과 30초 TTL)으로 cache penetration 방지, title_raw StringField로 자동완성 Lucene fallback 품질을 개선합니다.
BM25 수동 가중치(title:3, content:1)의 한계를 Learning to Rank(LTR)로 극복합니다. 카테고리 28개 자동 분류(키워드 기반, 정확도 83%) → SortedSetDocValuesFacetCounts 네이티브 Facet 전환 → 태그 216만 건 인덱싱을 1회 재색인으로 통합 반영합니다. LLM-as-a-Judge(Gemini)로 학습 데이터 900쌍을 생성하고(1차 실패 98% → 5초 딜레이+지수 백오프로 해결), XGBoost LambdaMART 14개 피처로 학습하여 NDCG@10을 0.6910 → 0.7387(+4.8%p) 개선합니다. XGBoost4J ARM64 네이티브 추론, Rescorer Top-200 재랭킹, RefreshListener 기반 FacetState 캐싱, MultiCollectorManager 단일 패스 수집까지 구현하지만, 2코어 ARM Free Tier에서 LTR ON 시 CPU 포화(72배 악화)를 k6로 실측하여 LTR_ENABLED=false로 비활성화합니다.
Lucene 기반 검색 엔진의 Recall과 Precision을 동시에 개선합니다. 동의어 확장(DB 기반 쿼리 타임)으로 "AI" 검색 시 "인공지능" 문서를 포함시키고, DirectSpellChecker로 "프로그래링" → "프로그래밍" 오타 교정을 구현합니다. UnifiedHighlighter + snippetSource 500자 StoredField로 검색어 주변 맥락 snippet을 제공하고, 무중단 전체 재색인 인프라(Directory Swap + SearcherManager 재생성)를 구축하여 12,156,589건(42GB)을 ~2시간 만에 재색인합니다. 인덱스 타임 동의어가 IDF를 왜곡하는 원리, Nori 사용자 사전 158,539개 적용, BM25 변형(BM25+/L/F) 불필요 판단 근거까지 정리합니다.
1,425만 건 Lucene 검색 엔진에 카테고리 필터링을 추가합니다. categoryId가 이미 LongField로 인덱싱되어 있지만 검색 쿼리(buildQuery)에서 사용하지 않고 있던 구조적 비대칭을 발견하고, Occur.FILTER 절로 해결합니다. DB Post-filter 방식이 pagination을 깨뜨리는 이유, FILTER가 MUST와 달리 스코어에 기여하지 않으면서 bitset 캐싱 대상이 되는 원리, DB GROUP BY 간이 Facet의 한계와 Lucene 네이티브 Facet 전환 계획까지 정리합니다.
단일 서버에서 100-150 VU가 한계였던 시스템을 분산 아키텍처(2 App + MySQL Replication + Redis 3샤드 + Kafka CDC)로 전환한 후, stress 테스트(200 VU, 25분)로 한계점을 재탐색합니다. 100 VU에서 P95 200ms(SLA 충족), 200 VU에서 에러율 0.09%(단일 서버 13.25% → 0.09%), 처리량 109 req/s(3.6배↑). App CPU가 여전히 근본 병목임을 소거법으로 확인하고, MySQL/Redis/Kafka/Nginx 모두 여유임을 실측합니다.
단일 Redis 인스턴스에서 KEYS 블로킹(34.6ms), 배치↔실시간 워크로드 간섭(GET 최악 15.5ms), volatile-lru 보안 위험을 실측하고, KEYS→SCAN 전환 + 3노드 Consistent Hashing + 블랙리스트 전용 인스턴스 격리로 해결합니다. 가상 노드 150개 ConcurrentSkipListMap 라우팅, 노드 장애 시 Lucene fallback, 100 VU 부하 테스트로 검증한 과정을 정리합니다.
PostService의 dual-write 구조(MySQL + Lucene 직접 호출)가 데이터 불일치, 강결합, 불완전한 캐시 무효화를 유발하는 문제를 점진적으로 해결합니다. Spring ApplicationEvent로 디커플링 → @ApplicationModuleListener 비동기 전환(쓰기 5,315ms→33ms) → Debezium + Kafka CDC로 binlog 기반 모든 변경 캡처까지. 100 VU 부하 테스트로 각 전환을 검증하고, dual-write를 원천 차단하여 검색 인덱스 정확성을 보장합니다.
GET 요청에 포함된 DB UPDATE가 R/W 분리와 충돌하여 500 에러가 발생한 문제를 Redis INCR + 30초 배치 flush로 해결합니다. REQUIRES_NEW, 비관적/낙관적 락, @Async, Caffeine 로컬 카운터 등 5개 대안을 비교 분석하고, Write-Behind 패턴으로 GET에서 DB 쓰기를 완전히 제거하여 에러율 11.10% → 0.00%, 상세 조회 응답시간 36% 개선을 달성합니다. Sentry·YouTube 등 현업 사례와 비용 분석, 면접 Q&A까지 포함합니다.
App CPU 100% 병목을 해소하기 위해 App 인스턴스를 2대로 확장합니다. Nginx map 기반 HTTP 메서드 라우팅(least_conn), Lucene Primary/Replica 모드 분리(SnapshotDeletionPolicy + Refresh Pause + rsync), TokenBlacklist Redis 전환, 조회수 Redis INCR 배치 flush 전환까지 — 100 VU 기준 에러율 13.25%→0.00%, P95 2,300ms→158ms, 평균 482ms→37ms로 개선합니다.
MySQL Replication으로 읽기/쓰기를 분리하고, Spring AbstractRoutingDataSource + LazyConnectionDataSourceProxy로 @Transactional(readOnly=true) 기반 자동 라우팅을 구현합니다. CLONE PLUGIN으로 133.5GB 초기 동기화, HikariCP 풀 분리(Primary 5 + Replica 15), k6 100 VU load 테스트로 R/W 분리 실측까지 정리합니다.
Caffeine(L1) + Redis(L2) 2계층 캐시를 구현하고, Trie 자동완성을 Redis flat KV O(1) GET으로 전환하여 Stateless 앱을 만든 뒤 k6 부하 테스트로 Before/After를 비교한 과정을 정리합니다. 분산 전환 순서(Redis → Replication → 스케일아웃)의 의존 관계와 비용 분석을 포함합니다.
k6 stress 테스트(200 VU, 25분)로 단일 서버(ARM 2코어, 12GB)의 한계점(~100-150 VU)을 수치로 확인하고, JVM/Tomcat 튜닝이 CPU-bound 병목에서 역효과를 낸 과정과 배포 미반영 사고를 기록합니다.
Lucene PrefixQuery의 사전순 자동완성을 검색 로그 기반 인메모리 Trie로 전환하여 인기순 정렬 + 한글 자모 중간 입력("삼ㅅ" → "삼성전자") + 초성 검색("ㅅㅅ" → "삼성전자")을 구현한 과정을 정리합니다.
Caffeine 로컬 캐시(L1)를 도입하여 검색/자동완성/상세 조회를 캐싱하고, @CacheEvict 무효화, Cache-Control 브라우저 캐싱, Actuator 모니터링까지 구현한 뒤 k6 부하 테스트로 Before/After를 비교한 과정을 정리합니다.
PhraseQuery(slop=2)로 구절 검색을 구현하고, FeatureField 기반 BM25 + 인기도 + 최신성 커뮤니티 랭킹을 적용한 뒤, P@10/MAP 지표로 검색 품질을 정량 평가한 과정을 정리합니다.
1,215만 건 테이블에서 COUNT(*) 제거(Page→Slice), 30페이지 제한, Deferred Join을 조합하여 최신 게시글 목록 조회를 19,424ms에서 8.33ms로 개선하고, k6 load 테스트(100 VU, 20분)에서 에러율 32.53%→0%를 달성한 과정을 정리합니다.
1,475만 건 OFFSET 페이지네이션에 Deferred Join을 적용하고, EXPLAIN으로 기대만큼 빠르지 않은 이유를 분석합니다. Slack·Twitter 등 실서비스의 Keyset Pagination 사례와 비교하며 다음 단계를 도출합니다.
MySQL FULLTEXT ngram의 구조적 한계(고빈도 토큰 타임아웃, 300GB+ 인덱스, false positive)를 분석하고, Lucene·Elasticsearch·벡터DB를 비용 관점에서 비교하여 임베디드 Lucene + Nori 형태소 분석기를 선택한 기술 결정 과정을 정리합니다.
B-Tree 인덱스의 한계를 넘어 FULLTEXT ngram 역색인으로 LIKE 검색을 대체하고, 57만 건 한국어 데이터에서 12초→6ms로 약 2,100배 성능을 개선한 과정과 ngram의 알려진 한계점을 정리한다.
자동완성 LIKE prefix 검색이 인덱스 없이 Full Table Scan으로 타임아웃되는 문제를 B-Tree 복합 인덱스(title, view_count DESC)로 해결하고, 단일 인덱스 대안과 커버링 인덱스, Trie 자료구조를 비교 검토한 과정을 정리한다.
LIKE 검색이 Full Table Scan으로 1,215만 행을 스캔하며 HikariCP 커넥션 풀을 고갈시켜 시스템을 마비시킨 원인을 분석하고, 긴급 완화 조치로 시스템 안정성을 확보한 과정을 정리한다.
나무위키, 위키피디아 덤프 데이터를 MySQL에 적재하고, 커뮤니티 수준의 트래픽을 감당할 수 있는 검색엔진을 만드는 프로젝트의 개요와 서버 구성을 정리한다.