Meilisearch 완벽 가이드 — 초고속 오픈소스 검색 엔진, Elasticsearch 대체
이 글의 핵심
Meilisearch는 "검색이 빨라야 한다"는 명제를 Rust로 구현한 오픈소스 검색 엔진입니다. Elasticsearch가 Java·복잡한 설정·무거운 인프라를 요구하는 반면, Meilisearch는 단일 바이너리·설정 제로·밀리초 응답·오타 허용으로 "개발자 친화적 검색"을 제공합니다. 2019년 출시 후 GitHub Star 50k+, Algolia의 오픈소스 대안으로 자리잡았습니다.
설치
Docker
docker run -d -p 7700:7700 \
-e MEILI_MASTER_KEY=your-secret-key \
-v $(pwd)/meili_data:/meili_data \
getmeili/meilisearch:latest
바이너리
# macOS/Linux
curl -L https://install.meilisearch.com | sh
# Windows (PowerShell)
Invoke-WebRequest -Uri https://github.com/meilisearch/meilisearch/releases/latest/download/meilisearch-windows-amd64.exe -OutFile meilisearch.exe
# 실행
./meilisearch --master-key="your-secret-key"
첫 검색
인덱스 생성 + 문서 추가
curl -X POST 'http://localhost:7700/indexes/movies/documents' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer your-secret-key' \
--data-binary @movies.json
// movies.json
[
{
"id": 1,
"title": "The Shawshank Redemption",
"genres": ["Drama"],
"year": 1994
},
{
"id": 2,
"title": "The Godfather",
"genres": ["Crime", "Drama"],
"year": 1972
}
]
검색
curl 'http://localhost:7700/indexes/movies/search?q=godfather' \
-H 'Authorization: Bearer your-secret-key'
JavaScript SDK
npm install meilisearch
import { MeiliSearch } from 'meilisearch';
const client = new MeiliSearch({
host: 'http://localhost:7700',
apiKey: 'your-secret-key',
});
// 인덱스 생성
const index = client.index('movies');
// 문서 추가
await index.addDocuments([
{ id: 1, title: 'Inception', year: 2010 },
{ id: 2, title: 'Interstellar', year: 2014 },
]);
// 검색
const results = await index.search('inter');
console.log(results.hits);
검색 옵션
Typo Tolerance
await index.search('intersttlar'); // 'Interstellar' 찾음
자동 오타 수정 (기본 활성화).
필터
await index.search('movie', {
filter: 'year > 2010 AND genres = "Action"',
});
// 인덱스 설정에서 필터 가능한 속성 지정
await index.updateFilterableAttributes(['year', 'genres']);
정렬
await index.search('', {
sort: ['year:desc'],
});
// 정렬 가능한 속성 지정
await index.updateSortableAttributes(['year', 'rating']);
Faceting (집계)
const results = await index.search('', {
facets: ['genres'],
});
console.log(results.facetDistribution);
// { genres: { "Action": 10, "Drama": 15 } }
// Facet 가능한 속성 지정
await index.updateFilterableAttributes(['genres']);
Highlighting
const results = await index.search('inter', {
attributesToHighlight: ['title'],
});
results.hits.forEach((hit) => {
console.log(hit._formatted.title);
// '<em>Inter</em>stellar'
});
Pagination
await index.search('movie', {
limit: 20,
offset: 40,
});
특정 필드만 검색
await index.search('nolan', {
attributesToSearchOn: ['director'],
});
// 검색 가능한 속성 순서
await index.updateSearchableAttributes(['title', 'director', 'description']);
인덱스 설정
동의어
await index.updateSynonyms({
'wolverine': ['xmen', 'logan'],
'batman': ['dark knight'],
});
Stop Words
await index.updateStopWords(['the', 'a', 'an']);
Ranking Rules
await index.updateRankingRules([
'words', // 검색어 매치 수
'typo', // 오타 수정 정도
'proximity', // 검색어 간 거리
'attribute', // 속성 순서
'sort', // 정렬
'exactness', // 정확도
]);
InstantSearch (React)
npm install react-instantsearch-dom algoliasearch instantsearch-meilisearch
import React from 'react';
import { InstantSearch, SearchBox, Hits, Configure } from 'react-instantsearch-dom';
import { instantMeiliSearch } from '@meilisearch/instant-meilisearch';
const searchClient = instantMeiliSearch(
'http://localhost:7700',
'your-search-key'
);
function Search() {
return (
<InstantSearch searchClient={searchClient} indexName="movies">
<Configure hitsPerPage={10} />
<SearchBox />
<Hits hitComponent={Hit} />
</InstantSearch>
);
}
function Hit({ hit }) {
return (
<div>
<h2>{hit.title}</h2>
<p>{hit.year}</p>
</div>
);
}
export default Search;
Next.js 통합
// app/search/page.tsx
'use client';
import { MeiliSearch } from 'meilisearch';
import { useState, useEffect } from 'react';
const client = new MeiliSearch({
host: process.env.NEXT_PUBLIC_MEILISEARCH_HOST!,
apiKey: process.env.NEXT_PUBLIC_MEILISEARCH_KEY!,
});
export default function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) return;
const search = async () => {
const index = client.index('movies');
const { hits } = await index.search(query);
setResults(hits);
};
const timer = setTimeout(search, 300);
return () => clearTimeout(timer);
}, [query]);
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search movies..."
/>
<ul>
{results.map((hit: any) => (
<li key={hit.id}>{hit.title}</li>
))}
</ul>
</div>
);
}
대량 데이터 추가
import { MeiliSearch } from 'meilisearch';
const client = new MeiliSearch({ host, apiKey });
const index = client.index('products');
const batchSize = 1000;
const products = [...]; // 대량 데이터
for (let i = 0; i < products.length; i += batchSize) {
const batch = products.slice(i, i + batchSize);
await index.addDocuments(batch);
}
업데이트 vs 교체
// 문서 추가 또는 업데이트
await index.addDocuments([{ id: 1, title: 'Updated' }]);
// 문서 완전 교체
await index.updateDocuments([{ id: 1, title: 'Replaced' }]);
// 문서 삭제
await index.deleteDocument(1);
await index.deleteDocuments([1, 2, 3]);
보안 (API Key)
Master Key
서버 전용, 모든 권한.
Search Key (Public)
클라이언트 노출 가능, 검색만 가능.
# Search Key 생성
curl -X POST 'http://localhost:7700/keys' \
-H 'Authorization: Bearer master-key' \
-H 'Content-Type: application/json' \
--data-binary '{
"description": "Search movies",
"actions": ["search"],
"indexes": ["movies"],
"expiresAt": null
}'
Tenant Token (멀티 테넌시)
import { generateTenantToken } from 'meilisearch';
const token = generateTenantToken(
'your-search-key',
{ filters: `user_id = ${userId}` },
{ apiKey: 'your-master-key' }
);
// 클라이언트에 전달
const client = new MeiliSearch({ host, apiKey: token });
유저별로 검색 결과를 자동 필터링.
Docker Compose
version: '3.8'
services:
meilisearch:
image: getmeili/meilisearch:latest
ports:
- "7700:7700"
environment:
- MEILI_MASTER_KEY=${MEILI_MASTER_KEY}
- MEILI_ENV=production
volumes:
- ./meili_data:/meili_data
프로덕션 배포
Reverse Proxy (Caddy)
search.example.com {
reverse_proxy localhost:7700
}
환경변수
MEILI_MASTER_KEY=your-secret-key
MEILI_ENV=production
MEILI_DB_PATH=/var/lib/meilisearch/data
MEILI_HTTP_ADDR=0.0.0.0:7700
성능 최적화
인덱스 설정
// 검색 불필요한 속성 제외
await index.updateSearchableAttributes(['title', 'description']);
// 표시 속성 제한
await index.updateDisplayedAttributes(['id', 'title', 'year']);
페이지네이션
// Offset 방식 (간단)
await index.search('query', { limit: 20, offset: 20 });
// Cursor 방식 (대용량)
// Meilisearch는 offset만 지원, cursor는 직접 구현
모니터링
curl 'http://localhost:7700/stats' \
-H 'Authorization: Bearer master-key'
{
"databaseSize": 1048576,
"lastUpdate": "2024-01-01T00:00:00Z",
"indexes": {
"movies": {
"numberOfDocuments": 1000,
"isIndexing": false,
"fieldDistribution": { ... }
}
}
}
트러블슈팅
검색 결과 없음
- 인덱스 이름 확인
searchableAttributes설정 확인- 문서 추가 완료 대기 (Task API)
필터링 안 됨
updateFilterableAttributes설정 확인- 필터 문법 검증 (
year > 2010)
성능 느림
- 인덱스 크기 확인 (
/stats) - 검색 속성 줄이기
- 서버 메모리 증설
API Key 에러
- Master Key vs Search Key 구분
- 환경변수
MEILI_MASTER_KEY확인
체크리스트
- Meilisearch 설치 (Docker or Binary)
- Master Key 설정
- 인덱스 생성 + 문서 추가
-
searchableAttributes·filterableAttributes설정 - Search Key 생성 (Public용)
- 클라이언트 통합 (React·Vue·Next.js)
- Typo tolerance·Highlighting 활성화
- 프로덕션 배포 (Reverse Proxy + SSL)
마무리
Meilisearch는 “검색 UX에 집중”한 오픈소스 검색 엔진입니다. Elasticsearch의 복잡함·Algolia의 비용을 피하고, Rust 성능 + 오타 허용 + instant search로 사용자 친화적 검색을 제공합니다. 2026년 현재 e-commerce·블로그·문서 사이트에서 빠르게 확산 중이고, Docker 한 줄 + SDK 몇 줄로 프로덕션급 검색 기능을 즉시 구축할 수 있습니다. Algolia 비용이 부담되거나 Elasticsearch가 과하다면, 지금 바로 Meilisearch로 전환해보세요.
관련 글
- Elasticsearch 완벽 가이드
- Typesense 가이드
- Algolia 가이드
- Docker 완벽 가이드