···11+## Unreleased
22+33+- Initial release with core Typesense API bindings
44+- Collection management (create, list, get, delete)
55+- Document operations (import, get, update, delete)
66+- Search with filtering, faceting, and highlighting
77+- Multi-search across collections
88+- Analytics rules and events
+15
LICENSE.md
···11+ISC License
22+33+Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>
44+55+Permission to use, copy, modify, and/or distribute this software for any
66+purpose with or without fee is hereby granted, provided that the above
77+copyright notice and this permission notice appear in all copies.
88+99+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
1010+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
1111+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
1212+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
1313+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
1414+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
1515+PERFORMANCE OF THIS SOFTWARE.
+64
README.md
···11+# typesense
22+33+OCaml bindings for the [Typesense](https://typesense.org/) search API.
44+55+Typesense is a fast, typo-tolerant search engine. This library provides
66+high-quality OCaml bindings using [Eio](https://github.com/ocaml-multicore/eio)
77+for async operations.
88+99+## Features
1010+1111+- Collection management (create, list, get, delete)
1212+- Document operations (import, get, update, delete, export)
1313+- Search with filtering, faceting, and highlighting
1414+- Multi-search across collections
1515+- Analytics rules and event tracking
1616+1717+## Installation
1818+1919+```
2020+opam install typesense
2121+```
2222+2323+## Usage
2424+2525+```ocaml
2626+open Typesense
2727+2828+let () =
2929+ Eio_main.run @@ fun env ->
3030+ let auth = Auth.create ~endpoint:"http://localhost:8108" ~api_key:"xyz" in
3131+ Client.with_client env auth @@ fun client ->
3232+3333+ (* Create a collection *)
3434+ let schema =
3535+ Collection.schema ~name:"books"
3636+ ~fields:
3737+ [
3838+ Collection.field ~name:"title" ~type_:"string" ();
3939+ Collection.field ~name:"author" ~type_:"string" ~facet:true ();
4040+ Collection.field ~name:"year" ~type_:"int32" ();
4141+ ]
4242+ ~default_sorting_field:"year" ()
4343+ in
4444+ let _ = Collection.create client schema in
4545+4646+ (* Search *)
4747+ let params = Search.params ~q:"harry" ~query_by:["title"; "author"] () in
4848+ let result = Search.search client ~collection:"books" params in
4949+ Printf.printf "Found %d books\n" result.found
5050+```
5151+5252+## Error Handling
5353+5454+All API operations raise `Eio.Io` exceptions with `Error.E` error codes:
5555+5656+```ocaml
5757+try Collection.get client ~name:"nonexistent" with
5858+| Eio.Io (Error.E { code = Not_found; message; _ }, _) ->
5959+ Printf.eprintf "Collection not found: %s\n" message
6060+```
6161+6262+## License
6363+6464+ISC License. See [LICENSE.md](LICENSE.md) for details.
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type rule_type = Popular_queries | Nohits_queries | Counter
77+88+let rule_type_to_string = function
99+ | Popular_queries -> "popular_queries"
1010+ | Nohits_queries -> "nohits_queries"
1111+ | Counter -> "counter"
1212+1313+let rule_type_of_string = function
1414+ | "popular_queries" -> Popular_queries
1515+ | "nohits_queries" -> Nohits_queries
1616+ | "counter" -> Counter
1717+ | s -> failwith ("Unknown rule type: " ^ s)
1818+1919+let rule_type_jsont =
2020+ Jsont.of_of_string ~kind:"RuleType" (fun s -> Ok (rule_type_of_string s))
2121+ ~enc:rule_type_to_string
2222+2323+type destination = { collection : string }
2424+2525+let destination_jsont =
2626+ Jsont.Object.map ~kind:"Destination" (fun collection -> { collection })
2727+ |> Jsont.Object.mem "collection" Jsont.string ~enc:(fun d -> d.collection)
2828+ |> Jsont.Object.skip_unknown
2929+ |> Jsont.Object.finish
3030+3131+type source = {
3232+ collections : string list;
3333+ events : Jsont.json option;
3434+}
3535+3636+let source_jsont =
3737+ let make collections events = { collections; events } in
3838+ Jsont.Object.map ~kind:"Source" make
3939+ |> Jsont.Object.mem "collections" (Jsont.list Jsont.string)
4040+ ~enc:(fun s -> s.collections)
4141+ |> Jsont.Object.opt_mem "events" Jsont.json ~enc:(fun s -> s.events)
4242+ |> Jsont.Object.skip_unknown
4343+ |> Jsont.Object.finish
4444+4545+type rule_params = {
4646+ source : source;
4747+ destination : destination;
4848+ limit : int option;
4949+ expand_query : bool option;
5050+}
5151+5252+let rule_params_jsont =
5353+ let make source destination limit expand_query =
5454+ { source; destination; limit; expand_query }
5555+ in
5656+ Jsont.Object.map ~kind:"RuleParams" make
5757+ |> Jsont.Object.mem "source" source_jsont ~enc:(fun p -> p.source)
5858+ |> Jsont.Object.mem "destination" destination_jsont ~enc:(fun p -> p.destination)
5959+ |> Jsont.Object.opt_mem "limit" Jsont.int ~enc:(fun p -> p.limit)
6060+ |> Jsont.Object.opt_mem "expand_query" Jsont.bool ~enc:(fun p -> p.expand_query)
6161+ |> Jsont.Object.skip_unknown
6262+ |> Jsont.Object.finish
6363+6464+type rule = {
6565+ name : string;
6666+ type_ : rule_type;
6767+ params : rule_params;
6868+}
6969+7070+let rule_jsont =
7171+ let make name type_ params = { name; type_; params } in
7272+ Jsont.Object.map ~kind:"Rule" make
7373+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun r -> r.name)
7474+ |> Jsont.Object.mem "type" rule_type_jsont ~enc:(fun r -> r.type_)
7575+ |> Jsont.Object.mem "params" rule_params_jsont ~enc:(fun r -> r.params)
7676+ |> Jsont.Object.skip_unknown
7777+ |> Jsont.Object.finish
7878+7979+let rule ~name ~type_ ~params = { name; type_; params }
8080+8181+let rule_params ~source_collections ~destination_collection ?limit
8282+ ?expand_query ?events () =
8383+ {
8484+ source = { collections = source_collections; events };
8585+ destination = { collection = destination_collection };
8686+ limit;
8787+ expand_query;
8888+ }
8989+9090+type rules_response = { rules : rule list }
9191+9292+let rules_response_jsont =
9393+ Jsont.Object.map ~kind:"RulesResponse" (fun rules -> { rules })
9494+ |> Jsont.Object.mem "rules" (Jsont.list rule_jsont) ~enc:(fun r -> r.rules)
9595+ |> Jsont.Object.skip_unknown
9696+ |> Jsont.Object.finish
9797+9898+let list_rules client =
9999+ let json = Client.request client ~method_:`GET ~path:"/analytics/rules" () in
100100+ let response = Encode.decode_or_raise rules_response_jsont json "list rules" in
101101+ response.rules
102102+103103+let get_rule client ~name =
104104+ let path = "/analytics/rules/" ^ Uri.pct_encode name in
105105+ let json = Client.request client ~method_:`GET ~path () in
106106+ Encode.decode_or_raise rule_jsont json ("get rule " ^ name)
107107+108108+let create_rule client rule =
109109+ let body = Encode.to_json_string rule_jsont rule in
110110+ let json = Client.request client ~method_:`POST ~path:"/analytics/rules" ~body () in
111111+ Encode.decode_or_raise rule_jsont json ("create rule " ^ rule.name)
112112+113113+let upsert_rule client rule =
114114+ let path = "/analytics/rules/" ^ Uri.pct_encode rule.name in
115115+ let body = Encode.to_json_string rule_jsont rule in
116116+ let json = Client.request client ~method_:`PUT ~path ~body () in
117117+ Encode.decode_or_raise rule_jsont json ("upsert rule " ^ rule.name)
118118+119119+type delete_result = { name : string }
120120+121121+let delete_result_jsont =
122122+ Jsont.Object.map ~kind:"DeleteResult" (fun name -> { name })
123123+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun r -> r.name)
124124+ |> Jsont.Object.skip_unknown
125125+ |> Jsont.Object.finish
126126+127127+let delete_rule client ~name =
128128+ let path = "/analytics/rules/" ^ Uri.pct_encode name in
129129+ let json = Client.request client ~method_:`DELETE ~path () in
130130+ let _ = Encode.decode_or_raise delete_result_jsont json ("delete rule " ^ name) in
131131+ ()
132132+133133+type event_type = Search | Click | Conversion | Visit | Custom of string
134134+135135+let event_type_to_string = function
136136+ | Search -> "search"
137137+ | Click -> "click"
138138+ | Conversion -> "conversion"
139139+ | Visit -> "visit"
140140+ | Custom s -> s
141141+142142+let event_type_of_string = function
143143+ | "search" -> Search
144144+ | "click" -> Click
145145+ | "conversion" -> Conversion
146146+ | "visit" -> Visit
147147+ | s -> Custom s
148148+149149+let event_type_jsont =
150150+ Jsont.of_of_string ~kind:"EventType" (fun s -> Ok (event_type_of_string s))
151151+ ~enc:event_type_to_string
152152+153153+type event = {
154154+ type_ : event_type;
155155+ name : string;
156156+ data : Jsont.json;
157157+}
158158+159159+let event_jsont =
160160+ let make type_ name data = { type_; name; data } in
161161+ Jsont.Object.map ~kind:"Event" make
162162+ |> Jsont.Object.mem "type" event_type_jsont ~enc:(fun e -> e.type_)
163163+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun e -> e.name)
164164+ |> Jsont.Object.mem "data" Jsont.json ~enc:(fun e -> e.data)
165165+ |> Jsont.Object.finish
166166+167167+let event ~type_ ~name ~data = { type_; name; data }
168168+169169+type event_response = { ok : bool }
170170+171171+let event_response_jsont =
172172+ Jsont.Object.map ~kind:"EventResponse" (fun ok -> { ok })
173173+ |> Jsont.Object.mem "ok" Jsont.bool ~enc:(fun r -> r.ok)
174174+ |> Jsont.Object.skip_unknown
175175+ |> Jsont.Object.finish
176176+177177+let create_event client event =
178178+ let body = Encode.to_json_string event_jsont event in
179179+ let json = Client.request client ~method_:`POST ~path:"/analytics/events" ~body () in
180180+ let _ = Encode.decode_or_raise event_response_jsont json "create event" in
181181+ ()
+113
lib/typesense/analytics.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Typesense analytics rules and events.
77+88+ This module provides support for creating and managing analytics rules
99+ and tracking events. Analytics rules automatically aggregate search queries
1010+ into collections for analysis. *)
1111+1212+(** {1 Rule Types} *)
1313+1414+type rule_type =
1515+ | Popular_queries (** Track popular search queries *)
1616+ | Nohits_queries (** Track queries with no results *)
1717+ | Counter (** Generic counter for events *)
1818+(** Type of analytics rule. *)
1919+2020+val rule_type_jsont : rule_type Jsont.t
2121+2222+type destination = { collection : string }
2323+(** Destination collection for aggregated data. *)
2424+2525+type source = {
2626+ collections : string list; (** Source collections to track *)
2727+ events : Jsont.json option; (** Event configuration *)
2828+}
2929+(** Source configuration for analytics. *)
3030+3131+type rule_params = {
3232+ source : source;
3333+ destination : destination;
3434+ limit : int option; (** Max entries to store *)
3535+ expand_query : bool option; (** Expand queries before storing *)
3636+}
3737+(** Parameters for analytics rules. *)
3838+3939+val rule_params :
4040+ source_collections:string list ->
4141+ destination_collection:string ->
4242+ ?limit:int ->
4343+ ?expand_query:bool ->
4444+ ?events:Jsont.json ->
4545+ unit ->
4646+ rule_params
4747+(** [rule_params ~source_collections ~destination_collection ...] creates rule
4848+ parameters. *)
4949+5050+type rule = {
5151+ name : string; (** Unique rule name *)
5252+ type_ : rule_type; (** Rule type *)
5353+ params : rule_params; (** Rule parameters *)
5454+}
5555+(** An analytics rule. *)
5656+5757+val rule : name:string -> type_:rule_type -> params:rule_params -> rule
5858+(** [rule ~name ~type_ ~params] creates an analytics rule. *)
5959+6060+val rule_jsont : rule Jsont.t
6161+(** JSON codec for rules. *)
6262+6363+(** {1 Rule Operations} *)
6464+6565+val list_rules : Client.t -> rule list
6666+(** [list_rules client] returns all analytics rules.
6767+ @raise Eio.Io with [Error.E] on API errors *)
6868+6969+val get_rule : Client.t -> name:string -> rule
7070+(** [get_rule client ~name] retrieves a rule by name.
7171+ @raise Eio.Io with [Error.E { code = Not_found; _ }] if not found *)
7272+7373+val create_rule : Client.t -> rule -> rule
7474+(** [create_rule client rule] creates a new analytics rule.
7575+ @raise Eio.Io with [Error.E { code = Conflict; _ }] if already exists *)
7676+7777+val upsert_rule : Client.t -> rule -> rule
7878+(** [upsert_rule client rule] creates or updates an analytics rule. *)
7979+8080+val delete_rule : Client.t -> name:string -> unit
8181+(** [delete_rule client ~name] deletes an analytics rule.
8282+ @raise Eio.Io with [Error.E { code = Not_found; _ }] if not found *)
8383+8484+(** {1 Event Types} *)
8585+8686+type event_type =
8787+ | Search (** Search event *)
8888+ | Click (** Click event *)
8989+ | Conversion (** Conversion event *)
9090+ | Visit (** Visit event *)
9191+ | Custom of string (** Custom event type *)
9292+(** Type of analytics event. *)
9393+9494+val event_type_jsont : event_type Jsont.t
9595+9696+type event = {
9797+ type_ : event_type; (** Event type *)
9898+ name : string; (** Event name (rule name to track) *)
9999+ data : Jsont.json; (** Event data *)
100100+}
101101+(** An analytics event. *)
102102+103103+val event : type_:event_type -> name:string -> data:Jsont.json -> event
104104+(** [event ~type_ ~name ~data] creates an analytics event. *)
105105+106106+val event_jsont : event Jsont.t
107107+(** JSON codec for events. *)
108108+109109+(** {1 Event Operations} *)
110110+111111+val create_event : Client.t -> event -> unit
112112+(** [create_event client event] records an analytics event.
113113+ @raise Eio.Io with [Error.E] on API errors *)
+28
lib/typesense/auth.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type t = { endpoint : string; api_key : string }
77+88+let create ~endpoint ~api_key = { endpoint; api_key }
99+1010+let from_env ?endpoint_var ?api_key_var () =
1111+ let endpoint_var = Option.value ~default:"TYPESENSE_API_ENDPOINT" endpoint_var in
1212+ let api_key_var = Option.value ~default:"TYPESENSE_API_KEY" api_key_var in
1313+ match (Sys.getenv_opt endpoint_var, Sys.getenv_opt api_key_var) with
1414+ | Some endpoint, Some api_key -> Some { endpoint; api_key }
1515+ | _ -> None
1616+1717+let endpoint t = t.endpoint
1818+let api_key t = t.api_key
1919+2020+let default_headers t =
2121+ [
2222+ ("X-TYPESENSE-API-KEY", t.api_key);
2323+ ("Content-Type", "application/json");
2424+ ("User-Agent", "OCaml-Typesense/1.0");
2525+ ]
2626+2727+let pp fmt t =
2828+ Format.fprintf fmt "Auth{endpoint=%s}" t.endpoint
+38
lib/typesense/auth.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Authentication for the Typesense API.
77+88+ This module handles authentication credentials for connecting to a Typesense
99+ server. Typesense uses API key authentication via the [X-TYPESENSE-API-KEY]
1010+ header. *)
1111+1212+type t
1313+(** Authentication credentials. *)
1414+1515+val create : endpoint:string -> api_key:string -> t
1616+(** [create ~endpoint ~api_key] creates authentication credentials directly.
1717+ @param endpoint The Typesense server URL (e.g., "http://localhost:8108")
1818+ @param api_key The Typesense API key *)
1919+2020+val from_env : ?endpoint_var:string -> ?api_key_var:string -> unit -> t option
2121+(** [from_env ?endpoint_var ?api_key_var ()] loads credentials from environment
2222+ variables.
2323+ @param endpoint_var Environment variable for endpoint (default: TYPESENSE_API_ENDPOINT)
2424+ @param api_key_var Environment variable for API key (default: TYPESENSE_API_KEY)
2525+ @return [Some t] if both variables are set, [None] otherwise *)
2626+2727+val endpoint : t -> string
2828+(** [endpoint t] returns the Typesense server endpoint. *)
2929+3030+val api_key : t -> string
3131+(** [api_key t] returns the API key. *)
3232+3333+val default_headers : t -> (string * string) list
3434+(** [default_headers t] returns the default HTTP headers for authentication.
3535+ Includes the API key header, Content-Type, and User-Agent. *)
3636+3737+val pp : Format.formatter -> t -> unit
3838+(** Pretty printer for authentication (shows endpoint only, not credentials). *)
+101
lib/typesense/client.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+let src = Logs.Src.create "typesense.client" ~doc:"Typesense API client"
77+88+module Log = (val Logs.src_log src : Logs.LOG)
99+1010+type t = { auth : Auth.t; session : Requests.t }
1111+1212+let create ?session ~sw env auth =
1313+ Log.info (fun m -> m "Creating Typesense client for %s" (Auth.endpoint auth));
1414+ let session = match session with
1515+ | Some s -> s
1616+ | None -> Requests.create ~sw ~follow_redirects:true ~verify_tls:true env
1717+ in
1818+ { auth; session }
1919+2020+let with_client ?session env auth f =
2121+ Eio.Switch.run @@ fun sw ->
2222+ let client = create ?session ~sw env auth in
2323+ f client
2424+2525+let auth_headers t =
2626+ Requests.Headers.of_list (Auth.default_headers t.auth)
2727+2828+let method_to_string = function
2929+ | `GET -> "GET"
3030+ | `POST -> "POST"
3131+ | `PUT -> "PUT"
3232+ | `DELETE -> "DELETE"
3333+ | `PATCH -> "PATCH"
3434+3535+let build_url base_url params =
3636+ match params with
3737+ | None -> base_url
3838+ | Some p ->
3939+ Uri.of_string base_url
4040+ |> Fun.flip
4141+ (List.fold_left (fun u (k, v) -> Uri.add_query_param' u (k, v)))
4242+ p
4343+ |> Uri.to_string
4444+4545+let make_request t ~method_ ~url ~body_opt =
4646+ let headers = auth_headers t in
4747+ match method_ with
4848+ | `GET -> Requests.get t.session ~headers url
4949+ | `POST -> Requests.post t.session ~headers ?body:body_opt url
5050+ | `PUT -> Requests.put t.session ~headers ?body:body_opt url
5151+ | `DELETE -> Requests.delete t.session ~headers url
5252+ | `PATCH -> Requests.patch t.session ~headers ?body:body_opt url
5353+5454+let request t ~method_ ~path ?params ?body () =
5555+ let url = build_url (Auth.endpoint t.auth ^ path) params in
5656+ Log.debug (fun m -> m "Request: %s %s" (method_to_string method_) path);
5757+ let body_opt =
5858+ Option.map (fun s -> Requests.Body.of_string Requests.Mime.json s) body
5959+ in
6060+ let response = make_request t ~method_ ~url ~body_opt in
6161+ let status = Requests.Response.status_code response in
6262+ Log.debug (fun m -> m "Response status: %d" status);
6363+ let json = Requests.Response.json response in
6464+ if status >= 400 then begin
6565+ let message =
6666+ match json with
6767+ | Jsont.Object (fields, _) -> (
6868+ let assoc = List.map (fun ((k, _), v) -> (k, v)) fields in
6969+ match List.assoc_opt "message" assoc with
7070+ | Some (Jsont.String (s, _)) -> s
7171+ | _ -> "Unknown error")
7272+ | _ -> "Unknown error"
7373+ in
7474+ Error.raise_with_context
7575+ (Error.make ~code:(Error.code_of_status status) ~message)
7676+ "%s" path
7777+ end;
7878+ json
7979+8080+let request_raw t ~method_ ~path ?params ?body () =
8181+ let url = build_url (Auth.endpoint t.auth ^ path) params in
8282+ Log.debug (fun m -> m "Request (raw): %s %s" (method_to_string method_) path);
8383+ let body_opt =
8484+ Option.map
8585+ (fun s ->
8686+ Requests.Body.of_string
8787+ (Requests.Mime.of_string "application/x-ndjson")
8888+ s)
8989+ body
9090+ in
9191+ let response = make_request t ~method_ ~url ~body_opt in
9292+ let status = Requests.Response.status_code response in
9393+ Log.debug (fun m -> m "Response status: %d" status);
9494+ let body_str = Requests.Response.text response in
9595+ if status >= 400 then
9696+ Error.raise_with_context
9797+ (Error.make ~code:(Error.code_of_status status) ~message:body_str)
9898+ "%s" path;
9999+ body_str
100100+101101+let pp fmt t = Format.fprintf fmt "Client(endpoint=%s)" (Auth.endpoint t.auth)
+71
lib/typesense/client.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** HTTP client for making requests to the Typesense API.
77+88+ This module provides the low-level HTTP client for communicating with the
99+ Typesense API. All API errors are raised as [Eio.Io] exceptions with
1010+ [Error.E] error codes, following the Eio error pattern.
1111+1212+ @raise Eio.Io with [Error.E error] for API errors *)
1313+1414+type t
1515+(** Type representing a Typesense HTTP client *)
1616+1717+val create :
1818+ ?session:Requests.t ->
1919+ sw:Eio.Switch.t ->
2020+ < clock : float Eio.Time.clock_ty Eio.Resource.t
2121+ ; net : [ `Generic | `Unix ] Eio.Net.ty Eio.Resource.t
2222+ ; fs : Eio.Fs.dir_ty Eio.Path.t
2323+ ; .. > ->
2424+ Auth.t ->
2525+ t
2626+(** [create ?session ~sw env auth] creates a new client with the given switch,
2727+ environment and authentication. If [session] is provided, it is reused;
2828+ otherwise a new session is created. The environment must have clock, net,
2929+ and fs capabilities. *)
3030+3131+val with_client :
3232+ ?session:Requests.t ->
3333+ < clock : float Eio.Time.clock_ty Eio.Resource.t
3434+ ; net : [ `Generic | `Unix ] Eio.Net.ty Eio.Resource.t
3535+ ; fs : Eio.Fs.dir_ty Eio.Path.t
3636+ ; .. > ->
3737+ Auth.t ->
3838+ (t -> 'a) ->
3939+ 'a
4040+(** [with_client ?session env auth f] runs [f] with a client that is
4141+ automatically cleaned up. If [session] is provided, it is reused. The
4242+ environment must have clock, net, and fs capabilities. *)
4343+4444+val request :
4545+ t ->
4646+ method_:[ `GET | `POST | `PUT | `DELETE | `PATCH ] ->
4747+ path:string ->
4848+ ?params:(string * string) list ->
4949+ ?body:string ->
5050+ unit ->
5151+ Jsont.json
5252+(** [request t ~method_ ~path ?params ?body ()] makes an HTTP request to the
5353+ Typesense API and returns the JSON response.
5454+ @param params Optional query parameters
5555+ @param body Optional JSON request body
5656+ @raise Eio.Io with [Error.E error] on API errors *)
5757+5858+val request_raw :
5959+ t ->
6060+ method_:[ `GET | `POST | `PUT | `DELETE | `PATCH ] ->
6161+ path:string ->
6262+ ?params:(string * string) list ->
6363+ ?body:string ->
6464+ unit ->
6565+ string
6666+(** [request_raw t ~method_ ~path ?params ?body ()] makes an HTTP request and
6767+ returns the raw response body as a string. Used for JSONL import responses.
6868+ @raise Eio.Io with [Error.E error] on API errors *)
6969+7070+val pp : Format.formatter -> t -> unit
7171+(** Pretty printer for client (shows endpoint only, not credentials) *)
+144
lib/typesense/collection.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type field = {
77+ name : string;
88+ type_ : string;
99+ facet : bool option;
1010+ optional : bool option;
1111+ index : bool option;
1212+ sort : bool option;
1313+ infix : bool option;
1414+ locale : string option;
1515+ num_dim : int option;
1616+}
1717+1818+let field_jsont =
1919+ let make name type_ facet optional index sort infix locale num_dim =
2020+ { name; type_; facet; optional; index; sort; infix; locale; num_dim }
2121+ in
2222+ Jsont.Object.map ~kind:"Field" make
2323+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun f -> f.name)
2424+ |> Jsont.Object.mem "type" Jsont.string ~enc:(fun f -> f.type_)
2525+ |> Jsont.Object.opt_mem "facet" Jsont.bool ~enc:(fun f -> f.facet)
2626+ |> Jsont.Object.opt_mem "optional" Jsont.bool ~enc:(fun f -> f.optional)
2727+ |> Jsont.Object.opt_mem "index" Jsont.bool ~enc:(fun f -> f.index)
2828+ |> Jsont.Object.opt_mem "sort" Jsont.bool ~enc:(fun f -> f.sort)
2929+ |> Jsont.Object.opt_mem "infix" Jsont.bool ~enc:(fun f -> f.infix)
3030+ |> Jsont.Object.opt_mem "locale" Jsont.string ~enc:(fun f -> f.locale)
3131+ |> Jsont.Object.opt_mem "num_dim" Jsont.int ~enc:(fun f -> f.num_dim)
3232+ |> Jsont.Object.skip_unknown
3333+ |> Jsont.Object.finish
3434+3535+let field ~name ~type_ ?facet ?optional ?index ?sort ?infix ?locale ?num_dim () =
3636+ { name; type_; facet; optional; index; sort; infix; locale; num_dim }
3737+3838+type schema = {
3939+ name : string;
4040+ fields : field list;
4141+ default_sorting_field : string option;
4242+ token_separators : string list option;
4343+ symbols_to_index : string list option;
4444+ enable_nested_fields : bool option;
4545+}
4646+4747+let schema_jsont =
4848+ let make name fields default_sorting_field token_separators symbols_to_index
4949+ enable_nested_fields =
5050+ {
5151+ name;
5252+ fields;
5353+ default_sorting_field;
5454+ token_separators;
5555+ symbols_to_index;
5656+ enable_nested_fields;
5757+ }
5858+ in
5959+ Jsont.Object.map ~kind:"Schema" make
6060+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun s -> s.name)
6161+ |> Jsont.Object.mem "fields" (Jsont.list field_jsont) ~enc:(fun s -> s.fields)
6262+ |> Jsont.Object.opt_mem "default_sorting_field" Jsont.string
6363+ ~enc:(fun s -> s.default_sorting_field)
6464+ |> Jsont.Object.opt_mem "token_separators" (Jsont.list Jsont.string)
6565+ ~enc:(fun s -> s.token_separators)
6666+ |> Jsont.Object.opt_mem "symbols_to_index" (Jsont.list Jsont.string)
6767+ ~enc:(fun s -> s.symbols_to_index)
6868+ |> Jsont.Object.opt_mem "enable_nested_fields" Jsont.bool
6969+ ~enc:(fun s -> s.enable_nested_fields)
7070+ |> Jsont.Object.skip_unknown
7171+ |> Jsont.Object.finish
7272+7373+let schema ~name ~fields ?default_sorting_field ?token_separators
7474+ ?symbols_to_index ?enable_nested_fields () =
7575+ {
7676+ name;
7777+ fields;
7878+ default_sorting_field;
7979+ token_separators;
8080+ symbols_to_index;
8181+ enable_nested_fields;
8282+ }
8383+8484+type t = {
8585+ name : string;
8686+ num_documents : int;
8787+ fields : field list;
8888+ default_sorting_field : string option;
8989+ enable_nested_fields : bool option;
9090+}
9191+9292+let jsont =
9393+ let make name num_documents fields default_sorting_field enable_nested_fields =
9494+ { name; num_documents; fields; default_sorting_field; enable_nested_fields }
9595+ in
9696+ Jsont.Object.map ~kind:"Collection" make
9797+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> c.name)
9898+ |> Jsont.Object.mem "num_documents" Jsont.int ~enc:(fun c -> c.num_documents)
9999+ |> Jsont.Object.mem "fields" (Jsont.list field_jsont) ~enc:(fun c -> c.fields)
100100+ |> Jsont.Object.opt_mem "default_sorting_field" Jsont.string
101101+ ~enc:(fun c -> c.default_sorting_field)
102102+ |> Jsont.Object.opt_mem "enable_nested_fields" Jsont.bool
103103+ ~enc:(fun c -> c.enable_nested_fields)
104104+ |> Jsont.Object.skip_unknown
105105+ |> Jsont.Object.finish
106106+107107+let name t = t.name
108108+let num_documents t = t.num_documents
109109+let fields t = t.fields
110110+let default_sorting_field t = t.default_sorting_field
111111+112112+let list client =
113113+ let json = Client.request client ~method_:`GET ~path:"/collections" () in
114114+ Encode.decode_or_raise (Jsont.list jsont) json "listing collections"
115115+116116+let get client ~name =
117117+ let path = "/collections/" ^ Uri.pct_encode name in
118118+ let json = Client.request client ~method_:`GET ~path () in
119119+ Encode.decode_or_raise jsont json ("getting collection " ^ name)
120120+121121+let create client schema =
122122+ let body = Encode.to_json_string schema_jsont schema in
123123+ let json = Client.request client ~method_:`POST ~path:"/collections" ~body () in
124124+ Encode.decode_or_raise jsont json ("creating collection " ^ schema.name)
125125+126126+type delete_result = { name : string }
127127+128128+let delete_result_jsont =
129129+ Jsont.Object.map ~kind:"DeleteResult" (fun name -> { name })
130130+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun r -> r.name)
131131+ |> Jsont.Object.skip_unknown
132132+ |> Jsont.Object.finish
133133+134134+let delete client ~name =
135135+ let path = "/collections/" ^ Uri.pct_encode name in
136136+ let json = Client.request client ~method_:`DELETE ~path () in
137137+ let _ = Encode.decode_or_raise delete_result_jsont json ("deleting collection " ^ name) in
138138+ ()
139139+140140+let exists client ~name =
141141+ try
142142+ let _ = get client ~name in
143143+ true
144144+ with Eio.Io (Error.E { code = Not_found; _ }, _) -> false
+114
lib/typesense/collection.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Typesense collection management.
77+88+ This module provides types and operations for managing Typesense collections.
99+ A collection is a group of related documents with a defined schema. *)
1010+1111+(** {1 Field Types} *)
1212+1313+type field = {
1414+ name : string; (** Field name *)
1515+ type_ : string; (** Field type (string, int32, float, bool, etc.) *)
1616+ facet : bool option; (** Whether field is facetable *)
1717+ optional : bool option; (** Whether field is optional *)
1818+ index : bool option; (** Whether to index this field *)
1919+ sort : bool option; (** Whether field is sortable *)
2020+ infix : bool option; (** Enable infix search *)
2121+ locale : string option; (** Locale for string fields *)
2222+ num_dim : int option; (** Number of dimensions for vector fields *)
2323+}
2424+(** A field definition in a collection schema. *)
2525+2626+val field :
2727+ name:string ->
2828+ type_:string ->
2929+ ?facet:bool ->
3030+ ?optional:bool ->
3131+ ?index:bool ->
3232+ ?sort:bool ->
3333+ ?infix:bool ->
3434+ ?locale:string ->
3535+ ?num_dim:int ->
3636+ unit ->
3737+ field
3838+(** [field ~name ~type_ ...] creates a field definition. Common types:
3939+ - ["string"], ["string[]] - Text fields
4040+ - ["int32"], ["int64"], ["float"] - Numeric fields
4141+ - ["bool"] - Boolean field
4242+ - ["auto"] - Auto-detect type
4343+ - ["float[]] - Vector field (requires [num_dim]) *)
4444+4545+val field_jsont : field Jsont.t
4646+(** JSON codec for fields. *)
4747+4848+(** {1 Schema Types} *)
4949+5050+type schema = {
5151+ name : string; (** Collection name *)
5252+ fields : field list; (** Field definitions *)
5353+ default_sorting_field : string option; (** Default field for sorting *)
5454+ token_separators : string list option; (** Custom token separators *)
5555+ symbols_to_index : string list option; (** Symbols to index *)
5656+ enable_nested_fields : bool option; (** Enable nested object fields *)
5757+}
5858+(** A collection schema for creating new collections. *)
5959+6060+val schema :
6161+ name:string ->
6262+ fields:field list ->
6363+ ?default_sorting_field:string ->
6464+ ?token_separators:string list ->
6565+ ?symbols_to_index:string list ->
6666+ ?enable_nested_fields:bool ->
6767+ unit ->
6868+ schema
6969+(** [schema ~name ~fields ...] creates a collection schema. *)
7070+7171+val schema_jsont : schema Jsont.t
7272+(** JSON codec for schemas. *)
7373+7474+(** {1 Collection Type} *)
7575+7676+type t = {
7777+ name : string; (** Collection name *)
7878+ num_documents : int; (** Number of documents in collection *)
7979+ fields : field list; (** Field definitions *)
8080+ default_sorting_field : string option; (** Default sorting field *)
8181+ enable_nested_fields : bool option; (** Whether nested fields are enabled *)
8282+}
8383+(** A collection as returned by the API. *)
8484+8585+val jsont : t Jsont.t
8686+(** JSON codec for collections. *)
8787+8888+(** {1 Accessors} *)
8989+9090+val name : t -> string
9191+val num_documents : t -> int
9292+val fields : t -> field list
9393+val default_sorting_field : t -> string option
9494+9595+(** {1 Operations} *)
9696+9797+val list : Client.t -> t list
9898+(** [list client] returns all collections.
9999+ @raise Eio.Io with [Error.E] on API errors *)
100100+101101+val get : Client.t -> name:string -> t
102102+(** [get client ~name] retrieves a collection by name.
103103+ @raise Eio.Io with [Error.E { code = Not_found; _ }] if not found *)
104104+105105+val create : Client.t -> schema -> t
106106+(** [create client schema] creates a new collection.
107107+ @raise Eio.Io with [Error.E { code = Conflict; _ }] if already exists *)
108108+109109+val delete : Client.t -> name:string -> unit
110110+(** [delete client ~name] deletes a collection.
111111+ @raise Eio.Io with [Error.E { code = Not_found; _ }] if not found *)
112112+113113+val exists : Client.t -> name:string -> bool
114114+(** [exists client ~name] returns [true] if the collection exists. *)
+128
lib/typesense/document.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type action = Create | Upsert | Update | Emplace
77+88+let action_to_string = function
99+ | Create -> "create"
1010+ | Upsert -> "upsert"
1111+ | Update -> "update"
1212+ | Emplace -> "emplace"
1313+1414+type import_result = {
1515+ success : bool;
1616+ error : string option;
1717+ document : string option;
1818+}
1919+2020+let import_result_jsont =
2121+ let make success error document = { success; error; document } in
2222+ Jsont.Object.map ~kind:"ImportResult" make
2323+ |> Jsont.Object.mem "success" Jsont.bool ~enc:(fun r -> r.success)
2424+ |> Jsont.Object.opt_mem "error" Jsont.string ~enc:(fun r -> r.error)
2525+ |> Jsont.Object.opt_mem "document" Jsont.string ~enc:(fun r -> r.document)
2626+ |> Jsont.Object.skip_unknown
2727+ |> Jsont.Object.finish
2828+2929+let import client ~collection ?(action = Upsert) ?(batch_size = 40)
3030+ ?(return_doc = false) ?(return_id = false) documents =
3131+ let path =
3232+ "/collections/" ^ Uri.pct_encode collection ^ "/documents/import"
3333+ in
3434+ let params =
3535+ [
3636+ ("action", action_to_string action);
3737+ ("batch_size", string_of_int batch_size);
3838+ ("return_doc", string_of_bool return_doc);
3939+ ("return_id", string_of_bool return_id);
4040+ ]
4141+ in
4242+ (* Convert documents to JSONL format *)
4343+ let body =
4444+ documents
4545+ |> List.map (fun doc -> Encode.to_json_string Jsont.json doc)
4646+ |> String.concat "\n"
4747+ in
4848+ let response = Client.request_raw client ~method_:`POST ~path ~params ~body () in
4949+ Encode.parse_jsonl import_result_jsont response
5050+5151+let get client ~collection ~id =
5252+ let path =
5353+ "/collections/" ^ Uri.pct_encode collection ^ "/documents/"
5454+ ^ Uri.pct_encode id
5555+ in
5656+ Client.request client ~method_:`GET ~path ()
5757+5858+let delete client ~collection ~id =
5959+ let path =
6060+ "/collections/" ^ Uri.pct_encode collection ^ "/documents/"
6161+ ^ Uri.pct_encode id
6262+ in
6363+ Client.request client ~method_:`DELETE ~path ()
6464+6565+let update client ~collection ~id document =
6666+ let path =
6767+ "/collections/" ^ Uri.pct_encode collection ^ "/documents/"
6868+ ^ Uri.pct_encode id
6969+ in
7070+ let body = Encode.to_json_string Jsont.json document in
7171+ Client.request client ~method_:`PATCH ~path ~body ()
7272+7373+let create client ~collection document =
7474+ let path = "/collections/" ^ Uri.pct_encode collection ^ "/documents" in
7575+ let body = Encode.to_json_string Jsont.json document in
7676+ Client.request client ~method_:`POST ~path ~body ()
7777+7878+type delete_by_query_result = { num_deleted : int }
7979+8080+let delete_by_query_result_jsont =
8181+ Jsont.Object.map ~kind:"DeleteByQueryResult" (fun num_deleted ->
8282+ { num_deleted })
8383+ |> Jsont.Object.mem "num_deleted" Jsont.int ~enc:(fun r -> r.num_deleted)
8484+ |> Jsont.Object.skip_unknown
8585+ |> Jsont.Object.finish
8686+8787+let delete_by_query client ~collection ~filter_by =
8888+ let path =
8989+ "/collections/" ^ Uri.pct_encode collection ^ "/documents"
9090+ in
9191+ let params = [ ("filter_by", filter_by) ] in
9292+ let json = Client.request client ~method_:`DELETE ~path ~params () in
9393+ let result =
9494+ Encode.decode_or_raise delete_by_query_result_jsont json "delete by query"
9595+ in
9696+ result.num_deleted
9797+9898+type export_params = {
9999+ filter_by : string option;
100100+ include_fields : string list option;
101101+ exclude_fields : string list option;
102102+}
103103+104104+let export_params ?filter_by ?include_fields ?exclude_fields () =
105105+ { filter_by; include_fields; exclude_fields }
106106+107107+let export client ~collection ?params () =
108108+ let path =
109109+ "/collections/" ^ Uri.pct_encode collection ^ "/documents/export"
110110+ in
111111+ let query_params =
112112+ match params with
113113+ | None -> []
114114+ | Some p ->
115115+ List.filter_map Fun.id
116116+ [
117117+ Option.map (fun v -> ("filter_by", v)) p.filter_by;
118118+ Option.map
119119+ (fun v -> ("include_fields", String.concat "," v))
120120+ p.include_fields;
121121+ Option.map
122122+ (fun v -> ("exclude_fields", String.concat "," v))
123123+ p.exclude_fields;
124124+ ]
125125+ in
126126+ let params = if query_params = [] then None else Some query_params in
127127+ let response = Client.request_raw client ~method_:`GET ~path ?params () in
128128+ Encode.parse_jsonl Jsont.json response
+97
lib/typesense/document.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Typesense document operations.
77+88+ This module provides operations for managing documents in Typesense
99+ collections. Documents are JSON objects stored in collections. *)
1010+1111+(** {1 Import Actions} *)
1212+1313+type action =
1414+ | Create (** Only create new documents (fail if exists) *)
1515+ | Upsert (** Create or replace documents *)
1616+ | Update (** Only update existing documents *)
1717+ | Emplace (** Create or update (merge) documents *)
1818+(** Action to take during document import. *)
1919+2020+(** {1 Import Results} *)
2121+2222+type import_result = {
2323+ success : bool; (** Whether the import succeeded *)
2424+ error : string option; (** Error message if failed *)
2525+ document : string option; (** Document JSON if return_doc was true *)
2626+}
2727+(** Result for a single document in a batch import. *)
2828+2929+val import_result_jsont : import_result Jsont.t
3030+(** JSON codec for import results. *)
3131+3232+(** {1 Document Operations} *)
3333+3434+val import :
3535+ Client.t ->
3636+ collection:string ->
3737+ ?action:action ->
3838+ ?batch_size:int ->
3939+ ?return_doc:bool ->
4040+ ?return_id:bool ->
4141+ Jsont.json list ->
4242+ import_result list
4343+(** [import client ~collection ?action ?batch_size documents] imports documents
4444+ in batch.
4545+ @param action Import action (default: Upsert)
4646+ @param batch_size Number of documents per batch (default: 40)
4747+ @param return_doc Return the document in the result
4848+ @param return_id Return the document ID in the result
4949+ @return List of import results, one per document *)
5050+5151+val get : Client.t -> collection:string -> id:string -> Jsont.json
5252+(** [get client ~collection ~id] retrieves a document by ID.
5353+ @raise Eio.Io with [Error.E { code = Not_found; _ }] if not found *)
5454+5555+val delete : Client.t -> collection:string -> id:string -> Jsont.json
5656+(** [delete client ~collection ~id] deletes a document by ID.
5757+ @return The deleted document
5858+ @raise Eio.Io with [Error.E { code = Not_found; _ }] if not found *)
5959+6060+val update : Client.t -> collection:string -> id:string -> Jsont.json -> Jsont.json
6161+(** [update client ~collection ~id document] updates a document by ID. Only the
6262+ fields present in [document] are updated.
6363+ @return The updated document
6464+ @raise Eio.Io with [Error.E { code = Not_found; _ }] if not found *)
6565+6666+val create : Client.t -> collection:string -> Jsont.json -> Jsont.json
6767+(** [create client ~collection document] creates a new document.
6868+ @return The created document with auto-generated ID if not specified
6969+ @raise Eio.Io with [Error.E { code = Conflict; _ }] if ID already exists *)
7070+7171+val delete_by_query : Client.t -> collection:string -> filter_by:string -> int
7272+(** [delete_by_query client ~collection ~filter_by] deletes all documents
7373+ matching the filter.
7474+ @return Number of documents deleted *)
7575+7676+(** {1 Export Operations} *)
7777+7878+type export_params = {
7979+ filter_by : string option;
8080+ include_fields : string list option;
8181+ exclude_fields : string list option;
8282+}
8383+(** Parameters for exporting documents. *)
8484+8585+val export_params :
8686+ ?filter_by:string ->
8787+ ?include_fields:string list ->
8888+ ?exclude_fields:string list ->
8989+ unit ->
9090+ export_params
9191+(** [export_params ...] creates export parameters. *)
9292+9393+val export :
9494+ Client.t -> collection:string -> ?params:export_params -> unit -> Jsont.json list
9595+(** [export client ~collection ?params ()] exports all documents from a
9696+ collection.
9797+ @return List of documents as JSON *)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Encoding utilities for Typesense API requests *)
77+88+let to_json_string : 'a Jsont.t -> 'a -> string =
99+ fun codec value ->
1010+ match Jsont_bytesrw.encode_string' codec value with
1111+ | Ok s -> s
1212+ | Error e -> failwith ("JSON encoding error: " ^ Jsont.Error.to_string e)
1313+1414+let from_json_string : 'a Jsont.t -> string -> ('a, string) result =
1515+ fun codec json_str ->
1616+ match Jsont_bytesrw.decode_string' codec json_str with
1717+ | Ok v -> Ok v
1818+ | Error e -> Error (Jsont.Error.to_string e)
1919+2020+let from_json : 'a Jsont.t -> Jsont.json -> ('a, string) result =
2121+ fun codec json ->
2222+ let json_str =
2323+ match Jsont_bytesrw.encode_string' Jsont.json json with
2424+ | Ok s -> s
2525+ | Error e ->
2626+ failwith ("Failed to re-encode json: " ^ Jsont.Error.to_string e)
2727+ in
2828+ from_json_string codec json_str
2929+3030+let to_json : 'a Jsont.t -> 'a -> (Jsont.json, string) result =
3131+ fun codec value ->
3232+ let json_str = to_json_string codec value in
3333+ match Jsont_bytesrw.decode_string' Jsont.json json_str with
3434+ | Ok json -> Ok json
3535+ | Error e -> Error (Jsont.Error.to_string e)
3636+3737+let decode_or_raise : 'a Jsont.t -> Jsont.json -> string -> 'a =
3838+ fun codec json context ->
3939+ match from_json codec json with
4040+ | Ok v -> v
4141+ | Error msg ->
4242+ Error.raise_with_context
4343+ (Error.make ~code:(Other 0) ~message:msg)
4444+ "%s" context
4545+4646+let parse_jsonl : 'a Jsont.t -> string -> 'a list =
4747+ fun codec response ->
4848+ String.split_on_char '\n' response
4949+ |> List.filter_map (fun line ->
5050+ let line = String.trim line in
5151+ if line = "" then None
5252+ else
5353+ match from_json_string codec line with
5454+ | Ok result -> Some result
5555+ | Error _ -> None)
+28
lib/typesense/encode.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Encoding utilities for Typesense API requests *)
77+88+val to_json_string : 'a Jsont.t -> 'a -> string
99+(** [to_json_string codec value] converts a value to JSON string using its
1010+ jsont codec. *)
1111+1212+val from_json_string : 'a Jsont.t -> string -> ('a, string) result
1313+(** [from_json_string codec str] parses a JSON string using a jsont codec. *)
1414+1515+val from_json : 'a Jsont.t -> Jsont.json -> ('a, string) result
1616+(** [from_json codec json] parses a Jsont.json value using a codec. *)
1717+1818+val to_json : 'a Jsont.t -> 'a -> (Jsont.json, string) result
1919+(** [to_json codec value] converts a value to Jsont.json using a codec. *)
2020+2121+val decode_or_raise : 'a Jsont.t -> Jsont.json -> string -> 'a
2222+(** [decode_or_raise codec json context] decodes JSON using the codec, or
2323+ raises a Typesense error with the given context if decoding fails. *)
2424+2525+val parse_jsonl : 'a Jsont.t -> string -> 'a list
2626+(** [parse_jsonl codec response] parses a JSONL (newline-delimited JSON)
2727+ response string into a list of values. Empty lines are skipped and
2828+ parse errors are silently dropped. *)
+54
lib/typesense/error.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type code =
77+ | Bad_request
88+ | Unauthorized
99+ | Not_found
1010+ | Conflict
1111+ | Unprocessable_entity
1212+ | Service_unavailable
1313+ | Other of int
1414+1515+type t = { code : code; message : string }
1616+type Eio.Exn.err += E of t
1717+1818+let pp_code fmt = function
1919+ | Bad_request -> Format.fprintf fmt "Bad_request"
2020+ | Unauthorized -> Format.fprintf fmt "Unauthorized"
2121+ | Not_found -> Format.fprintf fmt "Not_found"
2222+ | Conflict -> Format.fprintf fmt "Conflict"
2323+ | Unprocessable_entity -> Format.fprintf fmt "Unprocessable_entity"
2424+ | Service_unavailable -> Format.fprintf fmt "Service_unavailable"
2525+ | Other n -> Format.fprintf fmt "Other(%d)" n
2626+2727+let pp fmt t = Format.fprintf fmt "%a: %s" pp_code t.code t.message
2828+2929+let () =
3030+ Eio.Exn.register_pp (fun f -> function
3131+ | E e ->
3232+ Format.fprintf f "Typesense %a" pp e;
3333+ true
3434+ | _ -> false)
3535+3636+let code_of_status = function
3737+ | 400 -> Bad_request
3838+ | 401 -> Unauthorized
3939+ | 404 -> Not_found
4040+ | 409 -> Conflict
4141+ | 422 -> Unprocessable_entity
4242+ | 503 -> Service_unavailable
4343+ | n -> Other n
4444+4545+let make ~code ~message = { code; message }
4646+let code t = t.code
4747+let message t = t.message
4848+let raise e = Stdlib.raise (Eio.Exn.create (E e))
4949+5050+let raise_with_context e fmt =
5151+ Format.kasprintf
5252+ (fun context ->
5353+ Stdlib.raise (Eio.Exn.add_context (Eio.Exn.create (E e)) "%s" context))
5454+ fmt
+71
lib/typesense/error.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Typesense API error handling.
77+88+ This module defines protocol-level errors for the Typesense API, following
99+ the Eio error pattern for context-aware error handling.
1010+1111+ Errors are raised as [Eio.Io] exceptions:
1212+ {[
1313+ try Typesense.Collection.create client schema with
1414+ | Eio.Io (Typesense.Error.E { code = Unauthorized; message; _ }, _) ->
1515+ Printf.eprintf "Authentication failed: %s\n" message
1616+ | Eio.Io (Typesense.Error.E err, _) ->
1717+ Printf.eprintf "API error: %a\n" Typesense.Error.pp err
1818+ ]} *)
1919+2020+(** {1 Error Codes}
2121+2222+ These error codes correspond to HTTP status codes returned by Typesense. *)
2323+2424+type code =
2525+ | Bad_request (** 400 - Malformed request *)
2626+ | Unauthorized (** 401 - Invalid API key *)
2727+ | Not_found (** 404 - Resource not found *)
2828+ | Conflict (** 409 - Resource already exists *)
2929+ | Unprocessable_entity (** 422 - Invalid data *)
3030+ | Service_unavailable (** 503 - Server temporarily unavailable *)
3131+ | Other of int (** Other HTTP status code *)
3232+3333+(** {1 Error Type} *)
3434+3535+type t = {
3636+ code : code; (** The error code *)
3737+ message : string; (** Human-readable error message *)
3838+}
3939+(** The protocol-level error type. *)
4040+4141+(** {1 Eio Integration} *)
4242+4343+type Eio.Exn.err +=
4444+ | E of t (** Extend [Eio.Exn.err] with Typesense protocol errors. *)
4545+4646+val raise : t -> 'a
4747+(** [raise e] raises an [Eio.Io] exception for error [e]. Equivalent to
4848+ [Stdlib.raise (Eio.Exn.create (E e))]. *)
4949+5050+val raise_with_context : t -> ('a, Format.formatter, unit, 'b) format4 -> 'a
5151+(** [raise_with_context e fmt ...] raises an [Eio.Io] exception with context.
5252+ Equivalent to
5353+ [Stdlib.raise (Eio.Exn.add_context (Eio.Exn.create (E e)) fmt ...)]. *)
5454+5555+(** {1 Error Construction} *)
5656+5757+val make : code:code -> message:string -> t
5858+(** [make ~code ~message] creates an error value. *)
5959+6060+val code_of_status : int -> code
6161+(** [code_of_status status] converts an HTTP status code to an error code. *)
6262+6363+(** {1 Accessors} *)
6464+6565+val code : t -> code
6666+val message : t -> string
6767+6868+(** {1 Pretty Printing} *)
6969+7070+val pp_code : Format.formatter -> code -> unit
7171+val pp : Format.formatter -> t -> unit
+97
lib/typesense/multi_search.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type search_request = {
77+ collection : string;
88+ q : string;
99+ query_by : string list;
1010+ filter_by : string option;
1111+ sort_by : string option;
1212+ facet_by : string list option;
1313+ per_page : int option;
1414+ page : int option;
1515+ prefix : bool option;
1616+ include_fields : string list option;
1717+ exclude_fields : string list option;
1818+}
1919+2020+let search_request ~collection ~q ~query_by ?filter_by ?sort_by ?facet_by
2121+ ?per_page ?page ?prefix ?include_fields ?exclude_fields () =
2222+ {
2323+ collection;
2424+ q;
2525+ query_by;
2626+ filter_by;
2727+ sort_by;
2828+ facet_by;
2929+ per_page;
3030+ page;
3131+ prefix;
3232+ include_fields;
3333+ exclude_fields;
3434+ }
3535+3636+(* Helper codec for comma-separated string lists *)
3737+let comma_list_jsont =
3838+ let of_string s = Ok (String.split_on_char ',' s |> List.map String.trim) in
3939+ let to_string l = String.concat "," l in
4040+ Jsont.of_of_string ~kind:"comma_list" of_string ~enc:to_string
4141+4242+let search_request_jsont =
4343+ let make collection q query_by filter_by sort_by facet_by per_page page prefix
4444+ include_fields exclude_fields =
4545+ {
4646+ collection;
4747+ q;
4848+ query_by;
4949+ filter_by;
5050+ sort_by;
5151+ facet_by;
5252+ per_page;
5353+ page;
5454+ prefix;
5555+ include_fields;
5656+ exclude_fields;
5757+ }
5858+ in
5959+ Jsont.Object.map ~kind:"SearchRequest" make
6060+ |> Jsont.Object.mem "collection" Jsont.string ~enc:(fun r -> r.collection)
6161+ |> Jsont.Object.mem "q" Jsont.string ~enc:(fun r -> r.q)
6262+ |> Jsont.Object.mem "query_by" comma_list_jsont ~enc:(fun r -> r.query_by)
6363+ |> Jsont.Object.opt_mem "filter_by" Jsont.string ~enc:(fun r -> r.filter_by)
6464+ |> Jsont.Object.opt_mem "sort_by" Jsont.string ~enc:(fun r -> r.sort_by)
6565+ |> Jsont.Object.opt_mem "facet_by" comma_list_jsont ~enc:(fun r -> r.facet_by)
6666+ |> Jsont.Object.opt_mem "per_page" Jsont.int ~enc:(fun r -> r.per_page)
6767+ |> Jsont.Object.opt_mem "page" Jsont.int ~enc:(fun r -> r.page)
6868+ |> Jsont.Object.opt_mem "prefix" Jsont.bool ~enc:(fun r -> r.prefix)
6969+ |> Jsont.Object.opt_mem "include_fields" comma_list_jsont
7070+ ~enc:(fun r -> r.include_fields)
7171+ |> Jsont.Object.opt_mem "exclude_fields" comma_list_jsont
7272+ ~enc:(fun r -> r.exclude_fields)
7373+ |> Jsont.Object.skip_unknown
7474+ |> Jsont.Object.finish
7575+7676+type request = { searches : search_request list }
7777+7878+let request_jsont =
7979+ Jsont.Object.map ~kind:"MultiSearchRequest" (fun searches -> { searches })
8080+ |> Jsont.Object.mem "searches" (Jsont.list search_request_jsont)
8181+ ~enc:(fun r -> r.searches)
8282+ |> Jsont.Object.finish
8383+8484+type result = { results : Search.result list }
8585+8686+let result_jsont =
8787+ Jsont.Object.map ~kind:"MultiSearchResult" (fun results -> { results })
8888+ |> Jsont.Object.mem "results" (Jsont.list Search.result_jsont)
8989+ ~enc:(fun r -> r.results)
9090+ |> Jsont.Object.skip_unknown
9191+ |> Jsont.Object.finish
9292+9393+let search client requests =
9494+ let req = { searches = requests } in
9595+ let body = Encode.to_json_string request_jsont req in
9696+ let json = Client.request client ~method_:`POST ~path:"/multi_search" ~body () in
9797+ Encode.decode_or_raise result_jsont json "multi_search"
+61
lib/typesense/multi_search.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Typesense multi-search operations.
77+88+ This module provides support for executing multiple searches across different
99+ collections in a single request. This is more efficient than making separate
1010+ search requests. *)
1111+1212+(** {1 Search Request Type} *)
1313+1414+type search_request = {
1515+ collection : string; (** Collection to search *)
1616+ q : string; (** Query string *)
1717+ query_by : string list; (** Fields to search in *)
1818+ filter_by : string option; (** Filter expression *)
1919+ sort_by : string option; (** Sort expression *)
2020+ facet_by : string list option; (** Fields to facet on *)
2121+ per_page : int option; (** Results per page *)
2222+ page : int option; (** Page number *)
2323+ prefix : bool option; (** Enable prefix search *)
2424+ include_fields : string list option; (** Fields to include *)
2525+ exclude_fields : string list option; (** Fields to exclude *)
2626+}
2727+(** A single search request within a multi-search operation. *)
2828+2929+val search_request :
3030+ collection:string ->
3131+ q:string ->
3232+ query_by:string list ->
3333+ ?filter_by:string ->
3434+ ?sort_by:string ->
3535+ ?facet_by:string list ->
3636+ ?per_page:int ->
3737+ ?page:int ->
3838+ ?prefix:bool ->
3939+ ?include_fields:string list ->
4040+ ?exclude_fields:string list ->
4141+ unit ->
4242+ search_request
4343+(** [search_request ~collection ~q ~query_by ...] creates a search request. *)
4444+4545+val search_request_jsont : search_request Jsont.t
4646+(** JSON codec for search requests. *)
4747+4848+(** {1 Multi-Search Result} *)
4949+5050+type result = { results : Search.result list }
5151+(** Multi-search result containing results for each search request. *)
5252+5353+val result_jsont : result Jsont.t
5454+(** JSON codec for multi-search results. *)
5555+5656+(** {1 Multi-Search Operation} *)
5757+5858+val search : Client.t -> search_request list -> result
5959+(** [search client requests] executes multiple searches in a single request.
6060+ The results are returned in the same order as the requests.
6161+ @raise Eio.Io with [Error.E] on API errors *)
+261
lib/typesense/search.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type highlight = {
77+ field : string;
88+ snippet : string option;
99+ snippets : string list option;
1010+ matched_tokens : string list option;
1111+ indices : int list option;
1212+}
1313+1414+let highlight_jsont =
1515+ let make field snippet snippets matched_tokens indices =
1616+ { field; snippet; snippets; matched_tokens; indices }
1717+ in
1818+ Jsont.Object.map ~kind:"Highlight" make
1919+ |> Jsont.Object.mem "field" Jsont.string ~enc:(fun h -> h.field)
2020+ |> Jsont.Object.opt_mem "snippet" Jsont.string ~enc:(fun h -> h.snippet)
2121+ |> Jsont.Object.opt_mem "snippets" (Jsont.list Jsont.string)
2222+ ~enc:(fun h -> h.snippets)
2323+ |> Jsont.Object.opt_mem "matched_tokens" (Jsont.list Jsont.string)
2424+ ~enc:(fun h -> h.matched_tokens)
2525+ |> Jsont.Object.opt_mem "indices" (Jsont.list Jsont.int)
2626+ ~enc:(fun h -> h.indices)
2727+ |> Jsont.Object.skip_unknown
2828+ |> Jsont.Object.finish
2929+3030+type hit = {
3131+ document : Jsont.json;
3232+ highlights : highlight list option;
3333+ text_match : int64 option;
3434+ text_match_info : Jsont.json option;
3535+}
3636+3737+let hit_jsont =
3838+ let make document highlights text_match text_match_info =
3939+ { document; highlights; text_match; text_match_info }
4040+ in
4141+ Jsont.Object.map ~kind:"Hit" make
4242+ |> Jsont.Object.mem "document" Jsont.json ~enc:(fun h -> h.document)
4343+ |> Jsont.Object.opt_mem "highlights" (Jsont.list highlight_jsont)
4444+ ~enc:(fun h -> h.highlights)
4545+ |> Jsont.Object.opt_mem "text_match" Jsont.int64 ~enc:(fun h -> h.text_match)
4646+ |> Jsont.Object.opt_mem "text_match_info" Jsont.json
4747+ ~enc:(fun h -> h.text_match_info)
4848+ |> Jsont.Object.skip_unknown
4949+ |> Jsont.Object.finish
5050+5151+type facet_count = {
5252+ value : string;
5353+ count : int;
5454+ highlighted : string option;
5555+}
5656+5757+let facet_count_jsont =
5858+ let make value count highlighted = { value; count; highlighted } in
5959+ Jsont.Object.map ~kind:"FacetCount" make
6060+ |> Jsont.Object.mem "value" Jsont.string ~enc:(fun f -> f.value)
6161+ |> Jsont.Object.mem "count" Jsont.int ~enc:(fun f -> f.count)
6262+ |> Jsont.Object.opt_mem "highlighted" Jsont.string ~enc:(fun f -> f.highlighted)
6363+ |> Jsont.Object.skip_unknown
6464+ |> Jsont.Object.finish
6565+6666+type facet_stats = {
6767+ min : float option;
6868+ max : float option;
6969+ sum : float option;
7070+ avg : float option;
7171+ total_values : int option;
7272+}
7373+7474+let facet_stats_jsont =
7575+ let make min max sum avg total_values =
7676+ { min; max; sum; avg; total_values }
7777+ in
7878+ Jsont.Object.map ~kind:"FacetStats" make
7979+ |> Jsont.Object.opt_mem "min" Jsont.number ~enc:(fun f -> f.min)
8080+ |> Jsont.Object.opt_mem "max" Jsont.number ~enc:(fun f -> f.max)
8181+ |> Jsont.Object.opt_mem "sum" Jsont.number ~enc:(fun f -> f.sum)
8282+ |> Jsont.Object.opt_mem "avg" Jsont.number ~enc:(fun f -> f.avg)
8383+ |> Jsont.Object.opt_mem "total_values" Jsont.int ~enc:(fun f -> f.total_values)
8484+ |> Jsont.Object.skip_unknown
8585+ |> Jsont.Object.finish
8686+8787+type facet = {
8888+ field_name : string;
8989+ counts : facet_count list;
9090+ stats : facet_stats option;
9191+}
9292+9393+let facet_jsont =
9494+ let make field_name counts stats = { field_name; counts; stats } in
9595+ Jsont.Object.map ~kind:"Facet" make
9696+ |> Jsont.Object.mem "field_name" Jsont.string ~enc:(fun f -> f.field_name)
9797+ |> Jsont.Object.mem "counts" (Jsont.list facet_count_jsont) ~enc:(fun f -> f.counts)
9898+ |> Jsont.Object.opt_mem "stats" facet_stats_jsont ~enc:(fun f -> f.stats)
9999+ |> Jsont.Object.skip_unknown
100100+ |> Jsont.Object.finish
101101+102102+type result = {
103103+ hits : hit list;
104104+ found : int;
105105+ search_time_ms : int;
106106+ page : int option;
107107+ out_of : int option;
108108+ facet_counts : facet list option;
109109+ request_params : Jsont.json option;
110110+}
111111+112112+let result_jsont =
113113+ let make hits found search_time_ms page out_of facet_counts request_params =
114114+ { hits; found; search_time_ms; page; out_of; facet_counts; request_params }
115115+ in
116116+ Jsont.Object.map ~kind:"SearchResult" make
117117+ |> Jsont.Object.mem "hits" (Jsont.list hit_jsont) ~enc:(fun r -> r.hits)
118118+ |> Jsont.Object.mem "found" Jsont.int ~enc:(fun r -> r.found)
119119+ |> Jsont.Object.mem "search_time_ms" Jsont.int ~enc:(fun r -> r.search_time_ms)
120120+ |> Jsont.Object.opt_mem "page" Jsont.int ~enc:(fun r -> r.page)
121121+ |> Jsont.Object.opt_mem "out_of" Jsont.int ~enc:(fun r -> r.out_of)
122122+ |> Jsont.Object.opt_mem "facet_counts" (Jsont.list facet_jsont)
123123+ ~enc:(fun r -> r.facet_counts)
124124+ |> Jsont.Object.opt_mem "request_params" Jsont.json
125125+ ~enc:(fun r -> r.request_params)
126126+ |> Jsont.Object.skip_unknown
127127+ |> Jsont.Object.finish
128128+129129+type params = {
130130+ q : string;
131131+ query_by : string list;
132132+ filter_by : string option;
133133+ sort_by : string option;
134134+ facet_by : string list option;
135135+ max_facet_values : int option;
136136+ per_page : int option;
137137+ page : int option;
138138+ prefix : bool option;
139139+ infix : string option;
140140+ highlight_fields : string list option;
141141+ highlight_full_fields : string list option;
142142+ highlight_affix_num_tokens : int option;
143143+ highlight_start_tag : string option;
144144+ highlight_end_tag : string option;
145145+ snippet_threshold : int option;
146146+ num_typos : int option;
147147+ typo_tokens_threshold : int option;
148148+ drop_tokens_threshold : int option;
149149+ include_fields : string list option;
150150+ exclude_fields : string list option;
151151+ group_by : string list option;
152152+ group_limit : int option;
153153+ limit_hits : int option;
154154+ prioritize_exact_match : bool option;
155155+ prioritize_token_position : bool option;
156156+ exhaustive_search : bool option;
157157+ search_cutoff_ms : int option;
158158+ use_cache : bool option;
159159+ cache_ttl : int option;
160160+ enable_highlight_v1 : bool option;
161161+}
162162+163163+let params ~q ~query_by ?filter_by ?sort_by ?facet_by ?max_facet_values ?per_page
164164+ ?page ?prefix ?infix ?highlight_fields ?highlight_full_fields
165165+ ?highlight_affix_num_tokens ?highlight_start_tag ?highlight_end_tag
166166+ ?snippet_threshold ?num_typos ?typo_tokens_threshold ?drop_tokens_threshold
167167+ ?include_fields ?exclude_fields ?group_by ?group_limit ?limit_hits
168168+ ?prioritize_exact_match ?prioritize_token_position ?exhaustive_search
169169+ ?search_cutoff_ms ?use_cache ?cache_ttl ?enable_highlight_v1 () =
170170+ {
171171+ q;
172172+ query_by;
173173+ filter_by;
174174+ sort_by;
175175+ facet_by;
176176+ max_facet_values;
177177+ per_page;
178178+ page;
179179+ prefix;
180180+ infix;
181181+ highlight_fields;
182182+ highlight_full_fields;
183183+ highlight_affix_num_tokens;
184184+ highlight_start_tag;
185185+ highlight_end_tag;
186186+ snippet_threshold;
187187+ num_typos;
188188+ typo_tokens_threshold;
189189+ drop_tokens_threshold;
190190+ include_fields;
191191+ exclude_fields;
192192+ group_by;
193193+ group_limit;
194194+ limit_hits;
195195+ prioritize_exact_match;
196196+ prioritize_token_position;
197197+ exhaustive_search;
198198+ search_cutoff_ms;
199199+ use_cache;
200200+ cache_ttl;
201201+ enable_highlight_v1;
202202+ }
203203+204204+let params_to_query_params p =
205205+ let add_opt name opt acc =
206206+ match opt with Some v -> (name, v) :: acc | None -> acc
207207+ in
208208+ let add_opt_bool name opt acc =
209209+ match opt with
210210+ | Some v -> (name, string_of_bool v) :: acc
211211+ | None -> acc
212212+ in
213213+ let add_opt_int name opt acc =
214214+ match opt with Some v -> (name, string_of_int v) :: acc | None -> acc
215215+ in
216216+ let add_opt_list name opt acc =
217217+ match opt with
218218+ | Some v when v <> [] -> (name, String.concat "," v) :: acc
219219+ | _ -> acc
220220+ in
221221+ []
222222+ |> add_opt "q" (Some p.q)
223223+ |> add_opt "query_by" (Some (String.concat "," p.query_by))
224224+ |> add_opt "filter_by" p.filter_by
225225+ |> add_opt "sort_by" p.sort_by
226226+ |> add_opt_list "facet_by" p.facet_by
227227+ |> add_opt_int "max_facet_values" p.max_facet_values
228228+ |> add_opt_int "per_page" p.per_page
229229+ |> add_opt_int "page" p.page
230230+ |> add_opt_bool "prefix" p.prefix
231231+ |> add_opt "infix" p.infix
232232+ |> add_opt_list "highlight_fields" p.highlight_fields
233233+ |> add_opt_list "highlight_full_fields" p.highlight_full_fields
234234+ |> add_opt_int "highlight_affix_num_tokens" p.highlight_affix_num_tokens
235235+ |> add_opt "highlight_start_tag" p.highlight_start_tag
236236+ |> add_opt "highlight_end_tag" p.highlight_end_tag
237237+ |> add_opt_int "snippet_threshold" p.snippet_threshold
238238+ |> add_opt_int "num_typos" p.num_typos
239239+ |> add_opt_int "typo_tokens_threshold" p.typo_tokens_threshold
240240+ |> add_opt_int "drop_tokens_threshold" p.drop_tokens_threshold
241241+ |> add_opt_list "include_fields" p.include_fields
242242+ |> add_opt_list "exclude_fields" p.exclude_fields
243243+ |> add_opt_list "group_by" p.group_by
244244+ |> add_opt_int "group_limit" p.group_limit
245245+ |> add_opt_int "limit_hits" p.limit_hits
246246+ |> add_opt_bool "prioritize_exact_match" p.prioritize_exact_match
247247+ |> add_opt_bool "prioritize_token_position" p.prioritize_token_position
248248+ |> add_opt_bool "exhaustive_search" p.exhaustive_search
249249+ |> add_opt_int "search_cutoff_ms" p.search_cutoff_ms
250250+ |> add_opt_bool "use_cache" p.use_cache
251251+ |> add_opt_int "cache_ttl" p.cache_ttl
252252+ |> add_opt_bool "enable_highlight_v1" p.enable_highlight_v1
253253+ |> List.rev
254254+255255+let search client ~collection p =
256256+ let path =
257257+ "/collections/" ^ Uri.pct_encode collection ^ "/documents/search"
258258+ in
259259+ let params = params_to_query_params p in
260260+ let json = Client.request client ~method_:`GET ~path ~params () in
261261+ Encode.decode_or_raise result_jsont json "search"
+153
lib/typesense/search.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Typesense search operations.
77+88+ This module provides types and functions for searching documents in
99+ Typesense collections. *)
1010+1111+(** {1 Search Result Types} *)
1212+1313+type highlight = {
1414+ field : string; (** Field name that was highlighted *)
1515+ snippet : string option; (** Highlighted snippet (single value) *)
1616+ snippets : string list option; (** Highlighted snippets (array fields) *)
1717+ matched_tokens : string list option; (** Tokens that matched *)
1818+ indices : int list option; (** Indices of matched values in array *)
1919+}
2020+(** Highlighting information for a matched field. *)
2121+2222+val highlight_jsont : highlight Jsont.t
2323+2424+type hit = {
2525+ document : Jsont.json; (** The matched document *)
2626+ highlights : highlight list option; (** Highlighting information *)
2727+ text_match : int64 option; (** Text match score *)
2828+ text_match_info : Jsont.json option; (** Detailed match info *)
2929+}
3030+(** A single search hit. *)
3131+3232+val hit_jsont : hit Jsont.t
3333+3434+type facet_count = {
3535+ value : string; (** Facet value *)
3636+ count : int; (** Number of documents with this value *)
3737+ highlighted : string option; (** Highlighted value for facet search *)
3838+}
3939+(** Count for a single facet value. *)
4040+4141+val facet_count_jsont : facet_count Jsont.t
4242+4343+type facet_stats = {
4444+ min : float option;
4545+ max : float option;
4646+ sum : float option;
4747+ avg : float option;
4848+ total_values : int option;
4949+}
5050+(** Statistics for numeric facets. *)
5151+5252+val facet_stats_jsont : facet_stats Jsont.t
5353+5454+type facet = {
5555+ field_name : string; (** Faceted field name *)
5656+ counts : facet_count list; (** Value counts *)
5757+ stats : facet_stats option; (** Numeric statistics *)
5858+}
5959+(** Facet results for a field. *)
6060+6161+val facet_jsont : facet Jsont.t
6262+6363+type result = {
6464+ hits : hit list; (** Matched documents *)
6565+ found : int; (** Total documents matching query *)
6666+ search_time_ms : int; (** Search time in milliseconds *)
6767+ page : int option; (** Current page number *)
6868+ out_of : int option; (** Total documents in collection *)
6969+ facet_counts : facet list option; (** Facet results *)
7070+ request_params : Jsont.json option; (** Echo of request parameters *)
7171+}
7272+(** Search result containing hits and metadata. *)
7373+7474+val result_jsont : result Jsont.t
7575+7676+(** {1 Search Parameters} *)
7777+7878+type params = {
7979+ q : string; (** Query string (use "*" for all documents) *)
8080+ query_by : string list; (** Fields to search in *)
8181+ filter_by : string option; (** Filter expression *)
8282+ sort_by : string option; (** Sort expression *)
8383+ facet_by : string list option; (** Fields to facet on *)
8484+ max_facet_values : int option; (** Max facet values to return *)
8585+ per_page : int option; (** Results per page (default: 10) *)
8686+ page : int option; (** Page number (default: 1) *)
8787+ prefix : bool option; (** Enable prefix search *)
8888+ infix : string option; (** Infix search mode *)
8989+ highlight_fields : string list option; (** Fields to highlight *)
9090+ highlight_full_fields : string list option; (** Fields for full highlight *)
9191+ highlight_affix_num_tokens : int option; (** Tokens around highlight *)
9292+ highlight_start_tag : string option; (** Start tag for highlighting *)
9393+ highlight_end_tag : string option; (** End tag for highlighting *)
9494+ snippet_threshold : int option; (** Threshold for snippets *)
9595+ num_typos : int option; (** Max typos allowed *)
9696+ typo_tokens_threshold : int option; (** Typo token threshold *)
9797+ drop_tokens_threshold : int option; (** Token dropping threshold *)
9898+ include_fields : string list option; (** Fields to include in results *)
9999+ exclude_fields : string list option; (** Fields to exclude from results *)
100100+ group_by : string list option; (** Fields to group by *)
101101+ group_limit : int option; (** Max documents per group *)
102102+ limit_hits : int option; (** Max total hits *)
103103+ prioritize_exact_match : bool option; (** Prioritize exact matches *)
104104+ prioritize_token_position : bool option; (** Prioritize token position *)
105105+ exhaustive_search : bool option; (** Exhaustive search mode *)
106106+ search_cutoff_ms : int option; (** Search timeout *)
107107+ use_cache : bool option; (** Use search cache *)
108108+ cache_ttl : int option; (** Cache TTL in seconds *)
109109+ enable_highlight_v1 : bool option; (** Use v1 highlighting *)
110110+}
111111+(** Search parameters. *)
112112+113113+val params :
114114+ q:string ->
115115+ query_by:string list ->
116116+ ?filter_by:string ->
117117+ ?sort_by:string ->
118118+ ?facet_by:string list ->
119119+ ?max_facet_values:int ->
120120+ ?per_page:int ->
121121+ ?page:int ->
122122+ ?prefix:bool ->
123123+ ?infix:string ->
124124+ ?highlight_fields:string list ->
125125+ ?highlight_full_fields:string list ->
126126+ ?highlight_affix_num_tokens:int ->
127127+ ?highlight_start_tag:string ->
128128+ ?highlight_end_tag:string ->
129129+ ?snippet_threshold:int ->
130130+ ?num_typos:int ->
131131+ ?typo_tokens_threshold:int ->
132132+ ?drop_tokens_threshold:int ->
133133+ ?include_fields:string list ->
134134+ ?exclude_fields:string list ->
135135+ ?group_by:string list ->
136136+ ?group_limit:int ->
137137+ ?limit_hits:int ->
138138+ ?prioritize_exact_match:bool ->
139139+ ?prioritize_token_position:bool ->
140140+ ?exhaustive_search:bool ->
141141+ ?search_cutoff_ms:int ->
142142+ ?use_cache:bool ->
143143+ ?cache_ttl:int ->
144144+ ?enable_highlight_v1:bool ->
145145+ unit ->
146146+ params
147147+(** [params ~q ~query_by ...] creates search parameters. *)
148148+149149+(** {1 Search Operation} *)
150150+151151+val search : Client.t -> collection:string -> params -> result
152152+(** [search client ~collection params] searches for documents.
153153+ @raise Eio.Io with [Error.E] on API errors *)