OCaml bindings to the Typesense embeddings search API
at main 172 lines 6.1 kB view raw
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)