AACsearch
Руководства по миграции

С Meilisearch

Миграция с Meilisearch в AACsearch — экспорт индекса, маппинг схемы, перевод запросов.

С Meilisearch

Meilisearch и AACsearch похожи концептуально — типо-толерантный поиск с чистым API, без шардирования. Миграция — в основном механика: экспорт, реингест, перенастройка ранжирования. На типичный каталог — полдня.

С self-hosted Typesense — From Typesense, там почти identity-миграция.

Маппинг кратко

MeilisearchAACsearch
IndexIndex (SearchIndex + Typesense collection)
Primary key (primaryKey)id (string)
Searchable attributesquery_by на запросе
Displayed attributesinclude_fields на запросе
Filterable attributesПоля с facet: true или index: true
Sortable attributesПоля в sort_by — int / float / bool
Distinct attributegroup_by на запросе
Ranking rulesquery_by_weights + sort_by
Stop wordsСейчас фиксируется языковыми дефолтами коллекции
SynonymsSynonym sets (SearchIndexSynonym)
Facetingfacet_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 → у нас задаётся токенизером коллекции; разумные дефолты.
  • rankingRulesquery_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: Перевод запросов

MeilisearchAACsearch
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:&lt;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. distinctAttributegroup_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), и на запрос (имя поля). Сверяйте оба.
  • Не перенесли синонимы. Они вне документа — легко забыть.

См. также

On this page