Meilisearch 설계와 튜닝: 랭킹 규칙, 오타 허용·동의어, 다국어, Algolia와 비교

이 글의 핵심

Meilisearch는 Rust 기반의 오픈소스 검색 엔진으로, 밀리초 단위 응답과 오타 허용·동의어·필터·커스텀 랭킹을 API로 제어할 수 있습니다. 이 글에서는 핵심 개념, 인덱싱과 필터, 검색 파라미터와 랭킹, 다국어, Next.js·React 통합, Algolia와의 선택 기준을 한 흐름으로 정리합니다.

Meilisearch는 Rust로 구현된 오픈소스 검색 엔진으로, 전자상거래, 콘텐츠 사이트, SaaS 대시보드에서 흔히 필요한 입력 즉시 반응하는 검색을 단일 바이너리로 제공합니다. 데이터베이스의 LIKE나 기본 전문 검색(full-text search)으로는 다루기 어려운 랭킹, 오타 허용(typo tolerance), 필터 조합을 인덱스 설정과 검색 파라미터로 선언적으로 제어할 수 있다는 점이 장점입니다.

이 글은 인덱스, 문서, 태스크 같은 핵심 개념에서 시작해 인덱싱과 필터, 랭킹 규칙, 오타 허용과 동의어, 다국어, Next.js·React 연동, Algolia와의 비교 순으로 설명합니다. REST·JSON과 역인덱스 같은 기본 검색 용어에 익숙하다고 가정합니다.


Meilisearch가 해결하는 문제

서비스의 검색창은 문자열 매칭보다 사용자가 찾으려던 결과를 빠르게 보여 주는 UX 문제에 가깝습니다. 입력할 때마다 결과가 바로 갱신되어야 하고, 철자 실수나 띄어쓰기 차이에도 결과가 나와야 하며, 재고, 가격, 카테고리, 권한 같은 필터와 함께 동작해야 합니다. 여기에 검색 전용 클러스터를 Elasticsearch 수준으로 운영할 여력이 없는 팀이라는 조건이 붙는 경우가 많습니다.

Meilisearch는 이런 제품 검색 성격의 워크로드에 맞춰 기본 랭킹, 오타 허용, 동의어를 포함한 단순한 모델을 제공합니다. 반대로 대규모 로그 분석, 복잡한 집계, 무거운 텍스트 분석 파이프라인을 한 엔진에 담으려는 용도에는 맞지 않습니다.


아키텍처 개요

전형적인 구성에서는 애플리케이션 DB가 원본 데이터(source of truth)이고, 배치나 CDC, 주기적 동기화로 변경된 레코드를 Meilisearch에 문서로 적재합니다. 사용자 검색은 Meilisearch API로 직접 보내거나 백엔드 프록시를 거칩니다.

Meilisearch는 JSON 문서를 저장하면서 검색용 역인덱스를 유지합니다. 문서 추가와 설정 변경은 모두 비동기 태스크로 처리되므로, 쓰기 요청이 성공 응답을 받은 직후에 검색하면 아직 반영되지 않았을 수 있습니다. DB처럼 쓰기 직후 읽기 일관성을 가정하지 말고 최종 일관성을 전제로 설계해야 합니다.


핵심 개념

인덱스(index)

인덱스는 문서의 논리적 묶음입니다. products, articles, users처럼 도메인 단위로 나누는 경우가 많고, 검색 가능 필드, 필터 가능 필드, 랭킹 규칙 같은 설정은 인덱스마다 독립적으로 둡니다.

문서(document)와 기본 키(primary key)

각 문서는 JSON 객체이고, 고유 식별자인 기본 키가 필요합니다. 기본 키를 명시하지 않으면 Meilisearch는 이름이 id로 끝나는 속성을 찾아 추론하는데, 그런 속성이 여러 개이거나 없으면 오류가 납니다. 첫 적재 때 primaryKey를 명시하고 애플리케이션의 PK와 1:1로 맞춰 두면 동기화와 삭제가 쉬워집니다.

설정(settings)과 재색인

검색 가능 필드와 그 순서, 필터 가능 필드, 정렬 가능 필드, 랭킹 규칙, 오타 설정, 동의어는 인덱스 설정으로 관리합니다. 필터 가능 필드 추가처럼 색인 구조를 바꾸는 설정 변경은 전체 문서의 재색인을 일으키며, 이것도 태스크로 처리됩니다. 문서가 많은 인덱스라면 설정을 바꾸고 태스크가 끝날 때까지 기다린 뒤 트래픽을 돌리는 절차를 배포 파이프라인에 넣어 두는 편이 안전합니다.

태스크(task)

문서 추가, 삭제, 설정 변경 요청은 즉시 taskUid를 돌려주고 큐에서 순서대로 처리됩니다. /tasks/{taskUid}로 상태(enqueued, processing, succeeded, failed)를 조회할 수 있고, 실패하면 응답의 error 필드에 원인이 담깁니다. 대량 적재 시에는 애플리케이션 쪽에서 배치 크기와 동시 요청 수를 조절해 큐가 계속 밀리지 않게 해야 합니다.

API 키와 보안 모델

MEILI_MASTER_KEY를 설정하고 실행하면 Meilisearch는 기본 검색 키(Default Search API Key)와 기본 관리 키(Default Admin API Key)를 만들어 둡니다. 필요하면 /keys API로 허용할 동작(search, documents.add 등)과 인덱스, 만료일을 지정한 키를 추가로 만들 수 있습니다. 마스터 키는 키 관리에만 쓰고 애플리케이션에서 직접 쓰지 않는 것이 원칙입니다.

브라우저에는 검색만 가능하고 대상 인덱스가 제한된 키만 노출하고, 문서 적재와 설정 변경은 서버에서만 합니다. 관리 키나 마스터 키가 프론트엔드 번들에 들어가면 누구나 인덱스를 지우거나 덮어쓸 수 있습니다. Next.js라면 Route Handler 뒤에 두거나, API 게이트웨이에서 세션을 검증한 뒤 Meilisearch로 프록시하는 구성이 일반적입니다.


인덱싱과 필터링

검색 가능 필드(searchable attributes)

기본값은 모든 필드가 검색 대상이지만, 그대로 두면 관련 없는 필드에서 매칭되어 결과가 흐려집니다. 제목, 이름, SKU처럼 정확히 맞아야 하는 필드를 앞에 두고, 본문과 설명은 뒤에 두며, 내부 메모나 원본 데이터 필드는 searchableAttributes에서 빼는 식으로 정리합니다. 이 배열의 순서는 아래에서 설명할 랭킹 규칙에서 필드 중요도로 쓰입니다.

필터 가능 필드(filterable attributes)

filter와 facets에 쓰려는 필드는 미리 filterableAttributes에 등록해야 합니다. 등록하지 않은 필드로 필터를 걸면 오류가 나므로, 카테고리, 브랜드, 재고 여부, 가격, 공개 범위처럼 필터 UI에 들어갈 필드를 설계 단계에서 정해 둡니다. 등록한 필드마다 별도의 색인 구조가 생기므로 쓰지 않는 필드까지 넣으면 색인 시간과 디스크 사용량이 늘어납니다.

정렬 가능 필드(sortable attributes)

관련도순 대신 최신순, 가격순, 인기순 정렬을 하려면 해당 필드를 sortableAttributes에 등록합니다. 인기도처럼 자주 바뀌는 값으로 정렬하려면 그 값이 바뀔 때마다 문서를 갱신해야 하므로, 실시간 반영 대신 주기적인 배치로 점수 필드를 업데이트하는 경우가 많습니다.

문서 적재 방식

초기 구축이나 전체 재구축은 배치 임포트로, 평소에는 바뀐 레코드만 증분으로 보냅니다. DB에서 삭제된 레코드는 Meilisearch에서도 deleteDocument로 지워야 하는데, 이 삭제 동기화를 빠뜨리면 이미 없는 상품이 검색되는 문제가 생깁니다. 동기화 누락을 허용할 수 없다면 아웃박스 패턴으로 변경 이벤트를 DB 트랜잭션과 함께 기록하고, 별도 워커가 Meilisearch에 반영하면서 실패 시 재시도하게 합니다.

로컬 실행과 최소 예시

Docker로 띄운 뒤 HTTP API로 문서를 넣을 수 있습니다. 아래는 개념 확인용 예시이며, 프로덕션에서는 TLS, 강한 마스터 키, 네트워크 격리를 적용해야 합니다.

# 개발용: 마스터 키는 예시이며 실제로는 16바이트 이상의 랜덤 값을 사용합니다.
docker run -d --name meili -p 7700:7700 \
  -e MEILI_MASTER_KEY=dev_master_key_change_me \
  -v $(pwd)/meili_data:/meili_data \
  getmeili/meilisearch:v1.10

curl -X POST 'http://localhost:7700/indexes/products/documents?primaryKey=id' \
  -H 'Authorization: Bearer dev_master_key_change_me' \
  -H 'Content-Type: application/json' \
  --data-binary '[
    { "id": 1, "title": "무선 키보드", "category": "peripherals", "price": 89000 },
    { "id": 2, "title": "유선 마우스", "category": "peripherals", "price": 35000 }
  ]'

이미지 태그는 latest 대신 특정 버전으로 고정하는 편이 좋습니다. Meilisearch는 메이저 버전이 바뀌거나 v1 안에서도 일부 버전을 넘어갈 때 데이터 디렉터리를 그대로 읽지 못해 덤프와 복원이 필요할 수 있습니다(버전별 릴리스 노트를 확인하세요). 응답의 taskUid로 색인 완료를 확인한 뒤, 검색 요청에서 filter(예: category = peripherals, category가 filterableAttributes에 있어야 함)와 sort(정렬 가능 필드가 등록된 경우)를 조합합니다.


검색 파라미터와 랭킹

기본 검색 동작

검색 요청에는 쿼리 문자열과 함께 필터, 정렬, 페이지 파라미터를 붙입니다. 입력 즉시 반응하는 검색창에서는 limit을 작게 잡고, 결과 목록 페이지에서는 아래에서 설명할 페이지네이션 방식을 고릅니다.

랭킹 규칙(ranking rules)

Meilisearch는 버킷 정렬 방식으로 결과를 정렬합니다. 첫 번째 규칙으로 문서를 여러 묶음으로 나누고, 같은 묶음 안의 동점을 다음 규칙이 다시 나누는 식입니다. 그래서 규칙 배열의 순서가 그대로 검색 결과의 성격을 정합니다. v1 계열의 기본 순서는 다음과 같습니다.

["words", "typo", "proximity", "attribute", "sort", "exactness"]
규칙역할
words쿼리 단어 중 더 많은 단어가 매칭된 문서를 우선합니다. 기본 matchingStrategy: "last"에서는 결과가 부족하면 쿼리의 뒤쪽 단어부터 빼고 다시 매칭합니다.
typo오타 수가 적은 매칭을 우선합니다.
proximity쿼리 단어들이 문서 안에서 서로 가깝게 나타날수록 우선합니다.
attributesearchableAttributes에서 앞에 있는 필드에서 매칭될수록, 그리고 필드 안에서 앞쪽에 매칭될수록 우선합니다.
sort검색 요청에 sort 파라미터가 있을 때만 동작합니다. 앞쪽에 둘수록 정렬이 관련도보다 우선합니다.
exactness쿼리와 정확히 같은 단어로 매칭된 문서를 우선합니다.

최근 버전에서는 attribute를 필드 순서만 보는 attributeRank와 필드 안 단어 위치만 보는 wordPosition으로 나눠 그 사이에 다른 규칙을 끼울 수 있습니다. 사용하는 버전의 문서에서 지원 여부를 확인하세요. popularity:desc 같은 커스텀 규칙은 보통 텍스트 관련도 규칙 뒤에 두어, 관련도가 같은 문서끼리만 비즈니스 지표로 순서를 정하게 합니다. 커스텀 규칙에 쓰는 필드는 sortableAttributes에 등록해야 합니다.

하이라이트와 크롭

결과 UI에서는 attributesToRetrieve로 필요한 필드만 받고, attributesToHighlight와 attributesToCrop으로 매칭 부분 강조와 스니펫을 만듭니다. 큰 본문 필드를 통째로 받지 않으면 응답 크기와 렌더링 비용이 함께 줄어듭니다.

캐싱

검색은 읽기 요청이라 CDN이나 서버 캐시를 고려할 수 있지만, 쿼리 문자열과 필터 조합이 다양한 B2C 검색에서는 캐시 키가 너무 많아져 적중률이 낮습니다. 반대로 결과가 자주 바뀌지 않는 내부 관리자 검색이나 인기 검색어 결과는 짧은 TTL의 캐시가 효과적입니다.


오타 허용과 동의어

오타 허용(typo tolerance)

Meilisearch는 기본적으로 5글자 이상인 단어에 오타 1개, 9글자 이상인 단어에 오타 2개까지 허용하고, 짧은 단어에는 오타를 허용하지 않습니다. 이 기준은 typoTolerance.minWordSizeForTypos로 바꿀 수 있고, disableOnWords로 특정 단어를, disableOnAttributes로 특정 필드를 오타 허용에서 뺄 수 있습니다.

제품 코드나 SKU처럼 한 글자 차이가 다른 상품을 뜻하는 필드는 disableOnAttributes에 넣는 것이 좋습니다. 그렇지 않으면 AB-1234를 검색했을 때 AB-1235가 함께 나와 사용자가 잘못된 상품을 고를 수 있습니다. 브랜드명처럼 표기가 여러 가지인 경우는 오타 허용보다 동의어로 묶는 편이 정확합니다.

동의어(synonyms)

동의어는 “노트북·랩탑·laptop”, “스마트폰·휴대폰”처럼 같은 뜻의 단어를 묶어 검색어를 확장합니다. 양방향으로 쓰려면 각 단어에서 다른 단어들로의 매핑을 모두 등록해야 합니다. 처음부터 사전을 크게 만들기보다, 검색 로그에서 결과가 0건인 쿼리를 모아 점진적으로 채우는 쪽이 유지보수하기 쉽습니다.

불용어(stop words)

영어의 “the”, “a”처럼 너무 흔해 결과를 구분하는 데 도움이 안 되는 단어를 stopWords로 지정하면 검색에서 무시됩니다. 다만 제품명에 이런 단어가 들어가는 도메인에서는 오히려 결과를 망칠 수 있으므로, 일반적인 불용어 목록을 그대로 넣지 말고 실제 쿼리로 확인합니다. 한국어 조사는 토크나이저가 단어와 분리하는지에 따라 불용어 효과가 달라지므로 실제 색인 결과로 검증해야 합니다.


다국어 지원

Meilisearch의 토크나이저(charabia)는 텍스트의 문자 체계와 언어를 감지해 언어별로 분절과 정규화를 적용합니다. 영어처럼 공백으로 단어가 나뉘는 언어는 기본 설정으로 충분한 경우가 많지만, 한국어, 일본어, 중국어처럼 분절이 어려운 언어는 결과 품질 차이가 큽니다. 짧은 문자열에서는 언어 감지가 틀릴 수 있으므로(한자가 섞인 일본어를 중국어로 판단하는 등), 최근 버전에서는 localizedAttributes 설정과 검색 시 locales 파라미터로 필드의 언어를 명시할 수 있습니다.

여러 언어 콘텐츠를 다루는 방법은 크게 둘입니다. 한 인덱스에서 title_ko, title_en처럼 필드를 나누고 검색 가능 필드 순서를 조정하는 방법과, 로케일별로 인덱스를 나누어 랭킹, 동의어, 불용어를 따로 관리하는 방법입니다. 언어마다 동의어와 불용어가 달라지는 경우가 많아 인덱스 분리가 관리하기 쉬운 편입니다. 한국어 서비스에서는 사용자 입력의 유니코드 정규화(NFC와 NFD 차이로 macOS에서 입력한 한글이 자모 단위로 분리되는 문제)도 검색 전에 처리해야 합니다.


Next.js·React 연동

JavaScript SDK

서버와 브라우저 모두 meilisearch 패키지로 클라이언트를 만듭니다. 호스트 URL과 API 키는 환경 변수로 분리합니다.

// lib/meilisearch.ts: 서버 전용
import { MeiliSearch } from 'meilisearch';

export function getServerMeiliClient() {
  const host = process.env.MEILI_HOST;
  const apiKey = process.env.MEILI_API_KEY;
  if (!host || !apiKey) {
    throw new Error('Meilisearch 환경 변수가 설정되지 않았습니다.');
  }
  return new MeiliSearch({ host, apiKey });
}

최근 SDK 버전에서는 클래스 이름이 Meilisearch로 바뀌었으므로, 설치한 버전의 내보내기 이름을 확인하세요.

Route Handler로 프록시

Next.js App Router에서는 클라이언트가 Next.js API를 호출하고, 서버가 Meilisearch를 호출하는 구조로 키를 숨깁니다.

// app/api/search/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getServerMeiliClient } from '@/lib/meilisearch';

const ALLOWED_INDEXES = new Set(['products', 'articles']);

export async function GET(req: NextRequest) {
  const q = req.nextUrl.searchParams.get('q') ?? '';
  const indexUid = req.nextUrl.searchParams.get('index') ?? 'products';

  if (!ALLOWED_INDEXES.has(indexUid)) {
    return NextResponse.json({ error: 'unknown index' }, { status: 400 });
  }
  if (!q.trim()) {
    return NextResponse.json({ hits: [] });
  }

  const client = getServerMeiliClient();
  const result = await client.index(indexUid).search(q, {
    limit: 20,
    attributesToRetrieve: ['id', 'title', 'price', 'category'],
  });

  return NextResponse.json(result);
}

인덱스 이름을 쿼리 파라미터로 받는다면 반드시 허용 목록으로 검사해야 합니다. 서버 키는 모든 인덱스를 검색할 수 있으므로, 검사 없이 받은 값을 그대로 넘기면 클라이언트가 index=users처럼 노출하면 안 되는 인덱스를 검색할 수 있습니다. 이 계층에 레이트 리밋, 인증, 감사 로그를 추가하기도 쉽습니다.

InstantSearch 연동

Algolia의 InstantSearch에 익숙하다면 @meilisearch/instant-meilisearch 어댑터로 같은 위젯을 쓸 수 있습니다.

import { instantMeiliSearch } from '@meilisearch/instant-meilisearch';
import { InstantSearch, SearchBox, Hits, Highlight } from 'react-instantsearch';

const { searchClient } = instantMeiliSearch(
  process.env.NEXT_PUBLIC_MEILI_HOST!,
  process.env.NEXT_PUBLIC_MEILI_SEARCH_KEY! // 반드시 검색 전용 키
);

function Hit({ hit }: { hit: any }) {
  return <Highlight attribute="title" hit={hit} />;
}

export function ProductSearch() {
  return (
    <InstantSearch searchClient={searchClient} indexName="products">
      <SearchBox />
      <Hits hitComponent={Hit} />
    </InstantSearch>
  );
}

NEXT_PUBLIC_* 변수는 번들에 그대로 들어가므로, 여기에는 검색 권한만 있고 대상 인덱스가 제한된 키만 넣어야 합니다. 이 방식에서는 attributesToRetrieve도 클라이언트가 바꿀 수 있으므로, 노출하면 안 되는 필드는 애초에 인덱스에 넣지 않거나 displayedAttributes 설정에서 빼야 합니다. Algolia 전용 확장 위젯에 깊게 의존하고 있다면 일부는 대체 구현이 필요합니다.

디바운스와 요청 취소

직접 검색 UI를 만든다면 입력마다 요청이 나가므로 디바운스와 AbortController로 이전 요청을 취소해야 합니다. 그러지 않으면 늦게 도착한 이전 쿼리의 응답이 최신 결과를 덮어쓰는 경쟁 조건이 생깁니다.


파셋, 페이지네이션 한계, 멀티테넌시, 문서 갱신 방식

파셋(facet)은 “카테고리별 결과 개수”처럼 필터 UI 옆에 붙는 집계입니다. filterableAttributes에 등록된 필드만 facets 파라미터로 요청할 수 있고, 응답의 facetDistribution에 값별 개수가 담깁니다. 필터를 건 상태의 개수가 필요하면 같은 요청에 filter를 함께 보내면 됩니다.

페이지네이션에는 기억해 둘 한계가 있습니다. 기본 설정에서 limit/offset으로 접근할 수 있는 결과는 maxTotalHits(기본 1000개)까지이고, 이때 응답의 estimatedTotalHits는 정확한 수가 아니라 추정치입니다. “총 3,245건, 1/163페이지”처럼 정확한 전체 개수와 페이지 번호가 필요한 UI라면 page/hitsPerPage 파라미터를 써야 totalHits와 totalPages가 정확하게 옵니다. 검색 엔진은 앞쪽의 관련도 높은 결과를 보여 주는 도구라서, 수천 번째 결과까지 넘겨야 하는 화면이라면 필터를 더 좁히도록 UI를 설계하는 편이 맞습니다.

멀티테넌트 SaaS에서 사용자별로 결과를 나눌 때, 클라이언트가 filter: 'tenant_id = 123'을 직접 보내게 하면 개발자 도구로 값을 바꿔 다른 고객의 데이터를 검색할 수 있습니다. 테넌트 토큰(tenant token)은 필터 조건을 서버에서 서명해 토큰 안에 넣기 때문에 클라이언트가 조작할 수 없습니다. 서버에서 검색 권한 API 키로 토큰을 만들어 내려주고, 브라우저는 그 토큰으로만 검색합니다. 만료 시간을 짧게 주면 권한이 회수된 사용자의 토큰도 곧 무효화됩니다.

import { generateTenantToken } from 'meilisearch/token';
const token = await generateTenantToken({
  apiKey: searchKey, apiKeyUid: searchKeyUid,
  searchRules: { products: { filter: `tenant_id = ${tenantId}` } },
  expiresAt: new Date(Date.now() + 60 * 60 * 1000),
});

tenantId는 클라이언트 입력이 아니라 서버 세션에서 꺼낸 값이어야 합니다. 토큰 생성 함수의 위치와 시그니처는 SDK 버전에 따라 다르므로 설치한 버전의 문서를 확인하세요.

문서 갱신 방식은 이름 때문에 헷갈리기 쉽습니다. addDocuments는 같은 기본 키 문서를 통째로 교체하고(보내지 않은 필드는 사라짐), updateDocuments는 보낸 필드만 부분 수정합니다. 가격만 바꾸려고 addDocuments에 { id, price }만 보내면 제목과 설명이 지워집니다. 대량 적재는 배치로 나누되 기준은 문서 수보다 페이로드 크기가 맞습니다. 설명 텍스트가 긴 상품 데이터는 같은 1,000개라도 태스크 처리 시간이 길어져 큐가 밀릴 수 있으니, /tasks로 처리 시간을 보며 배치 크기를 조정합니다.


Algolia vs Meilisearch

어느 쪽이 항상 낫다기보다 조직의 제약에 따라 선택이 갈립니다.

관점AlgoliaMeilisearch
형태상용 SaaS, 사용량 기반 과금오픈소스 자체 호스팅 또는 Meilisearch Cloud
운영인프라 부담이 거의 없음직접 운영 시 백업, 업그레이드, 모니터링 필요
생태계오래된 UI 라이브러리와 통합, 자료가 많음InstantSearch 호환 어댑터 등으로 확장 중
기능 범위개인화, A/B 테스트, 분석 등 상용 기능핵심 검색 기능 중심의 단순한 API
데이터 위치Algolia 클라우드온프레미스나 자체 VPC에 둘 수 있음

검색이 매출과 직결되고 SLA, 기술 지원, 전 세계 분산 인프라가 필요하며 운영 인력을 들이고 싶지 않다면 Algolia가 맞습니다. 비용, 데이터 위치, 온프레미스 요구가 중요하고 팀이 검색 서버 하나를 직접 운영할 수 있다면 Meilisearch가 맞습니다.

Algolia에서 Meilisearch로 옮길 때는 랭킹 규칙, 동의어, 필터 문법이 다르므로 실제 검색 로그의 쿼리로 결과를 비교하는 PoC가 필요합니다. API 모양이 비슷하다고 결과가 같지는 않습니다.


역인덱스와 태스크 큐 관점의 내부 동작

문서를 색인하면 엔진은 필드별로 토큰화된 단어에서 문서 ID 목록으로 가는 역인덱스를 갱신합니다. Meilisearch는 이 데이터를 메모리 매핑된 LMDB 파일에 저장하므로, 인덱스 크기에 비해 메모리가 넉넉하면 대부분의 읽기가 디스크 I/O 없이 페이지 캐시에서 처리되어 짧은 응답 시간을 냅니다. 반대로 메모리가 부족하면 검색이 디스크를 읽게 되어 느려집니다.

문서 추가와 설정 변경은 디스크와 CPU를 많이 쓰는 색인 작업이므로 태스크 큐로 순서대로 처리됩니다. 그래서 운영 지표로는 검색 지연과 QPS뿐 아니라 태스크 대기 시간과 실패율도 봐야 합니다. 대량 마이그레이션 직후 검색 결과가 이상하면 색인이 아직 끝나지 않았는지부터 확인합니다.

무중단 설정 변경과 동기화

설정을 크게 바꿀 때는 products_new 같은 새 인덱스를 만들어 설정과 문서를 모두 넣고, 색인이 끝나면 /swap-indexes API로 기존 인덱스와 원자적으로 맞바꿀 수 있습니다. 애플리케이션 코드의 인덱스 이름을 바꿀 필요가 없고, 교체 순간에도 검색이 끊기지 않습니다.

적재 쪽에서는 배치 크기와 동시 요청 수를 제한하고 실패 시 지수 백오프로 재시도합니다. DB에서 지운 레코드가 검색에 남는 것은 사용자가 바로 알아차리는 문제이므로, 주기적으로 DB의 ID 목록과 인덱스의 문서 ID를 비교해 어긋난 문서를 정리하는 작업을 두면 좋습니다.


트러블슈팅

증상흔한 원인점검
검색 결과가 비어 있음색인이 아직 끝나지 않음, 필터 조건이 과도함태스크 상태, filterableAttributes 등록 여부
필터를 걸면 오류필드가 filterableAttributes에 없음설정 확인 후 재색인 완료 대기
응답이 느림큰 문서를 통째로 반환, 메모리 부족으로 디스크 읽기attributesToRetrieve 축소, 인덱스 크기 대비 메모리
오타 허용이 과하거나 부족함typoTolerance 설정스테이징에서 실제 쿼리로 비교
일부 필드가 사라짐부분 갱신에 addDocuments 사용updateDocuments로 변경

업그레이드 전에는 덤프(/dumps)를 만들어 두고, 버전을 올릴 때는 릴리스 노트에서 데이터 호환성과 설정 이름 변경을 확인합니다. 인덱스는 원본 DB에서 다시 만들 수 있더라도, 설정과 동의어 사전은 별도로 버전 관리해 두어야 재구축이 빠릅니다.


같이 보면 좋은 글