[Node.js-06] MongoDB 검색 기능

Comely·2025년 3월 12일

Node.js

목록 보기
6/14

기본 검색 구현

서버에서 검색 처리

app.get('/search', async (요청, 응답) => {
  let result = await db.collection('post').find({ 
    title: 요청.query.val 
  }).toArray()
  응답.render('search.ejs', { 글목록: result })
})

문제점: 정확한 단어만 검색 가능, 성능 이슈


Index 최적화

Index란?

  • Document들을 특정 필드 기준으로 미리 정렬해둔 구조
  • 검색 속도를 대폭 향상시킴
  • DB에서 데이터를 빠르게 찾기 위한 핵심 기술

Index 생성 방법

MongoDB 웹 인터페이스

  1. 해당 Collection 선택
  2. "Indexes" 메뉴 클릭
  3. "Create Index" 버튼 클릭
  4. 필드명과 데이터타입 설정

Index 설정 예시

{
  "title": "text",    // 문자 필드
  "price": 1,         // 숫자 필드 (1: 오름차순, -1: 내림차순)
  "date": -1          // 날짜 필드
}

Index를 활용한 검색

// Text Index 사용
db.collection('post').find({ 
  $text: { $search: '검색어' } 
}).toArray()

// 숫자 Index는 기본 문법 그대로 사용
db.collection('post').find({ 
  price: { $gte: 1000 } 
}).toArray()

성능 측정

.explain() 사용법

// Index 없이 검색
let result1 = await db.collection('post')
  .find({ title: '안녕' })
  .explain('executionStats')

// Index 활용 검색  
let result2 = await db.collection('post')
  .find({ $text: { $search: '안녕' } })
  .explain('executionStats')

성능 지표 해석

// 느린 검색 (COLLSCAN)
{
  "totalDocsExamined": 13,    // 13개 문서 모두 확인
  "executionStage": "COLLSCAN" // 전체 컬렉션 스캔
}

// 빠른 검색 (Index 활용)
{
  "totalDocsExamined": 1,     // 1개 문서만 확인
  "executionStage": "IXSCAN"  // Index 스캔
}

핵심: COLLSCAN이 안 나오면 빠른 검색


Index의 장단점

장점

  • 검색 속도 대폭 향상
  • 정렬 작업도 빨라짐
  • 데이터베이스 성능 최적화

단점

  1. 저장 공간 추가 사용: 컬렉션 크기만큼 추가 용량 필요
  2. CUD 작업 속도 저하: 데이터 변경시 Index도 같이 업데이트
  3. 한국어 한계: 정확한 단어만 검색 가능

Index 생성 원칙

  • 꼭 필요한 필드만 생성
  • 검색이 빈번한 필드 우선
  • 읽기 > 쓰기 비율이 높은 경우에 유리

Search Index (고급 검색)

기본 Index vs Search Index

구분기본 IndexSearch Index
검색 방식정확한 단어부분 검색 가능
한국어 지원제한적우수
설정 복잡도간단복잡
용도단순 검색검색엔진 수준

Search Index 동작 원리

  1. 텍스트 전처리

    "안녕하세요 여러분" 
    → ["안녕하세요", "여러분"] (조사 제거)
  2. 단어 정렬 및 인덱싱

    단어: "안녕하세요" → Document ID: [1, 3, 5]
    단어: "여러분"     → Document ID: [1, 2, 4]
  3. 검색 실행

    검색: "안녕" → "안녕하세요" 포함 문서 반환
  4. 점수 계산: 관련도에 따른 순위 자동 결정


Search Index 생성

1. MongoDB Atlas에서 생성

  1. Search 메뉴 접속
  2. Create Search Index 클릭
  3. Configuration 설정

2. 한국어 최적화 설정

{
  "analyzer": "lucene.korean",
  "mappings": {
    "dynamic": false,
    "fields": {
      "title": {
        "type": "string",
        "analyzer": "lucene.korean"
      },
      "content": {
        "type": "string", 
        "analyzer": "lucene.korean"
      }
    }
  }
}

3. 설정 옵션 설명

  • analyzer: "lucene.korean": 한국어 형태소 분석
  • dynamic: false: 지정한 필드만 인덱싱
  • fields: 검색 대상 필드 지정

Search Index 활용

기본 검색 구문

app.get('/search', async (요청, 응답) => {
  let 검색조건 = [
    {
      $search: {
        index: 'search_index_name',
        text: { 
          query: 요청.query.val, 
          path: 'title' 
        }
      }
    }
  ]
  
  let result = await db.collection('post')
    .aggregate(검색조건)
    .toArray()
    
  응답.render('search.ejs', { 글목록: result })
})

고급 검색 옵션

let 검색조건 = [
  {
    $search: {
      index: 'search_index_name',
      text: { 
        query: '검색어', 
        path: ['title', 'content']  // 여러 필드 검색
      }
    }
  },
  { $sort: { score: { $meta: 'textScore' } } },  // 관련도순 정렬
  { $limit: 10 },                                // 상위 10개만
  { $skip: 0 },                                  // 페이지네이션
  { 
    $project: { 
      title: 1, 
      content: 1, 
      score: { $meta: 'textScore' }              // 점수 포함
    } 
  }
]

Aggregate 연산자 활용

주요 연산자들

let 검색조건 = [
  // 1. 검색 실행
  {
    $search: {
      index: 'search_index',
      text: { query: '검색어', path: 'title' }
    }
  },
  
  // 2. 정렬
  { $sort: { _id: -1 } },              // 최신순
  { $sort: { score: { $meta: 'textScore' } } }, // 관련도순
  
  // 3. 결과 제한
  { $limit: 20 },                      // 상위 20개
  { $skip: 10 },                       // 10개 건너뛰기
  
  // 4. 필드 선택
  { 
    $project: { 
      title: 1,                        // 포함
      content: 0,                      // 제외
      _id: 1,
      score: { $meta: 'textScore' }    // 검색 점수
    } 
  },
  
  // 5. 조건 필터링
  { 
    $match: { 
      author: '특정작성자',
      createdAt: { $gte: new Date('2024-01-01') }
    } 
  }
]

연산자 조합 예시

// 검색 + 필터링 + 페이지네이션
app.get('/search', async (요청, 응답) => {
  let page = parseInt(요청.query.page) || 1
  let limit = 10
  
  let 검색조건 = [
    {
      $search: {
        index: 'search_index',
        text: { query: 요청.query.val, path: 'title' }
      }
    },
    { $match: { published: true } },              // 공개글만
    { $sort: { score: { $meta: 'textScore' } } }, // 관련도순
    { $skip: (page - 1) * limit },               // 페이지네이션
    { $limit: limit },
    { 
      $project: { 
        title: 1, 
        summary: 1, 
        author: 1,
        score: { $meta: 'textScore' }
      } 
    }
  ]
  
  let result = await db.collection('post')
    .aggregate(검색조건)
    .toArray()
    
  응답.render('search.ejs', { 
    글목록: result,
    currentPage: page 
  })
})

검색 최적화 팁

1. 검색어 전처리

// 검색어 정제
let searchQuery = 요청.query.val
  .trim()                    // 공백 제거
  .replace(/[^\w\s가-힣]/g, '') // 특수문자 제거
  .slice(0, 100)             // 길이 제한

2. 빈 검색어 처리

app.get('/search', async (요청, 응답) => {
  if (!요청.query.val || 요청.query.val.trim() === '') {
    return 응답.render('search.ejs', { 글목록: [] })
  }
  
  // 검색 로직...
})

3. 검색 결과 캐싱

// 인기 검색어나 최근 검색 결과 캐싱
const searchCache = new Map()

app.get('/search', async (요청, 응답) => {
  let cacheKey = 요청.query.val
  
  if (searchCache.has(cacheKey)) {
    return 응답.render('search.ejs', { 
      글목록: searchCache.get(cacheKey) 
    })
  }
  
  // 실제 검색 후 캐시 저장
  let result = await db.collection('post').aggregate(검색조건).toArray()
  searchCache.set(cacheKey, result)
  
  응답.render('search.ejs', { 글목록: result })
})

실전 구현 체크리스트

기본 검색

  • 검색 폼 구현
  • 기본 검색 API 구현
  • 검색 결과 페이지 구현

성능 최적화

  • 필요한 필드에 Index 생성
  • .explain()으로 성능 측정
  • COLLSCAN 제거 확인

고급 검색

  • Search Index 생성 (한국어 설정)
  • Aggregate 파이프라인 구성
  • 검색 점수 활용

사용자 경험

  • 검색어 자동완성
  • 검색 결과 하이라이팅
  • 페이지네이션 구현
  • 빈 결과 처리

보안 및 안정성

  • 검색어 길이 제한
  • 특수문자 필터링
  • 에러 처리
  • 검색 로그 기록
profile
App, Web Developer

0개의 댓글