엔지니어링

뛰어난 React 검색 환경을 신속하게 구축하는 방법

2019년 11월 15일: 이 포스팅의 코드는 누락된 중괄호를 포함하도록 업데이트되었습니다. 흐름 또한 런타임 오류를 피하기 위해 조정되었습니다.

검색 환경을 구축하는 것은 어려운 일입니다. 그냥 보기에는 쉬워 보일 수 있습니다. 검색 창을 만들고, 데이터를 데이터베이스에 넣은 다음, 사용자 입력값으로 데이터베이스에 대해 쿼리를 수행하면 되니까요. 하지만 데이터 모델링과 기본 로직은 물론이고 전반적인 설계와 사용자 경험에서도 고려해야 할 사항이 많습니다.

Elastic의 오픈 소스 Search UI 라이브러리를 사용하여 탁월한 React 기반 검색 환경을 구축하는 방법에 대해 살펴보겠습니다. 30분 정도 소요되며, 이후에는 필요한 모든 애플리케이션에 검색 기능을 적용할 수 있을 것입니다.

하지만 우선, 검색을 구축하는 것이 그렇게 어려운 이유는 무엇일까요?

검색은 어렵습니다

몇 주 전에 Falsehoods Programmers Believe About Search라는 제목의 훌륭한 기사가 실렸습니다. 이 기사는 개발자가 검색 개발 시 사용하는 잘못된 가정들을 모두 다루었습니다.

많은 사람이 다음과 같은 잘못된 가정을 믿고 있었습니다.

  • “무엇을 찾고 있는지 알고 있는 고객들은 여러분이 기대하는 방식으로 검색할 것입니다.”
  • “항상 쿼리를 성공적으로 구문 분석하는 쿼리 파서를 작성할 수 있습니다.”
  • “일단 설정한 후에는 다음 주에도 동일한 방식으로 검색이 작동할 것입니다.”
  • “동의어는 쉽습니다.”
  • ...그외에도 다른 많은 잘못된 가정들이 있습니다. 한 번 읽어 보시기 바랍니다!

문제는 검색에는 많은 어려움이 있으며 그 어려움이 그저 단순하지가 않다는 것입니다. 상태를 관리하고, 필터링, 패시팅, 정렬, 페이지 매김, 동의어, 언어 처리 등을 위한 구성 요소를 구축하는 방법에 대해 생각해 볼 필요가 있습니다. 이를 요약하면 다음과 같습니다.

뛰어난 검색 기능을 구축하려면, (1) 강력한 검색을 위한 API를 제공하는 검색 엔진과 (2) 검색 환경을 구성하는 검색 라이브러리라는 난이도가 있는 두 가지 부분이 필요합니다.

검색 엔진의 경우 Elastic App Search를 살펴보고,

검색 환경의 경우에는 OS 검색 라이브러리인 Search UI를 소개하겠습니다.

작업을 마치면 다음과 같은 형태가 됩니다.

image2.png

검색 엔진: Elastic App Search

App Search는 관리형 유료 서비스 또는 자가 관리형 무료 배포판으로 사용할 수 있습니다. 이 자습서에서는 관리형 서비스를 살펴보겠지만, 여러분이 직접 호스팅하는 경우 여러분의 팀은 기본 라이선스로 추가 비용 없이 Search UI 및 App Search를 사용할 수 있습니다.

역대 최고의 비디오 게임을 나타내는 문서를 검색 엔진에 인덱싱한 다음, 이를 검색할 수 있도록 검색 환경을 설계하고 최적화하는 것이 오늘의 계획입니다.

첫째, 14일 평가판에 가입합니다(신용 카드는 필요 없음).

엔진을 만듭니다. 13개 언어 중에서 선택할 수 있습니다.

video-games로 이름을 지정하고 언어는 영어로 설정하겠습니다.

image4.png

best video games data set를 다운로드한 다음 가져오기 도구를 사용하여 App Search에 업로드합니다.

그런 다음 엔진을 클릭하고 Credentials 탭을 선택합니다.

video-games 엔진으로만 엔진에 대한 액세스가 제한된 새로운 퍼블릭 검색 키를 만듭니다.

새로운 퍼블릭 검색 키호스트 식별자를 검색합니다.

많은 작업을 한 것 같지는 않지만, 이제 정교한 검색 API를 사용하여 비디오 게임 데이터를 검색할 수 있는 완전히 동작하는 검색 엔진을 갖추었습니다.

지금까지 수행한 작업은 다음과 같습니다.

  • 검색 엔진 생성
  • 문서 수집
  • 기본 스키마 생성
  • 브라우저에 노출할 수 있으며 범위가 지정된 일회용 자격 증명 검색

우선 App Search에 대한 설명은 여기까지 하고

Search UI를 사용하여 검색 환경을 구축해 보겠습니다.

검색 라이브러리: Search UI

create-react-app 스캐폴딩 유틸리티를 사용하여 React 앱을 만들려고 합니다.

npm install -g create-react-app
create-react-app video-game-search --use-npm
cd video-game-search

이 기반 내에 Search UI와 App Search 커넥터를 설치합니다.

npm install --save @elastic/react-search-ui @elastic/search-ui-app-search-connector

그리고 개발 모드에서 앱을 시작합니다.

npm start

선호하는 텍스트 편집기에서 src/App.js를 엽니다.

상용구 코드로 시작한 다음 패키지를 풀도록 하겠습니다.

주석을 참고하세요!

// 1단계: 문 가져오기
import React from "react";
import AppSearchAPIConnector from "@elastic/search-ui-app-search-connector";
import { SearchProvider, Results, SearchBox } from "@elastic/react-search-ui";
import { Layout } from "@elastic/react-search-ui-views";
import "@elastic/react-search-ui-views/lib/styles/styles.css";
// 2단계: 커넥터
const connector = new AppSearchAPIConnector({
searchKey: "[YOUR_SEARCH_KEY]",
engineName: "video-games",
hostIdentifier: "[YOUR_HOST_IDENTIFIER]"
});
// 3단계: 옵션 구성
const configurationOptions = {
apiConnector: connector
// 함께 채워보겠습니다.
};
// 4단계: SearchProvider: 마무리
export default function App() {
return (



);
}

1단계: 문 가져오기

Search UI 종속성과 React를 가져와야 합니다.

핵심 구성 요소, 커넥터, 뷰 구성 요소는 3개의 서로 다른 패키지에 포함되어 있습니다.

  • @elastic/search-ui-app-search-connector
  • @elastic/react-search-ui
  • @elastic/react-search-ui-views

진행하면서 각각에 대해 자세히 알아보도록 하겠습니다.

import React from "react";
import AppSearchAPIConnector from "@elastic/search-ui-app-search-connector";
import { SearchProvider, Results, SearchBox } from "@elastic/react-search-ui";
import { Layout } from "@elastic/react-search-ui-views";

또한, 이 프로젝트를 위한 기본 스타일시트를 가져오려고 합니다. 직접 CSS 코드를 작성하지 않고도 멋진 룩앤필을 얻을 수 있습니다.

import "@elastic/react-search-ui-views/lib/styles/styles.css";

2단계: 커넥터

가지고 있는 App Search의 퍼블릭 검색 키와 호스트 식별자를

사용할 시간입니다!

Search UI 내 커넥터 객체는 자격 증명을 사용하여 App Search에 연결하고 검색을 지원합니다.

const connector = new AppSearchAPIConnector({
searchKey: "[YOUR_SEARCH_KEY]",
engineName: "video-games",
hostIdentifier: "[YOUR_HOST_IDENTIFIER]"
});

Search UI는 모든 검색 API와 연동됩니다. 하지만 커넥터를 사용하면 추가 구성없이 검색 API가 작동합니다.

3단계: configurationOptions

configurationOptions에 대해 자세히 알아보기 전에 잠시 되짚어 보겠습니다.

데이터 세트를 검색 엔진으로 가져왔는데요. 그런데 어떤 종류의 데이터인가요?

데이터에 대해 더 많이 알수록 해당 데이터를 검색자에게 어떻게 제공해야 할지 더 잘 이해할 수 있습니다. 그러면 검색 환경을 구성하는 방법도 알 수 있습니다.

이 데이터 세트 내에서 가장 적합한 객체 하나를 살펴보겠습니다.

{ 
"id":"final-fantasy-vii-ps-1997",
"name":"Final Fantasy VII",
"year":1997,
"platform":"PS",
"genre":"Role-Playing",
"publisher":"Sony Computer Entertainment",
"global_sales":9.72,
"critic_score":92,
"user_score":9,
"developer":"SquareSoft",
"image_url":"https://r.hswstatic.com/w_907/gif/finalfantasyvii-MAIN.jpg"
}

이 객체에는 name, year, platform 등의 몇 가지 텍스트 필드와 critic_score, global_sales, user_score 등의 숫자 필드가 있습니다.

다음과 같은 세 가지 핵심 질문을 통해 탄탄한 검색 환경을 구축하기에 충분한 정보를 얻을 수 있습니다.

  • 대부분의 사람들은 어떻게 검색할까요? 비디오 게임 이름으로요.
  • 대부분의 사람들은 결과에서 어떤 내용을 보고 싶어 할까요? 비디오 게임의 이름, 장르, 게시자, 점수 및 플랫폼입니다.
  • 대부분의 사람들은 어떻게 필터링, 정렬 및 패시팅할까요? 점수, 장르, 게시자 및 플랫폼별로 합니다.

그런 다음 이러한 답변을 configurationOptions으로 변환할 수 있습니다.

const configurationOptions = {
apiConnector: connector,
searchQuery: {
search_fields: {
// 1. 비디오 게임 이름으로 검색합니다.
name: {}
},
// 2. 결과: 이름, 장르, 게시자, 점수 및 플랫폼.
result_fields: {
name: {
// snippet은 일치하는 검색 용어가 태그로 래핑됨을 의미합니다.
snippet: {
size: 75, // snippet을 75자로 제한합니다.
대체: 참 // ‘원시’ 결과로 대체합니다.
}
},
genre: {
snippet: {
size: 50,
fallback: true
}
},
publisher: {
snippet: {
size: 50,
fallback: true
}
},
critic_score: {
//점수는 숫자이므로 snippet을 사용하지 않습니다.
raw: {}
},
user_score: {
raw: {}
},
platform: {
snippet: {
size: 50,
fallback: true
}
},
image_url: {
raw: {}
}
},
// 3. 점수, 장르, 게시자 및 플랫폼으로 패시팅하여 나중에 필터를 구축할 때 사용합니다.
facets: {
user_score: {
type: "range",
ranges: [
{ from: 0, to: 5, name: "Not good" },
{ from: 5, to: 7, name: "Not bad" },
{ from: 7, to: 9, name: "Pretty good" },
{ from: 9, to: 10, name: "Must play!" }
]
},
critic_score: {
type: "range",
ranges: [
{ from: 0, to: 50, name: "Not good" },
{ from: 50, to: 70, name: "Not bad" },
{ from: 70, to: 90, name: "Pretty good" },
{ from: 90, to: 100, name: "Must play!" }
]
},
genre: { type: "value", size: 100 },
publisher: { type: "value", size: 100 },
platform: { type: "value", size: 100 }
}
}
};

Search UI를 검색 엔진에 연결했으며 이제 어떻게 데이터를 검색하고, 결과를 표시하며, 해당 결과를 살펴볼지 정하는 옵션을 갖추었습니다. 그러나 모든 것을 Search UI의 동적 프런트엔드 구성 요소에 연결하는 무언가가 필요합니다.

4단계: SearchProvider

이 모든 것을 관장하는 객체입니다. SearchProvider에는 다른 모든 구성 요소가 중첩됩니다.

Search UI는 일반적인 검색 레이아웃을 만드는 데 사용되는 Layout 구성 요소를 제공합니다. 세부적인 사용자 지정 옵션이 있지만 이 자습서에서는 다루지 않습니다.

이 자습서에서는 다음 두 가지 작업을 수행합니다.

  1. configurationOptions를 SearchProvider로 전달합니다.
  2. 일부 구조적 빌딩 블록을 Layout에 배치하고 두 가지 기본 구성 요소인 SearchBox와 Results를 추가합니다.
export default function App() {
return (



);
}

현재 프런트 엔드에서는 기본 사항을 설정했습니다. 이를 실행하기에 앞서 백 엔드에서 작업해야 할 세부적인 것들이 몇 가지 더 있습니다. 또한 정확도 모델을 작업하여 이 프로젝트의 고유한 요구 사항에 맞게 검색을 미세 조정해야 합니다.

이어서 App Search를 살펴보겠습니다.

다시 실습으로

App Search에는 강력하고 정교한 검색 엔진 기능이 있습니다. 이를 사용하면 한 때는 복잡했던 조정 작업이 훨씬 즐거워집니다. 클릭 몇 번으로 세분화된 관련성 조정 작업과 원활한 스키마 변경 작업을 수행할 수 있습니다. 

먼저 스키마를 변경하여 실제로 작동하는 것을 확인하겠습니다.

App Search에 로그인하고, video-games 엔진을 입력한 다음, Manage 섹션 아래에 있는 Schema를 클릭합니다.

스키마가 표시됩니다. 11개의 각 필드는 기본적으로 텍스트 필드입니다.

configurationOptions 객체에서, 숫자를 검색하는 데 도움이 되도록 user_score와 critic_score라는 두 개의 범위 패싯을 정의했습니다. 범위 패싯이 예상대로 작동하려면 필드 유형이 숫자여야 합니다.

각 필드 옆에 있는 드롭다운 메뉴를 클릭하여 유형을 number로 변경한 다음 Update Types를 클릭합니다.

image1.png

엔진이 즉시 인덱싱을 다시 수행합니다. 나중에 레이아웃에 패시팅 구성 요소를 추가하면 범위 필터가 예상대로 작동합니다. 이제 정말 중요한 기능입니다.

이 섹션은 관련성이 매우 높습니다

동의어, 큐레이션, 관련성 조정이라는 세 가지 핵심 관련성 기능이 있습니다.

사이드바의 Search Settings 섹션에서 각 기능을 선택합니다.

image8.png

동의어

어떤 사람들은 차를, 또 어떤 사람들은 자동차를, 또 다른 사람들은 승용차를 운전합니다. 인터넷은 전 세계를 아우르며 세계 각지의 사람들은 사물을 설명하기 위해 서로 다른 단어를 사용합니다. 동의어는 하나의 동일한 것으로 간주되는 일련의 용어를 만드는 데 도움이 됩니다.

비디오 게임 검색 엔진의 경우 사람들은 Final Fantasy를 찾으려고 할 수 있습니다. 하지만 대신 FF를 입력할 수도 있습니다.

Synonyms를 클릭한 다음, Create a Synonym Set를 선택하고, 해당 용어들을 입력합니다.

image6.png

Save를 클릭합니다. 원하는 수만큼 동의어를 추가할 수 있습니다.

이제 FF에 대한 검색에 Final Fantasy에 대한 검색과 동일한 가중치가 적용됩니다.

큐레이션

큐레이션은 선호도입니다. 만약 누군가가 Final Fantasy 또는 FF를 검색한다는 어떻게 될까요? 시리즈로 구성된 게임들이 많은데요. 어떤 버전이 표시될까요?

기본적으로 상위 5개 결과는 다음과 같습니다.

1. Final Fantasy VIII

2. Final Fantasy X

3. Final Fantasy Tactics

4. Final Fantasy IX

5. Final Fantasy XIII

그런데 뭔가 이상하네요. Final Fantasy VII은 전체 Final Fantasy 게임 중에서 최고였습니다. Final Fantasy XIII은 별로였고요! 😜

Final Fantasy를 검색하는 사람에게 Final Fantasy VII을 가장 먼저 보여주도록 할 수 있을까요? 그리고 Final Fantasy XIII을 검색 결과에서 제외할 수 있을까요?

물론 가능합니다!

Curations를 클릭하고 Final Fantasy라는 쿼리를 입력합니다.

다음으로 테이블의 맨 왼쪽에 있는 핸들 바를 잡아 Final Fantasy VII 문서를 Promited Documents 섹션으로 끌어서 놓습니다. 그런 다음 Final Fantasy XIII 문서에서 Hide Result 버튼을 클릭합니다(눈 모양에 사선이 있는 아이콘).

image7.png

이제 Final Fantasy 또는 FF를 검색하는 사람은 모두 Final Fantasy VII을 제일 먼저 보게 됩니다.

그리고 Final Fantasy XIII은 아예 보지 못할 것입니다. 멋지죠!

많은 문서를 승격시키고 숨길 수도 있습니다. 또한 승격된 문서를 정렬할 수 있으므로 각 쿼리의 상단에 표시되는 내용을 완벽하게 제어할 수 있습니다.

관련성 조정

사이드바에서 Relevance Tuning을 클릭합니다.

name 필드라는 하나의 텍스트 필드를 검색할텐데요. 만약 사람들이 name 필드 description 필드와 같이 여러 개의 텍스트 필드를 검색한다면 어떻게 될까요? 우리가 사용하고 있는 비디오 게임 데이터 세트에는 설명 필드가 포함되어 있지 않기 때문에 설명을 위해 모조 문서를 몇 개 만들겠습니다.

문서가 다음과 같은 형태라고 가정해 보겠습니다.

{ 
"name":"Magical Quest",
"description": "A dangerous journey through caves and such."
},
{
"name":"Dangerous Quest",
"description": "A magical journey filled with magical magic. Highly magic."
}

Magical Quest 게임을 찾고 싶은 사람이 있다면 Magical Quest를 쿼리로 입력할 것입니다. 하지만 첫 번째 결과로 Dangerous Quest가 표시될 것입니다.

image3.png

왜 그럴까요? “magical”이라는 단어가 Dangerous Quest의 설명에 3번 등장하고, 검색 엔진은 어떤 필드는 다른 필드보다 중요하다는 것을 모르기 때문입니다. 그런 다음 검색 엔진은 Dangerous Quest의 순위를 더 높입니다. 이 난제 때문에 관련성 조정이 존재합니다.

여러 필드 중 하나를 선택하여 관련성 가중치를 높일 수 있습니다.

image5.gif

가중치를 높이면 Magical Quest라는 올바른 항목이 상단으로 올라가는 것을 확인할 수 있습니다. name 필드가 더 중요해졌기 때문입니다. 슬라이더를 움직여 값을 높게 조정하고 Save를 클릭하기만 하면 됩니다.

지금까지 App Search를 사용하여 다음을 수행했습니다.

  • 스키마를 조정하고 user_scorecritic_scorenumber 필드로 변경했습니다.
  • 관련성 모델을 미세 조정했습니다.

이렇게 멋진 ‘대시보드’ 기능이 완성되었습니다. 각 기능에는 이에 해당하는 API 엔드포인트가 있으므로 GUI가 마음에 들지 않는 경우 프로그래밍 방식으로 작업할 수 있습니다.

이것으로 UI에 대한 설명을 마칩니다.

마무리

현재, 사용자의 UI가 작동해야 합니다. 일부 쿼리를 시험해보고 이곳 저곳을 둘러보세요. 가장 먼저 눈에 띄는 것은 필터링, 패시팅, 정렬 등 결과를 탐색할 수 있는 도구가 없지만 검색이 작동한다는 것입니다. 앞으로 UI를 보강해야 합니다.

초기 src/App.js 파일에서 다음과 같이 3가지 기본 구성 요소를 가져왔습니다.

import { SearchProvider, Results, SearchBox } from "@elastic/react-search-ui";

구성 옵션에 대해 정의한 내용을 고려하여 몇 가지 구성 요소를 더 추가하겠습니다.

다음 구성 요소를 가져오면 UI 내에서 누락된 기능이 활성화됩니다.

  • PagingInfo: 현재 페이지에 정보를 표시합니다.
  • ResultsPerPage: 각 페이지에 표시되는 결과 수를 구성합니다.
  • Paging: 여러 페이지를 탐색합니다.
  • Facet: 데이터 유형에 고유한 방식으로 데이터를 필터링하고 탐색합니다.
  • Sorting: 해당 필드에 대한 결과의 방향을 변경합니다.
import {
PagingInfo,
ResultsPerPage,
Paging,
Facet,
SearchProvider,
Results,
SearchBox,
Sorting
} from "@elastic/react-search-ui";

가져온 구성 요소는 Layout에 배치할 수 있습니다.

Layout 구성 요소는 페이지를 섹션으로 나누며, 구성 요소들은 속성을 통해 이러한 섹션에 배치될 수 있습니다.

여기에는 다음 섹션이 포함되어 있습니다.

  • header: 검색 상자/막대
  • bodyContent: 결과 컨테이너
  • sideContent: 패싯과 정렬 옵션이 포함된 사이드바
  • bodyHeader: 현재 페이지, 페이지당 결과 등 컨텍스트가 풍부한 정보로 결과를 둘러싼 ‘래퍼’
  • bodyFooter: 페이지 간 빠른 탐색을 위한 페이징 옵션

구성 요소는 데이터를 렌더링합니다. configurationOptions에 제공된 검색 설정을 기반으로 데이터를 가져옵니다. 이제 각 구성 요소를 적절한 Layout 섹션에 배치하겠습니다.

예를 들어, configurationOptions에서 5개의 패시팅 차원을 설명했으므로 5개의 Facet 구성 요소를 만들 것입니다. 각 Facet 구성 요소는 “필드” 속성을 데이터의 키로 사용합니다.

이를 Sorting 구성 요소와 함께 sideContent 섹션에 배치한 다음 Paging, PagingInfoResultsPerPage 구성 요소를 가장 적합한 섹션에 배치합니다.

이번에는 로컬 개발 환경에서 검색 경험을 살펴보겠습니다.

훨씬 좋군요! 검색 결과를 살펴볼 수 있는 풍부한 옵션이 있습니다.

여러 가지 정렬 옵션과 같은 몇 가지 추가 기능을 설명했으며 단일 플래그를 추가하여 게시자 패싯을 필터링 할 수 있게 했습니다. 빈 쿼리로 검색해보고 모든 옵션을 살펴보십시오.

마지막으로, 검색 환경의 마지막 한 가지 기능을 살펴보겠습니다. 인기 있는 기능인...

자동 완성 기능입니다.

자동 완성

검색자들은 즉각적인 피드백을 제공하는 자동 완성 기능을 좋아합니다. 제안은 결과쿼리라는 두 가지 버전으로 제공됩니다. 버전에 따라 검색자는 관련 결과를 받거나 결과로 이어질 수 있는 잠재적 쿼리를 받게 됩니다.

여기에서는 쿼리 제안 형태의 자동 완성에 초점을 맞추겠습니다.

이를 위해서는 간단하게 2가지를 변경해야 하는데요.

먼저, configurationOptions 객체에 자동 완성을 추가해야 합니다.

const configurationOptions = {
autocompleteQuery: {
suggestions: {
types: {
documents: {
// 제안을 검색할 필드
fields: ["name"]
}
},
// 표시할 제안 수
size: 5
}
},
...
};

둘째, SearchBox의 기능으로 자동 완성을 활성화해야 합니다.

...

다 됐습니다!

검색을 시도해보세요. 글자를 입력하면 자동 완성 쿼리 제안이 나타납니다.

요약

이제 멋지고 제대로 작동하는 검색 환경을 구축했습니다. 그리고 사람들이 검색을 구현하려고 할 때 종종 빠지는 함정들도 피했습니다. 30분 만에 완료했는데 괜찮지 않나요?

Search UI는 검색 환경을 신속하게 개발할 수 있는 유연하고 현대적인 React 프레임워크입니다. Elastic App Search는 Elasticsearch를 기반으로 구축된 강력한 검색 엔진입니다. 관리형 유료 서비스이며, 직접 실행하는 경우 해당하는 기본 라이선스만 있으면 무료로 사용할 수도 있습니다.

여러분이 Search UI로 어떤 검색 환경을 구축했는지 궁금합니다. Gitter에 들러서 프로젝트에 기여하는 것을 고려해보세요!