코드 인덱싱
코드 인덱싱 (Code Indexing)
AI 코딩 에이전트는 파일 이름이나 grep이 아니라 코드베이스를 의미적으로 검색할 수 있을 때("인증 로직 찾아 줘") 훨씬 잘 동작해요. Turso 기반 코드 인덱스는 식별자 전문 검색과 임베딩 벡터 유사도 검색을 데이터베이스 파일 하나에 담을 수 있어요.
출처: 문서
본문
AI 코딩 에이전트는 코드베이스를 의미적으로 검색할 수 있을 때("인증 로직 찾아 줘") 파일 이름이나 grep 검색보다 훨씬 잘 동작해요. Turso로 만든 코드 인덱스는 식별자 위주의 전문 검색(FTS)과 임베딩 벡터 유사도 검색을 결합할 수 있고, 이 모든 게 하나의 임베디드 데이터베이스 안에서 돌아가요.
이 가이드는 FTS와 벡터 검색을 모두 갖춘 코드 인덱서를 만드는 스키마와 쿼리를 다뤄요.
스키마 (Schema)
CREATE TABLE IF NOT EXISTS codebases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
root_path TEXT NOT NULL UNIQUE,
name TEXT NOT NULL DEFAULT '',
indexed_at INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS chunks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
codebase_id INTEGER NOT NULL REFERENCES codebases(id),
file_path TEXT NOT NULL,
chunk_key TEXT NOT NULL UNIQUE,
language TEXT NOT NULL,
kind TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
signature TEXT NOT NULL DEFAULT '',
snippet TEXT NOT NULL,
start_line INTEGER NOT NULL,
end_line INTEGER NOT NULL,
file_hash TEXT NOT NULL,
indexed_at INTEGER NOT NULL,
embedding F8_BLOB(384),
embedding_model TEXT DEFAULT ''
);
CREATE TABLE IF NOT EXISTS indexed_files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
codebase_id INTEGER NOT NULL REFERENCES codebases(id),
file_path TEXT NOT NULL,
file_hash TEXT NOT NULL,
chunk_count INTEGER NOT NULL DEFAULT 0,
indexed_at INTEGER NOT NULL,
UNIQUE(codebase_id, file_path)
);
구성 요소가 어떻게 맞물리는지
- **
codebases**는 각 프로젝트 루트를 등록해요. 하나의 데이터베이스로 여러 코드베이스를 인덱싱할 수 있어요. - **
chunks**는 소스 파일에서 뽑아낸 개별 의미 단위 — 함수, 구조체, 클래스, impl 블록 등 — 를 저장해요. 각 청크는name,signature, 코드snippet, 라인 범위, 그리고 (선택적으로) 벡터embedding을 가져요.chunk_key는 업서트(upsert) 연산을 위한 고유 식별자(예:file_path::kind::name)예요. - **
indexed_files**는 어떤 파일이 인덱싱됐는지와 그 콘텐츠 해시를 추적해서 증분 재인덱싱을 가능하게 해요 — 바뀐 파일만 다시 처리해요.
연결 (Connecting)
FTS 인덱스 기능은 연결 시점에 실험적 플래그가 필요해요:
import { connect } from "@tursodatabase/database";
const db = await connect(".index/code.db", {
experimental: ["index_method"],
});
await db.exec(SCHEMA);
experimental: ["index_method"]플래그는 Turso의USING fts인덱스 문법과fts_match()/fts_score()함수를 활성화해요.
파일 인덱싱하기
청크 업서트하기
파일을 파싱했으면 그 청크들을 업서트해요. 충돌 시 embedding = NULL로 설정하면 내용이 바뀌었을 때 강제로 재임베딩하게 돼요:
INSERT INTO chunks (codebase_id, file_path, chunk_key, language, kind, name,
signature, snippet, start_line, end_line, file_hash, indexed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(chunk_key) DO UPDATE SET
language = excluded.language,
kind = excluded.kind,
name = excluded.name,
signature = excluded.signature,
snippet = excluded.snippet,
start_line = excluded.start_line,
end_line = excluded.end_line,
file_hash = excluded.file_hash,
indexed_at = excluded.indexed_at,
embedding = NULL,
embedding_model = '';
인덱싱된 파일 추적하기
INSERT INTO indexed_files (codebase_id, file_path, file_hash, chunk_count, indexed_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(codebase_id, file_path) DO UPDATE SET
file_hash = excluded.file_hash,
chunk_count = excluded.chunk_count,
indexed_at = excluded.indexed_at;
바뀌지 않은 파일 건너뛰기
파일을 파싱하기 전에 바뀌었는지 확인해요:
SELECT file_hash FROM indexed_files WHERE codebase_id = ? AND file_path = ?;
해시가 같다면 그 파일은 완전히 건너뛰면 돼요.
임베딩 저장하기
임베딩은 Turso의 vector8() 함수로 int8 양자화 벡터로 저장할 수 있어요. 저장 공간이 1,536바이트(float32, 384차원)에서 395바이트로 줄어들어요:
UPDATE chunks SET embedding = vector8(?), embedding_model = ? WHERE chunk_key = ?;
vector8()의 파라미터는 JSON 문자열화된 float 배열이에요. 양자화는 Turso가 내부적으로 처리해요.
임베딩이 필요한 청크 찾기:
SELECT chunk_key, name, signature, file_path, kind, snippet
FROM chunks
WHERE codebase_id = ? AND (embedding IS NULL OR embedding_model != ?)
LIMIT ?;
전문 검색 (Full-text search)
Turso는 SQLite의 fts5 가상 테이블과 별개로, 가중치 적용 BM25 스코어링을 갖춘 FTS 인덱스를 제공해요.
FTS 인덱스 만들기
코드베이스마다 FTS 테이블을 만들고 chunks로 채워요:
-- Create the FTS table
CREATE TABLE IF NOT EXISTS fts_1 (
chunk_id INTEGER NOT NULL REFERENCES chunks(id) ON DELETE CASCADE,
name TEXT NOT NULL DEFAULT '',
signature TEXT NOT NULL DEFAULT ''
);
-- Populate from chunks
INSERT INTO fts_1 (chunk_id, name, signature)
SELECT id, name, signature FROM chunks WHERE codebase_id = 1;
-- Create the FTS index with weighted columns
CREATE INDEX IF NOT EXISTS idx_fts_1 ON fts_1
USING fts (name, signature)
WITH (
tokenizer = 'default',
weights = 'name=5.0,signature=3.0'
);
weights 파라미터가 BM25 스코어링을 조절해요 — 여기서는 함수/타입 name에 걸린 매치를 5배, signature 매치를 3배 가중했어요.
FTS로 검색하기
SELECT chunk_id, fts_score(name, signature, ?1) AS score
FROM fts_1
WHERE fts_match(name, signature, ?1)
ORDER BY score DESC
LIMIT ?;
그다음 전체 청크 데이터를 가져와요:
SELECT chunk_key, file_path, name, kind, signature, snippet, start_line, end_line
FROM chunks WHERE id = ?;
벡터 검색 (Vector search)
의미적/자연어 질의에는 벡터 코사인 거리를 사용해요:
SELECT chunk_key, file_path, name, kind, signature, snippet, start_line, end_line,
vector_distance_cos(embedding, vector8(?)) AS distance
FROM chunks
WHERE embedding IS NOT NULL
ORDER BY distance ASC
LIMIT ?;
점수는 1 - distance예요(코사인 거리를 코사인 유사도로 바꾼 값).
하이브리드 검색 (Hybrid search)
가장 좋은 결과를 내려면 Reciprocal Rank Fusion(RRF)으로 두 방식을 결합해요. FTS 쿼리와 벡터 쿼리를 각각 실행한 다음, 애플리케이션 코드에서 결과를 합쳐요:
// Run both searches
const ftsResults = await ftsSearch(query, limit);
const vecResults = await vectorSearch(queryEmbedding, limit);
// Reciprocal Rank Fusion
const k = 60; // RRF constant
const scores = new Map();
ftsResults.forEach((r, i) => {
const key = r.chunk_key;
scores.set(key, (scores.get(key) || 0) + 1 / (k + i + 1));
});
vecResults.forEach((r, i) => {
const key = r.chunk_key;
scores.set(key, (scores.get(key) || 0) + 1 / (k + i + 1));
});
// Sort by combined score
const merged = [...scores.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, limit);
오래된 파일 정리하기
코드베이스에서 파일이 삭제되면 관련 청크와 기록을 정리해요:
DELETE FROM chunks WHERE codebase_id = ? AND file_path = ?;
DELETE FROM indexed_files WHERE codebase_id = ? AND file_path = ?;
오래된 데이터를 지운 다음에는 인덱스 정합성을 위해 FTS 테이블을 다시 만들어요:
DROP TABLE IF EXISTS fts_1;
-- Then recreate and repopulate as shown above
핵심 설계 포인트
- 증분 인덱싱은 파일 해시를 통해 구현돼요. 바뀐 파일만 다시 파싱하고 다시 임베딩하기 때문에 큰 코드베이스에서도 업데이트가 빨라요.
- 두 가지 검색 모드 — 식별자/키워드 조회용 FTS와 의미/자연어 질의용 벡터 검색 — 가 각기 다른 용도를 커버해요. 하이브리드 검색은 둘을 결합해요.
- 가중치 FTS(
name=5.0, signature=3.0)는 결과가 함수와 타입 이름 쪽으로 쏠리게 해요. 개발자가 주로 찾는 게 바로 그거거든요. vector8()로 만드는 int8 양자화 벡터는 float32 대비 저장 공간을 75% 줄이면서 검색 품질에는 거의 영향을 주지 않아요.- 모든 것이 하나의 임베디드 데이터베이스 파일 안에서 동작하고 외부 서비스가 필요 없어요.
예시 (Example)
codemogger는 이 패턴을 AI 코딩 에이전트용 코드 인덱서로 구현한 MCP 서버예요.