Meilisearch 완벽 가이드 — 초고속 오픈소스 검색 엔진, Elasticsearch 대체

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 완벽 가이드