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
    <https://typesense.org> 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.

