NAME Typesense::Client - Perl client for the Typesense search engine SYNOPSIS use Typesense::Client; my $ts = Typesense::Client->new( url => 'http://localhost:8108', api_key => $ENV{TYPESENSE_API_KEY}, ); $ts->collections->create({ name => 'products', fields => [ { name => 'name', type => 'string' }, { name => 'brand', type => 'string', facet => \1 }, # JSON boolean { name => 'price', type => 'float' }, ], default_sorting_field => 'price', }); $ts->documents->import_docs('products', \@docs); # JSONL bulk load my $r = $ts->search('products', { q => 'aple', # typo tolerated query_by => 'name,brand', filter_by => 'price:[100..500]', }); say $r->{found}; DESCRIPTION A complete, dependency-light client for Typesense v28 and later. It covers collections, documents (including JSONL bulk import and export), aliases, single and federated search, synonyms, curation overrides, the analytics API, and scoped API keys. The client is a thin layer over the REST API: it builds requests, applies the API key, decodes JSON and turns failures into exceptions. It does not model schemas or validate documents - Typesense does that, and its error messages are good. Relationship to Search::Typesense Search::Typesense is an earlier and independent client, last released in 2021 against Typesense 0.19 and marked as alpha by its author. It covers collections and documents. This distribution exists because several parts of the API that production deployments depend on had no Perl binding at all: multi_search, aliases (which is how you reindex without downtime), synonyms, curation overrides, the analytics API, and scoped keys. It also differs in two design decisions: errors are exception objects rather than return values, and "fail_open" is offered for callers that must degrade instead of die. If Search::Typesense covers what you need, there is no reason to switch. CONSTRUCTOR my $ts = Typesense::Client->new(url => ..., api_key => ..., %options); * url (required) Base URL of the server, e.g. http://localhost:8108. A trailing slash is stripped. * api_key (required) Sent as the X-TYPESENSE-API-KEY header on every request. * connect_timeout, request_timeout Seconds, for ordinary requests. Default 0.3 and 1.5 - deliberately short, because a search that misses those deadlines is no longer useful for rendering a page. Raise them for interactive administration. * bulk_timeout Seconds, for import and export. Default 120. * fail_open When true, failures return undef and leave the exception in "last_error" instead of dying. See "ERROR HANDLING". * ua, bulk_ua Supply your own Mojo::UserAgent instances. Mostly useful in tests, where sharing Mojo::IOLoop->singleton with an in-process server matters. JSON BOOLEANS Typesense validates types strictly, and Perl has no native boolean to hand it. A schema flag written as facet => 1 reaches the server as the number 1 and is rejected: 400 The `facet` property of the field `brand` should be a boolean. Use a reference to a scalar, which Mojo::JSON encodes as a JSON boolean: { name => 'brand', type => 'string', facet => \1 } # true { name => 'brand', type => 'string', facet => \0 } # false The same applies to every other boolean the API takes - optional, index, sort, infix, store, enable_nested_fields, expand_query - and to booleans inside documents you index. This client passes your data through untouched by design, so the conversion is yours to make. ERROR HANDLING By default any transport or HTTP failure throws a Typesense::Client::Error, which stringifies to a full message: my $r = eval { $ts->search('products', { q => 'x', query_by => 'name' }) }; if (my $err = $@) { die $err unless ref $err; warn "search failed: $err"; } With fail_open => 1 nothing is thrown; the call returns undef and the error object is available afterwards: my $ts = Typesense::Client->new(..., fail_open => 1); my $r = $ts->search('products', { q => 'x', query_by => 'name' }) or fall_back_to_sql($ts->last_error); That mode exists for the search path of a public site, where the right answer to "the engine is down" is to serve something else, not to return a 500. METHODS collections, documents, aliases, synonyms, overrides, analytics, keys Resource accessors. Each returns a delegate object, created on first use: Typesense::Client::Collections, Typesense::Client::Documents, Typesense::Client::Aliases, Typesense::Client::Synonyms, Typesense::Client::Overrides, Typesense::Client::Analytics, Typesense::Client::Keys. search my $r = $ts->search($collection, \%params, %opt); GET /collections/{name}/documents/search. %params is passed through unchanged, so every search parameter Typesense supports is available. Query string keys are sorted, so equivalent calls produce byte-identical URLs - which is what makes the response cacheable upstream. %opt goes to "request", which in practice means headers: $ts->search('products', { q => 'laptop', query_by => 'name' }, headers => { 'x-typesense-user-id' => $session_id }); That header is what makes the analytics API attribute events to a person. Without it Typesense aggregates by IP address, and behind a reverse proxy that is a single visitor for the whole site. Pass the same identifier here that you pass as user_id to "event" in Typesense::Client::Analytics. multi_search my $r = $ts->multi_search(\@searches, \%common, %opt); POST /multi_search. Runs several searches in one round trip. Important: %common travels in the query string, and Typesense lets those values override the per-search ones in the body. A parameter that must differ between branches - drop_tokens_threshold is the usual one - has to be set inside each element of @searches and kept out of %common, or it silently has no effect. health, stats, metrics, debug /health, /stats.json, /metrics.json and /debug. server_version my $v = $ts->server_version; if ( $v->is_at_least('28.0') ) { ... } GET /debug, wrapped in a Typesense::Client::Version object that stringifies to the version and compares properly. Returns undef in fail_open mode when the server cannot be reached. request my $data = $ts->request($method, $path, %opt); The low-level escape hatch, for endpoints this module does not wrap yet. %opt accepts json (body to encode), raw (body sent verbatim, for JSONL), bulk (use the long-timeout agent), raw_response (return the undecoded body), ok_404 (treat 404 as success returning undef) and headers (a hash reference of extra request headers). Your headers are merged after the API key, so they win. That is what lets a single client send a per-request key - a scoped key derived for one customer, say - without building a second client for every tenant: $ts->search('products', \%params, headers => { 'X-TYPESENSE-API-KEY' => $scoped_key }); last_error The Typesense::Client::Error from the most recent failed call, or undef. Reset at the start of every request. Chiefly for fail_open mode. url, fail_open Read-only accessors for the corresponding constructor arguments. SEE ALSO https://typesense.org/docs/ - the API reference this module follows. Search::Typesense - the earlier Perl client; see "Relationship to Search::Typesense". AUTHOR SeHarrys COPYRIGHT AND LICENSE This software is copyright (c) 2026 by SeHarrys. This is free software; you can redistribute it and/or modify it under the terms of the Artistic License 2.0.