amdb의 제약 조건은 처음부터 하나였다. 코드가 머신 밖으로 나가면 안 된다. 보안 민감 환경에서는 클라우드 인덱싱 자체가 금지다. 그런데 "인증 처리하는 부분"같은 자연어 질의로 코드를 찾으려면 의미 검색이 필요하고, 의미 검색에는 임베딩이 필요하다. 임베딩 API를 호출하는 순간 제약이 깨진다. 답은 임베딩도 로컬에서 만드는 것이다.
fastembed: ONNX로 도는 로컬 임베딩
fastembed는 ONNX Runtime 위에서 임베딩 모델을 돌리는 라이브러리다. Rust에서 쓰면 Python 런타임도, GPU도, API 키도 필요 없다.
use fastembed::{EmbeddingModel, InitOptions, TextEmbedding};
fn build_embedder() -> anyhow::Result<TextEmbedding> {
let model = TextEmbedding::try_new(
InitOptions::new(EmbeddingModel::AllMiniLML6V2),
)?;
Ok(model)
}
fn embed_symbols(model: &mut TextEmbedding, docs: Vec<String>) -> anyhow::Result<Vec<Vec<f32>>> {
let embeddings = model.embed(docs, None)?;
Ok(embeddings)
}모델 파일은 첫 실행에 한 번 내려받고 이후엔 완전 오프라인으로 동작한다. 임베딩 대상은 파일 원문이 아니라 심볼 단위 텍스트다. 함수 이름, 시그니처, 위치 정보를 한 문서로 묶어 임베딩하면 검색 결과가 곧바로 심볼을 가리킨다.
벡터를 어디에 저장하나
전용 벡터 DB를 붙이고 싶은 유혹이 있지만, 심볼 수만 개 규모에서는 과하다. amdb는 심볼과 관계를 이미 SQLite에 저장하므로 임베딩도 같은 곳에 BLOB으로 넣는다.
검색은 브루트포스 코사인 유사도 스캔이다.
fn cosine(a: &[f32], b: &[f32]) -> f32 {
let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
dot / (norm_a * norm_b)
}수만 개 벡터의 브루트포스 스캔은 밀리초 단위로 끝난다. HNSW 같은 근사 인덱스는 수백만 벡터부터 의미가 있다. 규모를 재기 전에 인프라부터 늘리는 것은 로컬 도구에서 특히 나쁜 습관이다.
하이브리드: exact match를 앞에 세워라
벡터 검색만 쓰면 치명적인 역설이 생긴다. 사용자가 authenticate라고 정확한 함수 이름을 입력했는데, 의미상 비슷한 login_handler가 더 높은 점수로 올라오는 것이다. 정확히 아는 것을 물었는데 비슷한 것을 돌려주면 도구에 대한 신뢰가 무너진다.
그래서 amdb의 focus 모드는 2단계다.
- exact match — 질의를 심볼 이름 인덱스에서 먼저 찾는다. SQLite 인덱스 조회라 사실상 공짜다.
- vector search — 이름 매칭이 없을 때만 임베딩 유사도로 넘어간다. "login authentication jwt" 같은 자연어 질의가 여기서 잡힌다.
amdb generate --focus "authenticate"
amdb generate --focus "login authentication jwt"첫 번째 질의는 exact match에서 끝나고, 두 번째는 벡터 검색으로 흘러간다. 식별자를 아는 사용자에게는 결정론을, 개념만 아는 사용자에게는 의미 검색을 준다. 검색 시스템 설계에서 배운 가장 중요한 교훈이다: 저렴하고 정확한 경로를 항상 비싸고 유연한 경로 앞에 배치하라.
정리
- 로컬 우선 제약에서는 임베딩 생성부터 로컬이어야 한다. fastembed + ONNX면 API 키 없이 된다.
- 수만 심볼 규모에서 벡터 저장소는 SQLite BLOB + 브루트포스 코사인이면 충분하다.
- 벡터 검색 단독은 exact 질의를 배신한다. exact match → vector search 순서의 하이브리드가 답이다.