개발 블로그를 위한 MCP 서버 구축기 (2): 인메모리 역인덱스로 검색 기능 구현
· 약 8분
키워드 기반 검색을 위한 역인덱스(Inverted Index)를 설계하고 가중치 기반 점수 시스템을 구현합니다.
🎯 들어가며
1편에서는 Git 기반 MCP 서버의 기본 구조와 콘텐츠 조회 기능을 구현했습니다. 하지만 블로그 포스트가 늘어나면 문제가 생깁니다.
"Python에 대한 글이 있나요?"
목록을 일일이 확인하는 건 비효율적입니다. 검색 기능이 필요합니다.
📚 검색 시스템 설계
문제 정의
검색 기능을 구현하는 방법은 여러 가지가 있습니다:
| 방식 | 장점 | 단점 |
|---|---|---|
| 전체 스캔 | 구현 간단 | 파일이 많으면 느림 |
| 외부 검색 엔진 | 강력한 기능 | 복잡한 설정, 추가 인프라 |
| 인메모리 인덱스 | 빠른 검색, 로컬 실행 | 메모리 사용 |
MCP 서버는 로컬에서 실행되고, 콘텐츠 규모가 수백 개 수준이므로 인메모리 인덱스가 적합합니다.
역인덱스(Inverted Index)란?
일반적인 인덱스는 "문서 → 단어" 매핑입니다:
문서1: [python, machine, learning]
문서2: [react, javascript, frontend]
역인덱스는 이를 뒤집어서 "단어 → 문서" 매핑으로 만듭니다:
python: [문서1]
react: [문서2]
javascript: [문서2]
machine: [문서1]
learning: [문서1]
frontend: [문서2]
검색할 때 키워드로 바로 문서를 찾을 수 있어서 O(1) 시간에 검색이 가능합니다.
🏗️ 인덱스 구조 설계
MCP 서버의 인덱스는 4개의 Map으로 구성됩니다:
src/search-engine.js
export class SearchEngine {
constructor() {
this.index = {
posts: new Map(), // slug → post data
docs: new Map(), // path → doc data
tags: new Map(), // tag → [{type, id}]
keywords: new Map(), // keyword → [{type, id, weight}]
};
}
}
| Map | Key | Value | 용도 |
|---|---|---|---|
| posts | slug | 포스트 전체 데이터 | 블로그 포스트 저장 |
| docs | path | 문서 전체 데이터 | 기술 문서 저장 |
| tags | tag | 참조 배열 | 태그로 필터링 |
| keywords | keyword | 참조 + 가중치 배열 | 검색 |
🔧 인덱스 빌드 구현
전체 빌드 흐름
src/search-engine.js
async buildIndex(repoPath) {
console.error('[SearchEngine] Building index...');
const blogDir = path.join(repoPath, 'blog');
const docsDir = path.join(repoPath, 'docs');
// 블로그 포스트 인덱싱
await this.indexBlogPosts(blogDir);
// 문서 인덱싱
await this.indexDocs(docsDir);
console.error(`[SearchEngine] Index built: ${this.index.posts.size} posts, ${this.index.docs.size} docs`);
}
블로그 포스트 인덱싱
각 마크다운 파일을 읽고, 메타데이터를 추출해서 인덱스에 저장합니다:
src/search-engine.js
async indexBlogPosts(blogDir) {
const files = await fs.readdir(blogDir);
const mdFiles = files.filter(f => f.endsWith('.md'));
for (const file of mdFiles) {
const filePath = path.join(blogDir, file);
const fileContent = await fs.readFile(filePath, 'utf-8');
const parsed = matter(fileContent);
// 파일명에서 날짜와 slug 추출: YYYY-MM-DD-slug.md
const filename = path.basename(file, '.md');
const match = filename.match(/^(\d{4})-(\d{2})-(\d{2})-(.+)$/);
let date, slug;
if (match) {
date = `${match[1]}-${match[2]}-${match[3]}`;
slug = match[4];
}
const post = {
slug: parsed.data.slug || slug,
title: parsed.data.title || 'Untitled',
date: parsed.data.date || date,
tags: parsed.data.tags || [],
excerpt: this.extractExcerpt(parsed.content),
content: parsed.content,
type: 'blog'
};
// 1. 포스트 저장
this.index.posts.set(post.slug, post);
// 2. 태그 인덱싱
for (const tag of post.tags) {
if (!this.index.tags.has(tag)) {
this.index.tags.set(tag, []);
}
this.index.tags.get(tag).push({ type: 'blog', id: post.slug });
}
// 3. 키워드 인덱싱
this.indexKeywords(post.slug, 'blog', post.title, post.content, post.tags);
}
}
📊 가중치 기반 키워드 인덱싱
검색에서 중요한 건 **관련도(relevance)**입니다. 제목에 나오는 키워드가 본문에만 나오는 키워드보다 더 중요하죠.
가중치 시스템
| 위치 | 가중치 | 이유 |
|---|---|---|
| 제목 | 3 | 문서의 핵심 주제 |
| 태그 | 2 | 저자가 선택한 분류 |
| 본문 | 1 | 일반적인 언급 |
