grep+read 대비 토큰 98% 절감, 색인 250ms, 질의 1.5ms. GPU도 API 키도 필요 없는 CPU 전용 하이브리드 검색 + MCP 서버.
grep+read 시대가 끝나가는 이유.
2024~2026년 사이 Claude Code, Cursor, Codex 같은 AI 코딩 에이전트가 폭발적으로 늘었다. 그런데 이 에이전트들이 "이 함수 어디 있어?"를 찾기 위해 보통 grep으로 전체 파일을 훑고 read_file로 통째로 읽어들인다. 이 방식은 토큰을 폭식하고 응답이 느리다.
웹 검색 초창기와 비슷한 상황이다. 단순 키워드 grep만으로는 한계가 명확하고, 그렇다고 트랜스포머 임베딩 검색을 도입하면 GPU·API 키·수십 초의 색인 시간이 따라온다. Semble은 두 세계의 좋은 점만 골라 CPU에서 1.5ms 만에 동작하는 하이브리드 검색기를 내놓는다.
model2vec.getUserById처럼 정확한 심볼명 검색에 강하다.1/(k+r)로 점수를 매겨 더하는 기법. 점수 스케일이 다른 검색기들(BM25 vs 코사인 유사도)도 공정하게 섞인다. IR(정보검색) 분야 표준.uvx --from "semble[mcp]" semble 한 줄로 Claude Code·Cursor 같은 어떤 에이전트에든 꽂힌다.한 문장으로 줄이면.
Semble은 단순 검색 라이브러리가 아니라 "에이전트를 위한 코드 검색 인프라"다. 정적 임베딩(Model2Vec) + BM25를 RRF로 합치고, 코드 인지 청킹(tree-sitter)과 정의 부스트·식별자 stem 매칭·노이즈 페널티 같은 도메인 특화 리랭킹 신호를 얹는다.
결과 — 색인 263ms, 질의 1.5ms, NDCG@10 0.854(트랜스포머의 99%). GPU 없이, API 키 없이, MIT 라이선스로.
시대적 배경 + 경쟁 우위.
Claude Code/Cursor가 일상화되면서 "에이전트당 월 토큰비"가 실제 비용으로 잡히기 시작했다. 한 번의 코드 탐색에 grep+read 패턴은 수만 토큰을 태우고, 응답도 느리다. Semble은 같은 질의를 토큰 98% 절감 + 응답 10배 빠르게 해결한다. ROI가 명확하니 채택이 빠르다.
또 하나 — MCP 표준이 무르익었다. claude mcp add semble -s user -- uvx --from "semble[mcp]" semble 한 줄로 Claude Code·Cursor에 즉시 붙는다. "그래서 어떻게 써?"라는 마찰이 사실상 0이라는 것이 트렌딩의 또 다른 이유.
| 비교 항목 | grep + read | 트랜스포머 임베딩 | Semble |
|---|---|---|---|
| 색인 시간 | 없음 (그때그때 훑음) | 57초 | 263ms |
| 질의 응답 p50 | 12ms (ripgrep) | 16ms | 1.5ms |
| NDCG@10 (정확도) | 0.126 | 0.862 | 0.854 (99%) |
| GPU 필요 | 없음 | 권장 | 없음 (CPU만) |
| API 키 | 없음 | 임베딩 API면 필요 | 없음 |
| 토큰 비용 | 매우 높음 | 낮음 | 매우 낮음 (~98%↓) |
Normalized Discounted Cumulative Gain @10 — 검색 시스템이 상위 10개 결과를 얼마나 잘 정렬했는지 평가하는 IR 표준 지표. 1.0이 만점, 0이 최악. Semble의 0.854는 "상위권 검색 품질"이라는 뜻. 트랜스포머 기반(0.862)에 거의 안 밀리면서 200배 빠르다.
코어 의존성 · 개발 도구 · 배포.
코드 인지 청킹은 semble이 자체 구현 (src/semble/chunking/)하며, tree-sitter를 직접 사용한다. 외부 청킹 라이브러리 의존성 없음.
py.typed 마커 포함 (사용자 IDE에서도 타입 인식)pip install semble 또는 uv add sembleuvx --from "semble[mcp]" semble 한 줄로 어떤 에이전트에든 즉시 연결potion-code-16M (~16MB) 받음, 이후 오프라인 동작시스템 구조도 → 핵심 패턴 하나하나.
┌──────────────────────────────────────┐
│ 사용자 / AI 에이전트 │
│ ("save model to disk" 라고 질의) │
└──────────────┬───────────────────────┘
│ MCP Tool: search(repo, query)
▼
┌──────────────────────────────────────────────┐
│ SembleIndex (코어) │
│ │
│ ┌──────────┐ ┌────────────────────┐ │
│ │ from_path│ ── │ TreeSitter 자체 청킹 │ │
│ │ from_git │ │ (src/semble/chunking)│ │
│ └──────────┘ └─────────┬──────────┘ │
│ │ chunks │
│ ▼ │
│ ┌───────────────────────────────┐ │
│ │ 병렬 색인 (한 번만 실행) │ │
│ │ │ │
│ │ [A] Model2Vec │ │
│ │ potion-code-16M │ │
│ │ → 시맨틱 벡터 │ │
│ │ │ │
│ │ [B] BM25 (bm25s) │ │
│ │ → 어휘 인덱스 │ │
│ └─────────────┬─────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 질의(query) 처리 ~1.5ms │ │
│ │ │ │
│ │ 1) 두 검색기 점수 → RRF 융합 │ │
│ │ 2) 코드 인지 리랭킹: │ │
│ │ · 적응형 가중치 (심볼 vs 자연어) │ │
│ │ · 정의 부스트 (def/class) │ │
│ │ · 식별자 stem 매칭 │ │
│ │ · 파일 일관성 부스트 │ │
│ │ · 노이즈 페널티 (test/, legacy/) │ │
│ └──────────────┬───────────────────────┘ │
└──────────────────┼──────────────────────────┘
│ Top-K 청크
▼
┌──────────────────────┐
│ 결과 (Chunk 객체) │
│ · file_path │
│ · start_line/end │
│ · content │
└──────────────────────┘
일반 임베딩 모델은 질의마다 트랜스포머의 forward pass를 돌린다 (수십~수백 ms). Model2Vec은 미리 계산된 단어 임베딩을 그저 lookup + pooling 하기 때문에 0.02ms 단위로 끝난다. "느린 트랜스포머의 99% 품질을 1/200 시간에" 라는 Semble의 슬로건이 가능한 이유.
트랜스포머 임베딩 = 매번 통역사를 부르는 것 (정확하지만 느리고 비싸다). 정적 임베딩 = 미리 만들어둔 단어장 사전을 펼치는 것 (즉시 찾을 수 있지만 의미 결합은 평균치). 단어 단위 평균으로도 코드 검색에는 충분하다는 것이 Model2Vec의 발견.
시맨틱(의미)만으로는 getUserById 같은 정확한 심볼명을 놓치고, 어휘(BM25)만으로는 "사용자 정보 가져오기" 같은 자연어 질의를 못 푼다. Semble은 두 점수 리스트를 RRF로 합쳐 양쪽 약점을 메운다.
# RRF 핵심 공식 (개념) score(doc) = sum_over_retrievers( 1 / (k + rank_r(doc)) ) # k=60이 일반적. rank가 작을수록(상위일수록) 점수가 큼. # 점수 스케일이 다른 검색기들도 공정하게 섞임.
질의가 Foo::bar, _private, getUserById처럼 심볼 모양이면 BM25 비중을 키우고, "데이터베이스 연결 풀 설정" 같은 자연어면 시맨틱 비중을 키운다. 질의를 보고 동적으로 가중치를 조절하는 self-tuning 패턴.
융합 점수만으로는 "정의가 들어있는 청크"가 항상 1등이 되지 않는다. Semble은 class/def/func 키워드가 들어 있고 질의 식별자와 매칭되는 청크를 부스트하고, test_ 디렉토리·legacy/·.d.ts는 깎는다. "개발자가 원하는 것은 보통 정의이지 사용 예시가 아니다"라는 휴리스틱이 코드에 박혀 있는 셈.
단순 줄 단위 분할이 아니라 tree-sitter로 AST를 파싱해 함수/클래스 경계를 살린 채로 청크를 나눈다. 외부 청킹 라이브러리에 의존하지 않고 src/semble/chunking/에 자체 구현되어 있다. 결과 청크가 "잘려 있지 않아" 에이전트가 그대로 컨텍스트에 넣어도 의미가 통한다.
트랜스포머 임베딩으로 코드를 검색하면 getUserById를 찾아달라는 질의에 "사용자 정보를 조회하는 함수" 같은 청크가 1등이 된다. 정작 정확한 함수명은 4~5등으로 밀린다. 어휘 매칭 없이는 코드 검색이 절대 정확해지지 않는다.
BM25(어휘) + 정적 임베딩(의미)을 RRF로 섞고, "정의 부스트 / 식별자 stem 매칭 / 테스트 페널티" 같은 코드 도메인 휴리스틱을 얹는다. Semble의 NDCG@10 0.854는 이 조합에서 나온다.
어디에 뭐가 있는지.
semble/ ├── src/semble/ # 패키지 소스 (src-layout) │ ├── __init__.py # public API (SembleIndex 등 export) │ ├── cache.py 4.7KB # 인덱스 캐싱 │ ├── cli.py 8KB # `semble` 명령어 진입점 │ ├── mcp.py 9.9KB # MCP 서버 (search / find_related 툴) │ ├── search.py 5KB # 핵심 검색 로직 (RRF + 리랭킹) │ ├── stats.py 8.5KB # 코퍼스 통계 (BM25 정규화 등) │ ├── tokens.py 1.5KB # 토크나이저 / stem │ ├── types.py 1.6KB # Chunk, SearchResult dataclass │ ├── utils.py 1.5KB # 경로 유틸 / git clone 등 │ ├── version.py # setuptools_scm 버전 attr │ ├── chunking/ # tree-sitter 기반 코드 인지 청킹 (자체 구현) │ ├── index/ # 색인 구축 (Model2Vec + BM25) │ ├── installer/ # 설치 관련 유틸 │ ├── ranking/ # 리랭킹 신호 (definition boost, stem, ...) │ └── agents/ # agents/*.md 패키지 데이터 (MCP 프롬프트) │ ├── benchmarks/ # 63 레포 · 1,250 질의 벤치마크 스크립트 ├── tests/ # pytest 단위 테스트 ├── assets/images/ # 로고, 벤치마크 그래프 ├── .github/workflows/ # GitHub Actions CI │ ├── pyproject.toml # 패키지 메타 + 의존성 + ruff/mypy 설정 ├── uv.lock # uv 잠금 파일 (재현 가능 빌드) ├── Makefile # lint, test, benchmark 명령 └── README.md # 사용법 + 벤치마크 표
대형 레포처럼 보여도 핵심은 search.py + index/ + ranking/ + mcp.py 네 곳. 코드 베이스가 작아 학습용으로 정말 적합하다. 한 주 안에 전체를 다 읽을 수 있는 규모.
기술 영역마다 무엇을 가져갈지.
실습: potion-code-16M을 huggingface에서 받아 임의 문자열을 임베딩, sentence-transformers와 latency/품질 비교
b, 포화 파라미터 k1의 의미실습: 작은 위키 덤프에 BM25 직접 구현, bm25s와 결과 비교. "BM25 단독" vs "BM25+의미검색 RRF"를 한국어 문서에서 체감
(function_definition name: (identifier) @name))실습: 본인 GitHub 레포의 모든 함수를 추출하는 30줄짜리 Python 스크립트 만들기
실습: mcp.py 흉내내서 "내 노션 일기를 검색해주는 MCP 서버" 만들고 Claude Code에 꽂아보기
src-layout (import 격리), setuptools_scm 자동 버전, py.typed 마커project.scripts로 CLI 등록, optional-dependencies([mcp]·[benchmark]·[dev])로 설치 옵션 분리실습: 본인 사이드 프로젝트를 같은 구조로 리팩토링하고 PyPI test 서버에 올려보기
실습: benchmarks/ 스크립트를 읽고 본인 사내 코드베이스에 대해 ripgrep vs Semble 직접 비교 그래프 만들기
CPU만 있으면 어디서든.
| 항목 | 최소 / 권장 | 비고 |
|---|---|---|
| OS | Linux / macOS / Windows | Python wheel로 셋 다 지원 |
| Python | 3.10 이상 | 3.12 권장 (성능·타입 hint 모두 좋음) |
| CPU | 일반 노트북 CPU | M1/M2 맥북, Intel i5급 이상이면 쾌적 |
| RAM | 수백 MB ~ 4GB | 대형 모노레포(10만+ 파일)는 1~4GB 권장 |
| GPU | 필요 없음 | 모든 연산이 CPU + numpy |
| 네트워크 | 최초 1회만 | potion-code-16M ~16MB 다운로드. 이후 오프라인 |
| 디스크 | 수십 MB | 모델 + 색인 (코드 베이스 크기에 비례) |
이론상 가능하다. CPU·메모리만 견디면 어디서든 동작한다. 셀프호스팅 NAS 위에 MCP 서버로 띄워두고 집의 모든 에이전트가 같은 코드 검색기를 공유하는 시나리오도 충분히 현실적이다.
PowerShell에서 uv 설치 후 터미널을 재시작해야 PATH가 인식된다. 가상환경 활성화는 .venv\Scripts\activate. 일부 환경에서 tree-sitter-language-pack이 wheel을 못 찾으면 Microsoft Visual C++ Build Tools가 필요할 수 있다.
난이도별 실습 — Easy → Hard.
1) pip install semble 또는 uv add semble 2) 아래 Python 코드를 실행해 Semble 소스로 Semble을 검색하는 메타 실험 3) 어떤 청크가 리랭킹의 정의를 담고 있는지 직접 확인
from semble import SembleIndex
idx = SembleIndex.from_git("https://github.com/MinishLab/semble")
for r in idx.search("how does reranking work", top_k=3):
print(r.chunk.file_path, r.chunk.start_line)
배우는 것: Semble의 public API, from_git의 동작, 청크 단위 결과 구조
1) 아래 명령으로 MCP 서버 등록 2) Claude Code 재시작 3) "semble로 react 레포의 useEffect 정의 찾아줘"라고 물어봄 4) 평소 grep 대비 응답 속도 체감
claude mcp add semble -s user -- uvx --from "semble[mcp]" semble
배우는 것: MCP 서버 등록 흐름, 에이전트가 도구를 선택하는 방식, 토큰 사용량 비교 감각
1) 본인 회사/사이드 레포 5개 선정 2) ripgrep + read 패턴으로 검색 질의 10개 수행 → 응답 시간/토큰 추정치 기록 3) 같은 질의를 Semble로 수행 → 비교 그래프 작성 4) 결과를 사내 위키나 블로그에 정리
배우는 것: NDCG@K·p50/p95 measurement, 토큰 비용 절감 정량화, 사내 도입 근거 만들기
1) ranking/의 stem 매칭 로직 분석 2) 한국어/영문 혼용 식별자(사용자_정보 vs userInfo)를 같은 stem으로 묶는 규칙 추가 3) 테스트 케이스 추가 후 PR 시도
배우는 것: 오픈소스 기여 흐름, 식별자 정규화 휴리스틱 설계, 다국어 검색의 어려움
현재 Semble 인덱스는 세션 동안만 메모리에 캐시된다. 1) 인덱스를 디스크에 직렬화/역직렬화 2) watchfiles(이미 의존성에 포함)로 변경된 파일만 재청킹·재임베딩 3) 긴 코드베이스에서 콜드 스타트 비용을 0으로
배우는 것: 검색 시스템 운영 엔지니어링, 증분 색인, 파일 워처 패턴
주차별로 따라가면 검색 시스템 한 사이클.
TF-IDF, BM25, 한글 토크나이저 → 한국어 뉴스 1만 건에 대한 BM25 검색기 직접 구현
sentence-transformers, FAISS, 코사인 유사도 → 같은 뉴스 코퍼스에 시맨틱 검색기 추가
BM25 + 시맨틱 + RRF 1차 통합 데모. NDCG@10 측정으로 단독 vs 융합 비교
Model2Vec 원리와 distillation → 본인 도메인용 정적 임베딩 미세조정 노트북 만들기
tree-sitter query DSL 학습 → 임의의 깃 레포를 함수 단위로 청킹하는 라이브러리 구현
Anthropic MCP SDK 깊이 파기 → "내 코드 검색 MCP 서버" 직접 작성, Claude Code 연결
NDCG, MRR, 콜드/웜 latency → 본인 검색기 vs Semble 비교 리포트 작성
Python 패키징, uv lock, PyPI 게시 → 본인 라이브러리를 uv add로 설치 가능하게 공개
자주 마주칠 단어들.
1/(k+r)로 점수. 점수 스케일이 달라도 안정적.src/semble/chunking/에 자체 구현한 청킹. tree-sitter로 AST를 파싱해 함수·클래스 경계 단위로 코드를 자름. 외부 청킹 라이브러리 의존성 없음.uv가 제공하는 일회성 실행 도구. uvx --from "pkg" cmd 형태로 격리된 가상환경 실행.src/ 하위에 두는 구조. 개발 중 실수로 로컬 디렉토리에서 import되는 것을 막음.__version__을 관리할 필요 없음.공식 · 핵심 라이브러리 · 표준.
uv add semble 후 Semble 소스로 Semble을 검색하는 메타 실험. 결과 청크의 file_path / start_line을 눈으로 확인.claude mcp add semble -s user -- uvx --from "semble[mcp]" semble 한 줄. 평소 grep 패턴 대비 응답 속도/토큰 사용량 비교.search.py + index/ + ranking/ + mcp.py만 읽으면 전체 구조가 잡힌다. RRF 융합 + 코드 인지 리랭킹 흐름 손으로 따라가기.watchfiles로 증분 갱신을 직접 구현. 검색 시스템 운영의 실전 감각을 얻는다.