My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Squashed 'ocaml-typesense/' content from commit 8cedde1

git-subtree-dir: ocaml-typesense
git-subtree-split: 8cedde16cf8f510c71138d9d4867dc3b3b29aad2

+2112
+41
.gitignore
··· 1 + # OCaml build artifacts 2 + _build/ 3 + *.install 4 + *.byte 5 + *.native 6 + *.cmo 7 + *.cmi 8 + *.cma 9 + *.cmx 10 + *.cmxa 11 + *.cmxs 12 + *.o 13 + *.a 14 + 15 + # Dune 16 + _opam/ 17 + 18 + # Third-party sources (fetch locally with opam source) 19 + third_party/ 20 + 21 + # Editor files 22 + *.swp 23 + *.swo 24 + *~ 25 + .vscode/ 26 + .idea/ 27 + *.sublime-* 28 + 29 + # OS files 30 + .DS_Store 31 + Thumbs.db 32 + 33 + # Merlin (generated by dune) 34 + .merlin 35 + 36 + # Coverage 37 + _coverage/ 38 + *.coverage 39 + 40 + # Local opam switch 41 + _opam/
+1
.ocamlformat
··· 1 + version = 0.28.1
+8
CHANGES.md
··· 1 + ## Unreleased 2 + 3 + - Initial release with core Typesense API bindings 4 + - Collection management (create, list, get, delete) 5 + - Document operations (import, get, update, delete) 6 + - Search with filtering, faceting, and highlighting 7 + - Multi-search across collections 8 + - Analytics rules and events
+15
LICENSE.md
··· 1 + ISC License 2 + 3 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org> 4 + 5 + Permission to use, copy, modify, and/or distribute this software for any 6 + purpose with or without fee is hereby granted, provided that the above 7 + copyright notice and this permission notice appear in all copies. 8 + 9 + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 + PERFORMANCE OF THIS SOFTWARE.
+64
README.md
··· 1 + # typesense 2 + 3 + OCaml bindings for the [Typesense](https://typesense.org/) search API. 4 + 5 + Typesense is a fast, typo-tolerant search engine. This library provides 6 + high-quality OCaml bindings using [Eio](https://github.com/ocaml-multicore/eio) 7 + for async operations. 8 + 9 + ## Features 10 + 11 + - Collection management (create, list, get, delete) 12 + - Document operations (import, get, update, delete, export) 13 + - Search with filtering, faceting, and highlighting 14 + - Multi-search across collections 15 + - Analytics rules and event tracking 16 + 17 + ## Installation 18 + 19 + ``` 20 + opam install typesense 21 + ``` 22 + 23 + ## Usage 24 + 25 + ```ocaml 26 + open Typesense 27 + 28 + let () = 29 + Eio_main.run @@ fun env -> 30 + let auth = Auth.create ~endpoint:"http://localhost:8108" ~api_key:"xyz" in 31 + Client.with_client env auth @@ fun client -> 32 + 33 + (* Create a collection *) 34 + let schema = 35 + Collection.schema ~name:"books" 36 + ~fields: 37 + [ 38 + Collection.field ~name:"title" ~type_:"string" (); 39 + Collection.field ~name:"author" ~type_:"string" ~facet:true (); 40 + Collection.field ~name:"year" ~type_:"int32" (); 41 + ] 42 + ~default_sorting_field:"year" () 43 + in 44 + let _ = Collection.create client schema in 45 + 46 + (* Search *) 47 + let params = Search.params ~q:"harry" ~query_by:["title"; "author"] () in 48 + let result = Search.search client ~collection:"books" params in 49 + Printf.printf "Found %d books\n" result.found 50 + ``` 51 + 52 + ## Error Handling 53 + 54 + All API operations raise `Eio.Io` exceptions with `Error.E` error codes: 55 + 56 + ```ocaml 57 + try Collection.get client ~name:"nonexistent" with 58 + | Eio.Io (Error.E { code = Not_found; message; _ }, _) -> 59 + Printf.eprintf "Collection not found: %s\n" message 60 + ``` 61 + 62 + ## License 63 + 64 + ISC License. See [LICENSE.md](LICENSE.md) for details.
+36
dune-project
··· 1 + (lang dune 3.20) 2 + 3 + (name typesense) 4 + 5 + (generate_opam_files true) 6 + 7 + (maintenance_intent "(latest)") 8 + 9 + (license ISC) 10 + 11 + (authors "Anil Madhavapeddy <anil@recoil.org>") 12 + 13 + (maintainers "Anil Madhavapeddy <anil@recoil.org>") 14 + 15 + (source 16 + (uri "https://tangled.org/@anil.recoil.org/ocaml-typesense")) 17 + 18 + (homepage "https://tangled.org/@anil.recoil.org/ocaml-typesense") 19 + 20 + (bug_reports "https://tangled.org/@anil.recoil.org/ocaml-typesense/issues") 21 + 22 + (package 23 + (name typesense) 24 + (synopsis "OCaml bindings for the Typesense search API") 25 + (description 26 + "High-quality OCaml bindings to the Typesense search API using Eio for async operations. Provides collection management, document operations, search, multi-search, and analytics.") 27 + (depends 28 + (ocaml (>= 5.1.0)) 29 + (eio (>= 1.2)) 30 + (requests (>= 0.3.1)) 31 + (uri (>= 4.4.0)) 32 + (jsont (>= 0.1.1)) 33 + (jsont-bytesrw (>= 0.1.1)) 34 + (logs (>= 0.7.0)) 35 + (odoc :with-doc) 36 + (alcotest :with-test)))
+181
lib/typesense/analytics.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + type rule_type = Popular_queries | Nohits_queries | Counter 7 + 8 + let rule_type_to_string = function 9 + | Popular_queries -> "popular_queries" 10 + | Nohits_queries -> "nohits_queries" 11 + | Counter -> "counter" 12 + 13 + let rule_type_of_string = function 14 + | "popular_queries" -> Popular_queries 15 + | "nohits_queries" -> Nohits_queries 16 + | "counter" -> Counter 17 + | s -> failwith ("Unknown rule type: " ^ s) 18 + 19 + let rule_type_jsont = 20 + Jsont.of_of_string ~kind:"RuleType" (fun s -> Ok (rule_type_of_string s)) 21 + ~enc:rule_type_to_string 22 + 23 + type destination = { collection : string } 24 + 25 + let destination_jsont = 26 + Jsont.Object.map ~kind:"Destination" (fun collection -> { collection }) 27 + |> Jsont.Object.mem "collection" Jsont.string ~enc:(fun d -> d.collection) 28 + |> Jsont.Object.skip_unknown 29 + |> Jsont.Object.finish 30 + 31 + type source = { 32 + collections : string list; 33 + events : Jsont.json option; 34 + } 35 + 36 + let source_jsont = 37 + let make collections events = { collections; events } in 38 + Jsont.Object.map ~kind:"Source" make 39 + |> Jsont.Object.mem "collections" (Jsont.list Jsont.string) 40 + ~enc:(fun s -> s.collections) 41 + |> Jsont.Object.opt_mem "events" Jsont.json ~enc:(fun s -> s.events) 42 + |> Jsont.Object.skip_unknown 43 + |> Jsont.Object.finish 44 + 45 + type rule_params = { 46 + source : source; 47 + destination : destination; 48 + limit : int option; 49 + expand_query : bool option; 50 + } 51 + 52 + let rule_params_jsont = 53 + let make source destination limit expand_query = 54 + { source; destination; limit; expand_query } 55 + in 56 + Jsont.Object.map ~kind:"RuleParams" make 57 + |> Jsont.Object.mem "source" source_jsont ~enc:(fun p -> p.source) 58 + |> Jsont.Object.mem "destination" destination_jsont ~enc:(fun p -> p.destination) 59 + |> Jsont.Object.opt_mem "limit" Jsont.int ~enc:(fun p -> p.limit) 60 + |> Jsont.Object.opt_mem "expand_query" Jsont.bool ~enc:(fun p -> p.expand_query) 61 + |> Jsont.Object.skip_unknown 62 + |> Jsont.Object.finish 63 + 64 + type rule = { 65 + name : string; 66 + type_ : rule_type; 67 + params : rule_params; 68 + } 69 + 70 + let rule_jsont = 71 + let make name type_ params = { name; type_; params } in 72 + Jsont.Object.map ~kind:"Rule" make 73 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun r -> r.name) 74 + |> Jsont.Object.mem "type" rule_type_jsont ~enc:(fun r -> r.type_) 75 + |> Jsont.Object.mem "params" rule_params_jsont ~enc:(fun r -> r.params) 76 + |> Jsont.Object.skip_unknown 77 + |> Jsont.Object.finish 78 + 79 + let rule ~name ~type_ ~params = { name; type_; params } 80 + 81 + let rule_params ~source_collections ~destination_collection ?limit 82 + ?expand_query ?events () = 83 + { 84 + source = { collections = source_collections; events }; 85 + destination = { collection = destination_collection }; 86 + limit; 87 + expand_query; 88 + } 89 + 90 + type rules_response = { rules : rule list } 91 + 92 + let rules_response_jsont = 93 + Jsont.Object.map ~kind:"RulesResponse" (fun rules -> { rules }) 94 + |> Jsont.Object.mem "rules" (Jsont.list rule_jsont) ~enc:(fun r -> r.rules) 95 + |> Jsont.Object.skip_unknown 96 + |> Jsont.Object.finish 97 + 98 + let list_rules client = 99 + let json = Client.request client ~method_:`GET ~path:"/analytics/rules" () in 100 + let response = Encode.decode_or_raise rules_response_jsont json "list rules" in 101 + response.rules 102 + 103 + let get_rule client ~name = 104 + let path = "/analytics/rules/" ^ Uri.pct_encode name in 105 + let json = Client.request client ~method_:`GET ~path () in 106 + Encode.decode_or_raise rule_jsont json ("get rule " ^ name) 107 + 108 + let create_rule client rule = 109 + let body = Encode.to_json_string rule_jsont rule in 110 + let json = Client.request client ~method_:`POST ~path:"/analytics/rules" ~body () in 111 + Encode.decode_or_raise rule_jsont json ("create rule " ^ rule.name) 112 + 113 + let upsert_rule client rule = 114 + let path = "/analytics/rules/" ^ Uri.pct_encode rule.name in 115 + let body = Encode.to_json_string rule_jsont rule in 116 + let json = Client.request client ~method_:`PUT ~path ~body () in 117 + Encode.decode_or_raise rule_jsont json ("upsert rule " ^ rule.name) 118 + 119 + type delete_result = { name : string } 120 + 121 + let delete_result_jsont = 122 + Jsont.Object.map ~kind:"DeleteResult" (fun name -> { name }) 123 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun r -> r.name) 124 + |> Jsont.Object.skip_unknown 125 + |> Jsont.Object.finish 126 + 127 + let delete_rule client ~name = 128 + let path = "/analytics/rules/" ^ Uri.pct_encode name in 129 + let json = Client.request client ~method_:`DELETE ~path () in 130 + let _ = Encode.decode_or_raise delete_result_jsont json ("delete rule " ^ name) in 131 + () 132 + 133 + type event_type = Search | Click | Conversion | Visit | Custom of string 134 + 135 + let event_type_to_string = function 136 + | Search -> "search" 137 + | Click -> "click" 138 + | Conversion -> "conversion" 139 + | Visit -> "visit" 140 + | Custom s -> s 141 + 142 + let event_type_of_string = function 143 + | "search" -> Search 144 + | "click" -> Click 145 + | "conversion" -> Conversion 146 + | "visit" -> Visit 147 + | s -> Custom s 148 + 149 + let event_type_jsont = 150 + Jsont.of_of_string ~kind:"EventType" (fun s -> Ok (event_type_of_string s)) 151 + ~enc:event_type_to_string 152 + 153 + type event = { 154 + type_ : event_type; 155 + name : string; 156 + data : Jsont.json; 157 + } 158 + 159 + let event_jsont = 160 + let make type_ name data = { type_; name; data } in 161 + Jsont.Object.map ~kind:"Event" make 162 + |> Jsont.Object.mem "type" event_type_jsont ~enc:(fun e -> e.type_) 163 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun e -> e.name) 164 + |> Jsont.Object.mem "data" Jsont.json ~enc:(fun e -> e.data) 165 + |> Jsont.Object.finish 166 + 167 + let event ~type_ ~name ~data = { type_; name; data } 168 + 169 + type event_response = { ok : bool } 170 + 171 + let event_response_jsont = 172 + Jsont.Object.map ~kind:"EventResponse" (fun ok -> { ok }) 173 + |> Jsont.Object.mem "ok" Jsont.bool ~enc:(fun r -> r.ok) 174 + |> Jsont.Object.skip_unknown 175 + |> Jsont.Object.finish 176 + 177 + let create_event client event = 178 + let body = Encode.to_json_string event_jsont event in 179 + let json = Client.request client ~method_:`POST ~path:"/analytics/events" ~body () in 180 + let _ = Encode.decode_or_raise event_response_jsont json "create event" in 181 + ()
+113
lib/typesense/analytics.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Typesense analytics rules and events. 7 + 8 + This module provides support for creating and managing analytics rules 9 + and tracking events. Analytics rules automatically aggregate search queries 10 + into collections for analysis. *) 11 + 12 + (** {1 Rule Types} *) 13 + 14 + type rule_type = 15 + | Popular_queries (** Track popular search queries *) 16 + | Nohits_queries (** Track queries with no results *) 17 + | Counter (** Generic counter for events *) 18 + (** Type of analytics rule. *) 19 + 20 + val rule_type_jsont : rule_type Jsont.t 21 + 22 + type destination = { collection : string } 23 + (** Destination collection for aggregated data. *) 24 + 25 + type source = { 26 + collections : string list; (** Source collections to track *) 27 + events : Jsont.json option; (** Event configuration *) 28 + } 29 + (** Source configuration for analytics. *) 30 + 31 + type rule_params = { 32 + source : source; 33 + destination : destination; 34 + limit : int option; (** Max entries to store *) 35 + expand_query : bool option; (** Expand queries before storing *) 36 + } 37 + (** Parameters for analytics rules. *) 38 + 39 + val rule_params : 40 + source_collections:string list -> 41 + destination_collection:string -> 42 + ?limit:int -> 43 + ?expand_query:bool -> 44 + ?events:Jsont.json -> 45 + unit -> 46 + rule_params 47 + (** [rule_params ~source_collections ~destination_collection ...] creates rule 48 + parameters. *) 49 + 50 + type rule = { 51 + name : string; (** Unique rule name *) 52 + type_ : rule_type; (** Rule type *) 53 + params : rule_params; (** Rule parameters *) 54 + } 55 + (** An analytics rule. *) 56 + 57 + val rule : name:string -> type_:rule_type -> params:rule_params -> rule 58 + (** [rule ~name ~type_ ~params] creates an analytics rule. *) 59 + 60 + val rule_jsont : rule Jsont.t 61 + (** JSON codec for rules. *) 62 + 63 + (** {1 Rule Operations} *) 64 + 65 + val list_rules : Client.t -> rule list 66 + (** [list_rules client] returns all analytics rules. 67 + @raise Eio.Io with [Error.E] on API errors *) 68 + 69 + val get_rule : Client.t -> name:string -> rule 70 + (** [get_rule client ~name] retrieves a rule by name. 71 + @raise Eio.Io with [Error.E { code = Not_found; _ }] if not found *) 72 + 73 + val create_rule : Client.t -> rule -> rule 74 + (** [create_rule client rule] creates a new analytics rule. 75 + @raise Eio.Io with [Error.E { code = Conflict; _ }] if already exists *) 76 + 77 + val upsert_rule : Client.t -> rule -> rule 78 + (** [upsert_rule client rule] creates or updates an analytics rule. *) 79 + 80 + val delete_rule : Client.t -> name:string -> unit 81 + (** [delete_rule client ~name] deletes an analytics rule. 82 + @raise Eio.Io with [Error.E { code = Not_found; _ }] if not found *) 83 + 84 + (** {1 Event Types} *) 85 + 86 + type event_type = 87 + | Search (** Search event *) 88 + | Click (** Click event *) 89 + | Conversion (** Conversion event *) 90 + | Visit (** Visit event *) 91 + | Custom of string (** Custom event type *) 92 + (** Type of analytics event. *) 93 + 94 + val event_type_jsont : event_type Jsont.t 95 + 96 + type event = { 97 + type_ : event_type; (** Event type *) 98 + name : string; (** Event name (rule name to track) *) 99 + data : Jsont.json; (** Event data *) 100 + } 101 + (** An analytics event. *) 102 + 103 + val event : type_:event_type -> name:string -> data:Jsont.json -> event 104 + (** [event ~type_ ~name ~data] creates an analytics event. *) 105 + 106 + val event_jsont : event Jsont.t 107 + (** JSON codec for events. *) 108 + 109 + (** {1 Event Operations} *) 110 + 111 + val create_event : Client.t -> event -> unit 112 + (** [create_event client event] records an analytics event. 113 + @raise Eio.Io with [Error.E] on API errors *)
+28
lib/typesense/auth.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + type t = { endpoint : string; api_key : string } 7 + 8 + let create ~endpoint ~api_key = { endpoint; api_key } 9 + 10 + let from_env ?endpoint_var ?api_key_var () = 11 + let endpoint_var = Option.value ~default:"TYPESENSE_API_ENDPOINT" endpoint_var in 12 + let api_key_var = Option.value ~default:"TYPESENSE_API_KEY" api_key_var in 13 + match (Sys.getenv_opt endpoint_var, Sys.getenv_opt api_key_var) with 14 + | Some endpoint, Some api_key -> Some { endpoint; api_key } 15 + | _ -> None 16 + 17 + let endpoint t = t.endpoint 18 + let api_key t = t.api_key 19 + 20 + let default_headers t = 21 + [ 22 + ("X-TYPESENSE-API-KEY", t.api_key); 23 + ("Content-Type", "application/json"); 24 + ("User-Agent", "OCaml-Typesense/1.0"); 25 + ] 26 + 27 + let pp fmt t = 28 + Format.fprintf fmt "Auth{endpoint=%s}" t.endpoint
+38
lib/typesense/auth.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Authentication for the Typesense API. 7 + 8 + This module handles authentication credentials for connecting to a Typesense 9 + server. Typesense uses API key authentication via the [X-TYPESENSE-API-KEY] 10 + header. *) 11 + 12 + type t 13 + (** Authentication credentials. *) 14 + 15 + val create : endpoint:string -> api_key:string -> t 16 + (** [create ~endpoint ~api_key] creates authentication credentials directly. 17 + @param endpoint The Typesense server URL (e.g., "http://localhost:8108") 18 + @param api_key The Typesense API key *) 19 + 20 + val from_env : ?endpoint_var:string -> ?api_key_var:string -> unit -> t option 21 + (** [from_env ?endpoint_var ?api_key_var ()] loads credentials from environment 22 + variables. 23 + @param endpoint_var Environment variable for endpoint (default: TYPESENSE_API_ENDPOINT) 24 + @param api_key_var Environment variable for API key (default: TYPESENSE_API_KEY) 25 + @return [Some t] if both variables are set, [None] otherwise *) 26 + 27 + val endpoint : t -> string 28 + (** [endpoint t] returns the Typesense server endpoint. *) 29 + 30 + val api_key : t -> string 31 + (** [api_key t] returns the API key. *) 32 + 33 + val default_headers : t -> (string * string) list 34 + (** [default_headers t] returns the default HTTP headers for authentication. 35 + Includes the API key header, Content-Type, and User-Agent. *) 36 + 37 + val pp : Format.formatter -> t -> unit 38 + (** Pretty printer for authentication (shows endpoint only, not credentials). *)
+101
lib/typesense/client.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + let src = Logs.Src.create "typesense.client" ~doc:"Typesense API client" 7 + 8 + module Log = (val Logs.src_log src : Logs.LOG) 9 + 10 + type t = { auth : Auth.t; session : Requests.t } 11 + 12 + let create ?session ~sw env auth = 13 + Log.info (fun m -> m "Creating Typesense client for %s" (Auth.endpoint auth)); 14 + let session = match session with 15 + | Some s -> s 16 + | None -> Requests.create ~sw ~follow_redirects:true ~verify_tls:true env 17 + in 18 + { auth; session } 19 + 20 + let with_client ?session env auth f = 21 + Eio.Switch.run @@ fun sw -> 22 + let client = create ?session ~sw env auth in 23 + f client 24 + 25 + let auth_headers t = 26 + Requests.Headers.of_list (Auth.default_headers t.auth) 27 + 28 + let method_to_string = function 29 + | `GET -> "GET" 30 + | `POST -> "POST" 31 + | `PUT -> "PUT" 32 + | `DELETE -> "DELETE" 33 + | `PATCH -> "PATCH" 34 + 35 + let build_url base_url params = 36 + match params with 37 + | None -> base_url 38 + | Some p -> 39 + Uri.of_string base_url 40 + |> Fun.flip 41 + (List.fold_left (fun u (k, v) -> Uri.add_query_param' u (k, v))) 42 + p 43 + |> Uri.to_string 44 + 45 + let make_request t ~method_ ~url ~body_opt = 46 + let headers = auth_headers t in 47 + match method_ with 48 + | `GET -> Requests.get t.session ~headers url 49 + | `POST -> Requests.post t.session ~headers ?body:body_opt url 50 + | `PUT -> Requests.put t.session ~headers ?body:body_opt url 51 + | `DELETE -> Requests.delete t.session ~headers url 52 + | `PATCH -> Requests.patch t.session ~headers ?body:body_opt url 53 + 54 + let request t ~method_ ~path ?params ?body () = 55 + let url = build_url (Auth.endpoint t.auth ^ path) params in 56 + Log.debug (fun m -> m "Request: %s %s" (method_to_string method_) path); 57 + let body_opt = 58 + Option.map (fun s -> Requests.Body.of_string Requests.Mime.json s) body 59 + in 60 + let response = make_request t ~method_ ~url ~body_opt in 61 + let status = Requests.Response.status_code response in 62 + Log.debug (fun m -> m "Response status: %d" status); 63 + let json = Requests.Response.json response in 64 + if status >= 400 then begin 65 + let message = 66 + match json with 67 + | Jsont.Object (fields, _) -> ( 68 + let assoc = List.map (fun ((k, _), v) -> (k, v)) fields in 69 + match List.assoc_opt "message" assoc with 70 + | Some (Jsont.String (s, _)) -> s 71 + | _ -> "Unknown error") 72 + | _ -> "Unknown error" 73 + in 74 + Error.raise_with_context 75 + (Error.make ~code:(Error.code_of_status status) ~message) 76 + "%s" path 77 + end; 78 + json 79 + 80 + let request_raw t ~method_ ~path ?params ?body () = 81 + let url = build_url (Auth.endpoint t.auth ^ path) params in 82 + Log.debug (fun m -> m "Request (raw): %s %s" (method_to_string method_) path); 83 + let body_opt = 84 + Option.map 85 + (fun s -> 86 + Requests.Body.of_string 87 + (Requests.Mime.of_string "application/x-ndjson") 88 + s) 89 + body 90 + in 91 + let response = make_request t ~method_ ~url ~body_opt in 92 + let status = Requests.Response.status_code response in 93 + Log.debug (fun m -> m "Response status: %d" status); 94 + let body_str = Requests.Response.text response in 95 + if status >= 400 then 96 + Error.raise_with_context 97 + (Error.make ~code:(Error.code_of_status status) ~message:body_str) 98 + "%s" path; 99 + body_str 100 + 101 + let pp fmt t = Format.fprintf fmt "Client(endpoint=%s)" (Auth.endpoint t.auth)
+71
lib/typesense/client.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** HTTP client for making requests to the Typesense API. 7 + 8 + This module provides the low-level HTTP client for communicating with the 9 + Typesense API. All API errors are raised as [Eio.Io] exceptions with 10 + [Error.E] error codes, following the Eio error pattern. 11 + 12 + @raise Eio.Io with [Error.E error] for API errors *) 13 + 14 + type t 15 + (** Type representing a Typesense HTTP client *) 16 + 17 + val create : 18 + ?session:Requests.t -> 19 + sw:Eio.Switch.t -> 20 + < clock : float Eio.Time.clock_ty Eio.Resource.t 21 + ; net : [ `Generic | `Unix ] Eio.Net.ty Eio.Resource.t 22 + ; fs : Eio.Fs.dir_ty Eio.Path.t 23 + ; .. > -> 24 + Auth.t -> 25 + t 26 + (** [create ?session ~sw env auth] creates a new client with the given switch, 27 + environment and authentication. If [session] is provided, it is reused; 28 + otherwise a new session is created. The environment must have clock, net, 29 + and fs capabilities. *) 30 + 31 + val with_client : 32 + ?session:Requests.t -> 33 + < clock : float Eio.Time.clock_ty Eio.Resource.t 34 + ; net : [ `Generic | `Unix ] Eio.Net.ty Eio.Resource.t 35 + ; fs : Eio.Fs.dir_ty Eio.Path.t 36 + ; .. > -> 37 + Auth.t -> 38 + (t -> 'a) -> 39 + 'a 40 + (** [with_client ?session env auth f] runs [f] with a client that is 41 + automatically cleaned up. If [session] is provided, it is reused. The 42 + environment must have clock, net, and fs capabilities. *) 43 + 44 + val request : 45 + t -> 46 + method_:[ `GET | `POST | `PUT | `DELETE | `PATCH ] -> 47 + path:string -> 48 + ?params:(string * string) list -> 49 + ?body:string -> 50 + unit -> 51 + Jsont.json 52 + (** [request t ~method_ ~path ?params ?body ()] makes an HTTP request to the 53 + Typesense API and returns the JSON response. 54 + @param params Optional query parameters 55 + @param body Optional JSON request body 56 + @raise Eio.Io with [Error.E error] on API errors *) 57 + 58 + val request_raw : 59 + t -> 60 + method_:[ `GET | `POST | `PUT | `DELETE | `PATCH ] -> 61 + path:string -> 62 + ?params:(string * string) list -> 63 + ?body:string -> 64 + unit -> 65 + string 66 + (** [request_raw t ~method_ ~path ?params ?body ()] makes an HTTP request and 67 + returns the raw response body as a string. Used for JSONL import responses. 68 + @raise Eio.Io with [Error.E error] on API errors *) 69 + 70 + val pp : Format.formatter -> t -> unit 71 + (** Pretty printer for client (shows endpoint only, not credentials) *)
+144
lib/typesense/collection.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + type field = { 7 + name : string; 8 + type_ : string; 9 + facet : bool option; 10 + optional : bool option; 11 + index : bool option; 12 + sort : bool option; 13 + infix : bool option; 14 + locale : string option; 15 + num_dim : int option; 16 + } 17 + 18 + let field_jsont = 19 + let make name type_ facet optional index sort infix locale num_dim = 20 + { name; type_; facet; optional; index; sort; infix; locale; num_dim } 21 + in 22 + Jsont.Object.map ~kind:"Field" make 23 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun f -> f.name) 24 + |> Jsont.Object.mem "type" Jsont.string ~enc:(fun f -> f.type_) 25 + |> Jsont.Object.opt_mem "facet" Jsont.bool ~enc:(fun f -> f.facet) 26 + |> Jsont.Object.opt_mem "optional" Jsont.bool ~enc:(fun f -> f.optional) 27 + |> Jsont.Object.opt_mem "index" Jsont.bool ~enc:(fun f -> f.index) 28 + |> Jsont.Object.opt_mem "sort" Jsont.bool ~enc:(fun f -> f.sort) 29 + |> Jsont.Object.opt_mem "infix" Jsont.bool ~enc:(fun f -> f.infix) 30 + |> Jsont.Object.opt_mem "locale" Jsont.string ~enc:(fun f -> f.locale) 31 + |> Jsont.Object.opt_mem "num_dim" Jsont.int ~enc:(fun f -> f.num_dim) 32 + |> Jsont.Object.skip_unknown 33 + |> Jsont.Object.finish 34 + 35 + let field ~name ~type_ ?facet ?optional ?index ?sort ?infix ?locale ?num_dim () = 36 + { name; type_; facet; optional; index; sort; infix; locale; num_dim } 37 + 38 + type schema = { 39 + name : string; 40 + fields : field list; 41 + default_sorting_field : string option; 42 + token_separators : string list option; 43 + symbols_to_index : string list option; 44 + enable_nested_fields : bool option; 45 + } 46 + 47 + let schema_jsont = 48 + let make name fields default_sorting_field token_separators symbols_to_index 49 + enable_nested_fields = 50 + { 51 + name; 52 + fields; 53 + default_sorting_field; 54 + token_separators; 55 + symbols_to_index; 56 + enable_nested_fields; 57 + } 58 + in 59 + Jsont.Object.map ~kind:"Schema" make 60 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun s -> s.name) 61 + |> Jsont.Object.mem "fields" (Jsont.list field_jsont) ~enc:(fun s -> s.fields) 62 + |> Jsont.Object.opt_mem "default_sorting_field" Jsont.string 63 + ~enc:(fun s -> s.default_sorting_field) 64 + |> Jsont.Object.opt_mem "token_separators" (Jsont.list Jsont.string) 65 + ~enc:(fun s -> s.token_separators) 66 + |> Jsont.Object.opt_mem "symbols_to_index" (Jsont.list Jsont.string) 67 + ~enc:(fun s -> s.symbols_to_index) 68 + |> Jsont.Object.opt_mem "enable_nested_fields" Jsont.bool 69 + ~enc:(fun s -> s.enable_nested_fields) 70 + |> Jsont.Object.skip_unknown 71 + |> Jsont.Object.finish 72 + 73 + let schema ~name ~fields ?default_sorting_field ?token_separators 74 + ?symbols_to_index ?enable_nested_fields () = 75 + { 76 + name; 77 + fields; 78 + default_sorting_field; 79 + token_separators; 80 + symbols_to_index; 81 + enable_nested_fields; 82 + } 83 + 84 + type t = { 85 + name : string; 86 + num_documents : int; 87 + fields : field list; 88 + default_sorting_field : string option; 89 + enable_nested_fields : bool option; 90 + } 91 + 92 + let jsont = 93 + let make name num_documents fields default_sorting_field enable_nested_fields = 94 + { name; num_documents; fields; default_sorting_field; enable_nested_fields } 95 + in 96 + Jsont.Object.map ~kind:"Collection" make 97 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> c.name) 98 + |> Jsont.Object.mem "num_documents" Jsont.int ~enc:(fun c -> c.num_documents) 99 + |> Jsont.Object.mem "fields" (Jsont.list field_jsont) ~enc:(fun c -> c.fields) 100 + |> Jsont.Object.opt_mem "default_sorting_field" Jsont.string 101 + ~enc:(fun c -> c.default_sorting_field) 102 + |> Jsont.Object.opt_mem "enable_nested_fields" Jsont.bool 103 + ~enc:(fun c -> c.enable_nested_fields) 104 + |> Jsont.Object.skip_unknown 105 + |> Jsont.Object.finish 106 + 107 + let name t = t.name 108 + let num_documents t = t.num_documents 109 + let fields t = t.fields 110 + let default_sorting_field t = t.default_sorting_field 111 + 112 + let list client = 113 + let json = Client.request client ~method_:`GET ~path:"/collections" () in 114 + Encode.decode_or_raise (Jsont.list jsont) json "listing collections" 115 + 116 + let get client ~name = 117 + let path = "/collections/" ^ Uri.pct_encode name in 118 + let json = Client.request client ~method_:`GET ~path () in 119 + Encode.decode_or_raise jsont json ("getting collection " ^ name) 120 + 121 + let create client schema = 122 + let body = Encode.to_json_string schema_jsont schema in 123 + let json = Client.request client ~method_:`POST ~path:"/collections" ~body () in 124 + Encode.decode_or_raise jsont json ("creating collection " ^ schema.name) 125 + 126 + type delete_result = { name : string } 127 + 128 + let delete_result_jsont = 129 + Jsont.Object.map ~kind:"DeleteResult" (fun name -> { name }) 130 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun r -> r.name) 131 + |> Jsont.Object.skip_unknown 132 + |> Jsont.Object.finish 133 + 134 + let delete client ~name = 135 + let path = "/collections/" ^ Uri.pct_encode name in 136 + let json = Client.request client ~method_:`DELETE ~path () in 137 + let _ = Encode.decode_or_raise delete_result_jsont json ("deleting collection " ^ name) in 138 + () 139 + 140 + let exists client ~name = 141 + try 142 + let _ = get client ~name in 143 + true 144 + with Eio.Io (Error.E { code = Not_found; _ }, _) -> false
+114
lib/typesense/collection.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Typesense collection management. 7 + 8 + This module provides types and operations for managing Typesense collections. 9 + A collection is a group of related documents with a defined schema. *) 10 + 11 + (** {1 Field Types} *) 12 + 13 + type field = { 14 + name : string; (** Field name *) 15 + type_ : string; (** Field type (string, int32, float, bool, etc.) *) 16 + facet : bool option; (** Whether field is facetable *) 17 + optional : bool option; (** Whether field is optional *) 18 + index : bool option; (** Whether to index this field *) 19 + sort : bool option; (** Whether field is sortable *) 20 + infix : bool option; (** Enable infix search *) 21 + locale : string option; (** Locale for string fields *) 22 + num_dim : int option; (** Number of dimensions for vector fields *) 23 + } 24 + (** A field definition in a collection schema. *) 25 + 26 + val field : 27 + name:string -> 28 + type_:string -> 29 + ?facet:bool -> 30 + ?optional:bool -> 31 + ?index:bool -> 32 + ?sort:bool -> 33 + ?infix:bool -> 34 + ?locale:string -> 35 + ?num_dim:int -> 36 + unit -> 37 + field 38 + (** [field ~name ~type_ ...] creates a field definition. Common types: 39 + - ["string"], ["string[]] - Text fields 40 + - ["int32"], ["int64"], ["float"] - Numeric fields 41 + - ["bool"] - Boolean field 42 + - ["auto"] - Auto-detect type 43 + - ["float[]] - Vector field (requires [num_dim]) *) 44 + 45 + val field_jsont : field Jsont.t 46 + (** JSON codec for fields. *) 47 + 48 + (** {1 Schema Types} *) 49 + 50 + type schema = { 51 + name : string; (** Collection name *) 52 + fields : field list; (** Field definitions *) 53 + default_sorting_field : string option; (** Default field for sorting *) 54 + token_separators : string list option; (** Custom token separators *) 55 + symbols_to_index : string list option; (** Symbols to index *) 56 + enable_nested_fields : bool option; (** Enable nested object fields *) 57 + } 58 + (** A collection schema for creating new collections. *) 59 + 60 + val schema : 61 + name:string -> 62 + fields:field list -> 63 + ?default_sorting_field:string -> 64 + ?token_separators:string list -> 65 + ?symbols_to_index:string list -> 66 + ?enable_nested_fields:bool -> 67 + unit -> 68 + schema 69 + (** [schema ~name ~fields ...] creates a collection schema. *) 70 + 71 + val schema_jsont : schema Jsont.t 72 + (** JSON codec for schemas. *) 73 + 74 + (** {1 Collection Type} *) 75 + 76 + type t = { 77 + name : string; (** Collection name *) 78 + num_documents : int; (** Number of documents in collection *) 79 + fields : field list; (** Field definitions *) 80 + default_sorting_field : string option; (** Default sorting field *) 81 + enable_nested_fields : bool option; (** Whether nested fields are enabled *) 82 + } 83 + (** A collection as returned by the API. *) 84 + 85 + val jsont : t Jsont.t 86 + (** JSON codec for collections. *) 87 + 88 + (** {1 Accessors} *) 89 + 90 + val name : t -> string 91 + val num_documents : t -> int 92 + val fields : t -> field list 93 + val default_sorting_field : t -> string option 94 + 95 + (** {1 Operations} *) 96 + 97 + val list : Client.t -> t list 98 + (** [list client] returns all collections. 99 + @raise Eio.Io with [Error.E] on API errors *) 100 + 101 + val get : Client.t -> name:string -> t 102 + (** [get client ~name] retrieves a collection by name. 103 + @raise Eio.Io with [Error.E { code = Not_found; _ }] if not found *) 104 + 105 + val create : Client.t -> schema -> t 106 + (** [create client schema] creates a new collection. 107 + @raise Eio.Io with [Error.E { code = Conflict; _ }] if already exists *) 108 + 109 + val delete : Client.t -> name:string -> unit 110 + (** [delete client ~name] deletes a collection. 111 + @raise Eio.Io with [Error.E { code = Not_found; _ }] if not found *) 112 + 113 + val exists : Client.t -> name:string -> bool 114 + (** [exists client ~name] returns [true] if the collection exists. *)
+128
lib/typesense/document.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + type action = Create | Upsert | Update | Emplace 7 + 8 + let action_to_string = function 9 + | Create -> "create" 10 + | Upsert -> "upsert" 11 + | Update -> "update" 12 + | Emplace -> "emplace" 13 + 14 + type import_result = { 15 + success : bool; 16 + error : string option; 17 + document : string option; 18 + } 19 + 20 + let import_result_jsont = 21 + let make success error document = { success; error; document } in 22 + Jsont.Object.map ~kind:"ImportResult" make 23 + |> Jsont.Object.mem "success" Jsont.bool ~enc:(fun r -> r.success) 24 + |> Jsont.Object.opt_mem "error" Jsont.string ~enc:(fun r -> r.error) 25 + |> Jsont.Object.opt_mem "document" Jsont.string ~enc:(fun r -> r.document) 26 + |> Jsont.Object.skip_unknown 27 + |> Jsont.Object.finish 28 + 29 + let import client ~collection ?(action = Upsert) ?(batch_size = 40) 30 + ?(return_doc = false) ?(return_id = false) documents = 31 + let path = 32 + "/collections/" ^ Uri.pct_encode collection ^ "/documents/import" 33 + in 34 + let params = 35 + [ 36 + ("action", action_to_string action); 37 + ("batch_size", string_of_int batch_size); 38 + ("return_doc", string_of_bool return_doc); 39 + ("return_id", string_of_bool return_id); 40 + ] 41 + in 42 + (* Convert documents to JSONL format *) 43 + let body = 44 + documents 45 + |> List.map (fun doc -> Encode.to_json_string Jsont.json doc) 46 + |> String.concat "\n" 47 + in 48 + let response = Client.request_raw client ~method_:`POST ~path ~params ~body () in 49 + Encode.parse_jsonl import_result_jsont response 50 + 51 + let get client ~collection ~id = 52 + let path = 53 + "/collections/" ^ Uri.pct_encode collection ^ "/documents/" 54 + ^ Uri.pct_encode id 55 + in 56 + Client.request client ~method_:`GET ~path () 57 + 58 + let delete client ~collection ~id = 59 + let path = 60 + "/collections/" ^ Uri.pct_encode collection ^ "/documents/" 61 + ^ Uri.pct_encode id 62 + in 63 + Client.request client ~method_:`DELETE ~path () 64 + 65 + let update client ~collection ~id document = 66 + let path = 67 + "/collections/" ^ Uri.pct_encode collection ^ "/documents/" 68 + ^ Uri.pct_encode id 69 + in 70 + let body = Encode.to_json_string Jsont.json document in 71 + Client.request client ~method_:`PATCH ~path ~body () 72 + 73 + let create client ~collection document = 74 + let path = "/collections/" ^ Uri.pct_encode collection ^ "/documents" in 75 + let body = Encode.to_json_string Jsont.json document in 76 + Client.request client ~method_:`POST ~path ~body () 77 + 78 + type delete_by_query_result = { num_deleted : int } 79 + 80 + let delete_by_query_result_jsont = 81 + Jsont.Object.map ~kind:"DeleteByQueryResult" (fun num_deleted -> 82 + { num_deleted }) 83 + |> Jsont.Object.mem "num_deleted" Jsont.int ~enc:(fun r -> r.num_deleted) 84 + |> Jsont.Object.skip_unknown 85 + |> Jsont.Object.finish 86 + 87 + let delete_by_query client ~collection ~filter_by = 88 + let path = 89 + "/collections/" ^ Uri.pct_encode collection ^ "/documents" 90 + in 91 + let params = [ ("filter_by", filter_by) ] in 92 + let json = Client.request client ~method_:`DELETE ~path ~params () in 93 + let result = 94 + Encode.decode_or_raise delete_by_query_result_jsont json "delete by query" 95 + in 96 + result.num_deleted 97 + 98 + type export_params = { 99 + filter_by : string option; 100 + include_fields : string list option; 101 + exclude_fields : string list option; 102 + } 103 + 104 + let export_params ?filter_by ?include_fields ?exclude_fields () = 105 + { filter_by; include_fields; exclude_fields } 106 + 107 + let export client ~collection ?params () = 108 + let path = 109 + "/collections/" ^ Uri.pct_encode collection ^ "/documents/export" 110 + in 111 + let query_params = 112 + match params with 113 + | None -> [] 114 + | Some p -> 115 + List.filter_map Fun.id 116 + [ 117 + Option.map (fun v -> ("filter_by", v)) p.filter_by; 118 + Option.map 119 + (fun v -> ("include_fields", String.concat "," v)) 120 + p.include_fields; 121 + Option.map 122 + (fun v -> ("exclude_fields", String.concat "," v)) 123 + p.exclude_fields; 124 + ] 125 + in 126 + let params = if query_params = [] then None else Some query_params in 127 + let response = Client.request_raw client ~method_:`GET ~path ?params () in 128 + Encode.parse_jsonl Jsont.json response
+97
lib/typesense/document.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Typesense document operations. 7 + 8 + This module provides operations for managing documents in Typesense 9 + collections. Documents are JSON objects stored in collections. *) 10 + 11 + (** {1 Import Actions} *) 12 + 13 + type action = 14 + | Create (** Only create new documents (fail if exists) *) 15 + | Upsert (** Create or replace documents *) 16 + | Update (** Only update existing documents *) 17 + | Emplace (** Create or update (merge) documents *) 18 + (** Action to take during document import. *) 19 + 20 + (** {1 Import Results} *) 21 + 22 + type import_result = { 23 + success : bool; (** Whether the import succeeded *) 24 + error : string option; (** Error message if failed *) 25 + document : string option; (** Document JSON if return_doc was true *) 26 + } 27 + (** Result for a single document in a batch import. *) 28 + 29 + val import_result_jsont : import_result Jsont.t 30 + (** JSON codec for import results. *) 31 + 32 + (** {1 Document Operations} *) 33 + 34 + val import : 35 + Client.t -> 36 + collection:string -> 37 + ?action:action -> 38 + ?batch_size:int -> 39 + ?return_doc:bool -> 40 + ?return_id:bool -> 41 + Jsont.json list -> 42 + import_result list 43 + (** [import client ~collection ?action ?batch_size documents] imports documents 44 + in batch. 45 + @param action Import action (default: Upsert) 46 + @param batch_size Number of documents per batch (default: 40) 47 + @param return_doc Return the document in the result 48 + @param return_id Return the document ID in the result 49 + @return List of import results, one per document *) 50 + 51 + val get : Client.t -> collection:string -> id:string -> Jsont.json 52 + (** [get client ~collection ~id] retrieves a document by ID. 53 + @raise Eio.Io with [Error.E { code = Not_found; _ }] if not found *) 54 + 55 + val delete : Client.t -> collection:string -> id:string -> Jsont.json 56 + (** [delete client ~collection ~id] deletes a document by ID. 57 + @return The deleted document 58 + @raise Eio.Io with [Error.E { code = Not_found; _ }] if not found *) 59 + 60 + val update : Client.t -> collection:string -> id:string -> Jsont.json -> Jsont.json 61 + (** [update client ~collection ~id document] updates a document by ID. Only the 62 + fields present in [document] are updated. 63 + @return The updated document 64 + @raise Eio.Io with [Error.E { code = Not_found; _ }] if not found *) 65 + 66 + val create : Client.t -> collection:string -> Jsont.json -> Jsont.json 67 + (** [create client ~collection document] creates a new document. 68 + @return The created document with auto-generated ID if not specified 69 + @raise Eio.Io with [Error.E { code = Conflict; _ }] if ID already exists *) 70 + 71 + val delete_by_query : Client.t -> collection:string -> filter_by:string -> int 72 + (** [delete_by_query client ~collection ~filter_by] deletes all documents 73 + matching the filter. 74 + @return Number of documents deleted *) 75 + 76 + (** {1 Export Operations} *) 77 + 78 + type export_params = { 79 + filter_by : string option; 80 + include_fields : string list option; 81 + exclude_fields : string list option; 82 + } 83 + (** Parameters for exporting documents. *) 84 + 85 + val export_params : 86 + ?filter_by:string -> 87 + ?include_fields:string list -> 88 + ?exclude_fields:string list -> 89 + unit -> 90 + export_params 91 + (** [export_params ...] creates export parameters. *) 92 + 93 + val export : 94 + Client.t -> collection:string -> ?params:export_params -> unit -> Jsont.json list 95 + (** [export client ~collection ?params ()] exports all documents from a 96 + collection. 97 + @return List of documents as JSON *)
+10
lib/typesense/dune
··· 1 + (library 2 + (public_name typesense) 3 + (name typesense) 4 + (libraries 5 + eio 6 + requests 7 + jsont 8 + jsont.bytesrw 9 + uri 10 + logs))
+55
lib/typesense/encode.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Encoding utilities for Typesense API requests *) 7 + 8 + let to_json_string : 'a Jsont.t -> 'a -> string = 9 + fun codec value -> 10 + match Jsont_bytesrw.encode_string' codec value with 11 + | Ok s -> s 12 + | Error e -> failwith ("JSON encoding error: " ^ Jsont.Error.to_string e) 13 + 14 + let from_json_string : 'a Jsont.t -> string -> ('a, string) result = 15 + fun codec json_str -> 16 + match Jsont_bytesrw.decode_string' codec json_str with 17 + | Ok v -> Ok v 18 + | Error e -> Error (Jsont.Error.to_string e) 19 + 20 + let from_json : 'a Jsont.t -> Jsont.json -> ('a, string) result = 21 + fun codec json -> 22 + let json_str = 23 + match Jsont_bytesrw.encode_string' Jsont.json json with 24 + | Ok s -> s 25 + | Error e -> 26 + failwith ("Failed to re-encode json: " ^ Jsont.Error.to_string e) 27 + in 28 + from_json_string codec json_str 29 + 30 + let to_json : 'a Jsont.t -> 'a -> (Jsont.json, string) result = 31 + fun codec value -> 32 + let json_str = to_json_string codec value in 33 + match Jsont_bytesrw.decode_string' Jsont.json json_str with 34 + | Ok json -> Ok json 35 + | Error e -> Error (Jsont.Error.to_string e) 36 + 37 + let decode_or_raise : 'a Jsont.t -> Jsont.json -> string -> 'a = 38 + fun codec json context -> 39 + match from_json codec json with 40 + | Ok v -> v 41 + | Error msg -> 42 + Error.raise_with_context 43 + (Error.make ~code:(Other 0) ~message:msg) 44 + "%s" context 45 + 46 + let parse_jsonl : 'a Jsont.t -> string -> 'a list = 47 + fun codec response -> 48 + String.split_on_char '\n' response 49 + |> List.filter_map (fun line -> 50 + let line = String.trim line in 51 + if line = "" then None 52 + else 53 + match from_json_string codec line with 54 + | Ok result -> Some result 55 + | Error _ -> None)
+28
lib/typesense/encode.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Encoding utilities for Typesense API requests *) 7 + 8 + val to_json_string : 'a Jsont.t -> 'a -> string 9 + (** [to_json_string codec value] converts a value to JSON string using its 10 + jsont codec. *) 11 + 12 + val from_json_string : 'a Jsont.t -> string -> ('a, string) result 13 + (** [from_json_string codec str] parses a JSON string using a jsont codec. *) 14 + 15 + val from_json : 'a Jsont.t -> Jsont.json -> ('a, string) result 16 + (** [from_json codec json] parses a Jsont.json value using a codec. *) 17 + 18 + val to_json : 'a Jsont.t -> 'a -> (Jsont.json, string) result 19 + (** [to_json codec value] converts a value to Jsont.json using a codec. *) 20 + 21 + val decode_or_raise : 'a Jsont.t -> Jsont.json -> string -> 'a 22 + (** [decode_or_raise codec json context] decodes JSON using the codec, or 23 + raises a Typesense error with the given context if decoding fails. *) 24 + 25 + val parse_jsonl : 'a Jsont.t -> string -> 'a list 26 + (** [parse_jsonl codec response] parses a JSONL (newline-delimited JSON) 27 + response string into a list of values. Empty lines are skipped and 28 + parse errors are silently dropped. *)
+54
lib/typesense/error.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + type code = 7 + | Bad_request 8 + | Unauthorized 9 + | Not_found 10 + | Conflict 11 + | Unprocessable_entity 12 + | Service_unavailable 13 + | Other of int 14 + 15 + type t = { code : code; message : string } 16 + type Eio.Exn.err += E of t 17 + 18 + let pp_code fmt = function 19 + | Bad_request -> Format.fprintf fmt "Bad_request" 20 + | Unauthorized -> Format.fprintf fmt "Unauthorized" 21 + | Not_found -> Format.fprintf fmt "Not_found" 22 + | Conflict -> Format.fprintf fmt "Conflict" 23 + | Unprocessable_entity -> Format.fprintf fmt "Unprocessable_entity" 24 + | Service_unavailable -> Format.fprintf fmt "Service_unavailable" 25 + | Other n -> Format.fprintf fmt "Other(%d)" n 26 + 27 + let pp fmt t = Format.fprintf fmt "%a: %s" pp_code t.code t.message 28 + 29 + let () = 30 + Eio.Exn.register_pp (fun f -> function 31 + | E e -> 32 + Format.fprintf f "Typesense %a" pp e; 33 + true 34 + | _ -> false) 35 + 36 + let code_of_status = function 37 + | 400 -> Bad_request 38 + | 401 -> Unauthorized 39 + | 404 -> Not_found 40 + | 409 -> Conflict 41 + | 422 -> Unprocessable_entity 42 + | 503 -> Service_unavailable 43 + | n -> Other n 44 + 45 + let make ~code ~message = { code; message } 46 + let code t = t.code 47 + let message t = t.message 48 + let raise e = Stdlib.raise (Eio.Exn.create (E e)) 49 + 50 + let raise_with_context e fmt = 51 + Format.kasprintf 52 + (fun context -> 53 + Stdlib.raise (Eio.Exn.add_context (Eio.Exn.create (E e)) "%s" context)) 54 + fmt
+71
lib/typesense/error.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Typesense API error handling. 7 + 8 + This module defines protocol-level errors for the Typesense API, following 9 + the Eio error pattern for context-aware error handling. 10 + 11 + Errors are raised as [Eio.Io] exceptions: 12 + {[ 13 + try Typesense.Collection.create client schema with 14 + | Eio.Io (Typesense.Error.E { code = Unauthorized; message; _ }, _) -> 15 + Printf.eprintf "Authentication failed: %s\n" message 16 + | Eio.Io (Typesense.Error.E err, _) -> 17 + Printf.eprintf "API error: %a\n" Typesense.Error.pp err 18 + ]} *) 19 + 20 + (** {1 Error Codes} 21 + 22 + These error codes correspond to HTTP status codes returned by Typesense. *) 23 + 24 + type code = 25 + | Bad_request (** 400 - Malformed request *) 26 + | Unauthorized (** 401 - Invalid API key *) 27 + | Not_found (** 404 - Resource not found *) 28 + | Conflict (** 409 - Resource already exists *) 29 + | Unprocessable_entity (** 422 - Invalid data *) 30 + | Service_unavailable (** 503 - Server temporarily unavailable *) 31 + | Other of int (** Other HTTP status code *) 32 + 33 + (** {1 Error Type} *) 34 + 35 + type t = { 36 + code : code; (** The error code *) 37 + message : string; (** Human-readable error message *) 38 + } 39 + (** The protocol-level error type. *) 40 + 41 + (** {1 Eio Integration} *) 42 + 43 + type Eio.Exn.err += 44 + | E of t (** Extend [Eio.Exn.err] with Typesense protocol errors. *) 45 + 46 + val raise : t -> 'a 47 + (** [raise e] raises an [Eio.Io] exception for error [e]. Equivalent to 48 + [Stdlib.raise (Eio.Exn.create (E e))]. *) 49 + 50 + val raise_with_context : t -> ('a, Format.formatter, unit, 'b) format4 -> 'a 51 + (** [raise_with_context e fmt ...] raises an [Eio.Io] exception with context. 52 + Equivalent to 53 + [Stdlib.raise (Eio.Exn.add_context (Eio.Exn.create (E e)) fmt ...)]. *) 54 + 55 + (** {1 Error Construction} *) 56 + 57 + val make : code:code -> message:string -> t 58 + (** [make ~code ~message] creates an error value. *) 59 + 60 + val code_of_status : int -> code 61 + (** [code_of_status status] converts an HTTP status code to an error code. *) 62 + 63 + (** {1 Accessors} *) 64 + 65 + val code : t -> code 66 + val message : t -> string 67 + 68 + (** {1 Pretty Printing} *) 69 + 70 + val pp_code : Format.formatter -> code -> unit 71 + val pp : Format.formatter -> t -> unit
+97
lib/typesense/multi_search.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + type search_request = { 7 + collection : string; 8 + q : string; 9 + query_by : string list; 10 + filter_by : string option; 11 + sort_by : string option; 12 + facet_by : string list option; 13 + per_page : int option; 14 + page : int option; 15 + prefix : bool option; 16 + include_fields : string list option; 17 + exclude_fields : string list option; 18 + } 19 + 20 + let search_request ~collection ~q ~query_by ?filter_by ?sort_by ?facet_by 21 + ?per_page ?page ?prefix ?include_fields ?exclude_fields () = 22 + { 23 + collection; 24 + q; 25 + query_by; 26 + filter_by; 27 + sort_by; 28 + facet_by; 29 + per_page; 30 + page; 31 + prefix; 32 + include_fields; 33 + exclude_fields; 34 + } 35 + 36 + (* Helper codec for comma-separated string lists *) 37 + let comma_list_jsont = 38 + let of_string s = Ok (String.split_on_char ',' s |> List.map String.trim) in 39 + let to_string l = String.concat "," l in 40 + Jsont.of_of_string ~kind:"comma_list" of_string ~enc:to_string 41 + 42 + let search_request_jsont = 43 + let make collection q query_by filter_by sort_by facet_by per_page page prefix 44 + include_fields exclude_fields = 45 + { 46 + collection; 47 + q; 48 + query_by; 49 + filter_by; 50 + sort_by; 51 + facet_by; 52 + per_page; 53 + page; 54 + prefix; 55 + include_fields; 56 + exclude_fields; 57 + } 58 + in 59 + Jsont.Object.map ~kind:"SearchRequest" make 60 + |> Jsont.Object.mem "collection" Jsont.string ~enc:(fun r -> r.collection) 61 + |> Jsont.Object.mem "q" Jsont.string ~enc:(fun r -> r.q) 62 + |> Jsont.Object.mem "query_by" comma_list_jsont ~enc:(fun r -> r.query_by) 63 + |> Jsont.Object.opt_mem "filter_by" Jsont.string ~enc:(fun r -> r.filter_by) 64 + |> Jsont.Object.opt_mem "sort_by" Jsont.string ~enc:(fun r -> r.sort_by) 65 + |> Jsont.Object.opt_mem "facet_by" comma_list_jsont ~enc:(fun r -> r.facet_by) 66 + |> Jsont.Object.opt_mem "per_page" Jsont.int ~enc:(fun r -> r.per_page) 67 + |> Jsont.Object.opt_mem "page" Jsont.int ~enc:(fun r -> r.page) 68 + |> Jsont.Object.opt_mem "prefix" Jsont.bool ~enc:(fun r -> r.prefix) 69 + |> Jsont.Object.opt_mem "include_fields" comma_list_jsont 70 + ~enc:(fun r -> r.include_fields) 71 + |> Jsont.Object.opt_mem "exclude_fields" comma_list_jsont 72 + ~enc:(fun r -> r.exclude_fields) 73 + |> Jsont.Object.skip_unknown 74 + |> Jsont.Object.finish 75 + 76 + type request = { searches : search_request list } 77 + 78 + let request_jsont = 79 + Jsont.Object.map ~kind:"MultiSearchRequest" (fun searches -> { searches }) 80 + |> Jsont.Object.mem "searches" (Jsont.list search_request_jsont) 81 + ~enc:(fun r -> r.searches) 82 + |> Jsont.Object.finish 83 + 84 + type result = { results : Search.result list } 85 + 86 + let result_jsont = 87 + Jsont.Object.map ~kind:"MultiSearchResult" (fun results -> { results }) 88 + |> Jsont.Object.mem "results" (Jsont.list Search.result_jsont) 89 + ~enc:(fun r -> r.results) 90 + |> Jsont.Object.skip_unknown 91 + |> Jsont.Object.finish 92 + 93 + let search client requests = 94 + let req = { searches = requests } in 95 + let body = Encode.to_json_string request_jsont req in 96 + let json = Client.request client ~method_:`POST ~path:"/multi_search" ~body () in 97 + Encode.decode_or_raise result_jsont json "multi_search"
+61
lib/typesense/multi_search.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Typesense multi-search operations. 7 + 8 + This module provides support for executing multiple searches across different 9 + collections in a single request. This is more efficient than making separate 10 + search requests. *) 11 + 12 + (** {1 Search Request Type} *) 13 + 14 + type search_request = { 15 + collection : string; (** Collection to search *) 16 + q : string; (** Query string *) 17 + query_by : string list; (** Fields to search in *) 18 + filter_by : string option; (** Filter expression *) 19 + sort_by : string option; (** Sort expression *) 20 + facet_by : string list option; (** Fields to facet on *) 21 + per_page : int option; (** Results per page *) 22 + page : int option; (** Page number *) 23 + prefix : bool option; (** Enable prefix search *) 24 + include_fields : string list option; (** Fields to include *) 25 + exclude_fields : string list option; (** Fields to exclude *) 26 + } 27 + (** A single search request within a multi-search operation. *) 28 + 29 + val search_request : 30 + collection:string -> 31 + q:string -> 32 + query_by:string list -> 33 + ?filter_by:string -> 34 + ?sort_by:string -> 35 + ?facet_by:string list -> 36 + ?per_page:int -> 37 + ?page:int -> 38 + ?prefix:bool -> 39 + ?include_fields:string list -> 40 + ?exclude_fields:string list -> 41 + unit -> 42 + search_request 43 + (** [search_request ~collection ~q ~query_by ...] creates a search request. *) 44 + 45 + val search_request_jsont : search_request Jsont.t 46 + (** JSON codec for search requests. *) 47 + 48 + (** {1 Multi-Search Result} *) 49 + 50 + type result = { results : Search.result list } 51 + (** Multi-search result containing results for each search request. *) 52 + 53 + val result_jsont : result Jsont.t 54 + (** JSON codec for multi-search results. *) 55 + 56 + (** {1 Multi-Search Operation} *) 57 + 58 + val search : Client.t -> search_request list -> result 59 + (** [search client requests] executes multiple searches in a single request. 60 + The results are returned in the same order as the requests. 61 + @raise Eio.Io with [Error.E] on API errors *)
+261
lib/typesense/search.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + type highlight = { 7 + field : string; 8 + snippet : string option; 9 + snippets : string list option; 10 + matched_tokens : string list option; 11 + indices : int list option; 12 + } 13 + 14 + let highlight_jsont = 15 + let make field snippet snippets matched_tokens indices = 16 + { field; snippet; snippets; matched_tokens; indices } 17 + in 18 + Jsont.Object.map ~kind:"Highlight" make 19 + |> Jsont.Object.mem "field" Jsont.string ~enc:(fun h -> h.field) 20 + |> Jsont.Object.opt_mem "snippet" Jsont.string ~enc:(fun h -> h.snippet) 21 + |> Jsont.Object.opt_mem "snippets" (Jsont.list Jsont.string) 22 + ~enc:(fun h -> h.snippets) 23 + |> Jsont.Object.opt_mem "matched_tokens" (Jsont.list Jsont.string) 24 + ~enc:(fun h -> h.matched_tokens) 25 + |> Jsont.Object.opt_mem "indices" (Jsont.list Jsont.int) 26 + ~enc:(fun h -> h.indices) 27 + |> Jsont.Object.skip_unknown 28 + |> Jsont.Object.finish 29 + 30 + type hit = { 31 + document : Jsont.json; 32 + highlights : highlight list option; 33 + text_match : int64 option; 34 + text_match_info : Jsont.json option; 35 + } 36 + 37 + let hit_jsont = 38 + let make document highlights text_match text_match_info = 39 + { document; highlights; text_match; text_match_info } 40 + in 41 + Jsont.Object.map ~kind:"Hit" make 42 + |> Jsont.Object.mem "document" Jsont.json ~enc:(fun h -> h.document) 43 + |> Jsont.Object.opt_mem "highlights" (Jsont.list highlight_jsont) 44 + ~enc:(fun h -> h.highlights) 45 + |> Jsont.Object.opt_mem "text_match" Jsont.int64 ~enc:(fun h -> h.text_match) 46 + |> Jsont.Object.opt_mem "text_match_info" Jsont.json 47 + ~enc:(fun h -> h.text_match_info) 48 + |> Jsont.Object.skip_unknown 49 + |> Jsont.Object.finish 50 + 51 + type facet_count = { 52 + value : string; 53 + count : int; 54 + highlighted : string option; 55 + } 56 + 57 + let facet_count_jsont = 58 + let make value count highlighted = { value; count; highlighted } in 59 + Jsont.Object.map ~kind:"FacetCount" make 60 + |> Jsont.Object.mem "value" Jsont.string ~enc:(fun f -> f.value) 61 + |> Jsont.Object.mem "count" Jsont.int ~enc:(fun f -> f.count) 62 + |> Jsont.Object.opt_mem "highlighted" Jsont.string ~enc:(fun f -> f.highlighted) 63 + |> Jsont.Object.skip_unknown 64 + |> Jsont.Object.finish 65 + 66 + type facet_stats = { 67 + min : float option; 68 + max : float option; 69 + sum : float option; 70 + avg : float option; 71 + total_values : int option; 72 + } 73 + 74 + let facet_stats_jsont = 75 + let make min max sum avg total_values = 76 + { min; max; sum; avg; total_values } 77 + in 78 + Jsont.Object.map ~kind:"FacetStats" make 79 + |> Jsont.Object.opt_mem "min" Jsont.number ~enc:(fun f -> f.min) 80 + |> Jsont.Object.opt_mem "max" Jsont.number ~enc:(fun f -> f.max) 81 + |> Jsont.Object.opt_mem "sum" Jsont.number ~enc:(fun f -> f.sum) 82 + |> Jsont.Object.opt_mem "avg" Jsont.number ~enc:(fun f -> f.avg) 83 + |> Jsont.Object.opt_mem "total_values" Jsont.int ~enc:(fun f -> f.total_values) 84 + |> Jsont.Object.skip_unknown 85 + |> Jsont.Object.finish 86 + 87 + type facet = { 88 + field_name : string; 89 + counts : facet_count list; 90 + stats : facet_stats option; 91 + } 92 + 93 + let facet_jsont = 94 + let make field_name counts stats = { field_name; counts; stats } in 95 + Jsont.Object.map ~kind:"Facet" make 96 + |> Jsont.Object.mem "field_name" Jsont.string ~enc:(fun f -> f.field_name) 97 + |> Jsont.Object.mem "counts" (Jsont.list facet_count_jsont) ~enc:(fun f -> f.counts) 98 + |> Jsont.Object.opt_mem "stats" facet_stats_jsont ~enc:(fun f -> f.stats) 99 + |> Jsont.Object.skip_unknown 100 + |> Jsont.Object.finish 101 + 102 + type result = { 103 + hits : hit list; 104 + found : int; 105 + search_time_ms : int; 106 + page : int option; 107 + out_of : int option; 108 + facet_counts : facet list option; 109 + request_params : Jsont.json option; 110 + } 111 + 112 + let result_jsont = 113 + let make hits found search_time_ms page out_of facet_counts request_params = 114 + { hits; found; search_time_ms; page; out_of; facet_counts; request_params } 115 + in 116 + Jsont.Object.map ~kind:"SearchResult" make 117 + |> Jsont.Object.mem "hits" (Jsont.list hit_jsont) ~enc:(fun r -> r.hits) 118 + |> Jsont.Object.mem "found" Jsont.int ~enc:(fun r -> r.found) 119 + |> Jsont.Object.mem "search_time_ms" Jsont.int ~enc:(fun r -> r.search_time_ms) 120 + |> Jsont.Object.opt_mem "page" Jsont.int ~enc:(fun r -> r.page) 121 + |> Jsont.Object.opt_mem "out_of" Jsont.int ~enc:(fun r -> r.out_of) 122 + |> Jsont.Object.opt_mem "facet_counts" (Jsont.list facet_jsont) 123 + ~enc:(fun r -> r.facet_counts) 124 + |> Jsont.Object.opt_mem "request_params" Jsont.json 125 + ~enc:(fun r -> r.request_params) 126 + |> Jsont.Object.skip_unknown 127 + |> Jsont.Object.finish 128 + 129 + type params = { 130 + q : string; 131 + query_by : string list; 132 + filter_by : string option; 133 + sort_by : string option; 134 + facet_by : string list option; 135 + max_facet_values : int option; 136 + per_page : int option; 137 + page : int option; 138 + prefix : bool option; 139 + infix : string option; 140 + highlight_fields : string list option; 141 + highlight_full_fields : string list option; 142 + highlight_affix_num_tokens : int option; 143 + highlight_start_tag : string option; 144 + highlight_end_tag : string option; 145 + snippet_threshold : int option; 146 + num_typos : int option; 147 + typo_tokens_threshold : int option; 148 + drop_tokens_threshold : int option; 149 + include_fields : string list option; 150 + exclude_fields : string list option; 151 + group_by : string list option; 152 + group_limit : int option; 153 + limit_hits : int option; 154 + prioritize_exact_match : bool option; 155 + prioritize_token_position : bool option; 156 + exhaustive_search : bool option; 157 + search_cutoff_ms : int option; 158 + use_cache : bool option; 159 + cache_ttl : int option; 160 + enable_highlight_v1 : bool option; 161 + } 162 + 163 + let params ~q ~query_by ?filter_by ?sort_by ?facet_by ?max_facet_values ?per_page 164 + ?page ?prefix ?infix ?highlight_fields ?highlight_full_fields 165 + ?highlight_affix_num_tokens ?highlight_start_tag ?highlight_end_tag 166 + ?snippet_threshold ?num_typos ?typo_tokens_threshold ?drop_tokens_threshold 167 + ?include_fields ?exclude_fields ?group_by ?group_limit ?limit_hits 168 + ?prioritize_exact_match ?prioritize_token_position ?exhaustive_search 169 + ?search_cutoff_ms ?use_cache ?cache_ttl ?enable_highlight_v1 () = 170 + { 171 + q; 172 + query_by; 173 + filter_by; 174 + sort_by; 175 + facet_by; 176 + max_facet_values; 177 + per_page; 178 + page; 179 + prefix; 180 + infix; 181 + highlight_fields; 182 + highlight_full_fields; 183 + highlight_affix_num_tokens; 184 + highlight_start_tag; 185 + highlight_end_tag; 186 + snippet_threshold; 187 + num_typos; 188 + typo_tokens_threshold; 189 + drop_tokens_threshold; 190 + include_fields; 191 + exclude_fields; 192 + group_by; 193 + group_limit; 194 + limit_hits; 195 + prioritize_exact_match; 196 + prioritize_token_position; 197 + exhaustive_search; 198 + search_cutoff_ms; 199 + use_cache; 200 + cache_ttl; 201 + enable_highlight_v1; 202 + } 203 + 204 + let params_to_query_params p = 205 + let add_opt name opt acc = 206 + match opt with Some v -> (name, v) :: acc | None -> acc 207 + in 208 + let add_opt_bool name opt acc = 209 + match opt with 210 + | Some v -> (name, string_of_bool v) :: acc 211 + | None -> acc 212 + in 213 + let add_opt_int name opt acc = 214 + match opt with Some v -> (name, string_of_int v) :: acc | None -> acc 215 + in 216 + let add_opt_list name opt acc = 217 + match opt with 218 + | Some v when v <> [] -> (name, String.concat "," v) :: acc 219 + | _ -> acc 220 + in 221 + [] 222 + |> add_opt "q" (Some p.q) 223 + |> add_opt "query_by" (Some (String.concat "," p.query_by)) 224 + |> add_opt "filter_by" p.filter_by 225 + |> add_opt "sort_by" p.sort_by 226 + |> add_opt_list "facet_by" p.facet_by 227 + |> add_opt_int "max_facet_values" p.max_facet_values 228 + |> add_opt_int "per_page" p.per_page 229 + |> add_opt_int "page" p.page 230 + |> add_opt_bool "prefix" p.prefix 231 + |> add_opt "infix" p.infix 232 + |> add_opt_list "highlight_fields" p.highlight_fields 233 + |> add_opt_list "highlight_full_fields" p.highlight_full_fields 234 + |> add_opt_int "highlight_affix_num_tokens" p.highlight_affix_num_tokens 235 + |> add_opt "highlight_start_tag" p.highlight_start_tag 236 + |> add_opt "highlight_end_tag" p.highlight_end_tag 237 + |> add_opt_int "snippet_threshold" p.snippet_threshold 238 + |> add_opt_int "num_typos" p.num_typos 239 + |> add_opt_int "typo_tokens_threshold" p.typo_tokens_threshold 240 + |> add_opt_int "drop_tokens_threshold" p.drop_tokens_threshold 241 + |> add_opt_list "include_fields" p.include_fields 242 + |> add_opt_list "exclude_fields" p.exclude_fields 243 + |> add_opt_list "group_by" p.group_by 244 + |> add_opt_int "group_limit" p.group_limit 245 + |> add_opt_int "limit_hits" p.limit_hits 246 + |> add_opt_bool "prioritize_exact_match" p.prioritize_exact_match 247 + |> add_opt_bool "prioritize_token_position" p.prioritize_token_position 248 + |> add_opt_bool "exhaustive_search" p.exhaustive_search 249 + |> add_opt_int "search_cutoff_ms" p.search_cutoff_ms 250 + |> add_opt_bool "use_cache" p.use_cache 251 + |> add_opt_int "cache_ttl" p.cache_ttl 252 + |> add_opt_bool "enable_highlight_v1" p.enable_highlight_v1 253 + |> List.rev 254 + 255 + let search client ~collection p = 256 + let path = 257 + "/collections/" ^ Uri.pct_encode collection ^ "/documents/search" 258 + in 259 + let params = params_to_query_params p in 260 + let json = Client.request client ~method_:`GET ~path ~params () in 261 + Encode.decode_or_raise result_jsont json "search"
+153
lib/typesense/search.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Typesense search operations. 7 + 8 + This module provides types and functions for searching documents in 9 + Typesense collections. *) 10 + 11 + (** {1 Search Result Types} *) 12 + 13 + type highlight = { 14 + field : string; (** Field name that was highlighted *) 15 + snippet : string option; (** Highlighted snippet (single value) *) 16 + snippets : string list option; (** Highlighted snippets (array fields) *) 17 + matched_tokens : string list option; (** Tokens that matched *) 18 + indices : int list option; (** Indices of matched values in array *) 19 + } 20 + (** Highlighting information for a matched field. *) 21 + 22 + val highlight_jsont : highlight Jsont.t 23 + 24 + type hit = { 25 + document : Jsont.json; (** The matched document *) 26 + highlights : highlight list option; (** Highlighting information *) 27 + text_match : int64 option; (** Text match score *) 28 + text_match_info : Jsont.json option; (** Detailed match info *) 29 + } 30 + (** A single search hit. *) 31 + 32 + val hit_jsont : hit Jsont.t 33 + 34 + type facet_count = { 35 + value : string; (** Facet value *) 36 + count : int; (** Number of documents with this value *) 37 + highlighted : string option; (** Highlighted value for facet search *) 38 + } 39 + (** Count for a single facet value. *) 40 + 41 + val facet_count_jsont : facet_count Jsont.t 42 + 43 + type facet_stats = { 44 + min : float option; 45 + max : float option; 46 + sum : float option; 47 + avg : float option; 48 + total_values : int option; 49 + } 50 + (** Statistics for numeric facets. *) 51 + 52 + val facet_stats_jsont : facet_stats Jsont.t 53 + 54 + type facet = { 55 + field_name : string; (** Faceted field name *) 56 + counts : facet_count list; (** Value counts *) 57 + stats : facet_stats option; (** Numeric statistics *) 58 + } 59 + (** Facet results for a field. *) 60 + 61 + val facet_jsont : facet Jsont.t 62 + 63 + type result = { 64 + hits : hit list; (** Matched documents *) 65 + found : int; (** Total documents matching query *) 66 + search_time_ms : int; (** Search time in milliseconds *) 67 + page : int option; (** Current page number *) 68 + out_of : int option; (** Total documents in collection *) 69 + facet_counts : facet list option; (** Facet results *) 70 + request_params : Jsont.json option; (** Echo of request parameters *) 71 + } 72 + (** Search result containing hits and metadata. *) 73 + 74 + val result_jsont : result Jsont.t 75 + 76 + (** {1 Search Parameters} *) 77 + 78 + type params = { 79 + q : string; (** Query string (use "*" for all documents) *) 80 + query_by : string list; (** Fields to search in *) 81 + filter_by : string option; (** Filter expression *) 82 + sort_by : string option; (** Sort expression *) 83 + facet_by : string list option; (** Fields to facet on *) 84 + max_facet_values : int option; (** Max facet values to return *) 85 + per_page : int option; (** Results per page (default: 10) *) 86 + page : int option; (** Page number (default: 1) *) 87 + prefix : bool option; (** Enable prefix search *) 88 + infix : string option; (** Infix search mode *) 89 + highlight_fields : string list option; (** Fields to highlight *) 90 + highlight_full_fields : string list option; (** Fields for full highlight *) 91 + highlight_affix_num_tokens : int option; (** Tokens around highlight *) 92 + highlight_start_tag : string option; (** Start tag for highlighting *) 93 + highlight_end_tag : string option; (** End tag for highlighting *) 94 + snippet_threshold : int option; (** Threshold for snippets *) 95 + num_typos : int option; (** Max typos allowed *) 96 + typo_tokens_threshold : int option; (** Typo token threshold *) 97 + drop_tokens_threshold : int option; (** Token dropping threshold *) 98 + include_fields : string list option; (** Fields to include in results *) 99 + exclude_fields : string list option; (** Fields to exclude from results *) 100 + group_by : string list option; (** Fields to group by *) 101 + group_limit : int option; (** Max documents per group *) 102 + limit_hits : int option; (** Max total hits *) 103 + prioritize_exact_match : bool option; (** Prioritize exact matches *) 104 + prioritize_token_position : bool option; (** Prioritize token position *) 105 + exhaustive_search : bool option; (** Exhaustive search mode *) 106 + search_cutoff_ms : int option; (** Search timeout *) 107 + use_cache : bool option; (** Use search cache *) 108 + cache_ttl : int option; (** Cache TTL in seconds *) 109 + enable_highlight_v1 : bool option; (** Use v1 highlighting *) 110 + } 111 + (** Search parameters. *) 112 + 113 + val params : 114 + q:string -> 115 + query_by:string list -> 116 + ?filter_by:string -> 117 + ?sort_by:string -> 118 + ?facet_by:string list -> 119 + ?max_facet_values:int -> 120 + ?per_page:int -> 121 + ?page:int -> 122 + ?prefix:bool -> 123 + ?infix:string -> 124 + ?highlight_fields:string list -> 125 + ?highlight_full_fields:string list -> 126 + ?highlight_affix_num_tokens:int -> 127 + ?highlight_start_tag:string -> 128 + ?highlight_end_tag:string -> 129 + ?snippet_threshold:int -> 130 + ?num_typos:int -> 131 + ?typo_tokens_threshold:int -> 132 + ?drop_tokens_threshold:int -> 133 + ?include_fields:string list -> 134 + ?exclude_fields:string list -> 135 + ?group_by:string list -> 136 + ?group_limit:int -> 137 + ?limit_hits:int -> 138 + ?prioritize_exact_match:bool -> 139 + ?prioritize_token_position:bool -> 140 + ?exhaustive_search:bool -> 141 + ?search_cutoff_ms:int -> 142 + ?use_cache:bool -> 143 + ?cache_ttl:int -> 144 + ?enable_highlight_v1:bool -> 145 + unit -> 146 + params 147 + (** [params ~q ~query_by ...] creates search parameters. *) 148 + 149 + (** {1 Search Operation} *) 150 + 151 + val search : Client.t -> collection:string -> params -> result 152 + (** [search client ~collection params] searches for documents. 153 + @raise Eio.Io with [Error.E] on API errors *)
+15
lib/typesense/typesense.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** OCaml bindings for the Typesense search API. *) 7 + 8 + module Auth = Auth 9 + module Error = Error 10 + module Client = Client 11 + module Collection = Collection 12 + module Document = Document 13 + module Search = Search 14 + module Multi_search = Multi_search 15 + module Analytics = Analytics
+89
lib/typesense/typesense.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** OCaml bindings for the Typesense search API. 7 + 8 + Typesense is a fast, typo-tolerant search engine. This library provides 9 + high-quality OCaml bindings using Eio for async operations. 10 + 11 + {2 Quick Start} 12 + 13 + {[ 14 + open Typesense 15 + 16 + let () = 17 + Eio_main.run @@ fun env -> 18 + let auth = Auth.create ~endpoint:"http://localhost:8108" ~api_key:"xyz" in 19 + Client.with_client env auth @@ fun client -> 20 + 21 + (* Create a collection *) 22 + let schema = 23 + Collection.schema ~name:"books" 24 + ~fields: 25 + [ 26 + Collection.field ~name:"title" ~type_:"string" (); 27 + Collection.field ~name:"author" ~type_:"string" ~facet:true (); 28 + Collection.field ~name:"year" ~type_:"int32" (); 29 + ] 30 + ~default_sorting_field:"year" () 31 + in 32 + let _ = Collection.create client schema in 33 + 34 + (* Search *) 35 + let params = Search.params ~q:"harry" ~query_by:["title"; "author"] () in 36 + let result = Search.search client ~collection:"books" params in 37 + Printf.printf "Found %d books\n" result.found 38 + ]} 39 + 40 + {2 Error Handling} 41 + 42 + All API operations raise [Eio.Io] exceptions with [Error.E] error codes: 43 + 44 + {[ 45 + try Collection.get client ~name:"nonexistent" with 46 + | Eio.Io (Error.E { code = Not_found; message; _ }, _) -> 47 + Printf.eprintf "Collection not found: %s\n" message 48 + ]} 49 + *) 50 + 51 + (** {1 Authentication} *) 52 + 53 + module Auth = Auth 54 + (** API key authentication for Typesense. *) 55 + 56 + (** {1 Error Handling} *) 57 + 58 + module Error = Error 59 + (** Protocol-level errors with Eio integration. *) 60 + 61 + (** {1 HTTP Client} *) 62 + 63 + module Client = Client 64 + (** Low-level HTTP client for the Typesense API. *) 65 + 66 + (** {1 Collections} *) 67 + 68 + module Collection = Collection 69 + (** Collection schema and CRUD operations. *) 70 + 71 + (** {1 Documents} *) 72 + 73 + module Document = Document 74 + (** Document import, export, and CRUD operations. *) 75 + 76 + (** {1 Search} *) 77 + 78 + module Search = Search 79 + (** Search parameters and results. *) 80 + 81 + (** {1 Multi-Search} *) 82 + 83 + module Multi_search = Multi_search 84 + (** Search multiple collections in one request. *) 85 + 86 + (** {1 Analytics} *) 87 + 88 + module Analytics = Analytics 89 + (** Analytics rules and event tracking. *)
+38
typesense.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "OCaml bindings for the Typesense search API" 4 + description: 5 + "High-quality OCaml bindings to the Typesense search API using Eio for async operations. Provides collection management, document operations, search, multi-search, and analytics." 6 + maintainer: ["Anil Madhavapeddy <anil@recoil.org>"] 7 + authors: ["Anil Madhavapeddy <anil@recoil.org>"] 8 + license: "ISC" 9 + homepage: "https://tangled.org/@anil.recoil.org/ocaml-typesense" 10 + bug-reports: "https://tangled.org/@anil.recoil.org/ocaml-typesense/issues" 11 + depends: [ 12 + "dune" {>= "3.20"} 13 + "ocaml" {>= "5.1.0"} 14 + "eio" {>= "1.2"} 15 + "requests" {>= "0.3.1"} 16 + "uri" {>= "4.4.0"} 17 + "jsont" {>= "0.1.1"} 18 + "jsont-bytesrw" {>= "0.1.1"} 19 + "logs" {>= "0.7.0"} 20 + "odoc" {with-doc} 21 + "alcotest" {with-test} 22 + ] 23 + build: [ 24 + ["dune" "subst"] {dev} 25 + [ 26 + "dune" 27 + "build" 28 + "-p" 29 + name 30 + "-j" 31 + jobs 32 + "@install" 33 + "@runtest" {with-test} 34 + "@doc" {with-doc} 35 + ] 36 + ] 37 + dev-repo: "https://tangled.org/@anil.recoil.org/ocaml-typesense" 38 + x-maintenance-intent: ["(latest)"]