С Meilisearch
Миграция с Meilisearch в AACsearch — экспорт индекса, маппинг схемы, перевод запросов.
С Meilisearch
Meilisearch и AACsearch похожи концептуально — типо-толерантный поиск с чистым API, без шардирования. Миграция — в основном механика: экспорт, реингест, перенастройка ранжирования. На типичный каталог — полдня.
С self-hosted Typesense — From Typesense, там почти identity-миграция.
Маппинг кратко
| Meilisearch | AACsearch |
|---|---|
| Index | Index (SearchIndex + Typesense collection) |
Primary key (primaryKey) | id (string) |
| Searchable attributes | query_by на запросе |
| Displayed attributes | include_fields на запросе |
| Filterable attributes | Поля с facet: true или index: true |
| Sortable attributes | Поля в sort_by — int / float / bool |
| Distinct attribute | group_by на запросе |
| Ranking rules | query_by_weights + sort_by |
| Stop words | Сейчас фиксируется языковыми дефолтами коллекции |
| Synonyms | Synonym sets (SearchIndexSynonym) |
| Faceting | facet_by |
Шаг 1: Экспорт индекса
meilisearch-cli или dump API:
# Постранично, по одному документу в строке
curl "http://localhost:7700/indexes/products/documents?limit=1000" \
-H "Authorization: Bearer YOUR_API_KEY"
# Или dump инстанса
curl -X POST "http://localhost:7700/dumps" \
-H "Authorization: Bearer YOUR_API_KEY"Надёжнее — пройтись по offset/limit пока не вычерпаете, и писать в NDJSON:
import { writeFileSync, appendFileSync } from "node:fs";
const HOST = "http://localhost:7700";
const KEY = process.env.MEILI_KEY;
const INDEX = "products";
writeFileSync("/tmp/products.ndjson", "");
let offset = 0;
const limit = 1000;
while (true) {
const res = await fetch(
`${HOST}/indexes/${INDEX}/documents?offset=${offset}&limit=${limit}`,
{ headers: { Authorization: `Bearer ${KEY}` } },
);
const { results } = await res.json();
if (results.length === 0) break;
for (const doc of results) {
appendFileSync("/tmp/products.ndjson", JSON.stringify(doc) + "\n");
}
offset += limit;
}Шаг 2: Экспорт settings
curl "http://localhost:7700/indexes/products/settings" \
-H "Authorization: Bearer YOUR_API_KEY" \
> /tmp/products.settings.jsonПеренесите:
searchableAttributes→ поля вquery_byна запросе.filterableAttributes→ поля сindex: trueилиfacet: trueв схеме.sortableAttributes→ поля, допустимые вsort_by(число/bool).synonyms→ synonym sets AACsearch.stopWords→ у нас задаётся токенизером коллекции; разумные дефолты.rankingRules→query_by_weights+ продуманныйsort_by.
Шаг 3: Схема AACsearch
Документы Meilisearch:
{
"id": "sku-123",
"name": "Running Shoe",
"description": "Cushioned…",
"price": 99.95,
"tags": ["sport", "outdoor"],
"available": true
}Схема:
await client.searchIndex.create.call({
slug: "products",
fields: [
{ name: "name", type: "string" },
{ name: "description", type: "string", optional: true },
{ name: "price", type: "float", facet: true },
{ name: "tags", type: "string[]", facet: true },
{ name: "available", type: "bool", facet: true },
{ name: "tenantId", type: "string", facet: true }, // для multi-tenant
],
defaultSortingField: "price",
});Если корпус Meilisearch single-tenant (один инстанс на клиента), tenantId можно пропустить.
Если консолидируете в один индекс AACsearch — поле обязательно, а на каждом браузерном запросе
должен быть scoped-токен.
Шаг 4: Ingest
Тот же паттерн, что в миграции с БД — батчи по 500 через searchDocument.bulkUpsert. Буфер впитывает.
Шаг 5: Перевод запросов
| Meilisearch | AACsearch |
|---|---|
POST /indexes/products/search { q: "running" } | POST /api/v1/indexes/products/search { q: "running", query_by: "name,description" } |
{ q: "running", filter: "price < 100 AND available = true" } | { q: "running", filter_by: "price:<100 && available:=true" } |
{ q: "running", facets: ["tags"] } | { q: "running", facet_by: "tags" } |
{ q: "running", sort: ["price:asc"] } | { q: "running", sort_by: "price:asc" } |
{ q: "running", attributesToHighlight: ["name"] } | { q: "running", highlight_full_fields: "name" } |
{ q: "running", limit: 20, offset: 40 } | { q: "running", per_page: 20, page: 3 } |
Заметные отличия:
- Синтаксис фильтров. У Meilisearch
=,<,AND; у AACsearch:=,:<,&&. Оба разумны; один — куда переводить. - Пагинация — page, не offset.
page(с 1) иper_page. - Multi-search. У обоих есть; форма JSON отличается — см. Multi-search.
- Distinct.
distinctAttribute→group_by; семантика близкая (один документ на группу).
Шаг 6: Тюнинг ранжирования
Дефолтное ранжирование Meilisearch — [words, typo, proximity, attribute, sort, exactness]. Эффективное ранжирование AACsearch — query_by_weights + sort_by. Что важно:
- Правило
attribute(поля раньше вsearchableAttributesранжируют выше) →query_by_weights:name=3,description=1. typoвоспроизводится дефолтной типо-толерантностью с порогами по длине.proximity(близость совпадений) — дефолт AACsearch; кодировать не нужно.
После ingest — валидация по 100-query сэмплу (см. обзор). Большая часть drift — из-за весов attribute.
Шаг 7: Обновление приложения
У Meilisearch свой SDK; у AACsearch:
@repo/search-client(браузер).@repo/api/client(server, oRPC).- Чистый HTTP — см. Search API.
Формы не drop-in. Самый похожий паттерн:
// Meilisearch
const result = await meili.index("products").search("running", {
filter: "price < 100",
sort: ["price:asc"],
});
// AACsearch (server-side, oRPC)
const result = await client.searchDocument.search.call({
indexSlug: "products",
q: "running",
filterBy: "price:<100",
sortBy: "price:asc",
});Валидация и cutover
Прогон чек-листа. Специфика для Meilisearch:
- Убедитесь, что synonym sets применены к новому индексу. Синонимы — не часть экспорта документов.
- Сверьте facet counts side-by-side. Обычно одинаковы; расхождение — отсутствует
facet: true. - Тестируйте порог типо-толерантности. Если на коротких запросах раньше был низкий, а у AACsearch дефолт — результаты различатся.
Частые ошибки
- Импорт без
tenantIdпри консолидации. Если переезжаете с per-tenant Meilisearch на общий индекс AACsearch — поле обязательно, scoped-токены должны быть готовы до cutover. - Filterable atributes только в схеме. Filterable влияет и на индекс документа (
index: true), и на запрос (имя поля). Сверяйте оба. - Не перенесли синонимы. Они вне документа — легко забыть.
См. также
- Обзор миграции
- Чек-лист
- From Typesense — ближе к identity-миграции