Meilisearch, szybkie wyszukiwanie z tolerancją literówek
Meilisearch to napisany w Rust silnik wyszukiwania pełnotekstowego z własnym API HTTP, tolerancją literówek włączoną domyślnie i sortowaniem wyników po trafności. Wersja 1.53.1 ukazała się 13 sierpnia 2026 roku, repozytorium meilisearch/meilisearch ma około 59 tysięcy gwiazdek, a licencja jest mieszana: MIT dla rdzenia i Business Source License dla modułów Enterprise.
Co Meilisearch właściwie robi
Meilisearch przyjmuje dokumenty JSON, buduje z nich indeks i wystawia je przez REST API. Nie jest bazą danych, w której trzymasz stan aplikacji. Jest indeksem wtórnym, który musisz zasilać z prawdziwego źródła prawdy i utrzymywać w synchronizacji. Jeśli usuniesz jego katalog danych, nie tracisz danych biznesowych, tylko czas potrzebny na ponowne zaindeksowanie.
Zapis dokumentów jest asynchroniczny. Wywołanie dodające dokumenty zwraca zadanie z identyfikatorem, a właściwe indeksowanie dzieje się w kolejce w tle. To ma dwie konsekwencje. Po pierwsze, po zapisie dokument nie jest natychmiast wyszukiwalny i w testach musisz poczekać na zakończenie zadania. Po drugie, przy dużych partiach danych opłaca się wysyłać jedną dużą paczkę zamiast tysiąca małych, bo każda zmiana ustawień lub porcja dokumentów tworzy osobne zadanie.
Sercem trafności jest lista reguł rankingu, stosowanych po kolei jak sortowanie kubełkowe. Domyślna lista w wersji 1.53.1 wygląda tak: words, typo, proximity, attributeRank, sort, wordPosition, exactness. To istotna zmiana wobec starszych opisów, które wymieniają sześć reguł z pojedynczym attribute. Reguła attribute nadal jest akceptowana przy zapisie ustawień, ale domyślny zestaw dzieli ją teraz na dwie: rangę atrybutu i pozycję słowa w atrybucie. Jeśli kopiujesz konfigurację z tutoriala sprzed dwóch lat, sprawdź, co faktycznie zwraca GET /indexes/:uid/settings.
Tolerancja literówek działa domyślnie i jest sterowana progami długości słowa. Domyślnie jedna literówka jest dopuszczalna od pięciu znaków, a dwie od dziewięciu. Można ją wyłączyć globalnie, dla wybranych słów, dla wybranych atrybutów albo osobno dla ciągów zawierających cyfry.
Czego Meilisearch nie robi: nie wykonuje złączeń między indeksami w rozumieniu SQL, nie zastępuje bazy transakcyjnej i nie zapewnia sam z siebie wysokiej dostępności w wersji otwartej. Replikacja i podział na fragmenty to osobna historia licencyjna, opisana niżej.
Wersja, licencja i stan projektu
Projekt żyje. Wersja 1.53.1 wyszła 13 sierpnia 2026 roku, ostatnia zmiana w gałęzi głównej pochodzi z 14 sierpnia, repozytorium nie jest zarchiwizowane, ma 2672 rozgałęzienia i 311 otwartych zgłoszeń. Obraz getmeili/meilisearch w rejestrze Docker Hub przekroczył 48 milionów pobrań.
Licencja jest miejscem, w którym pięć źródeł podaje trzy różne odpowiedzi, więc warto zauważyć różnicę zanim wpiszesz cokolwiek do rejestru zależności. Plik LICENSE w repozytorium kończy się linią SPDX-License-Identifier: MIT AND BUSL-1.1. Plik Cargo.toml w katalogu głównym deklaruje w sekcji workspace.package po prostu license = "MIT". Odznaka w pliku README również mówi wyłącznie o MIT. Interfejs programistyczny GitHuba zwraca dla tego repozytorium NOASSERTION, czyli przyznaje się, że nie umie tego zaklasyfikować. Klienty w rejestrach pakietów, czyli meilisearch w wersji 0.60.0 na npm i meilisearch w wersji 0.43.0 na PyPI, są czystym MIT, co potwierdza plik LICENSE w opublikowanej paczce.
Stan faktyczny jest taki: rdzeń jest na MIT, a pliki oznaczone jako Enterprise Edition podlegają licencji BSL 1.1 w wersji dostosowanej przez Meili SAS. Dodatkowe zezwolenie w tej licencji brzmi jednoznacznie: użycie w celach nieprodukcyjnych, czyli testy, rozwój i ewaluacja. Użycie produkcyjne wymaga umowy komercyjnej. Licencją docelową po czterech latach od publikacji danej wersji jest MIT.
Które pliki to obejmuje, da się sprawdzić dokładnie, bo każdy z nich ma nagłówek This file is part of Meilisearch Enterprise Edition (EE). W wersji 1.53.1 jest ich osiem i dotyczą trzech obszarów: podziału danych na fragmenty w crates/milli/src/sharding/enterprise_edition.rs, sieci wielowęzłowej wraz z wyszukiwaniem federacyjnym przez sieć oraz migawek do magazynu zgodnego z S3. Innymi słowy, wszystko, co jest jednym procesem na jednej maszynie, zostaje na MIT. Skalowanie poziome i replikacja to obszar płatny, mimo że kod leży publicznie w tym samym repozytorium.
Instalacja i pierwszy indeks
Najprostszy start to jeden kontener i dwa wywołania curl.
# uruchomienie z kluczem administracyjnym i trwałym katalogiem danych
docker run -d --name meili -p 7700:7700 \
-e MEILI_MASTER_KEY=zmien_ten_klucz \
-v $PWD/meili_data:/meili_data \
getmeili/meilisearch:v1.53.1
# dodanie dokumentów, zwraca taskUid, nie gotowy indeks
curl -X POST 'http://localhost:7700/indexes/products/documents?primaryKey=id' \
-H 'Authorization: Bearer zmien_ten_klucz' \
-H 'Content-Type: application/json' \
--data '[{"id":1,"name":"Kurtka puchowa","brand":"Nordkapp","price":499,"inStock":true}]'
# sprawdzenie, czy zadanie się zakończyło
curl 'http://localhost:7700/tasks?limit=1' \
-H 'Authorization: Bearer zmien_ten_klucz'
# wyszukiwanie z literówką w zapytaniu
curl -X POST 'http://localhost:7700/indexes/products/search' \
-H 'Authorization: Bearer zmien_ten_klucz' \
-H 'Content-Type: application/json' \
--data '{"q":"kurtak puhowa","limit":10,"showRankingScore":true}'Klucz główny podany w MEILI_MASTER_KEY służy do administracji i nigdy nie powinien trafić do przeglądarki. Do wyszukiwania po stronie klienta generuje się osobny klucz z uprawnieniem search, a przy wielu najemcach dodatkowo token dzierżawcy z wbudowanym filtrem.
Klient JavaScript w wersji 0.60.0 nie ma zależności i wymaga Node w wersji ^20.19.0 || >=22.12.0. Ma też pułapkę migracyjną: eksportowana klasa nazywa się Meilisearch, a nie MeiliSearch z wielkim S, jak w starszych przykładach. Aliasu nie ma, więc stary import po prostu zwróci undefined.
import { Meilisearch } from 'meilisearch'
const client = new Meilisearch({
host: 'http://localhost:7700',
apiKey: process.env.MEILI_MASTER_KEY
})
const index = client.index('products')
// czekanie na zakończenie zadania, inaczej test szuka w pustym indeksie
await index.addDocuments(documents, { primaryKey: 'id' }).waitTask()
const results = await index.search('kurtak', {
limit: 20,
filter: 'inStock = true AND price < 600',
sort: ['price:asc'],
attributesToHighlight: ['name'],
showRankingScore: true
})Ustawienia indeksu, które decydują o trafności
Domyślna konfiguracja daje przyzwoity wynik na małym zbiorze i rozpada się na dużym. Trzy ustawienia robią największą różnicę: kolejność w searchableAttributes, lista filterableAttributes oraz limity stronicowania.
{
"searchableAttributes": ["name", "brand", "description"],
"filterableAttributes": ["brand", "price", "inStock", "categories"],
"sortableAttributes": ["price", "createdAt"],
"rankingRules": [
"words",
"typo",
"proximity",
"attributeRank",
"sort",
"wordPosition",
"exactness"
],
"typoTolerance": {
"enabled": true,
"minWordSizeForTypos": { "oneTypo": 5, "twoTypos": 9 },
"disableOnWords": ["Nordkapp"],
"disableOnAttributes": ["sku"],
"disableOnNumbers": true
},
"pagination": { "maxTotalHits": 1000 },
"faceting": { "maxValuesPerFacet": 100 },
"searchCutoffMs": 150
}Kolejność w searchableAttributes nie jest kosmetyczna. Reguła attributeRank porządkuje wyniki według tego, w którym atrybucie znalazło się dopasowanie, więc atrybut wymieniony pierwszy waży najwięcej. Wrzucenie długiego opisu przed nazwę produktu to najczęstsza przyczyna skarg na jakość wyników.
maxTotalHits domyślnie wynosi 1000 i jest twardym sufitem liczby zwracanych trafień, niezależnie od stronicowania. Przy budowie eksportu albo mapy strony trzeba go świadomie podnieść, bo inaczej dane po prostu urwą się w połowie. maxValuesPerFacet domyślnie wynosi 100, co przy fasecie z tysiącem marek daje niekompletną listę filtrów.
searchCutoffMs domyślnie nie jest ustawione, a wtedy silnik używa progu 1500 milisekund. Po przekroczeniu progu wyszukiwanie zwraca to, co zdążyło policzyć, i nie zgłasza błędu. Jeśli budujesz podpowiedzi przy pisaniu, ustaw wartość niższą, na przykład 100 do 200 milisekund, bo lepiej zwrócić przybliżony wynik szybko niż dokładny po sekundzie. Deklaracje o czasach odpowiedzi poniżej pięćdziesięciu milisekund traktuj jako cel projektowy, a nie gwarancję. Zmierz je na swoich danych, swoim sprzęcie i swoim rozkładzie zapytań.
disableOnNumbers z wartością true zasługuje na osobną uwagę w sklepach i katalogach części. Bez tego zapytanie o numer katalogowy 14520 dopasuje się do 14521, bo to jedna literówka odległości.
Wyszukiwanie wektorowe i hybrydowe
Wyszukiwanie semantyczne jest wbudowane i nie wymaga osobnej bazy wektorowej. Konfiguruje się je w ustawieniu embedders, gdzie każdy wpis ma nazwę i pole source o jednej z wartości: openAi, huggingFace, ollama, userProvided, rest albo composite.
{
"embedders": {
"opisy": {
"source": "openAi",
"model": "text-embedding-3-small",
"apiKey": "sk-...",
"dimensions": 1536,
"documentTemplate": "Produkt {{doc.name}} marki {{doc.brand}}. {{doc.description}}",
"binaryQuantized": false
},
"wlasne": {
"source": "userProvided",
"dimensions": 384
}
}
}Przy source równym openAi, huggingFace, ollama lub rest Meilisearch sam liczy osadzenia podczas indeksowania, co oznacza, że wywołuje zewnętrzne API i płacisz za każdy przeliczony dokument. Przy userProvided wektory dostarczasz sam w zarezerwowanym polu _vectors i silnik ich nie przelicza. To jedyny wariant, w którym masz pełną kontrolę nad kosztem i nad tym, jaki model liczy osadzenia.
Wyszukiwanie hybrydowe uruchamia się obiektem hybrid w zapytaniu. Pole embedder jest wymagane, a semanticRatio przyjmuje wartości od zera do jedynki, gdzie zero oznacza czyste dopasowanie po słowach, a jedynka czystą semantykę. Domyślną wartością jest 0,5.
{
"q": "ciepła kurtka na zimę w góry",
"hybrid": { "embedder": "opisy", "semanticRatio": 0.7 },
"limit": 20,
"filter": "inStock = true",
"showRankingScore": true,
"retrieveVectors": false
}To zmienia pozycję Meilisearcha wobec baz wektorowych. Jeśli potrzebujesz wyszukiwania po produktach lub artykułach z jednoczesnym dopasowaniem po słowach i po znaczeniu, jeden proces załatwia sprawę i nie musisz łączyć wyników z dwóch systemów. Jeśli natomiast budujesz potok z milionami wektorów, kwantyzacją, wieloma kolekcjami i strojeniem parametrów indeksu przybliżonego, dedykowana baza jak Qdrant albo Pinecone daje znacznie więcej kontroli. Meilisearch celuje w wyszukiwanie produktowe wzbogacone o semantykę, a nie w bycie magazynem wektorów dla systemu RAG.
Kiedy wystarczy Postgres, a kiedy nie
To najważniejsze pytanie w tym temacie i odpowiedź nie brzmi "zawsze osobny silnik". PostgreSQL ma wbudowane wyszukiwanie pełnotekstowe i przy rozsądnych wymaganiach naprawdę wystarcza.
-- kolumna generowana z wagami, indeks GIN i sortowanie po trafności
ALTER TABLE products ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('simple', coalesce(name, '')), 'A') ||
setweight(to_tsvector('simple', coalesce(description, '')), 'B')
) STORED;
CREATE INDEX products_search_idx ON products USING GIN (search_vector);
SELECT id, name, ts_rank(search_vector, query) AS rank
FROM products, websearch_to_tsquery('simple', 'kurtka puchowa') AS query
WHERE search_vector @@ query AND in_stock
ORDER BY rank DESC
LIMIT 20;Ten kod działa, jest transakcyjny, nie wymaga synchronizacji i nie dokłada usługi do utrzymania. Zostań przy nim, jeśli zapytania są całymi słowami, użytkownicy wpisują je poprawnie, a wyniki mogą być sortowane po prostej mierze trafności.
Trzy rzeczy każą sięgnąć po osobny silnik. Pierwsza to literówki. Postgres domyślnie nie toleruje ich wcale, bo tsquery porównuje lematy dokładnie. Rozszerzenie pg_trgm pozwala liczyć podobieństwo trigramowe, ale to osobny mechanizm, który trzeba ręcznie spleść z tsvector, a wynikowe zapytanie z dwoma indeksami i sztucznym łączeniem ocen bywa trudne do utrzymania. Druga to wyszukiwanie w trakcie pisania. Prefiksy w Postgresie realizuje się przez to_tsquery('kurt:*'), co w połączeniu z literówkami i wieloma słowami szybko przestaje działać sensownie. Meilisearch buduje struktury prefiksowe podczas indeksowania, sterowane ustawieniem prefixSearch o domyślnej wartości indexingTime. Trzecia to opóźnienie. Zapytanie z ts_rank musi policzyć ocenę dla każdego pasującego wiersza przed sortowaniem, więc przy szerokich zapytaniach koszt rośnie z liczbą trafień, a nie z liczbą zwracanych wyników.
Jest też droga pośrednia. Jeśli już stoisz na Supabase albo innym hostowanym Postgresie, dołożenie pg_trgm obok pgvector daje przyzwoity kompromis bez nowej usługi. Koszt utrzymania osobnego silnika to nie tylko serwer, ale też potok synchronizacji, obsługa rozjazdu danych i ponowne indeksowanie po każdej zmianie schematu.
Meilisearch a alternatywy
| Rozwiązanie | Model uruchomienia | Literówki | Wektory | Kiedy wybrać |
|---|---|---|---|---|
| Meilisearch | jeden proces, MIT plus moduły BSL | domyślnie, konfigurowalne progi | wbudowane, tryb hybrydowy | wyszukiwarka w produkcie, podpowiedzi przy pisaniu |
| PostgreSQL z tsvector | rozszerzenie w istniejącej bazie | tylko przez pg_trgm, ręcznie | przez pgvector | jedno źródło prawdy, proste zapytania |
| Elasticsearch | klaster JVM, AGPLv3 lub ELv2 lub SSPL | zapytanie fuzzy, ręcznie | pole gęste i kNN | logi, analityka, złożone agregacje |
| Qdrant | osobna usługa, Apache 2.0 | brak pełnotekstowych z tolerancją | rdzeń produktu | duża skala wektorowa, systemy RAG |
| Algolia | wyłącznie usługa zamknięta | domyślnie | wbudowane | brak zespołu do utrzymania infrastruktury |
Wobec Elasticsearcha różnica sprowadza się do przeznaczenia. Elasticsearch jest systemem rozproszonym do wielu zastosowań naraz, z agregacjami i analityką logów, i wymaga strojenia sterty JVM oraz planowania fragmentów. Meilisearch jest pojedynczym plikiem wykonywalnym, który po godzinie daje dobre wyszukiwanie w katalogu, ale nie zrobi ci hurtowni zdarzeń. Wobec Redisa z modułem wyszukiwania różnica leży w trwałości i modelu danych, bo Redis pozostaje przede wszystkim magazynem w pamięci.
Chmura, wersja Enterprise i koszty
Samodzielne uruchomienie rdzenia jest darmowe i bez ograniczeń funkcji, dopóki nie dotykasz modułów Enterprise. Meilisearch Cloud to usługa zarządzana i tu warto uważnie przeczytać stronę cennika, bo podaje dwie różne liczby w dwóch miejscach.
Nagłówek planu Cloud mówi "od 20 dolarów miesięcznie". Kalkulator kosztów na tej samej stronie w ustawieniu wyjściowym pokazuje co innego: rozliczenie zasobowe od 23 dolarów miesięcznie za instancję XS z połową rdzenia wirtualnego i jednym gigabajtem pamięci, na co składa się 18 dolarów za instancję i 5 dolarów za 32 gibibajty dysku, oraz rozliczenie użyciowe od 30 dolarów miesięcznie za plan bazowy obejmujący 100 tysięcy dokumentów i 50 tysięcy wyszukiwań. Nie potrafię pogodzić kwoty 20 z kwotą 23, więc podaję obie i sugeruję uruchomienie kalkulatora na własnych liczbach przed planowaniem budżetu. Okres próbny trwa 14 dni i nie wymaga karty.
Plan Enterprise nie ma podanej ceny. Wymienione przy nim funkcje to gwarancja dostępności do 99,999 procent, dedykowany kanał wsparcia, logowanie jednokrotne przez SAML, zgodność SOC 2 Type II, zaawansowana analityka kliknięć i konwersji, personalizacja wyników, dynamiczne reguły wyszukiwania oraz podział na fragmenty wraz z replikacją. Ostatnia pozycja jest tą samą funkcjonalnością, którą w repozytorium znajdziesz jako pliki Enterprise Edition na licencji BSL. Praktyczny wniosek: jeśli twoim wymaganiem jest wysoka dostępność wyszukiwarki, wersja darmowa jej nie da, a obejście przez samodzielne uruchomienie kodu z katalogów Enterprise byłoby użyciem produkcyjnym zabronionym przez licencję.
To jest realne ryzyko przywiązania i lepiej nazwać je wprost. Rdzeń możesz uruchomić u siebie na zawsze, ale ścieżka rozwoju powyżej jednej maszyny prowadzi albo do umowy komercyjnej, albo do chmury dostawcy.
Typowe błędy
Traktowanie zapisu jako synchronicznego. Test, który dodaje dokument i od razu szuka, będzie migotał. Zawsze czekaj na zakończenie zadania.
Wystawienie klucza głównego w kodzie klienta. Klucz z MEILI_MASTER_KEY pozwala kasować indeksy. Do przeglądarki trafia wyłącznie klucz z uprawnieniem wyszukiwania, a przy wielu klientach token dzierżawcy z wbudowanym filtrem.
Pozostawienie maxTotalHits na wartości domyślnej przy eksportach i mapach strony. Tysiąc trafień to sufit, nie sugestia.
Kopiowanie listy rankingRules ze starych materiałów. Zestaw domyślny w 1.53.1 ma siedem pozycji z attributeRank i wordPosition, a nie sześć z attribute.
Włączanie tolerancji literówek na polach identyfikatorów. Numery katalogowe i kody SKU wymagają disableOnAttributes albo disableOnNumbers, inaczej wyszukiwarka podpowie sąsiedni numer.
Wpisanie do rejestru zależności samego MIT. Poprawny opis to MIT AND BUSL-1.1, zgodnie z plikiem licencyjnym, nawet jeśli Cargo.toml i odznaka w README mówią inaczej.
Zakładanie, że wersja hybrydowa jest darmowa w każdym sensie. Kod jest na MIT, ale przy źródle innym niż userProvided każde indeksowanie generuje wywołania płatnego API osadzeń.
FAQ
Czy Meilisearch jest na licencji MIT?
Rdzeń tak, ale nie cały projekt. Plik licencyjny w repozytorium deklaruje MIT AND BUSL-1.1. Osiem plików oznaczonych jako Enterprise Edition, obejmujących podział na fragmenty, sieć wielowęzłową i migawki do S3, podlega Business Source License 1.1, która zezwala tylko na użycie nieprodukcyjne. Po czterech latach od publikacji danej wersji te pliki przechodzą na MIT.
Czy Meilisearch zastąpi mi Postgresa?
Nie, bo to indeks wtórny bez transakcji i bez roli źródła prawdy. Zastępuje natomiast wyszukiwanie w Postgresie wtedy, gdy potrzebujesz tolerancji literówek, dopasowania po prefiksach w trakcie pisania i przewidywalnie niskiego opóźnienia niezależnego od liczby trafień.
Czy jest wyszukiwanie wektorowe i hybrydowe?
Tak, oba. Osadzenia konfiguruje się ustawieniem embedders, a zapytanie hybrydowe obiektem hybrid z polami embedder i semanticRatio. Wartość 0 daje czyste dopasowanie po słowach, wartość 1 czystą semantykę, domyślna to 0,5.
Które funkcje są tylko w wersji płatnej?
Wysoka dostępność przez replikację i podział danych na fragmenty, migawki do magazynu S3, a po stronie chmury dodatkowo logowanie SAML, analityka kliknięć, personalizacja i dynamiczne reguły wyszukiwania. Cała reszta, łącznie z wyszukiwaniem hybrydowym i wielodostępnością, jest w wersji otwartej.
Ile kosztuje Meilisearch Cloud?
Strona cennika podaje w nagłówku 20 dolarów miesięcznie, a jej kalkulator w ustawieniu wyjściowym pokazuje 23 dolary za najmniejszą instancję w rozliczeniu zasobowym i 30 dolarów za plan bazowy w rozliczeniu użyciowym. Rozbieżność jest w samym cenniku, więc policz swój przypadek kalkulatorem przed decyzją.
Czy da się uruchomić Meilisearcha na kilku węzłach za darmo?
Nie w sposób zgodny z licencją. Kod sieci wielowęzłowej i fragmentów jest publiczny, ale objęty BSL z zakazem użycia produkcyjnego. Dla wersji darmowej realnym planem jest jedna instancja z migawkami i szybkim odtwarzaniem indeksu ze źródła prawdy.