Exa, wyszukiwarka semantyczna dla modeli językowych
Exa to płatne API wyszukiwania webowego, które zamiast samej listy linków zwraca tekst stron gotowy do wklejenia w prompt. Klienty exa-py i exa-js stoją na wersji 2.18.1 z 14 sierpnia 2026 roku i mają licencję MIT, ale sama wyszukiwarka jest zamknięta i rozliczana za każde żądanie, po 7 dolarów za tysiąc wywołań endpointu /search.
Co Exa właściwie zwraca
Różnica zaczyna się w tym, co przychodzi w odpowiedzi. Klasyczne API wyszukiwarki zwraca tytuł, adres i dwuzdaniowy fragment, a po pełną treść trzeba iść osobno, zwykle własnym pobieraczem. Exa domyślnie dołącza tekst strony do każdego wyniku: w SDK 2.18.1 pominięcie parametru contents oznacza {"text": {"maxCharacters": 10000}}, a żeby dostać samą listę adresów, trzeba jawnie podać contents=False. To odwrócenie domyślności ma bezpośrednie przełożenie na rachunek i wrócę do niego przy cenniku.
Endpointów, których realnie używa się w kodzie agenta, jest kilka. /search wykonuje zapytanie i zwraca wyniki razem z treścią. /contents pobiera treść dla adresów, które już znasz. /answer zwraca odpowiedź wygenerowaną przez model razem z cytowaniami. /monitors uruchamia zaplanowane wyszukiwania zgłaszające nowe trafienia. Osobno stoi Agent API do dłuższych zadań badawczych, rozliczane zupełnie inaczej niż reszta.
Jeden szczegół z popularnych opisów Exa jest już nieaktualny i lepiej go nie przepisywać. Metoda find_similar w Pythonie oraz findSimilar w TypeScripcie są w 2.18.1 oznaczone jako przestarzałe, a docstring podaje ścieżkę migracji wprost: zamiast find_similar(url) należy wywołać search("pages similar to " + url). Kod nadal działa, lecz budowanie nowej integracji wokół tej metody nie ma sensu, bo jej typ opcji RegularSearchOptions jest już opisany w typach jako przeznaczony do usunięcia.
Wersje, licencja i co naprawdę jest otwarte
Sprawdzenie licencji z trzech niezależnych miejsc wypada tu wzorowo, co nie jest regułą w tej kategorii narzędzi. Plik LICENSE w gałęzi master obu repozytoriów, exa-labs/exa-py i exa-labs/exa-js, zawiera tekst MIT z notą praw autorskich Exa Labs z roku 2024. Pole license w PyPI dla exa-py ma wartość MIT, wsparte klasyfikatorem License :: OSI Approved :: MIT License, a w rejestrze npm exa-js deklaruje MIT.
Trzecie źródło, czyli zawartość opublikowanej paczki, też się zgadza. Wheel exa_py-2.18.1-py3-none-any.whl waży 103 kB, zawiera plik exa_py-2.18.1.dist-info/licenses/LICENSE i realny kod: sam exa_py/api.py ma 3507 linii, obok niego siedzą moduły websets, agent, research i monitors. Tarball exa-js-2.18.1.tgz rozpakowuje się do około 1,2 MB, ma package/LICENSE i zbudowane pliki dist/index.js, dist/index.mjs oraz deklaracje typów. Żadnej atrapy, żadnego pustego metapakietu.
Tu kończy się dobra wiadomość. Licencja MIT obejmuje klienta HTTP, a nie wyszukiwarkę. Indeks, model osadzeń, infrastruktura pobierania stron i logika rankingu są zamknięte i nie ma wariantu do samodzielnego hostowania. Kod, który dostajesz za darmo, służy wyłącznie do rozmowy z https://api.exa.ai. To pełne przywiązanie do dostawcy: jeśli Exa podniesie ceny, zmieni zachowanie rankingu albo zniknie, nie masz ścieżki awaryjnej poza przepisaniem warstwy wyszukiwania na inne API. Różni się to od układu, jaki opisaliśmy przy Firecrawl, gdzie serwer na AGPL-3.0 daje przynajmniej teoretyczną możliwość uruchomienia u siebie.
Tempo wydań jest wysokie. PyPI notuje 122 wydania exa-py, npm 115 wydań exa-js, a wersje 2.16.0, 2.17.0, 2.18.0 i 2.18.1 ukazały się między 2 lipca a 14 sierpnia 2026 roku. Numery obu pakietów są zsynchronizowane, co ułatwia pracę w projekcie mieszanym. Cena tego tempa to częste oznaczanie pól jako przestarzałe: w samym module api.py przestarzałe są start_crawl_date, end_crawl_date, contents.context, highlights.numSentences oraz highlights.highlightsPerUrl.
Zależności warto zobaczyć przed instalacją, bo obie paczki ciągną więcej, niż sugeruje słowo klient. exa-py wymaga Pythona co najmniej 3.9 i deklaruje httpx>=0.28.1, httpcore>=1.0.9, openai>=1.48, pydantic>=2.10.6, requests>=2.32.3, python-dotenv>=1.0.1 oraz typing-extensions>=4.12.2. exa-js zależy od zod w wersji ^3.22.0, openai w ^5.0.1, cross-fetch, dotenv i zod-to-json-schema. Obecność SDK OpenAI jako zależności twardej bywa zaskoczeniem w projekcie, który OpenAI wcale nie używa.
Tryby wyszukiwania i pierwsze zapytanie
Parametr type przyjmuje w 2.18.1 siedem wartości: auto, fast, deep-lite, deep, deep-reasoning, neural oraz instant. Domyślne auto samo wybiera algorytm, neural wymusza wyszukiwanie oparte na osadzeniach, instant to wariant o niskim opóźnieniu, a trzy warianty deep uruchamiają wieloetapowe badanie z rozbijaniem zapytania na podzapytania.
pip install exa-py==2.18.1
npm install exa-js@2.18.1
export EXA_API_KEY="twoj-klucz"Klient czyta klucz ze zmiennej EXA_API_KEY, jeśli nie podasz go w konstruktorze. Brak obu źródeł kończy się wyjątkiem ValueError już na etapie tworzenia obiektu, a nie przy pierwszym żądaniu.
from exa_py import Exa
exa = Exa() # czyta EXA_API_KEY ze srodowiska
response = exa.search(
"raporty o zuzyciu energii przez centra danych w 2026",
type="auto",
category="publication",
num_results=8,
start_published_date="2026-01-01",
exclude_domains=["reddit.com", "quora.com"],
contents={
"text": {"max_characters": 4000, "verbosity": "compact"},
"highlights": {"query": "zuzycie energii", "max_characters": 600},
},
)
print(response.resolved_search_type) # 'neural' albo 'keyword'
print(response.cost_dollars.total)
for result in response.results:
print(result.title, result.url, len(result.text))Pole category przyjmuje sześć wartości: company, news, publication, personal site, financial report oraz people. Nie jest to filtr po domenie, tylko przełącznik wyspecjalizowanego indeksu, więc category="company" zwraca wyniki z dodatkowymi polami encji, a nie te same strony w innej kolejności.
Najciekawsze w tej odpowiedzi jest resolved_search_type, w TypeScripcie resolvedSearchType. Przy type="auto" pole mówi, czy Exa ostatecznie użyła trybu neural, czy keyword. Jest to jedyny sposób, żeby po fakcie zobaczyć, którą ścieżką poszło zapytanie, i pierwsza rzecz, jaką warto zalogować przy strojeniu jakości wyników.
Pobieranie treści, świeżość i podstrony
Endpoint /contents przydaje się, gdy adresy masz już z innego źródła, na przykład z mapy strony albo z bazy. Metoda get_contents przyjmuje pojedynczy adres, listę adresów albo listę wcześniejszych wyników.
result = exa.get_contents(
["https://exa.ai/pricing", "https://docs.exa.ai/reference/error-codes"],
livecrawl="preferred",
livecrawl_timeout=10000,
max_age_hours=0,
filter_empty_results=True,
subpages=2,
subpage_target=["pricing", "faq"],
extras={"links": 5, "image_links": 2},
text={"max_characters": 8000, "exclude_sections": ["navigation", "footer"]},
summary={"query": "ile kosztuje tysiac zapytan"},
)Parametr livecrawl ma pięć wartości: always, fallback, never, auto oraz preferred. Sterują tym, czy Exa ma pobrać stronę na żywo, czy zadowolić się kopią z indeksu. Osobno działa max_age_hours, gdzie zero oznacza zawsze świeże pobranie, minus jeden zakazuje pobierania na żywo i wymusza korzystanie z pamięci podręcznej, a wartość dodatnia wyznacza dopuszczalny wiek kopii w godzinach.
Jest tu pułapka, którą łatwo przeoczyć w dokumentacji pól. Trzy opcje tekstu, czyli verbosity, include_sections i exclude_sections, działają wyłącznie wtedy, gdy ustawisz max_age_hours=0. Sekcje da się oznaczać wartościami header, navigation, banner, body, sidebar, footer i metadata, ale bez świeżego pobrania filtr zostanie po cichu zignorowany i dostaniesz pełną kopię z indeksu, płacąc za znaki, których nie chciałeś.
Wersja TypeScript ma te same możliwości z nazwami w notacji wielbłądziej. Poniżej odpowiednik zapytania z odpowiedzią generowaną przez model.
import Exa from "exa-js";
const exa = new Exa(process.env.EXA_API_KEY);
const answer = await exa.answer(
"Jaki jest limit darmowych kredytow w Exa i co sie dzieje po jego przekroczeniu?",
{
text: true,
model: "exa-pro",
systemPrompt: "Odpowiadaj krotko i podawaj zrodla.",
}
);
console.log(answer.answer);
console.log(answer.citations.map((c) => c.url));
console.log(answer.costDollars?.total, answer.requestId);
const stream = exa.streamAnswer("Co zmienilo sie w exa-js 2.18?", { text: false });
for await (const chunk of stream) {
process.stdout.write(chunk.content ?? "");
}Parametr model przyjmuje dokładnie dwie wartości, exa oraz exa-pro. Odpowiedź niesie requestId, który przydaje się przy zgłaszaniu problemów, oraz costDollars z rozbiciem kosztu. Struktura kosztu ma pole total, a pod nim search z podpolami neural i keyword oraz contents z podpolami text i summary.
Cennik i arytmetyka rachunku
Cennik jest w całości pay as you go, bez abonamentu i bez minimalnego zobowiązania. Poniżej stawki z tabeli w dokumentacji dostawcy, sprawdzone 22 sierpnia 2026 roku.
| Endpoint | Cena bazowa za 1000 żądań | Każdy wynik powyżej 10 | Streszczenia AI |
|---|---|---|---|
| /search | 7 USD | 1 USD za 1000 wyników | 1 USD za 1000 stron |
| /answer | 5 USD | nie dotyczy | nie dotyczy |
| /monitors | 15 USD | 1 USD za 1000 wyników | 1 USD za 1000 stron |
| /contents | 1 USD za 1000 stron na typ treści | nie dotyczy | 1 USD za 1000 stron |
Wyszukiwanie głębokie stoi obok tej tabeli i tu dwa miejsca u dostawcy podają liczby inaczej. Strona exa.ai/pricing rozbija je na dwie kolumny, Deep Search po 12 USD i Deep-Reasoning Search po 15 USD za tysiąc żądań, natomiast podsumowanie w dokumentacji skleja to w jeden przedział od 12 do 15 USD. Obie liczby opisują ten sam produkt, tylko na różnym poziomie szczegółowości, i przy szacowaniu budżetu bezpieczniej brać górną wartość.
Sformułowanie o cenie za tysiąc żądań bywa mylące, bo cena bazowa obejmuje do dziesięciu wyników. Zapytanie z num_results=30 kosztuje więc 7 USD za tysiąc żądań plus 20 tysięcy dodatkowych wyników po 1 USD za tysiąc, czyli 27 USD za tysiąc takich zapytań, prawie czterokrotność stawki podstawowej. Do tego zwrot treści to /contents po 1 USD za tysiąc stron i na typ treści, więc żądanie o text i highlights naraz płaci się dwa razy. Ponieważ SDK domyślnie prosi o tekst, rachunek za treść pojawi się nawet wtedy, gdy o nim nie pomyślisz.
Plan darmowy nazywa się Starter i daje 20 USD kredytów przy rejestracji oraz 10 USD kredytów co miesiąc, bez podawania karty. Dokumentacja przelicza startowe 20 USD na około 2800 wyszukiwań, co zgadza się z arytmetyką: 20 podzielone przez 7 dolarów za tysiąc daje 2857 żądań przy dziesięciu wynikach i bez dodatkowych typów treści. Do tego limit 5 zapytań na sekundę oraz 3 równoległe uruchomienia agenta. Tu jednak dostawca podaje dwie różne liczby: strona cennika przypisuje planowi Starter 5 zapytań na sekundę, a osobna strona dokumentacji o limitach wymienia 10 zapytań na sekundę dla /search jako wartość domyślną, bez rozbicia na plany. Plan płatny ma według cennika 10 zapytań na sekundę i 25 równoległych uruchomień agenta, więc najprawdopodobniej strona dokumentacji opisuje ten drugi przypadek. Zanim zaplanujesz przepustowość, sprawdź limit na własnym kluczu, zamiast ufać którejkolwiek z tych stron.
Jedna liczba na stronie cennika nie domyka się arytmetycznie i lepiej ją odnotować. Nagłówek obiecuje ponad 120 USD kredytów rocznie, tymczasem 10 USD miesięcznie razy 12 miesięcy daje dokładnie 120 USD. Powyżej tej kwoty wychodzi się dopiero po doliczeniu jednorazowych 20 USD za rejestrację, czyli 140 USD w pierwszym roku i równe 120 USD w każdym kolejnym.
Po wyczerpaniu kredytów API zwraca kod 402 Payment Required z komunikatem o wyczerpaniu środków lub przekroczeniu budżetu klucza. Przekroczenie limitu zapytań na sekundę daje 429. Pełna lista kodów błędów jest w dokumentacji, a samo rozróżnienie ma znaczenie w kodzie obsługi błędów, bo 429 warto ponawiać z odczekaniem, a 402 nigdy się samo nie naprawi.
import httpx
from exa_py import Exa
exa = Exa()
try:
response = exa.search("nowe modele osadzen", type="fast", contents=False)
except httpx.HTTPStatusError as error:
status = error.response.status_code
if status == 402:
raise RuntimeError("Kredyty Exa wyczerpane, doladuj konto")
if status == 429:
raise RuntimeError("Przekroczony limit zapytan na sekunde, ponow z odczekaniem")
if status == 501:
raise RuntimeError("/answer nie potrafil odpowiedziec na to zapytanie")
raisePlan Developer to te same stawki za żądanie plus rozliczenie kartą lub przelewem, wsparcie mailowe, SOC 2 Type II, limit 10 zapytań na sekundę i 25 równoległych agentów. Plan Enterprise ma cenę na zapytanie i dokłada rzeczy, które w regulowanych branżach bywają wymagane: brak retencji danych, HIPAA, własne indeksy, logowanie SSO do panelu oraz do 1000 wyników na jedno wyszukiwanie.
Kiedy semantyka przegrywa z dopasowaniem słów
Wyszukiwanie oparte na osadzeniach wygrywa tam, gdzie zapytanie opisuje pojęcie, a nie ciąg znaków. Zdanie w rodzaju "artykuły opisujące, dlaczego zespoły rezygnują z mikroserwisów" nie zawiera żadnego słowa, które musiałoby wystąpić w dobrym wyniku, więc dopasowanie słów kluczowych radzi sobie z nim słabo, a model osadzeń dobrze. Podobnie działa szukanie stron podobnych do znanej strony oraz zapytania w stylu "firmy robiące X w regionie Y".
Odwrotna sytuacja jest równie częsta i nie należy jej ukrywać. Jeśli szukasz dokładnej nazwy własnej, numeru wersji, identyfikatora zgłoszenia albo kodu błędu w rodzaju TS2345 czy ECONNREFUSED, model osadzeń zwróci strony tematycznie bliskie, ale niekoniecznie tę jedną zawierającą szukany ciąg. Klasyczny indeks odwrócony poradzi sobie lepiej i szybciej. Jeśli w prawym górnym rogu Twojej aplikacji jest pole wyszukiwania po nazwie produktu, semantyka nic tam nie poprawi.
Exa ma na to trzy furtki. Pierwsza to include_text i exclude_text, listy ciągów, które muszą albo nie mogą wystąpić w tekście strony, nakładane na wynik wyszukiwania. Druga to type="keyword" wymuszane pośrednio przez auto, którego decyzję odczytasz z resolved_search_type. Trzecia to zwykłe przyznanie, że dane zapytanie należy skierować gdzie indziej, bo agent nie ma obowiązku mieć jednego narzędzia wyszukiwania.
W praktyce najlepiej sprawdza się układ, w którym warstwa routingu decyduje po kształcie zapytania. Zapytania z cudzysłowem, kodem błędu albo identyfikatorem idą do wyszukiwarki słów kluczowych, zapytania opisowe do Exa. Taki router pisze się w LangChain albo w LlamaIndex jako zwykły wybór narzędzia i kosztuje kilkanaście linii.
Exa obok Tavily i Firecrawl
Trzy narzędzia bywają wrzucane do jednego worka, choć rozwiązują różne problemy. Poniżej różnice, które realnie wpływają na wybór.
| Cecha | Exa | Tavily | Firecrawl |
|---|---|---|---|
| Główne zadanie | wyszukiwanie semantyczne we własnym indeksie | wyszukiwanie z oceną trafności dla agenta | pobieranie i konwersja znanych stron |
| Licencja klienta | MIT | MIT | MIT |
| Kod usługi | zamknięty, brak hostowania u siebie | zamknięty, brak hostowania u siebie | AGPL-3.0, hostowanie u siebie możliwe |
| Rozliczenie | kwotowo za żądanie i za stronę | kredyty | kredyty |
| Treść strony w odpowiedzi | domyślnie tak, do 10000 znaków | tak, fragmenty z oceną | tak, markdown całej strony |
Tavily jest najbliższym sąsiadem, bo też adresuje agentów i też zwraca fragmenty gotowe do promptu. Różni się modelem rozliczenia oraz tym, że Exa stawia na własny indeks przeszukiwany osadzeniami, podczas gdy Tavily układa wyniki pod ocenę trafności dla zadanego pytania. Jeśli zależy Ci na zapytaniach opisowych i na podobieństwie do znanej strony, Exa wypadnie lepiej. Jeśli liczysz koszt w prostych kredytach i chcesz jednego wywołania na jedno pytanie, Tavily jest prostsze w oszacowaniu.
Firecrawl nie jest wyszukiwarką i porównywanie go z Exa co do jakości wyników mija się z celem. Firecrawl bierze adres i zwraca markdown, dobrze radzi sobie ze stronami wymagającymi wykonania skryptów i pozwala przejść po całej witrynie. Exa pobiera treść przy okazji wyszukiwania, ale nie zastąpi pełnego przejścia po dokumentacji. Naturalny układ to Exa do znalezienia adresów i Firecrawl do ich dokładnego pobrania.
Gdy zadanie wymaga zalogowania się, klikania i wypełniania formularzy, żadne z tych trzech narzędzi nie wystarczy i trzeba sięgnąć po sterowanie przeglądarką w rodzaju browser-use. API wyszukiwania widzi tylko to, co jest publicznie dostępne dla robota.
Typowe błędy
Pierwszy i najdroższy to zapomnienie o contents=False. Jeśli agent wykonuje wyszukiwanie tylko po to, żeby wybrać adresy do dalszego przetwarzania, domyślne dziesięć tysięcy znaków tekstu na wynik to czysty koszt i niepotrzebne tokeny w kontekście.
Drugi to ustawianie wysokiego num_results bez policzenia stawki za wyniki powyżej dziesiątego. Trzydzieści wyników kosztuje prawie cztery razy tyle co dziesięć, a modele i tak rzadko czytają dalej niż kilka pierwszych pozycji.
Trzeci to jednoczesne proszenie o text, highlights i summary. Każdy typ treści jest rozliczany osobno po 1 USD za tysiąc stron, więc komplet potraja tę część rachunku. Zwykle wystarczy jedno, a highlights z sensownym query daje krótszy i lepiej dopasowany kontekst niż surowy tekst.
Czwarty to poleganie na verbosity albo na exclude_sections bez ustawienia max_age_hours=0. Filtr sekcji zostanie pominięty bez ostrzeżenia, a Ty będziesz wierzył, że menu i stopka nie trafiają do promptu.
Piąty to budowanie nowego kodu na find_similar. Metoda jest oznaczona jako przestarzała w 2.18.1 razem z całym typem opcji, którego używa.
Szósty to traktowanie licencji MIT jako gwarancji niezależności. Możesz zrobić z klientem, co chcesz, ale bez konta i klucza do api.exa.ai ten klient nie robi nic. Przy wyborze warto z góry założyć, że warstwa wyszukiwania w Twojej aplikacji będzie musiała mieć interfejs, który da się podmienić.
FAQ
Czy licencja MIT oznacza, że Exa jest projektem otwartym?
Otwarte są tylko biblioteki klienckie. Repozytoria exa-labs/exa-py i exa-labs/exa-js mają plik LICENSE z tekstem MIT, rejestry deklarują MIT, a opublikowane paczki zawierają zarówno plik licencyjny, jak i pełny kod. Wyszukiwarka, indeks i model osadzeń pozostają zamknięte i płatne, a wariantu do uruchomienia na własnym serwerze nie ma.
Ile realnie kosztuje tysiąc wyszukiwań?
Przy dziesięciu wynikach i bez dodatkowych typów treści jest to 7 USD za tysiąc żądań /search. Każde tysiąc wyników powyżej dziesiątego dokłada 1 USD, każdy typ treści dokłada 1 USD za tysiąc stron, a wyszukiwanie głębokie zaczyna się od 12 USD i sięga 15 USD za tysiąc żądań, zależnie od wariantu.
Co się dzieje po wyczerpaniu darmowych kredytów?
API zaczyna zwracać kod 402 Payment Required, czyli sygnał o wyczerpaniu środków na koncie albo o przekroczeniu budżetu przypisanego do klucza. Żądania nie są kolejkowane ani realizowane na kredyt. Osobno działa kod 429 oznaczający przekroczenie limitu zapytań na sekundę, który w planie Starter wynosi 5.
Kiedy nie warto sięgać po Exa?
Gdy szukasz dokładnych ciągów znaków, takich jak kod błędu, numer wersji czy identyfikator zgłoszenia, bo tam dopasowanie słów kluczowych jest szybsze i celniejsze. Gdy potrzebujesz przejść po całej dokumentacji i zamienić ją na markdown, bo to zadanie dla narzędzia pobierającego. Gdy nie możesz przyjąć przywiązania do jednego zamkniętego dostawcy.
Czy da się używać Exa bez SDK?
Tak, całość działa jako zwykłe REST API pod https://api.exa.ai z kluczem w nagłówku x-api-key. SDK dokłada typy, obsługę strumieniowania i konwersję nazw pól z notacji podkreślnikowej na wielbłądzią, ale nie robi nic, czego nie da się wywołać żądaniem HTTP.
Jak sprawdzić, czy zapytanie poszło ścieżką semantyczną?
Odpowiedź niesie pole resolved_search_type w Pythonie, a resolvedSearchType w TypeScripcie, wypełniane przy type="auto" wartością neural albo keyword. Logowanie tego pola razem z cost_dollars i request_id daje podstawę do oceny, które zapytania w ogóle korzystają z wyszukiwania semantycznego.