<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>seunghyun-bloom.log</title>
        <link>https://velog.io/</link>
        <description>Flutter developer</description>
        <lastBuildDate>Fri, 15 Dec 2023 18:18:12 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>seunghyun-bloom.log</title>
            <url>https://velog.velcdn.com/images/seunghyun-bloom/profile/ae0eea00-b0f3-4058-85ec-a1f77b6ad02e/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. seunghyun-bloom.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/seunghyun-bloom" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[MongoDB] Atlas Search 한글 검색 설정]]></title>
            <link>https://velog.io/@seunghyun-bloom/MongoDB-Atlas-Search-%ED%95%9C%EA%B8%80-%EA%B2%80%EC%83%89-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@seunghyun-bloom/MongoDB-Atlas-Search-%ED%95%9C%EA%B8%80-%EA%B2%80%EC%83%89-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Fri, 15 Dec 2023 18:18:12 GMT</pubDate>
            <description><![CDATA[<p>MongoDB의 atlas search를 이용해 검색 기능을 구축해보겠습니다.
Atlas search 는 ElasticSearch 와 같은 <a href="https://www.mongodb.com/docs/atlas/atlas-search/atlas-search-overview/#fts-architecture"><strong>Apache Lucene</strong></a> 기반의 검색엔진을 제공해주는데요,
이 Lucene의 장점은 다양한 언어에 형태소 분석 기능을 제공한다는 점입니다. (영어, 한글 등..)</p>
<p>오늘은 nodejs의 <a href="https://nestjs.com/">NestJS 프레임워크</a>를 이용해서 간단하게 검색 API를 구축하고
테스트해 보겠습니다.</p>
<p>아래 순서대로 진행을 해보겠습니다</p>
<ol>
<li>한글 샘플 데이터 로드하기</li>
<li>Atlas 에서 Search Index 만들기</li>
<li>NestJS 프로젝트 생성</li>
<li>MongoDB client 환경 구축</li>
<li>aggregate search query 코드 작성</li>
<li>검색 API 테스트</li>
</ol>
<hr>
<h3 id="사전-준비사항">사전 준비사항</h3>
<p>이번 포스트에서는 아래 작업까지는 다루지 않습니다.</p>
<h4 id="nodejs">nodeJS</h4>
<ul>
<li>npm 설치</li>
</ul>
<p>아래 공식 홈페이지에서 운영체제에 맞게 다운로드 받으셔서 준비해주세요
LTS 버전으로 받으시는 것을 추천드립니다.</p>
<blockquote>
<p><a href="https://nodejs.org/en/download">참고: nodeJS 공식 홈페이지 - 다운로드</a></p>
</blockquote>
<h4 id="mongodb">MongoDB</h4>
<ul>
<li>MongoDB 회원가입</li>
<li>Organazation 설정</li>
<li>Project 생성</li>
<li>Cluster 생성</li>
</ul>
<p>아래 공식 문서를 따라 무료 플랜의 Cluster를 준비해주세요</p>
<blockquote>
<p><a href="https://www.mongodb.com/docs/atlas/getting-started/#get-started-with-atlas">참고: MongoDB Document - Get Started with Atlas</a></p>
</blockquote>
<p>Cluster Tier를 설정하실때, Serverless로 생성하지 않도록 주의해주세요.
MongoDB는 현재 <a href="https://www.mongodb.com/docs/atlas/reference/serverless-instance-limitations/#unsupported-actions"><strong><em>Serverless 티어에서 Atlas Search를 지원하지 않습니다</em></strong></a></p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/761383f3-7fa1-45fe-8251-e34dcc7146bc/image.png" alt=""></p>
<hr>
<h2 id="1-한글-샘플-데이터-로드하기">1. 한글 샘플 데이터 로드하기</h2>
<p>먼저 검색에 사용할 샘플 데이터를 준비해주겠습니다.
저희는 MongoDB의 웹 콘솔인 <a href="https://www.mongodb.com/atlas">Atlas</a> 에서 새 Database와 Collection을 만들고 Documents 여러개를 넣어주겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/adfad46b-021c-4e00-925d-5b73595b82e9/image.png" alt=""></p>
<p>처음 Atlas 에 진입하시면 위 화면을 만나게 되실텐데요, 이 화면은 Seunghyun&#39;s org 라는 Organazation 의 Project 리스트를 보여주는 곳입니다.</p>
<p>참고로 간단히 설명 드리자면 MongoDB의 구조는 아래와 같다고 보시면 됩니다.</p>
<ul>
<li>MongoDB 의 구조</li>
</ul>
<blockquote>
<p>Organization (조직)
        ↓
Project (프로젝트)
        ↓
Cluster (서버)
        ↓
Database (Collection들의 집합)
        ↓
Collection (Document들의 집합)
        ↓
Document (여러 필드로 구성)
        ↓
Field (각각의 데이터)</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/dbf493b2-969a-487b-bf5b-cde55299a1ea/image.png" alt=""></p>
<p>왼쪽 메뉴바에서 <strong>Database</strong> 클릭</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/ea5002e6-ad74-4433-adb1-e47497d3fd60/image.png" alt=""></p>
<p><strong><em>Browse Collections</em></strong> 클릭</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/3ea7dd2b-3ba5-43e7-bc86-223afe7a2698/image.png" alt=""></p>
<p>Cluster 관리 콘솔에 진입했는데요,
들어온 김에 설명드리자면
위에서 설명 드린 <strong>MongoDB 의 구조</strong>에서 Database ~ Field 가 이렇게 생겼습니다.</p>
<p>그리고 위에 보이는 샘플 데이터들은 MongoDB에서 <em>기본으로 제공해주는 샘플 데이터</em> 들입니다.
당연히 영문 데이터들이고, 어차피 사용하지 않을 것이기 때문에 기본 샘플 데이터가 <em>없으셔도 괜찮습니다.</em></p>
<p>저희는 한글 검색을 위해 한글 데이터를 따로 넣어주겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/570335d8-f92b-4ea0-aded-9973a4094715/image.png" alt=""></p>
<p><strong>+ Create Database</strong> 버튼 클릭</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/80a46d36-9566-4f2c-ad99-d99160a1f831/image.png" alt=""></p>
<p>새로 만들 Database와 Collection에 이름을 지정해줍니다
저는 아이돌 그룹 멤버들의 데이터를 넣어줄 것이기 때문에 아래와 같이 이름을 지어주겠습니다.</p>
<blockquote>
<p>Database name : sample_search
Collection name : idols
Additional Preferences : (그냥 냅두기)</p>
</blockquote>
<p>다 적었으면 <strong>Create</strong> 버튼 클릭</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/4f4e219a-449d-47c6-adb3-df49f5186bf4/image.png" alt=""></p>
<p>sample_search 데이터베이스 안에 idol 컬렉션이 잘 생성되었고,
이제 Document들을 넣어주겠습니다.</p>
<p><strong>INSERT DOCUMENT</strong> 버튼 클릭</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/6910f089-7601-4f06-a734-24d99c3c6b77/image.png" alt=""></p>
<ol>
<li>우측 상단에 VIEW 를 <code>{}</code> 로 선택</li>
<li>아래 JSON 코드 입력</li>
<li><strong>INSERT</strong> 버튼 클릭</li>
</ol>
<pre><code class="language-typescript">[
  {
    &quot;name&quot;: &quot;안유진&quot;,
    &quot;group&quot;: &quot;아이브&quot;,
    &quot;introduction&quot;: &quot;김채원과 함께 아이즈원 출신이고, 현 아이브의 리더이다. 맑은 눈의 광인&quot;,
    &quot;favorite_food&quot;: [ &quot;김치찌개&quot;, &quot;햄버거&quot;, &quot;떡볶이&quot; ]
  },
  {
    &quot;name&quot;: &quot;김채원&quot;,
    &quot;group&quot;: &quot;르세라핌&quot;,
    &quot;introduction&quot;: &quot;안유진과 함께 아이즈원 출신이고, 현 르세라핌의 리더이다. 별명은 쌈아치&quot;,
    &quot;favorite_food&quot;: [ &quot;김치&quot;, &quot;부대찌개&quot;, &quot;라면&quot; ]
  },
  {
    &quot;name&quot;: &quot;사쿠라&quot;,
    &quot;group&quot;: &quot;르세라핌&quot;,
    &quot;introduction&quot;: &quot;김채원과 같은 그룹의 멤버. 일본인이다&quot;,
    &quot;favorite_food&quot;: [ &quot;김치찌개&quot;, &quot;산낙지&quot;, &quot;크루아상&quot; ]
  },
  {
    &quot;name&quot;: &quot;김민지&quot;,
    &quot;group&quot;: &quot;뉴진스&quot;,
    &quot;introduction&quot;: &quot;매우 잘나가는 글로벌 스타. 인간 사이다라고도 불린다&quot;,
    &quot;favorite_food&quot;: [ &quot;하리보&quot;, &quot;라면&quot;, &quot;사이다&quot; ]
  }
]</code></pre>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/3731d025-b6b0-4f0c-a9a8-e65fbf263a12/image.png" alt=""></p>
<p>준비한 Document 들이 잘 들어갔습니다.</p>
<hr>
<h2 id="2-atlas-에서-search-index-만들기">2. Atlas 에서 Search Index 만들기</h2>
<p>MongoDB 에서 검색을 하려면 Search Index 를 먼저 설정해주어야 합니다.
Atlas 에서 Search Index를 설정하겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/c3b11ec2-853b-4eb6-849f-0897a2fce80f/image.png" alt=""></p>
<p>좌측 메뉴바에서 <strong>Atlas Search</strong>를 클릭해서 설정 화면으로 진입합니다</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/0353ee47-1aca-4076-9c38-c5ff47e09236/image.png" alt=""></p>
<p>위 화면이 나오면 잘 들어오신건데요, 나중에 Search Index를 만들면 Overview 화면이 나오게 될 겁니다.
참고로 <a href="https://www.mongodb.com/docs/atlas/atlas-search/limitations/#fts-m0--free-cluster---m2--and-m5-limitations">M0 tier (무료 플랜)에서는 Search Index를 최대 3개</a>까지 만들 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/84df7e87-5768-42c3-9842-a612e3c0d32b/image.png" alt=""></p>
<p>Configuration Method 설정을 해줘야 하는데 저희는 보기 편하게 Visual Editor를 선택하겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/dcbbc35e-62ff-4f45-839f-90ff7e84bd49/image.png" alt=""></p>
<p>Index Name을 자유롭게 정해주시면 됩니다.
되도록이면 명칭을 보고 의미를 유추할 수 있도록 짓는 것이 좋기 때문에 저는 <code>idol_search_index</code> 로 짓겠습니다.</p>
<p>그리고, index를 적용시킬 컬렉션을 선택해주고</p>
<p><strong>Next</strong> 버튼 클릭</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/399edb39-4afe-4ee0-a3d0-c41331df30aa/image.png" alt=""></p>
<p>지금 보시면 default 값으로 <strong>Index Analyzer</strong>와 <strong>Search Analyzer</strong>가 <code>lucene.standard</code>로 되어 있는데요,
저 Analyzer가 검색어를 입력 받았을 때, 그 검색어를 분석해서 Querying 하도록 도와주는 엔진 역할입니다.</p>
<p>그치만 저 <code>lucene.standard</code>는 한글을 제대로 분석해주지 못하기 때문에 한글 전용 엔진으로 바꿔주겠습니다.</p>
<p><strong>Refine Your Index</strong> 버튼을 클릭해서 설정 화면으로 이동</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/351d1a26-2e99-41cd-8ce2-65c3a2879e79/image.png" alt=""></p>
<p><strong>Index Analyzer</strong>와 <strong>Search Analyzer</strong> 를 <code>lucene.nori</code> 로 변경해 줍니다.</p>
<p><a href="https://esbook.kimjmin.net/06-text-analysis/6.7-stemming/6.7.2-nori"><strong>lucene.nori</strong></a> 는 한글 형태소 분석기로 현 최대의 검색엔진인 <a href="https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-nori.html">Elastic Search에서 공식적으로 개발해서 지원</a>하고 있습니다.</p>
<p>lucene.standard 와의 차이점을 예시를 통해 보여드리자면,</p>
<p>똑같이 <em>&quot;동해물과 백두산이&quot;</em> 라고 검색을 한다면</p>
<ul>
<li><p>lucene.standard
  → &quot;동해물과&quot;, &quot;백두산이&quot;</p>
</li>
<li><p>lucene.nori
  → &quot;동해&quot;, &quot;물&quot;, &quot;과&quot;, &quot;백두&quot;, &quot;산&quot;, &quot;이&quot;</p>
</li>
</ul>
<p>이렇게 형태소를 파악하고 분리해서 단어를 쪼개는데, 이때 쪼개어진 하나 하나의 객체를 <strong>&quot;token&quot;</strong> 이라고 하며
이 토큰으로 Querying 을 해서 데이터를 가져오게 됩니다.</p>
<blockquote>
<p><a href="https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-nori.html">참고: Elastic 가이드북 (김종민 개발자님)</a></p>
</blockquote>
<p>나머지 Search Index 설정은 기본으로 두고, <strong>Save Changes</strong> 버튼 클릭해서 설정을 저장해주겠습니다
그리고 <strong>Create Search Index</strong> 버튼을 클릭해서 Search Index 생성</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/89d44a99-632c-4630-834a-59be44d96e2c/image.png" alt=""></p>
<p>Index를 생성하는데 30초 정도 소요가 됩니다.
시간이 지나면 Search Index가 잘 생성된 것을 볼 수 있습니다.</p>
<hr>
<h2 id="3-nestjs-프로젝트-생성">3. NestJS 프로젝트 생성</h2>
<p>저희는 NodeJS 환경에서 MongoDB를 연동해서 간단한 검색 API를 구축해볼건데요,
TypeScript 기반의 <a href="https://nestjs.com/"><strong>NestJS</strong></a> 프레임워크를 통해 구축해보겠습니다.</p>
<blockquote>
<p>이 포스트의 주제는 MongoDB Atlas Search 이기 때문에 NestJS 에 대해서는 자세히 다루지 않겠습니다.
NestJS 를 처음 접하시거나 초보자 분들은 잘 이해가 안되시더라도 우선 따라오시길 바랍니다.
NestJS 에 대해서는 나중에 제대로 설명하는 별도의 포스트를 작성하겠습니다.</p>
</blockquote>
<p>먼저 <a href="https://docs.nestjs.com/#installation">NestJS 설치</a>를 하겠습니다.
터미널을 열고</p>
<pre><code>npm i -g @nestjs/cli</code></pre><p>잘 설치가 되었는지 확인해봅니다.
아래 커맨드를 쳤을때 버전 정보가 잘 나오면 설치가 잘 된겁니다</p>
<pre><code>nest -v</code></pre><p>설치가 다 되었으면, <code>cd</code> 커맨드로 NestJS 프로젝트를 생성할 폴더로 이동하셔서
프로젝트를 생성해줍니다.</p>
<p>저는 mongodb_search 라는 이름으로 NestJS 프로젝트를 생성하겠습니다.</p>
<pre><code>nest new mongodb_search</code></pre><p>package manager 를 선택하라는 요청이 뜨면 <code>npm</code>을 골라줍니다</p>
<p>여기까지 하시면 NestJS 프로젝트가 잘 생성이 되셨을텐데요,
새로 만드신 프로젝트에 들어가셔서 VSCode 를 띄우겠습니다</p>
<pre><code>cd mongodb_search
code .</code></pre><p>제 terminal 기록을 보여드리겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/cdcc2b68-3603-4819-9fa5-636d1a17feb8/image.png" alt=""></p>
<p>이제부터는 VSCode 에서 작업을 하겠습니다.</p>
<p>계속해서 터미널에서 NestJs CLI 명령어로 <em>idol</em> 이라는 resource를 생성해주겠습니다</p>
<pre><code>nest g res idol
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? No</code></pre><p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/61bee3b4-2d0f-4e8d-a855-21354bb953ef/image.png" alt=""></p>
<p><em>src</em> 폴더 아래에 <em>idol</em> 폴더가 생성되고 그 안에 5개의 파일이 들어가 있는 것을 볼 수 있습니다. 이 하나의 구성이 한 묶음이며 resource 라 부릅니다.</p>
<blockquote>
<p>controller.spec.ts : 테스트용
<a href="https://docs.nestjs.com/controllers">controller.ts : 라우팅 역할 ⭐️</a>
module.ts : 각 파일들을 사용할 수 있게 연결해주는 역할
service.spec.ts : 테스트용
<a href="https://docs.nestjs.com/providers#services">service.ts : function들 모음 ⭐️</a></p>
</blockquote>
<p>오늘은 <em>idol.controller.ts</em> 와 <em>idol.service.ts</em> 만으로 간단하게 검색 API만 만들어보겠습니다.</p>
<hr>
<h2 id="4-mongodb-client-환경-구축">4. MongoDB client 환경 구축</h2>
<p>terminal에서 npm 으로 MongoDB의 공식 nodejs 패키지인 <a href="https://www.mongodb.com/docs/drivers/node/current/"><code>mongodb</code></a> 패키지를 설치해주겠습니다.</p>
<pre><code>npm i mongodb</code></pre><p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/06e8db48-7422-4d27-94cf-bb0b4c492993/image.png" alt=""></p>
<p><strong>package.json</strong> 에서 잘 설치가 된 것이 확인됩니다.</p>
<p>다음은 MongoDB의 <strong>Atlas</strong>로 이동하겠습니다</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/6e872a68-b130-49d4-88d6-67e55066a91a/image.png" alt=""></p>
<p><strong>Connect</strong> 버튼 클릭</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/74128f6a-2bd2-4d98-99d2-e870a7eac82c/image.png" alt=""></p>
<p><strong>Drivers</strong> 클릭</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/ddb573bd-1cd2-48aa-9e30-00caff0b2482/image.png" alt=""></p>
<p><strong>Connection String</strong> 복사하고 close 해줍니다</p>
<blockquote>
<p>Connection String 이란 Atlas 외부_(ex: nodejs, Compass ...)_에서 MongoDB Cluster 에 연결하고자 할때
사용하는 String 형태의 연결 URI 입니다
<a href="https://www.mongodb.com/basics/mongodb-connection-string">참고: MongoDB 공식 홈페이지 - Introduction to MongoDB Connection Strings</a></p>
</blockquote>
<hr>
<h2 id="5-aggregate-search-query-코드-작성">5. aggregate search query 코드 작성</h2>
<ul>
<li><strong>idol.service.ts</strong><pre><code class="language-typescript">import { Injectable } from &#39;@nestjs/common&#39;;
import { MongoClient } from &#39;mongodb&#39;;
</code></pre>
</li>
</ul>
<p>@Injectable()
export class IdolService {
  searchIdols(searchValue: string): Promise&lt;object[]&gt; {
    const connectionString = &#39;mongodb+srv://seunghyun_bloomlab:(여기에 비밀번호를 입력)@cluster0.36jdvjx.mongodb.net/?retryWrites=true&amp;w=majority&#39;;
    const mongoClient = new MongoClient(connectionString);
    const idolCollection = mongoClient.db(&#39;sample_search&#39;).collection(&#39;idols&#39;)</p>
<pre><code>return idolCollection.aggregate([
  {
    $search: {
      index: &#39;idol_search_index&#39;, // 사용할 search index 의 index명 입력
      compound: { // 다중 조건 Querying 할 때 쓰는 operator
        should: [
          {
            text: {
              query: searchValue,
              path: &quot;name&quot;,
              score: { constant: { value: 10 } }, // 이름에 검색어가 있으면 10점
            },
          },
          {
            text: {
              query: searchValue,
              path: &quot;gruop&quot;,
              score: { constant: { value: 5 } }, // 그룹명에 검색어가 있으면 5점
            },
          },
          {
            text: {
              query: searchValue,
              path: &quot;introduction&quot;,
              score: { constant: { value: 3 } }, // 소개글에 검색어가 있으면 3점
            },
          },
          {
            text: {
              query: searchValue,
              path: &quot;favorite_food&quot;,
              score: { constant: { value: 1 } }, // 최애음식 목록에 검색어가 있으면 1점
            },
          },
        ],
        minimumShouldMatch: 1, // 최소 하나는 일치해야 리턴
      },
    },
  },
  {
    $project: { // 표시할 데이터 정의 (1 이면 보이고, 0이면 보이지 않음)
      _id: 0,
      name: 1,
      group: 1,
      introduction: 1,
      favorite_food: 1,
      score: { $meta: &quot;searchScore&quot; }
    },
  },
]).toArray();</code></pre><p>  }
}</p>
<pre><code>
제가 주석 단 부분을 집중해서 보시면 각 필드별로 검색어가 있으면 미리 정의한 점수를 부여하고 이를 합산해서 점수가 높은 순서대로 나오도록 코드를 작성했습니다.

&gt; [참고: MongoDB - Atlas Search - compound](https://www.mongodb.com/docs/atlas/atlas-search/compound/)


- idol.controller.ts

```typescript
import { Controller, Get, Param } from &#39;@nestjs/common&#39;;
import { IdolService } from &#39;./idol.service&#39;;

@Controller(&#39;idol&#39;)
export class IdolController {
  constructor(private readonly idolService: IdolService) { }

  @Get(&#39;search/:value&#39;)
  search(@Param(&#39;value&#39;) searchValue: string): Promise&lt;object[]&gt; {
    return this.idolService.searchIdols(searchValue);
  }
}</code></pre><p>controller.ts 는 NestJS 프레임워크에서 API의 라우팅을 담당합니다.
위 API의 path는 <code>localhost:3000/idol/search/(검색어를 여기에 입력)</code> 이렇게 형성되게 됩니다.</p>
<p>참고로 <em>favorite_food</em> 필드의 타입이 <code>Array</code> 라서 <a href="https://www.mongodb.com/docs/atlas/atlas-search/text/">text</a> operator 대신 <a href="https://www.mongodb.com/docs/atlas/atlas-search/in/">in</a> 을 써야 하는 것 아닌가 생각이 들 수 있는데요</p>
<p>그럴 필요없이 text 를 써주시면 됩니다</p>
<blockquote>
<p>in 을 썼을 때에는 오히려 에러 발생 : <code>This analyzer is expected to produce exactly one token, but got many</code></p>
</blockquote>
<hr>
<h2 id="6-검색-api-테스트">6. 검색 API 테스트</h2>
<p>이제 검색 API가 완성되었으니 서버를 run 하고 검색어를 입력해서 테스트를 해보겠습니다</p>
<pre><code>npm run start:dev</code></pre><p>서버가 잘 실행이 되었다면 이제 API 테스트를 해볼건데요,
<a href="https://www.postman.com/">Postman</a> 을 이용해서 테스트를 해보겠습니다</p>
<p>Postman 이 설치되지 않으신 분들은 크롬 브라우저에서 테스트를 하셔도 무방합니다.</p>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/b9b6aa47-3ab0-49eb-809a-20b1b7a29889/image.png" alt=""></p>
<p>자 그럼 Postman 을 열고 테스트를 시작해보겠습니다.</p>
<h3 id="6-1-테스트-필드별-가중치-부여">6-1 [테스트] 필드별 가중치 부여</h3>
<pre><code>[GET] http://localhost:3000/idol/search/김채원</code></pre><p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/846e2762-02db-4145-b407-2f1ae5bfc305/image.png" alt=""></p>
<p><strong>&quot;김채원&quot;</strong> 을 검색했더니 결과가 위와 같이 나왔습니다.</p>
<p>아까 <em>idol.service.ts</em> 에서 필드별로 스코어를 정의해서 검색 결과가 나오게끔 코드를 작성했었는데요,</p>
<blockquote>
<p>name : 10 점
group : 5 점
introduction : 3 점
favorite_food : 1 점</p>
</blockquote>
<p>으로 검색어가 각 필드에서 찾아지면 스코어를 부여하고 이를 합산해서 높은 순서대로 데이터를 넘겨받게 됩니다.</p>
<h3 id="6-2-테스트-한글-형태소-분석">6-2 [테스트] 한글 형태소 분석</h3>
<pre><code>[GET] http://localhost:3000/idol/search/맑은김치를 좋아하는 사쿠라</code></pre><p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/95fec6cb-7206-403b-8a97-2d691ce5f282/image.png" alt=""></p>
<p>지금 첫째로 보실 포인트는 lucene.nori 가 제 검색어 <strong>&quot;맑은김치를 좋아하는 사쿠라&quot;</strong> 를 형태소 분석하여
여러개의 낱말(token)으로 쪼갠 뒤 색을 진행했다는 것인데요.</p>
<p>심지어 &quot;맑은김치&quot; 는 중간에 띄어쓰기도 안했는데 분석해서 토큰으로 나눴습니다</p>
<blockquote>
<ul>
<li>before
  &quot;맑은김치를 좋아하는 사쿠라&quot;</li>
</ul>
</blockquote>
<ul>
<li>after
  [&quot;맑은&quot;, &quot;김치&quot;, &quot;좋아&quot;, &quot;사쿠라&quot;]</li>
</ul>
<p>두번째 포인트는 <strong>&quot;김치&quot;</strong> 라는 낱말로 &quot;<strong>김치</strong>찌개&quot;를 찾았다는 점입니다.
이는 검색어가 꼭 필드값에 완전히 일치하지 않아도 검색이 된다는 것을 뜻합니다.</p>
<hr>
<h2 id="마치며">마치며</h2>
<p>이 포스트는 MongoDB 의 Atlas Search 가 이 정도 퀄리티의 검색 기능도 제공한다는 점을
알려주기 위해 작성되었습니다.</p>
<p>회사에서나 개인적으로 프로젝트를 기획하고 설계하다 보면 매번 DB를 어디에 둘까를 고민하게 되는데요
그런 고민에 있어서 제 포스트를 통해 MongoDB도 고려 선상에 들게 된다면 보람찰 것 같습니다.</p>
<p>저 같은 경우는 앱 개발 프로젝트를 진행하면서
비용과 관리의 효율성을 이유로 Serverless로 DB를 구축하고자 하였는데요</p>
<p>MongoDB의 Serverless 는 2가지의 이유로 인해 메인 DB로 쓰기엔 맞지 않았고</p>
<ol>
<li>Atlas Search를 제공하지 않음</li>
<li>한국 Region을 지원하지 않음 (가장 가까운 Region은 싱가포르)</li>
</ol>
<p>결국 <a href="https://firebase.google.com/?hl=ko"><strong>Firebase</strong></a>의 <a href="https://firebase.google.com/docs/firestore?hl=ko"><strong>Cloud Firestore</strong></a>를 메인 DB 로 두고, 검색 부분만 MongoDB로 쓰고 있습니다.</p>
<p>이상으로 포스트를 마치겠습니다.
궁금한 점이 있거나 오류를 발견하시면 댓글로 남겨주시면 감사하겠습니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Text의 Size를 구해서 형광펜 친 모양 꾸미기]]></title>
            <link>https://velog.io/@seunghyun-bloom/Flutter-Text%EC%9D%98-Size%EB%A5%BC-%EA%B5%AC%ED%95%B4%EC%84%9C-%ED%98%95%EA%B4%91%ED%8E%9C-%EC%B9%9C-%EB%AA%A8%EC%96%91-%EA%BE%B8%EB%AF%B8%EA%B8%B0</link>
            <guid>https://velog.io/@seunghyun-bloom/Flutter-Text%EC%9D%98-Size%EB%A5%BC-%EA%B5%AC%ED%95%B4%EC%84%9C-%ED%98%95%EA%B4%91%ED%8E%9C-%EC%B9%9C-%EB%AA%A8%EC%96%91-%EA%BE%B8%EB%AF%B8%EA%B8%B0</guid>
            <pubDate>Fri, 28 Oct 2022 07:30:33 GMT</pubDate>
            <description><![CDATA[<h3 id="text에-형광펜을-친-모양으로-예쁘게-꾸미고-싶다면-어떻게-해야-될까요">Text에 형광펜을 친 모양으로 예쁘게 꾸미고 싶다면 어떻게 해야 될까요?</h3>
<p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/d0c15d8e-e41c-485b-8325-c20040e01db0/image.png" alt=""></p>
<blockquote>
<p>맨 밑에 바로 사용 가능한 Full code가 있으니 급하신 분은 모든 Step을 건너뛰고 맨 밑으로 가서 Full code를 가져가세요  :)</p>
</blockquote>
<h3 id="step-1--새로운-stateless-widget-생성">Step 1 : 새로운 Stateless Widget 생성</h3>
<p>먼저 <code>HighLightedText</code> 라는 이름의 새로운 <code>Stateless Widget</code>을 만들어 주겠습니다.</p>
<p>이 위젯은 <code>String(문장)</code>, <code>double(폰트 사이즈)</code>, <code>Color(형광펜 색깔)</code>타입의 parameter 3개를 받아오도록 하겠습니다.</p>
<pre><code>class HighlightedText extends StatelessWidget {
  final String data;
  final Color color;
  final double fontSize;
  const HighLightText(
    this.data, {
    super.key,
    required this.color,
    this.fontSize = 14,
  });

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}</code></pre><h3 id="step-2--widget-구조-구상">Step 2 : widget 구조 구상</h3>
<p>위 사진처럼 텍스트에 형광펜 친 모양을 하기 위해서는
<code>Stack</code> 위젯을 사용해서 <code>Text</code> 위에 반투명한 색을 입힌 <code>Container</code>를 덮어 씌워 주기로 합니다.</p>
<pre><code>return Stack(
      children: [
        Text(
          data,
          style: TextStyle(
            fontSize: fontSize,
            fontWeight: FontWeight.bold,
            colot: color,
          ),
        ),
        Positioned(
          child: Container(
            width: ?,
            height: ?,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(2),
              color: color.withOpacity(0.2),
            ),
          ),
        ),
      ],
    );</code></pre><h3 id="step-3--text의-사이즈-구하기-중요-⭐️">Step 3 : Text의 사이즈 구하기 (중요 ⭐️)</h3>
<p>가장 중요한 것은 <code>Container</code>의 width, height를 어떻게 정의하냐인데요,
딱 <code>Text</code>의 길이만큼만 형광펜을 쳐야하기 때문에 <code>Text</code>의 size를 구해줘야 합니다.
그러기 위해서 <code>Text</code>의 사이즈를 구하는 method를 만들어주겠습니다.</p>
<pre><code>  Size getTextSize({
    required String text,
    required TextStyle style,
    required BuildContext context,
  }) {
    final Size size = (TextPainter(
      text: TextSpan(text: text, style: style),
      maxLines: 1,
      textScaleFactor: MediaQuery.of(context).textScaleFactor,
      textDirection: TextDirection.ltr,
    )..layout())
        .size;
    return size;
  }</code></pre><blockquote>
<p>참고: <a href="https://stackoverflow.com/a/62536187/16696093">https://stackoverflow.com/a/62536187/16696093</a></p>
</blockquote>
<h3 id="step-4--도출된-size로-형광펜-container-사이즈-적용하기">Step 4 : 도출된 Size로 형광펜 Container 사이즈 적용하기</h3>
<p>Stateless Widets의 빌드 단계에서 <strong>textSize</strong>라는 이름으로 새 <code>Size</code> 타입의 variable을 생성하여 값을 받아옵니다.</p>
<p>그리고 형광펜 <code>Container</code>에 width, height을 적용하고
<code>Position</code>에도 적용하여 형광펜이 글씨 중간부터 아래까지만 쳐지도록 하겠습니다.</p>
<pre><code>final Size textSize = getTextSize(
      text: data,
      style: TextStyle(
        fontSize: fontSize,
        fontWeight: FontWeight.bold,
        color: color,
      ),
      context: context,
    );</code></pre><pre><code>return Stack(
      children: [
        Text(
          data,
          style: TextStyle(
            fontSize: fontSize,
            fontWeight: FontWeight.bold,
            color: color,
          ),
        ),
        Positioned(
          top: textSize.height / 2,
          child: Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(2),
              color: color.withOpacity(0.2),
            ),
            height: textSize.height / 2,
            width: textSize.width,
          ),
        )
      ],
    );</code></pre><h3 id="full-code">Full code</h3>
<pre><code>class HighLightedText extends StatelessWidget {
  final String data;
  final Color color;
  final double fontSize;

  const HighLightedText(
    this.data, {
    super.key,
    required this.color,
    this.fontSize = 14,
  });

  Size getTextSize({
    required String text,
    required TextStyle style,
    required BuildContext context,
  }) {
    final Size size = (TextPainter(
      text: TextSpan(text: text, style: style),
      maxLines: 1,
      textScaleFactor: MediaQuery.of(context).textScaleFactor,
      textDirection: TextDirection.ltr,
    )..layout())
        .size;
    return size;
  }

  @override
  Widget build(BuildContext context) {
    final TextStyle textStyle = TextStyle(
      fontSize: fontSize,
      color: color,
      fontWeight: FontWeight.bold,
    );
    final Size textSize = getTextSize(
      text: data,
      style: textStyle,
      context: context,
    );
    return Stack(
      children: [
        Text(data, style: textStyle),
        Positioned(
          top: textSize.height / 2,
          child: Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(2),
              color: color.withOpacity(0.2),
            ),
            height: textSize.height / 2,
            width: textSize.width,
          ),
        )
      ],
    );
  }
}</code></pre><h3 id="활용예시">활용예시</h3>
<pre><code>class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const [
            Text(&#39;이런식으로 &#39;, style: TextStyle(fontSize: 20)),
            HighLightedText(&#39;형광펜&#39;, color: Colors.amber, fontSize: 20),
            Text(&#39;을 치니까 예쁘다!&#39;, style: TextStyle(fontSize: 20)),
          ],
        ),
      ),
    );
  }
}</code></pre><p><img src="https://velog.velcdn.com/images/seunghyun-bloom/post/16942be9-1a30-477a-909b-1d97777b0f43/image.png" alt=""></p>
<hr>
<h3 id="flutter-package-updated-20221123">Flutter Package (updated: 2022.11.23)</h3>
<blockquote>
<p>위 내용은 pub.dev에 <a href="https://pub.dev/packages/colored_text"><strong><em>colored_text</em></strong></a>로 등록하였습니다.
이제 flutter package로 간편하게 사용 가능합니다 :)</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android Studio Arctic Fox 2020.3.1 update error (m1 mac)]]></title>
            <link>https://velog.io/@seunghyun-bloom/Android-Studio-Arctic-Fox-2020.3.1-update-error-m1-mac</link>
            <guid>https://velog.io/@seunghyun-bloom/Android-Studio-Arctic-Fox-2020.3.1-update-error-m1-mac</guid>
            <pubDate>Mon, 13 Sep 2021 13:02:18 GMT</pubDate>
            <description><![CDATA[<h3 id="자꾸-android-studio-arctic-fox-202031-patch2로-업뎃이-안되는-문제">자꾸 Android Studio Arctic fox 2020.3.1 patch2로 업뎃이 안되는 문제</h3>
<p>Some conflicts were found in the installation area.
라고 뜨면서 업데이트를 못하게 한다.</p>
<p><img src="https://images.velog.io/images/seunghyun-bloom/post/abbbe9b4-a3d5-4e01-a095-0185f272d3e2/image.png" alt=""></p>
<blockquote>
<p>Contents/jre/Contents/Home/Frameworks/JavaNativeFoundation.framework/Versions/A/._JavaNativeFoundation.tbd</p>
</blockquote>
<h4 id="--그냥-안스를-새로-다운받아서-applications응용-프로그램에-덮어씌우기-해버렸다-해결">-&gt; 그냥 안스를 새로 다운받아서 Applications(응용 프로그램)에 덮어씌우기 해버렸다 (해결)</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] CocoaPods's specs repository is too out-of-date to satisfy dependencies. (m1 mac)]]></title>
            <link>https://velog.io/@seunghyun-bloom/Flutter-CocoaPodss-specs-repository-is-too-out-of-date-to-satisfy-dependencies.-m1-mac</link>
            <guid>https://velog.io/@seunghyun-bloom/Flutter-CocoaPodss-specs-repository-is-too-out-of-date-to-satisfy-dependencies.-m1-mac</guid>
            <pubDate>Fri, 27 Aug 2021 11:46:54 GMT</pubDate>
            <description><![CDATA[<p>m1 맥북에서 플러터 프로젝트에 파이어베이스를 연동하고 <code>flutter run</code> 을 실행했더니 아래와 같은 에러메시지가 나왔다.</p>
<pre><code>Error: CocoaPods&#39;s specs repository is too out-of-date to satisfy dependencies.
To update the CocoaPods specs, run:
  pod repo update



Error running pod install
Error launching application on iPhone 12 Pro Max.</code></pre><h3 id="해결방법">해결방법</h3>
<ol>
<li>ios 폴더 안의 Podfile 을 연다.</li>
<li>platform :ios, &#39;9.0&#39;  -&gt;  platform :ios, &#39;10.0&#39;  로 변경</li>
<li>xcode로 들어가서 deployment info의 아이폰 세팅도 10.0 으로 변경
<img src="https://images.velog.io/images/seunghyun-bloom/post/585e68da-89ac-4a78-9771-2b543433e470/image.png" alt=""></li>
<li>terminal 에서 ios 폴더로 진입 : run <code>cd ios</code></li>
<li>Podfile.lock 삭제 : run <code>rm podfile.lock</code></li>
<li>Run <code>sudo arch -x86_64 gem install ffi</code></li>
<li>Run <code>arch -x86_64 pod repo update</code></li>
<li>Run <code>arch -x86_64 pod install</code></li>
</ol>
<p>에러 해결!</p>
<p>참고로 저는 가장 최신버전의 firebase_core 1.6.0, firebase_auth 3.1.0 을 사용했습니다. (2021.08.27 기준)</p>
<blockquote>
<p>source: <a href="https://stackoverflow.com/a/68406454/16696093">https://stackoverflow.com/a/68406454/16696093</a></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>