Firestore로 토이 프로젝트의 DB를 관리하고 있던 도중 검색을 구현하기 위해 Algolia라는 검색 엔진 API를 사용해 보았다.
Firestore에서도 query, where를 이용하여 검색을 구현할 수 있다. 하지만 Firestore 자체 기능에서는 전체 텍스트 검색을 제공하지 않는다.
아래의 두 쿼리문 중 하나를 사용하면 간단한 검색을 수행할 수 있다.
where("text", ">=", filter), where("text", "<=", filter + "\uf8ff")
orderBy("text"), startAt(keyword), endAt(keyword + "\uf8ff")
하지만 이는 문자열이 특정 키워드로 시작하거나 키워드로 끝나는 경우만 잡아낸다.
예를 들어 '버터 갈릭 감자튀김'이 있을 때
'버터' 또는 '튀김'으로는 검색이 가능하지만 '갈릭'으로 검색했을 경우에는 검색이 불가능하다.
이전에 트위터 클론 코딩 프로젝트를 했을 때는 이런 한계가 있더라도 위의 방식으로만 검색을 구현했었다. 하지만 성능면에서 아쉬움이 남았어서 이번 프로젝트에서는 외부 엔진을 사용해서 전체 텍스트 검색을 구현해 보았다.
Firebase에서는 Elastic, Algolia, Typesense 세 가지 외부 검색 서비스를 이용하여 전체 텍스트 검색을 수행할 수 있다고 한다. 나는 이 중에서 사용법이 가장 간단하다는 Algolia를 사용해 보았다. 외부 서비스를 이용한 검색은 Firestore의 데이터를 외부 서비스의 테이블과 연동하여, 해당 외부 서비스에서 제공하는 API를 통해 데이터를 검색할 수 있도록 한다. 두 테이블 간 실시간 데이터 연동을 위해 Firebase의 Cloud Functions 또는 확장 프로그램으로 Firestore의 데이터 변화를 감지한다. 두 가지 모두 Firebase 프로젝트를 Blaze 요금제로 업그레이드해야 사용할 수 있다. 나는 좀 더 편리한 Firebase Extensions을 사용했다.
Algolia 및 Firebase Extensions 세팅은 아래 포스팅을 참고
나는 구현하면서 필요한 일부 설정을 추가해 주었다.
Firebase Extensions
Indexable Fields
- Algolia index의 records에 어떤 필드를 연동할지 결정한다. (default: 전체 필드)
- 검색 및 검색 결과에 필요한 필드 전부 포함해야 한다.
└ 검색 결과로 Firestore가 아닌 Algolia에 저장된 records가 반환되기 때문에 검색할 필드뿐만 아니라 검색 결과로 필요한 필드도 포함해야 한다. (ex. 이미지 경로, 사용자 닉네임 등)
Algolia Index Configuration
searchable attributes
- 검색을 수행할 필드 추가 (default : 전체 필드)
- 필드가 객체 타입이고 해당 객체의 특정 필드를 검색하려면 객체가 아닌 필드를 넣어야 한다. (ex. locationInfo.address)
facets - Attributes for faceting
- 필터로 배열을 검색할 경우, 해당 배열 필드를 포함해야 한다.
검색 구현하기
Algoliasearch 설치
npm i algoliasearch
Algoliasearch Client 설정
import algoliasearch from "algoliasearch";
const algoliaClient = algoliasearch(
process.env.ALGOLIA_APP_KEY, // Algolia Aplication ID
process.env.ALGOLIA_SEARCH_KEY, // Algolia Search-only API Key
);
export default algoliaClient;
검색 로직 작성
내가 원하는 검색은 위와 같이 키워드 검색이 가능하며 동시에 카테고리, 별점에 따라 필터가 가능해야 했다.
카테고리는 다중 선택이 가능하다. 키워드 검색은 맛집 이름, 내용, 위치 등 여러 필드에 수행된다.
import algoliaClient from '@libs/algolia';
const search = async (categories, minScore, maxScore, keyword) => {
const index = algoliaClient.initIndex('posts');
const searchQuery = {};
if (categories.length > 0)
searchQuery.filters = categories
.map((tag) => `categories:${tag}`)
.join(' OR ');
if (!(minScore === 0 && maxScore === 5))
searchQuery.numericFilters = [
`ratingValue >= ${minScore}`,
`ratingValue <=${maxScore}`,
];
const res = await index.search(keyword, searchQuery);
return res.hits;
};
export default search;
일반 쿼리는 filters에 추가하고, 숫자와 관련된 쿼리는 numericFilters에 추가한다.
categories의 경우 ['korean', 'western']과 같이 배열 타입의 필드이다. 카테고리별 필터를 위해 OR을 join하여 복합 쿼리를 작성한다.
쿼리 예시)
카테고리에 korean을 포함하는 문서
fitlter: 'categories:korean'
카테고리에 korean 또는 western을 포함하는 문서
filter: 'categories:korean OR categories:western'
위와 같은 배열 필드를 기준으로 필터하기 위해서는 이전에 언급한 facets - Attributes for faceting를 설정해주어야 했다.
search 메서드는 query 뿐만 아니라 pagination과 같은 다양한 request options을 파라미터로 가지고 있다.
관련된 부분은 공식 문서를 참고하자
구현 결과
잘못된 부분이나 개선할 점이 있으면 댓글 남겨주세요!
'Develop > JavaScript' 카테고리의 다른 글
[JavaScript] Vanilla JavaScript에 PWA 적용하기 with Webpack, Workbox (0) | 2024.05.18 |
---|---|
[JavaScript] 카카오맵 API를 이용한 장소 검색 및 위치 추가 구현하기 (0) | 2024.03.19 |
Vanilla JavaScript 프로젝트 초기 설정하기 (ES6) (0) | 2024.02.06 |
[Jest] 자바스크립트 테스트 코드 작성해 보기 (0) | 2023.09.07 |
[JavaScript] localStorage 사용하기 (0) | 2023.02.22 |