[9일차] Node.js, MongoDB - Part 3 : 검색기능 만들기 - URL query string, indexing 개념설명, Search index, 회원기능 포함하기

흑염소·2023년 9월 18일

Node.js로 게시판 기능을 만들어보자!

📕 URL query string

알다시피 서버로 데이터 보낼 땐 POST 요청을 쓰면 된다.
그러면 요청.body 이런 식으로 꺼내 쓸 수 있는데 이것 말고도 GET 요청으로 URL에서 데이터 꺼내와 쓰는 방법이 있다.
그게 query string(혹은 query parameter)

query string 전송하기

<div class="container input-group mb-2">
  <input class="form-control">
  <button class="input-group-append btn btn-danger" id="search">검색</button>
</div>

<script>
  $('#search').click(function(){
    window.location.replace('/search?value=이닦기')
  });
</script>

스크립트 구역을 보면 URL을 다음과 같이 변경했다.
?이름=값 형식으로 쿼리를 보낼 수 있는데 위 예시에서는 /search로 GET요청을 날리는 순간 'value'라는 이름으로 '이닦기'라는 값을 서버로 보낸다.
응용해보면,

<div class="container input-group mb-2">
  <input class="form-control" id="search-input">
  <button class="input-group-append btn btn-danger" id="search">검색</button>
</div>

<script>
  $('#search').click(function(){
    var 입력한값 = $('#search-input').val();
    window.location.replace('/search?value=' + 입력한값)
  });
</script>

검색 input 박스 id를 넣고 그 value 값을 읽어와서 파라미터로 보내면 된다.
이제 검색창에 입력한 값이 그대로 서버로 전송된다.

서버에서 query string 확인하기

(server.js)

app.get('/search', (요청, 응답)=>{
  console.log(요청.query);
})

요청.query하면 query string 전부를 꺼내볼 수 있다.
object 자료형으로 전달되기 때문에 요청.query.value를 한다면 그 이름의 값이 출력된다.

이제 사용자가 입력한 검색어인 게시물을 꺼내보자.

app.get('/search', (요청, 응답)=>{
  console.log(요청.query);
  db.collection('post').find({제목 : 요청.query.value}).toArray((에러, 결과)=>{
    console.log(결과)
    
    // 검색결과 페이지 렌더링
    응답.render('search.ejs', { posts : 결과 })
  })
})

db의 post에서 요청.query.value인 제목을 가진 데이터 모두를 배열로 결과에 출력해준다.
(findOne이 아닌 find인 경우 전부 찾아줌)
마무리로 응답.render로 검색결과 페이지에 렌더링 시켜주면 검색창 기능이 잘 구동된다.

이제 검색한 단어를 부분 포함하기만 해도 같이 검색되도록 추가 기능을 구현하자.

📗 indexing 개념설명

검색기능의 문제점이 있다.
'글쓰기'라고 검색하면 '글쓰기를 잘해야합니다' 이런 게시물은 찾지 못한다.
어떻게 해결하는가
쉬운 해결법으론 정규식을 쓰는게 있다.

db.collection('post').find({제목 : /글쓰기/}) 

정규식은 문자를 검사하는 식이다.
// 안에다가 문자를 담으면 검사해준다.
/글쓰기/ 라고 쓰면 문자에 글쓰기라는게 들어있는지 검사해주므로 이거 쓰면 구현은 된다.

하지만 문제점이 있는데,

  1. 정확히 일치하는 것만 찾아준다.
  2. find()로 다 찾는건 오래걸린다.

이 문제를 해결하기 위한 다른 방법이 indexing이다.

Binary Search와 indexing 개념

원하는 게시물을 찾는것은 원래 느리다.
보통 게시물 100만개가 있으면 하나하나 전부 탐색하기 때문이다.
그래서 데이터베이스는 Binary Search라는 알고리즘을 사용하는데
이 방식은 1부터 100까지 숫자를 하나하나 찾는게 아니라 50 이상인지 물어보는 비교형식으로 검색을 해준다.
최소한의 질문으로 빠르게 찾아낼 수 있기 때문에 속도면에서 강점이 있다.
이걸 쓰려면 기본 조건이 있는데 숫자가 1부터 100까지 미리 정렬되어 있어야 한다.
그리고 미리 정렬해두는 것을 indexing이라고 함.

DB에 index 생성하기

MongoDB Atlas에 들어가서 indexing 해보자.
인덱싱 해줄 데이터베이스에 들어가면 Indexes 라는 탭이 있다.
들어가서 INDEX 생성해주면 됨.

그리고 검색 조건을 설정해준다.

{ 인덱스만들항목이름 : 'text' }
이렇게 기입하면 끝.

글자인 경우 text
숫자인 경우 1 또는 -1 기입 (오름차순 내림차순)

인덱싱이란 용어가 어려워 보이지만 그냥 collection을 정렬된 사본으로 하나 더 만들어주는 작업일 뿐이다.

[참고]

정규식을 사용하면 항상 index를 사용하는게 아니다.
정규식을 쓸 때 시작하는 단어가 'A'인걸 찾아달라고 할때 index를 사용하고
'A'가 포함된걸 전부 찾아달라고 할때는 index를 사용하지 않는다.
만능은 아니라는 소리다.
다음 강의에서 이 부분도 해결하기로!

서버 find() 코드 수정하기

위에서 만든 인덱스를 활용하기 위해 서버 코드도 변경해야한다.
코드 변경 안하면 원래대로 모든 항목 full scan함.
인덱스 만든거 사용하는 코드로 변경하자.

app.get('/search', (요청, 응답)=>{
  console.log(요청.query);
  db.collection('post').find( { $text : { $search: 요청.query.value }} ).toArray((에러, 결과)=>{
    console.log(결과)
    응답.render('search.ejs', {posts : 결과})
  })
})

이제 모든 항목 검색이 된다.
find() 안에 $text~~ 코드를 적어주면 text index 에서 검색이 가능하다.
그리고 이렇게 기능개발해두면 간단한 검색엔진처럼 검색도 된다.

  1. 빠른검색
  2. or 검색 가능
  3. '-' 제외기능

자세하게 설명하면 검색창에
이닦기 글쓰기 라고 검색하면 이닦기 or 글쓰기 포함된 모든 문서를 찾아줌
이닦기 -글쓰기 라고 검색하면 이닦기인데 글쓰기라는 단어 제외하고 검색해줌
이닦기 글쓰기 라고 검색하면 정확히 이닦기 글쓰기라는 phrase가 포함된 문서 검색함

text index 문제점

큰 문제점이 있다.
index는 띄어쓰기 기준으로 단어를 저장한다.
한국 중국 일본어에는 취약점이다.

해결책 :

  1. text index 쓰지 않고 검색할 문서의 양을 제한하기 (skip(), limit() 이런 함수를 이용해서 pagination 기능을 개발하기)
  2. text search 기능으로 만들기 (nGram 이런 알고리즘 대신 쓰면 되는데 번거로움)
  3. 신기술 - Atlas 자체적인 기능 활용하기

3번째 방법으로 다음 강의 이어간다.

📘 Search index

위에서 진행했던 index 쿨하게 삭제한다.
MongoDB Atlas에서 Search Indexes 라는 것을 만들도록 한다.


이 경로대로 따라 만든다.
그리고

  1. Index Name 제대로 설정해준다.
  2. 인덱스 적용해줄 Database Collection 이름 맞는지 확인한다.

  1. 언어 설정을 한국어로 변경해준다.
  2. 변경했으면 생성한다.

indexing 하면 DB용량 차지하므로 필요한것만 indexing 하도록 하자.

이제 서버 코드를 또 수정하자

aggregate()

find() 대신 aggregate()를 사용한다.
검색조건을 여러개 달 수 있고 데이터 파이프라인 구축 가능하다.
예시보면서 이해하자.

// 검색기능 만들기
app.get('/search', (요청, 응답) => {
    console.log('검색요청:', 요청.query.value);

    // aggregate()에 사용할 검색조건
    var 검색조건 = [
        {
            $search: {
                index: 'titleSearch', // 내가 Atlas에 만든 search index명
                text: {
                    query: 요청.query.value,
                    path: '제목' // 제목날짜 둘다 찾고 싶으면 ['재목','날짜']
                }
            }
        },
        { $sort: { _id : 1 } }, // 값을 기준한 정렬 : 1 또는 -1
        { $limit : 10 }, // 제한걸기 : 위에서 n개 까지만 검색
        { $project : {제목: 1, _id: 0, score: { $meta : "searchScore" }} } // 1은 가져오기 0은 해당값 가져오지말기 score는 검색된 정도
    ]
    
    db.collection('post').aggregate(검색조건).toArray((에러,결과)=>{
        console.log(결과);
        // 검색결과 페이지 렌더링
        응답.render('search.ejs', { posts : 결과 })
    })
})

검색용 연산자 중에 $sort, $limit, $project는 부가적인 기능을 추가로 적어둔 것이므로 나중에 필요하다면 꺼내서 사용하자. (이 외에도 백만개의 $연산자가 있으니 필요할 때 검색해서 사용하기)
집중해야 할 부분은 $search 부분임.
이 연산자를 통해 검색조건을 만들고 aggregate() 함수에 연결해서 DB 데이터를 원하는대로 조작 가능하게 됐다.

📙 회원기능 포함하기

개발해둔 게시판과 회원기능을 합쳐서 업그레이드 시켜보자.
기존 게시판의 글삭제 기능을 본인 글만 삭제 가능하도록 재구성하자.

지금은 글 발행할 때 제목, 날짜, id만 저장하게 해놨다.
여기에 작성자 항목을 추가해서 글 발행시 작성자 비교 후 발행되도록 수정한다.

  1. 글발행하는 코드 수정
app.post('/add', function (요청, 응답) {
  console.log(요청.user._id)
  응답.send('전송완료');
  db.collection('counter').findOne({ name: '게시물갯수' }, function (에러, 결과) {
    var 총게시물갯수 = 결과.totalPost;
    var post = { _id: 총게시물갯수 + 1, 작성자: 요청.user._id , 제목: 요청.body.title, 날짜: 요청.body.date }
    db.collection('post').insertOne( post , function (에러, 결과) {
      db.collection('counter').updateOne({ name: '게시물갯수' }, { $inc: { totalPost: 1 } }, function (에러, 결과) {
        if (에러) { return console.log(에러) }
      })
    });
  });
});

게시물 저장할 때 { 작성자 : 요청.user._id } 이것도 저장하라고 바꿔줬다.
자바스크립트로 new Date() 이렇게 쓰면 그 자리에 날짜데이터가 남는데 그걸 그대로 DB에 저장하면 날짜 저장도 가능하다.

  1. 게시물 삭제기능 업그레이드
app.delete('/delete', function (요청, 응답) {
    요청.body._id = parseInt(요청.body._id);
    //요청.body에 담겨온 게시물번호를 가진 글을 db에서 찾아서 삭제해주세요
    db.collection('post').deleteOne({_id : 요청.body._id, 작성자 : 요청.user._id }, function (에러, 결과) {
      console.log('삭제완료');
      console.log('에러',에러)
      응답.status(200).send({ message: '성공했습니다' });
    })
});

삭제요청시 유저의 { _id : 요청.body._id }{ 작성자 : 지금로그인한사용자의_id } 를 가지고 있을 경우에만 삭제되도록 변경했다.
조건에 일치하는 데이터만을 삭제하기 때문에 다른 이름의 이용자가 로그인한 경우 삭제가 불가능한 구조가 됐다.

웬만한 기능은 전부 마무리 했으니 이후에 응용버전으로 암호화 라이브러리 사용해서 보안 높이는 작업 진행하기로...!

profile
매일 TIL 중인 비전공자 프론트 개발자

0개의 댓글