메이리서치(Meilisearch) 오픈소스 검색 엔진 실전 튜토리얼. Docker 로컬 설치, 인덱스·문서 업로드, Node.js SDK 동기화, 필터·정렬·동의어·랭킹 규칙, Next.js App Router API 프록시, 검색 전용 API 키 분리와 프로덕션 배포 체크리스트까지. 일래스틱서치 대신 가벼운 전문 검색이 필요한 백엔드·프론트엔드 개발자를 위한 단계별 가이드.
상품명 검색에서 LIKE '%노트북%' 를 쓰다가 한글 오타·동의어·관련도 순 정렬이 막히면, 그때부터 전문 검색 엔진이 필요해진다. 일래스틱서치(Elasticsearch)는 강력하지만 클러스터 운영 비용이 크고, 사이드 프로젝트나 중소 트래픽 SaaS에는 과한 경우가 많다.
메이리서치(Meilisearch)는 루스트(Rust)로 만든 오픈소스 검색 엔진이다. 도커 한 줄로 띄우고, REST API와 자바스크립트 SDK로 문서를 넣고, 타이핑 즉시 결과(typo-tolerant)를 돌려준다. 이 튜토리얼은 로컬 설치부터 인덱스 설계, 노드 SDK, 필터·랭킹·동의어, 넥스트 앱 라우터 연동까지 단계별로 따라 할 수 있게 정리한다.
도커로 5분 안에 로컬 검색 서버 실행
인덱스 생성·문서 업로드·검색 쿼리 실습
필터·정렬·랭킹 규칙·동의어 설정
넥스트 앱 라우터 API 라우트 연동 패턴
프로덕션 배포 시 키·백업·보안 체크포인트
클라이언트 → REST/SDK → 메이리서치 인덱스 → 랭킹·필터 파이프라인으로 이어지는 기본 흐름
언제 메이리서치를 쓰는가 — SQL LIKE의 한계
관계형 DB의 LIKE 검색은 구현이 쉽지만 한계가 분명하다. 오타에 약하고, 관련도(relevance) 점수가 없으며, 접두·접미 와일드카드는 인덱스를 거의 못 탄다. 트래픽이 늘면 전체 테이블 스캔에 가까운 비용이 난다.
메이리서치는 이런 문제를 검색 전용 엔진으로 분리한다. 문서를 JSON으로 넣고, 필드를 searchable·filterable·sortable로 선언하면, 타이핑 중에도 오타를 감안해 상위 결과를 돌려준다. 일래스틱서치·알고리아(Algolia)와 비교하면 설치·운영 부담이 훨씬 가볍다.
실무에서 잘 맞는 경우:
상품·문서·헬프센터·블로그 전문 검색
관리자 패널의 빠른 필터 검색 UI
멀티 테넌트 SaaS의 테넌트별 인덱스 또는 필터 격리
일래스틱 클러스터를 유지할 인력이 없는 소규모 팀
맞지 않는 경우도 있다. 초대규모 로그 분석·복잡한 집계 파이프라인이 필요하면 일래스틱·오픈서치 쪽이 낫다. 이 글은 제품 검색·사이트 내 검색에 초점을 둔다.
도커로 로컬 설치하기 — 5분 실행
공식 도커 이미지를 쓰면 별도 바이너리 설치 없이 바로 서버를 띄울 수 있다. 마스터 키는 로컬 개발용으로 길게 잡고, 프로덕션에서는 반드시 강한 랜덤 값으로 교체한다. 데이터는 볼륨에 마운트해 컨테이너를 지워도 인덱스가 남도록 한다.
docker run — 메이리서치 즉시 실행
# 데이터 디렉터리 준비 후 컨테이너 실행
mkdir -p ./meili_data
docker run -d \
--name meilisearch \
-p 7700:7700 \
-v "$(pwd)/meili_data:/meili_data" \
-e MEILI_ENV=development \
-e MEILI_MASTER_KEY='dev-master-key-change-me-32chars!!' \
getmeili/meilisearch:v1.11
# 헬스 체크
curl -s http://127.0.0.1:7700/health
# 기대 응답: {"status":"available"}
팀 공유용으로는 도커 컴포즈가 편하다. 아래 파일을 프로젝트 루트에 두고 docker compose up -d 하면 동일 환경이 재현된다.
DB가 진실 공급원(source of truth)이고, 메이리서치는 파생 인덱스라는 점을 기억하자. 상품 생성·수정·삭제 트랜잭션이 끝난 직후 동기화 함수를 호출하거나, 아웃박스 패턴으로 비동기 반영한다. 동기화가 한두 번 실패해도 주문이 깨지지 않게 설계하는 것이 핵심이다.
검색·필터·정렬 실전 패턴
검색 품질은 "어떤 필드를 검색·필터·정렬 가능하게 열었는가"에서 갈린다. 기본값만 쓰면 필터 쿼리가 거절된다. 인덱스 설정에서 filterableAttributes·sortableAttributes를 먼저 선언한다.
인덱스 설정 + 필터 검색 예시
import { productsIndex } from './meili.js'
// 1) 한 번만 설정 (배포 스크립트 또는 마이그레이션)
await productsIndex.updateSettings({
searchableAttributes: ['name', 'brand', 'description'],
filterableAttributes: ['category', 'brand', 'price', 'stock'],
sortableAttributes: ['price', 'stock', 'updatedAt'],
displayedAttributes: ['id', 'name', 'brand', 'price', 'category', 'stock'],
})
// 2) 검색 API
export async function searchProducts({ q, category, maxPrice, page = 1 }) {
const limit = 20
const offset = (page - 1) * limit
const filters = []
if (category) filters.push(`category = "${category}"`)
if (typeof maxPrice === 'number') filters.push(`price <= ${maxPrice}`)
const result = await productsIndex.search(q || '', {
filter: filters.length ? filters.join(' AND ') : undefined,
sort: ['price:asc'],
limit,
offset,
attributesToHighlight: ['name', 'description'],
})
return {
hits: result.hits,
estimatedTotalHits: result.estimatedTotalHits,
processingTimeMs: result.processingTimeMs,
}
}
필터 문법은 SQL과 비슷하다. AND/OR, 비교 연산, 배열 필드 소속을 지원한다. 페이지네이션은 offset+limit 또는 page 파라미터를 쓴다. 하이라이트 필드를 켜면 UI에서 매칭 구간을 강조할 수 있다.
빈 쿼리(q: '') + 필터만으로 "카테고리 전체 목록 정렬"도 가능하다. 카탈로그 브라우징과 검색을 한 엔진으로 묶을 때 유용하다.
랭킹 규칙과 동의어 — 검색 품질 다듬기
기본 랭킹도 쓸 만하지만, 이커머스·콘텐츠 사이트는 동의어와 커스텀 랭킹 규칙을 손보면 체감이 확 달라진다. 예를 들어 "노트북" 검색에 "울트라북·랩탑"을 묶고, 재고 있는 상품·신상품을 살짝 위로 올린다.
동의어·랭킹 규칙 업데이트
import { productsIndex } from './meili.js'
await productsIndex.updateSettings({
synonyms: {
노트북: ['울트라북', '랩탑', 'laptop', 'notebook'],
키보드: ['keyboard', '기계식'],
},
// 왼쪽일수록 우선순위 높음. 기본 규칙을 복사한 뒤 커스텀 필드를 끼운다.
rankingRules: [
'words',
'typo',
'proximity',
'attribute',
'sort',
'exactness',
'stock:desc', // 재고 많은 상품 우선 (예시)
],
typoTolerance: {
enabled: true,
minWordSizeForTypos: {
oneTypo: 4,
twoTypos: 8,
},
},
stopWords: ['및', '의', '를', '을', 'the', 'a'],
})
팁: 랭킹 규칙을 자주 바꾸면 A/B로 검색 클릭률을 비교하는 것이 좋다. 커스텀 규칙(stock:desc 등)을 너무 앞에 두면 관련도가 무너질 수 있다. 보통 words·typo·proximity 뒤에 두는 편이 안전하다.
Next.js App Router 연동 — API 라우트 패턴
브라우저에 마스터 키를 넣으면 안 된다. 검색 요청은 서버 API 라우트가 받아서 메이리서치에 프록시한다. 검색 전용 API 키를 별도로 발급하면 권한을 더 좁힐 수 있다.
버전은 배포 시점에 공식 도커 태그를 확인한 뒤 고정하는 것을 권장한다. 이 글의 예시는 v1.11 계열을 기준으로 했다.
자주 묻는 질문
메이리서치와 일래스틱서치 중 무엇을 고를까?
제품·사이트 내 검색, 운영 인력 1~2명, 빠른 설치가 목표면 메이리서치가 적합하다. 대규모 로그 분석, 복잡한 집계·파이프라인, 기존 ELK 스택이 있으면 일래스틱서치·오픈서치가 맞다. 둘을 혼용하는 팀도 있다(앱 검색 vs 관측).
알고리아 대신 셀프호스팅하는 이유는?
알고리아는 관리형 품질·속도가 뛰어나지만 레코드·검색 수 과금이 부담될 수 있다. 데이터 주권·비용 예측·온프레 요구가 있으면 메이리서치 셀프호스팅이 유리하다. 반대로 인프라를 전혀 만지기 싫으면 알고리아·메이리 클라우드 같은 관리형이 낫다.
PostgreSQL full-text search만으로 부족한가?
단순 키워드·언어 설정이 고정된 내부 도구는 PG FTS로 충분한 경우가 많다. 오타 허용, 타이핑 즉시 검색, 동의어·랭킹 튜닝, 프론트 하이라이트가 필요해지면 전용 엔진이 생산성이 높다. 트래픽이 커질수록 LIKE/FTS 부하를 검색 엔진으로 분리하는 편이 안전하다.
인덱스와 DB를 어떻게 동기화하나?
DB 커밋 후 증분 업서트, 실패 시 재시도 큐, 야간 전체 리인덱스를 기본 세트로 둔다. 이벤트 소싱·CDC가 있으면 그 스트림을 소비해 문서를 갱신한다. 검색 결과가 "조금 늦은" 것은 허용하고, 주문·결제 같은 정합성이 중요한 흐름은 DB를 직접 보게 한다.
멀티 테넌트 SaaS에서 데이터 격리 방법은?
테넌트마다 인덱스를 나누거나, 단일 인덱스에 tenantId 필터를 서버가 강제하는 방식이 일반적이다. 클라이언트가 보낸 tenantId를 그대로 믿지 말고, 세션·JWT의 테넌트 클레임으로 필터를 합성한다. 테넌트 수·문서 규모에 따라 인덱스 분리 비용을 저울질한다.
한글 형태소 분석기가 별도로 필요한가?
많은 제품 검색 시나리오는 기본 설정 + 동의어 + searchable 필드 설계로 충분히 시작한다. 법률·의료처럼 전문 용어 분해가 중요하면 토크나이징·정규화 전략을 문서화하고 테스트 쿼리 세트를 만든다. 무조건 외부 형태소 분석기를 붙이기보다, 실제 실패 쿼리 로그를 보고 보강하는 편이 효율적이다.
Meilisearch메이리서치검색 엔진DockerNode.jsNext.js오픈소스APISDK풀텍스트 검색