OCaml bindings to the Typesense embeddings search API
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6type t = {
7 client : Typesense.t;
8 session : Session.t;
9 fs : Eio.Fs.dir_ty Eio.Path.t;
10 profile : string option;
11}
12
13let create_with_session ~sw ~env ?requests_config ?profile ~session () =
14 let fs = env#fs in
15 let server_url = Session.server_url session in
16 let api_key = Session.api_key session in
17 (* Create a Requests session, optionally from cmdliner config *)
18 let requests_session = match requests_config with
19 | Some config -> Requests.Cmd.create config env sw
20 | None -> Requests.create ~sw env
21 in
22 (* Set the X-TYPESENSE-API-KEY header *)
23 let requests_session =
24 Requests.set_default_header requests_session "X-TYPESENSE-API-KEY" api_key
25 in
26 let client = Typesense.create ~session:requests_session ~sw env ~base_url:server_url in
27 { client; session; fs; profile }
28
29let login ~sw ~env ?requests_config ?profile ~server_url ~api_key () =
30 let fs = env#fs in
31 (* Create session with API key header *)
32 let requests_session = match requests_config with
33 | Some config -> Requests.Cmd.create config env sw
34 | None -> Requests.create ~sw env
35 in
36 let requests_session =
37 Requests.set_default_header requests_session "X-TYPESENSE-API-KEY" api_key
38 in
39 let client = Typesense.create ~session:requests_session ~sw env ~base_url:server_url in
40 (* Validate by calling the health endpoint *)
41 let health = Typesense.Health.health client () in
42 if not (Typesense.Health.Status.ok health) then
43 failwith "Health check failed - server not OK";
44 (* Create and save session *)
45 let session = Session.create ~server_url ~api_key () in
46 Session.save fs ?profile session;
47 (* Set as current profile if first login or explicitly requested *)
48 let profiles = Session.list_profiles fs in
49 let profile_name = Option.value ~default:Session.default_profile profile in
50 if profiles = [] || Option.is_some profile then
51 Session.set_current_profile fs profile_name;
52 { client; session; fs; profile }
53
54let resume ~sw ~env ?requests_config ?profile ~session () =
55 create_with_session ~sw ~env ?requests_config ?profile ~session ()
56
57let logout t =
58 Session.clear t.fs ?profile:t.profile ()
59
60let client t = t.client
61let session t = t.session
62let profile t = t.profile
63let fs t = t.fs
64
65(** {1 JSONL Helpers} *)
66
67let parse_jsonl codec response =
68 String.split_on_char '\n' response
69 |> List.filter_map (fun line ->
70 let line = String.trim line in
71 if line = "" then None
72 else
73 match Jsont_bytesrw.decode_string codec line with
74 | Ok result -> Some result
75 | Error _ -> None)
76
77let to_json_string codec value =
78 match Jsont_bytesrw.encode_string' codec value with
79 | Ok s -> s
80 | Error e -> failwith ("JSON encoding error: " ^ Jsont.Error.to_string e)
81
82(** {1 JSONL Import/Export} *)
83
84type import_action = Create | Upsert | Update | Emplace
85
86let action_to_string = function
87 | Create -> "create"
88 | Upsert -> "upsert"
89 | Update -> "update"
90 | Emplace -> "emplace"
91
92type import_result = {
93 success : bool;
94 error : string option;
95 document : string option;
96}
97
98let import_result_jsont =
99 let make success error document = { success; error; document } in
100 Jsont.Object.map ~kind:"ImportResult" make
101 |> Jsont.Object.mem "success" Jsont.bool ~enc:(fun r -> r.success)
102 |> Jsont.Object.opt_mem "error" Jsont.string ~enc:(fun r -> r.error)
103 |> Jsont.Object.opt_mem "document" Jsont.string ~enc:(fun r -> r.document)
104 |> Jsont.Object.skip_unknown
105 |> Jsont.Object.finish
106
107let import t ~collection ?(action = Upsert) ?(batch_size = 40)
108 ?(return_doc = false) ?(return_id = false) documents =
109 let base_url = Typesense.base_url t.client in
110 let session = Typesense.session t.client in
111 (* Build URL with query params *)
112 let params =
113 [
114 ("action", action_to_string action);
115 ("batch_size", string_of_int batch_size);
116 ("return_doc", string_of_bool return_doc);
117 ("return_id", string_of_bool return_id);
118 ]
119 in
120 let query_string =
121 params
122 |> List.map (fun (k, v) -> Uri.pct_encode k ^ "=" ^ Uri.pct_encode v)
123 |> String.concat "&"
124 in
125 let url =
126 base_url ^ "/collections/" ^ Uri.pct_encode collection ^ "/documents/import?" ^ query_string
127 in
128 (* Convert documents to JSONL format *)
129 let body =
130 documents
131 |> List.map (fun doc -> to_json_string Jsont.json doc)
132 |> String.concat "\n"
133 in
134 let response = Requests.post session ~body:(Requests.Body.text body) url in
135 if not (Requests.Response.ok response) then
136 failwith ("Import failed: " ^ string_of_int (Requests.Response.status_code response));
137 parse_jsonl import_result_jsont (Requests.Response.text response)
138
139type export_params = {
140 filter_by : string option;
141 include_fields : string list option;
142 exclude_fields : string list option;
143}
144
145let export_params ?filter_by ?include_fields ?exclude_fields () =
146 { filter_by; include_fields; exclude_fields }
147
148let export t ~collection ?params () =
149 let base_url = Typesense.base_url t.client in
150 let session = Typesense.session t.client in
151 let query_params =
152 match params with
153 | None -> []
154 | Some p ->
155 List.filter_map Fun.id
156 [
157 Option.map (fun v -> ("filter_by", v)) p.filter_by;
158 Option.map
159 (fun v -> ("include_fields", String.concat "," v))
160 p.include_fields;
161 Option.map
162 (fun v -> ("exclude_fields", String.concat "," v))
163 p.exclude_fields;
164 ]
165 in
166 let url =
167 base_url ^ "/collections/" ^ Uri.pct_encode collection ^ "/documents/export"
168 in
169 let response = Requests.get session ~params:query_params url in
170 if not (Requests.Response.ok response) then
171 failwith ("Export failed: " ^ string_of_int (Requests.Response.status_code response));
172 parse_jsonl Jsont.json (Requests.Response.text response)