Packages

A Meilisearch client for Gleam

Current section

Files

Jump to
dewey src dewey.gleam
Raw

src/dewey.gleam

import gleam/bool
import gleam/dynamic/decode
import gleam/http.{Delete, Get, Patch, Post, Put}
import gleam/http/request.{type Request}
import gleam/http/response.{type Response}
import gleam/int
import gleam/json
import gleam/list
import gleam/option.{type Option, None}
import gleam/result
import gleam/string
import gleam/time/duration
import gleam/time/timestamp.{type Timestamp}
// ----- PUBLIC HELPER TYPES AND FNS -----
pub type ParseResponseFunc(data_type) =
fn(Response(String)) -> Result(data_type, DeweyError)
pub type DeweyError {
JSONDecodeError(err: json.DecodeError)
APIError(message: String, code: String, error_type: String, link: String)
// This should only occur if the assumption about the shape of the error object is incorrect.
// The error object type is outlined at: https://www.meilisearch.com/docs/reference/errors/overview
UnexpectedAPIError
}
pub type Operation(data_type) =
#(Request(String), ParseResponseFunc(data_type))
// ----- CLIENT -----
pub opaque type Client {
Client(url: String, api_key: String)
}
pub fn new_client(url: String, api_key: String) -> Result(Client, Nil) {
// TODO: Check for valid http/https url
Ok(Client(url:, api_key:))
}
// ----- TASKS -----
/// A summarized version of the Task type, returned as a response to
/// endpoints that create a Task object. To retrieve a more detailed look at a task
/// use the get_task function.
pub type SummarizedTask {
SummarizedTask(
task_uid: Int,
index_uid: Option(String),
status: TaskStatus,
task_type: TaskType,
enqueued_at: timestamp.Timestamp,
)
}
fn parse_summarized_task_response(
resp: Response(String),
) -> Result(SummarizedTask, DeweyError) {
use resp_body <- result.try(verify_response(resp))
json.parse(resp_body, summarized_task_decoder())
|> result.map_error(JSONDecodeError)
}
/// A detailed description of a task generated by asynchronous endpoints.
pub type Task {
Task(
uid: Int,
index_uid: Option(String),
batch_uid: Int,
status: TaskStatus,
task_type: TaskType,
canceled_by: Option(Int),
details: Option(TaskDetails),
error: Option(TaskError),
// TODO: Figure out how to parse an ISO8601 duration
// duration: duration.Duration,
enqueued_at: timestamp.Timestamp,
started_at: Option(timestamp.Timestamp),
finished_at: Option(timestamp.Timestamp),
)
}
/// The current status of a task.
pub type TaskStatus {
Enqueued
Processing
Succeeded
Failed
Canceled
// This should never happen.
// https://www.meilisearch.com/docs/reference/api/tasks#status
UnexpectedTaskStatus
}
fn string_to_task_status(str: String) -> TaskStatus {
case str {
"enqueued" -> Enqueued
"processing" -> Processing
"succeeded" -> Succeeded
"failed" -> Failed
"canceled" -> Canceled
_ -> UnexpectedTaskStatus
}
}
fn task_status_to_string(status: TaskStatus) -> String {
case status {
Enqueued -> "enqueued"
Processing -> "processing"
Succeeded -> "succeeded"
Failed -> "failed"
Canceled -> "canceled"
UnexpectedTaskStatus -> ""
}
}
/// The type of task.
pub type TaskType {
IndexCreation
IndexUpdate
IndexDeletion
IndexSwap
DocumentAdditionOrUpdate
DocumentDeletion
SettingsUpdate
DumpCreation
TaskCancelation
TaskDeletion
UpgradeDatabase
DocumentEdition
SnapshotCreation
// This should never happen.
// https://www.meilisearch.com/docs/reference/api/tasks#type
UnexpectedTaskType
}
fn string_to_task_type(str: String) -> TaskType {
case str {
"indexCreation" -> IndexCreation
"indexUpdate" -> IndexUpdate
"indexDeletion" -> IndexDeletion
"indexSwap" -> IndexSwap
"documentAdditionOrUpdate" -> DocumentAdditionOrUpdate
"documentDeletion" -> DocumentDeletion
"settingsUpdate" -> SettingsUpdate
"dumpCreation" -> DumpCreation
"taskCancelation" -> TaskCancelation
"taskDeletion" -> TaskDeletion
"upgradeDatabase" -> UpgradeDatabase
"documentEdition" -> DocumentEdition
"snapshotCreation" -> SnapshotCreation
_ -> UnexpectedTaskType
}
}
fn task_type_to_string(task_type: TaskType) -> String {
case task_type {
IndexCreation -> "indexCreation"
IndexUpdate -> "indexUpdate"
IndexDeletion -> "indexDeletion"
IndexSwap -> "indexSwap"
DocumentAdditionOrUpdate -> "documentAdditionOrUpdate"
DocumentDeletion -> "documentDeletion"
SettingsUpdate -> "settingsUpdate"
DumpCreation -> "dumpCreation"
TaskCancelation -> "taskCancelation"
TaskDeletion -> "taskDeletion"
UpgradeDatabase -> "upgradeDatabase"
DocumentEdition -> "documentEdition"
SnapshotCreation -> "snapshotCreation"
UnexpectedTaskType -> ""
}
}
/// A record describing the details of a specific task.
pub type TaskDetails {
DocumentAdditionOrUpdateDetails(
received_documents: Int,
indexed_documents: Option(Int),
)
DocumentDeletionDetails(
provided_ids: Int,
original_filter: Option(String),
deleted_documents: Option(Int),
)
IndexCreationOrUpdateDetails(primary_key: Option(String))
IndexDeletionDetails(deleted_documents: Option(Int))
IndexSwapDetails(swaps: List(Swap))
// TODO: fully implement missing features
// missing: synonyms, typoTolerance, pagination, faceting, filterable_attributes
SettingsUpdateDetails(
ranking_rules: Option(List(String)),
distinct_attribute: Option(String),
searchable_attributes: Option(List(String)),
displayed_attributes: Option(List(String)),
sortable_attributes: Option(List(String)),
stop_words: Option(List(String)),
)
DumpCreationDetails(dump_uid: Option(String))
TaskCancelationDetails(
matched_tasks: Int,
canceled_tasks: Option(Int),
original_filter: String,
)
TaskDeletionDetails(
matched_tasks: Int,
deleted_tasks: Option(Int),
original_filter: String,
)
}
/// An error message that can accompany a failed task.
pub type TaskError {
TaskError(message: String, code: String, error_type: String, link: String)
}
pub type TasksParam {
TasksByUIDs(List(Int))
TasksByBatchUIDs(List(Int))
TasksByStatuses(List(TaskStatus))
TasksByTypes(List(TaskType))
TasksByIndexUIDs(List(String))
TasksLimit(Int)
TasksFromTaskUID(Int)
TasksInReverse(Bool)
TasksBeforeEnqueuedAt(timestamp.Timestamp)
TasksAfterEnqueuedAt(timestamp.Timestamp)
TasksBeforeStartedAt(timestamp.Timestamp)
TasksAfterStartedAt(timestamp.Timestamp)
TasksBeforeFinishedAt(timestamp.Timestamp)
TasksAfterFinishedAt(timestamp.Timestamp)
}
fn tasks_param_to_tuple(param: TasksParam) -> #(String, String) {
case param {
TasksByUIDs(uids) -> {
let uids = list.map(uids, int.to_string)
#("uids", string.join(uids, ","))
}
TasksByBatchUIDs(batch_uids) -> {
let batch_uids = list.map(batch_uids, int.to_string)
#("batchUids", string.join(batch_uids, ","))
}
TasksByStatuses(statuses) -> {
let statuses = list.map(statuses, task_status_to_string)
#("statuses", string.join(statuses, ","))
}
TasksByTypes(types) -> {
let types = list.map(types, task_type_to_string)
#("types", string.join(types, ","))
}
TasksByIndexUIDs(index_uids) -> #("indexUids", string.join(index_uids, ","))
TasksLimit(limit) -> #("limit", int.to_string(limit))
TasksFromTaskUID(from_uid) -> #("from", int.to_string(from_uid))
TasksInReverse(reverse) -> #(
"reverse",
string.lowercase(bool.to_string(reverse)),
)
TasksBeforeEnqueuedAt(before_enqueued_at) -> #(
"beforeEnqueuedAt",
timestamp.to_rfc3339(before_enqueued_at, duration.seconds(0)),
)
TasksAfterEnqueuedAt(after_enqueued_at) -> #(
"afterEnqueuedAt",
timestamp.to_rfc3339(after_enqueued_at, duration.seconds(0)),
)
TasksBeforeStartedAt(before_started_at) -> #(
"beforeStartedAt",
timestamp.to_rfc3339(before_started_at, duration.seconds(0)),
)
TasksAfterStartedAt(after_started_at) -> #(
"afterStartedAt",
timestamp.to_rfc3339(after_started_at, duration.seconds(0)),
)
TasksBeforeFinishedAt(before_finished_at) -> #(
"beforeFinishedAt",
timestamp.to_rfc3339(before_finished_at, duration.seconds(0)),
)
TasksAfterFinishedAt(after_finished_at) -> #(
"afterFinishedAt",
timestamp.to_rfc3339(after_finished_at, duration.seconds(0)),
)
}
}
pub fn get_tasks(
client: Client,
params: List(TasksParam),
) -> Operation(List(Task)) {
let url = client.url <> "/tasks"
let req =
base_request(url, client.api_key)
|> request.set_method(Get)
|> request.set_query(list.map(params, tasks_param_to_tuple))
#(req, fn(resp: Response(String)) -> Result(List(Task), DeweyError) {
use resp_body <- result.try(verify_response(resp))
let decoder = {
use results <- decode.field("results", decode.list(task_decoder()))
decode.success(results)
}
json.parse(resp_body, decoder)
|> result.map_error(JSONDecodeError)
})
}
pub fn get_one_task(client: Client, task_uid: Int) -> Operation(Task) {
let url = client.url <> "/tasks/" <> int.to_string(task_uid)
let req =
base_request(url, client.api_key)
|> request.set_method(Get)
#(req, fn(resp: Response(String)) -> Result(Task, DeweyError) {
use resp_body <- result.try(verify_response(resp))
json.parse(resp_body, task_decoder())
|> result.map_error(JSONDecodeError)
})
}
pub fn cancel_tasks(
client: Client,
params: List(TasksParam),
) -> Operation(SummarizedTask) {
let url = client.url <> "/tasks/cancel"
let req =
base_request(url, client.api_key)
|> request.set_method(Post)
|> request.set_query(list.map(params, tasks_param_to_tuple))
#(req, parse_summarized_task_response)
}
pub fn delete_tasks(
client: Client,
params: List(TasksParam),
) -> Operation(SummarizedTask) {
let url = client.url <> "/tasks"
let req =
base_request(url, client.api_key)
|> request.set_method(Delete)
|> request.set_query(list.map(params, tasks_param_to_tuple))
#(req, parse_summarized_task_response)
}
pub fn delete_all_tasks(client: Client) -> Operation(SummarizedTask) {
let url = client.url <> "/tasks"
let req =
base_request(url, client.api_key)
|> request.set_method(Delete)
|> request.set_query([#("statuses", "failed,canceled,succeeded")])
#(req, parse_summarized_task_response)
}
// ----- INDEXES -----
/// A collection of documents, much like a table in MySQL or a collection in MongoDB.
///
/// Indexes are outlined further at: https://www.meilisearch.com/docs/learn/getting_started/indexes
pub type Index {
Index(
uid: String,
created_at: Timestamp,
updated_at: Timestamp,
primary_key: String,
)
}
/// Returns an Operation record for the Get All Indexes endpoint
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/indexes#list-all-indexes
pub fn get_all_indexes(client: Client) -> Operation(List(Index)) {
let url = client.url <> "/indexes"
let req =
base_request(url, client.api_key)
|> request.set_method(Get)
#(req, fn(resp: Response(String)) -> Result(List(Index), DeweyError) {
use resp_body <- result.try(verify_response(resp))
let decoder = {
use indexes <- decode.field("results", decode.list(index_decoder()))
decode.success(indexes)
}
json.parse(resp_body, decoder)
|> result.map_error(JSONDecodeError)
})
}
/// Returns an Operation record for the Get One Index endpoint
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/indexes#get-one-index
pub fn get_one_index(client: Client, index_uid: String) -> Operation(Index) {
let url = client.url <> "/indexes/" <> index_uid
let req =
base_request(url, client.api_key)
|> request.set_method(Get)
#(req, fn(resp: Response(String)) -> Result(Index, DeweyError) {
use resp_body <- result.try(verify_response(resp))
json.parse(resp_body, index_decoder())
|> result.map_error(JSONDecodeError)
})
}
/// Returns an Operation record for the Create Index endpoint
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/indexes#create-an-index
pub fn create_index(
client: Client,
index_uid: String,
primary_key: Option(String),
) -> Operation(SummarizedTask) {
let url = client.url <> "/indexes"
let req_body =
json.object([
#("uid", json.string(index_uid)),
#("primaryKey", json.nullable(primary_key, json.string)),
])
|> json.to_string
let req =
base_request(url, client.api_key)
|> request.set_method(Post)
|> request.set_header("Content-Type", "application/json")
|> request.set_body(req_body)
#(req, parse_summarized_task_response)
}
/// Returns an Operation record for the Update Index endpoint
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/indexes#update-an-index
pub fn update_index(
client: Client,
index_uid: String,
new_primary_key: Option(String),
) -> Operation(SummarizedTask) {
let url = client.url <> "/indexes/" <> index_uid
let req_body =
json.object([#("primaryKey", json.nullable(new_primary_key, json.string))])
|> json.to_string
let req =
base_request(url, client.api_key)
|> request.set_method(Patch)
|> request.set_header("Content-Type", "application/json")
|> request.set_body(req_body)
#(req, parse_summarized_task_response)
}
/// Returns an Operation record for the Delete Index endpoint.
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/indexes#delete-an-index
pub fn delete_index(
client: Client,
index_uid: String,
) -> Operation(SummarizedTask) {
let url = client.url <> "/indexes/" <> index_uid
let req =
base_request(url, client.api_key)
|> request.set_method(Delete)
#(req, parse_summarized_task_response)
}
/// Returns an Operation record for the Swap Indexes endpoint
///
/// This function does not include the rename parameter, and therefore does not rename
/// indexes when the second index does not exist. To rename indexes, use the rename_indexes
/// function instead.
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/indexes#swap-indexes
pub fn swap_indexes(
client: Client,
index_uids_to_swap: List(#(String, String)),
) -> Operation(SummarizedTask) {
let url = client.url <> "/swap-indexes"
let indexes_list =
list.map(index_uids_to_swap, fn(tuple) { [tuple.0, tuple.1] })
let req_body =
json.array(indexes_list, fn(indexes) {
json.object([#("indexes", json.array(indexes, json.string))])
})
|> json.to_string
let req =
base_request(url, client.api_key)
|> request.set_method(Post)
|> request.set_header("Content-Type", "application/json")
|> request.set_body(req_body)
#(req, parse_summarized_task_response)
}
/// Returns an Operation record for the Swap Indexes endpoint, utilizing the
/// rename parameter.
///
/// This function includes the rename parameter within the request body, meaning that
/// the first index_uid will be renamed to the second index_uid listed (assuming the second uid does not exist).
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/indexes#swap-indexes
pub fn rename_indexes(
client: Client,
index_uids_to_swap: List(#(String, String)),
) -> Operation(SummarizedTask) {
let url = client.url <> "/swap-indexes"
let indexes_list =
list.map(index_uids_to_swap, fn(tuple) { [tuple.0, tuple.1] })
let req_body =
json.array(indexes_list, fn(indexes) {
json.object([
#("indexes", json.array(indexes, json.string)),
#("rename", json.bool(True)),
])
})
|> json.to_string
let req =
base_request(url, client.api_key)
|> request.set_method(Post)
|> request.set_header("Content-Type", "application/json")
|> request.set_body(req_body)
#(req, parse_summarized_task_response)
}
// ------ DOCUMENTS -----
/// A record containing the response from the Get Documents endpoint.
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/documents#get-documents-with-post
pub type DocumentsResponse(document_type) {
DocumentsResponse(
results: List(document_type),
offset: Int,
limit: Int,
total: Int,
)
}
/// A collection of options when using the Get Documents endpoint.
///
/// These options are outlined at: https://www.meilisearch.com/docs/reference/api/documents#body
pub type GetDocumentsOptions {
GetDocumentsOptions(
offset: Int,
limit: Int,
fields: Option(List(String)),
filter: Option(String),
retrieve_vectors: Bool,
sort: Option(String),
ids: Option(List(String)),
)
}
/// A function returning the default set of options for the Get Documents endpoint.
///
/// These options are outlined at: https://www.meilisearch.com/docs/reference/api/documents#body
pub fn default_get_documents_options() -> GetDocumentsOptions {
GetDocumentsOptions(
offset: 0,
limit: 20,
fields: None,
filter: None,
retrieve_vectors: False,
sort: None,
ids: None,
)
}
/// Returns an Operation record for the Get Documents endpoint.
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/documents#get-documents-with-post
pub fn get_documents(
client: Client,
index_uid: String,
options: GetDocumentsOptions,
documents_decoder: decode.Decoder(document_type),
) -> Operation(DocumentsResponse(document_type)) {
let url = client.url <> "/indexes/" <> index_uid <> "/documents/fetch"
let json_string_array = json.array(_, json.string)
let req_body =
json.object([
#("offset", json.int(options.offset)),
#("limit", json.int(options.limit)),
#("fields", json.nullable(options.fields, json_string_array)),
#("filter", json.nullable(options.filter, json.string)),
#("retrieveVectors", json.bool(options.retrieve_vectors)),
#("sort", json.nullable(options.sort, json.string)),
#("ids", json.nullable(options.ids, json_string_array)),
])
|> json.to_string
let req =
base_request(url, client.api_key)
|> request.set_method(Post)
|> request.set_header("Content-Type", "application/json")
|> request.set_body(req_body)
#(req, fn(resp: Response(String)) -> Result(
DocumentsResponse(document_type),
DeweyError,
) {
use resp_body <- result.try(verify_response(resp))
let decoder = {
use results <- decode.field("results", decode.list(documents_decoder))
use offset <- decode.field("offset", decode.int)
use limit <- decode.field("limit", decode.int)
use total <- decode.field("total", decode.int)
decode.success(DocumentsResponse(results:, offset:, limit:, total:))
}
json.parse(resp_body, decoder)
|> result.map_error(JSONDecodeError)
})
}
/// Returns an Operation record for the Get One Document endpoint.
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/documents#get-one-document
pub fn get_one_document(
client: Client,
index_uid: String,
document_id: String,
document_decoder: decode.Decoder(document_type),
) -> Operation(document_type) {
let url =
client.url <> "/indexes/" <> index_uid <> "/documents/" <> document_id
let req =
base_request(url, client.api_key)
|> request.set_method(Get)
#(req, fn(resp: Response(String)) -> Result(document_type, DeweyError) {
use resp_body <- result.try(verify_response(resp))
json.parse(resp_body, document_decoder)
|> result.map_error(JSONDecodeError)
})
}
/// Returns an Operation record for the Add or Replace Documents endpoint.
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/documents#add-or-replace-documents
pub fn add_or_replace_documents(
client: Client,
index_uid: String,
documents: List(document_type),
document_encoder: fn(document_type) -> json.Json,
) -> Operation(SummarizedTask) {
let url = client.url <> "/indexes/" <> index_uid <> "/documents"
let req_body =
json.array(documents, document_encoder)
|> json.to_string
let req =
base_request(url, client.api_key)
|> request.set_method(Post)
|> request.set_header("Content-Type", "application/json")
|> request.set_body(req_body)
#(req, parse_summarized_task_response)
}
/// Returns an Operation record for the Add or Update Documents endpoint.
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/documents#add-or-update-documents
pub fn add_or_update_documents(
client: Client,
index_uid: String,
documents: List(document_type),
document_encoder: fn(document_type) -> json.Json,
) -> Operation(SummarizedTask) {
let url = client.url <> "/indexes/" <> index_uid <> "/documents"
let req_body =
json.array(documents, document_encoder)
|> json.to_string
let req =
base_request(url, client.api_key)
|> request.set_method(Put)
|> request.set_header("Content-Type", "application/json")
|> request.set_body(req_body)
#(req, parse_summarized_task_response)
}
/// Returns an Operation record for the Delete All Documents endpoint.
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/documents#delete-all-documents
pub fn delete_all_documents(
client: Client,
index_uid: String,
) -> Operation(SummarizedTask) {
let url = client.url <> "/indexes/" <> index_uid <> "/documents"
let req =
base_request(url, client.api_key)
|> request.set_method(Delete)
#(req, parse_summarized_task_response)
}
/// Returns an Operation record for the Delete One Document endpoint.
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/documents#delete-one-document
pub fn delete_one_document(
client: Client,
index_uid: String,
document_id: String,
) -> Operation(SummarizedTask) {
let url =
client.url <> "/indexes/" <> index_uid <> "/documents/" <> document_id
let req =
base_request(url, client.api_key)
|> request.set_method(Delete)
#(req, parse_summarized_task_response)
}
/// Returns an Operation record for the Delete Documents by Filter endpoint.
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/documents#delete-documents-by-filter
pub fn delete_documents_by_filter(
client: Client,
index_uid: String,
filter: String,
) -> Operation(SummarizedTask) {
let url = client.url <> "/indexes/" <> index_uid <> "/documents/delete"
let req_body =
json.object([#("filter", json.string(filter))])
|> json.to_string
let req =
base_request(url, client.api_key)
|> request.set_method(Post)
|> request.set_header("Content-Type", "application/json")
|> request.set_body(req_body)
#(req, parse_summarized_task_response)
}
/// Returns an Operation record for the Delete Documents by Batch endpoint.
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/documents#delete-documents-by-batch
pub fn delete_documents_by_batch(
client: Client,
index_uid: String,
batch: List(String),
) -> Operation(SummarizedTask) {
let url = client.url <> "/indexes/" <> index_uid <> "/documents/delete-batch"
let req_body =
json.array(batch, json.string)
|> json.to_string
let req =
base_request(url, client.api_key)
|> request.set_method(Post)
|> request.set_header("Content-Type", "application/json")
|> request.set_body(req_body)
#(req, parse_summarized_task_response)
}
// ----- SEARCH -----
/// Collection of Search Parameter records for adjusting the search functionality.
///
/// Keep in mind that some of these parameters change the structure of the documents returned.
/// You will have to adjust your document decoder function to account for the changes caused.
/// For this reason, it is important to read through the search API documentation to understand
/// how each parameter can affect the document return type.
///
/// For more details visit this link: https://www.meilisearch.com/docs/reference/api/search#search-parameters
pub type SearchParam {
SearchOffset(offset: Int)
SearchLimit(limit: Int)
SearchHitsPerPage(hits: Int)
SearchPage(page_num: Int)
SearchFilter(filter: String)
// TODO: SearchFacets(facets: String)
SearchDistinctAttribute(attribute: String)
SearchAttributesToRetrieve(attributes: List(String))
SearchAttributesToCrop(attributes: List(String))
SearchCropLength(length: Int)
SearchCropMarker(marker: String)
SearchAttributesToHighlight(attributes: List(String))
SearchHighlightPreTag(pre_tag: String)
SearchHighlighPostTag(post_tag: String)
SearchShowMatchesPosition(show_matches_position: Bool)
SearchSortAttributes(attributes: List(String))
SearchMatchingStrategy(strategy: String)
SearchShowRankingScore(show_ranking_score: Bool)
SearchShowRankingScoreDetails(show_ranking_score_details: Bool)
SearchRankingScoreThreshold(threshold: Float)
SearchAttributesToSearchOn(attributes: List(String))
// TODO: SearchHybrid
SearchVector(vector: List(Float))
SearchRetrieveVectors(retrieve_vectors: Bool)
SearchLocales(locales: List(String))
// TODO: SearchMedia
// TODO: SearchPersonalize
}
fn search_param_to_json_field(param: SearchParam) -> #(String, json.Json) {
case param {
SearchOffset(offset:) -> #("offset", json.int(offset))
SearchLimit(limit:) -> #("limit", json.int(limit))
SearchHitsPerPage(hits:) -> #("hitsPerPage", json.int(hits))
SearchPage(page_num:) -> #("page", json.int(page_num))
SearchFilter(filter:) -> #("filter", json.string(filter))
SearchDistinctAttribute(attribute:) -> #("distinct", json.string(attribute))
SearchAttributesToRetrieve(attributes:) -> #(
"attributesToRetrieve",
json.array(attributes, json.string),
)
SearchAttributesToCrop(attributes:) -> #(
"attributesToCrop",
json.array(attributes, json.string),
)
SearchCropLength(length:) -> #("cropLength", json.int(length))
SearchCropMarker(marker:) -> #("cropMarker", json.string(marker))
SearchAttributesToHighlight(attributes:) -> #(
"attributesToHighlight",
json.array(attributes, json.string),
)
SearchHighlightPreTag(pre_tag:) -> #(
"highlightPreTag",
json.string(pre_tag),
)
SearchHighlighPostTag(post_tag:) -> #(
"highlightPostTag",
json.string(post_tag),
)
SearchShowMatchesPosition(show_matches_position:) -> #(
"showMatchesPosition",
json.bool(show_matches_position),
)
SearchSortAttributes(attributes:) -> #(
"sort",
json.array(attributes, json.string),
)
SearchMatchingStrategy(strategy:) -> #(
"matchingStrategy",
json.string(strategy),
)
SearchShowRankingScore(show_ranking_score:) -> #(
"showRankingScore",
json.bool(show_ranking_score),
)
SearchShowRankingScoreDetails(show_ranking_score_details:) -> #(
"showRankingScoreDetails",
json.bool(show_ranking_score_details),
)
SearchRankingScoreThreshold(threshold:) -> #(
"rankingScoreThreshold",
json.float(threshold),
)
SearchAttributesToSearchOn(attributes:) -> #(
"attributesToSearchOn",
json.array(attributes, json.string),
)
SearchVector(vector:) -> #("vector", json.array(vector, json.float))
SearchRetrieveVectors(retrieve_vectors:) -> #(
"retrieveVectors",
json.bool(retrieve_vectors),
)
SearchLocales(locales:) -> #("locales", json.array(locales, json.string))
}
}
pub type SearchResponse(document_type) {
SearchResponse(
hits: List(document_type),
offset: Int,
limit: Int,
estimated_total_hits: Option(Int),
total_hits: Option(Int),
total_pages: Option(Int),
hits_per_page: Option(Int),
processing_time_ms: Int,
query: String,
request_uid: String,
)
}
/// Returns an Operation record for the Search endpoint
///
/// This endpoint is outlined at: https://www.meilisearch.com/docs/reference/api/search#search-in-an-index-with-post
pub fn search(
client: Client,
index_uid: String,
query: String,
document_decoder: decode.Decoder(document_type),
params: List(SearchParam),
) -> Operation(SearchResponse(document_type)) {
let url = client.url <> "/indexes/" <> index_uid <> "/search"
let param_fields = [
#("q", json.string(query)),
..list.map(params, search_param_to_json_field)
]
let req_body =
json.object(param_fields)
|> json.to_string
let req =
base_request(url, client.api_key)
|> request.set_method(Post)
|> request.set_header("Content-Type", "application/json")
|> request.set_body(req_body)
#(req, fn(resp: Response(String)) -> Result(
SearchResponse(document_type),
DeweyError,
) {
use resp_body <- result.try(verify_response(resp))
let decoder = {
use hits <- decode.field("hits", decode.list(document_decoder))
use offset <- decode.field("offset", decode.int)
use limit <- decode.field("limit", decode.int)
use estimated_total_hits <- decode.optional_field(
"estimatedTotalHits",
None,
decode.optional(decode.int),
)
use total_hits <- decode.optional_field(
"totalHits",
None,
decode.optional(decode.int),
)
use total_pages <- decode.optional_field(
"totalPages",
None,
decode.optional(decode.int),
)
use hits_per_page <- decode.optional_field(
"hitsPerPage",
None,
decode.optional(decode.int),
)
use processing_time_ms <- decode.field("processingTimeMs", decode.int)
use query <- decode.field("query", decode.string)
use request_uid <- decode.field("requestUid", decode.string)
decode.success(SearchResponse(
hits:,
offset:,
limit:,
estimated_total_hits:,
total_hits:,
total_pages:,
hits_per_page:,
processing_time_ms:,
query:,
request_uid:,
))
}
json.parse(resp_body, decoder)
|> result.map_error(JSONDecodeError)
})
}
// ----- DECODERS -----
fn summarized_task_decoder() -> decode.Decoder(SummarizedTask) {
use task_uid <- decode.field("taskUid", decode.int)
use index_uid <- decode.field("indexUid", decode.optional(decode.string))
use status_str <- decode.field("status", decode.string)
use type_str <- decode.field("type", decode.string)
use enqueued_at_str <- decode.field("enqueuedAt", decode.string)
let status = string_to_task_status(status_str)
let task_type = string_to_task_type(type_str)
// This should never fail, as the API should always return an RFC3339 string
// https://www.meilisearch.com/docs/reference/api/tasks#enqueuedat
let enqueued_at =
timestamp.parse_rfc3339(enqueued_at_str)
|> result.unwrap(timestamp.from_unix_seconds(0))
decode.success(SummarizedTask(
task_uid:,
index_uid:,
status:,
task_type:,
enqueued_at:,
))
}
fn index_decoder() -> decode.Decoder(Index) {
use uid <- decode.field("uid", decode.string)
use created_at_string <- decode.field("createdAt", decode.string)
use updated_at_string <- decode.field("updatedAt", decode.string)
use primary_key <- decode.field("primaryKey", decode.string)
let created_at =
timestamp.parse_rfc3339(created_at_string)
|> result.unwrap(timestamp.from_unix_seconds(0))
let updated_at =
timestamp.parse_rfc3339(updated_at_string)
|> result.unwrap(timestamp.from_unix_seconds(0))
decode.success(Index(uid:, created_at:, updated_at:, primary_key:))
}
fn task_decoder() -> decode.Decoder(Task) {
use uid <- decode.field("uid", decode.int)
use batch_uid <- decode.field("batchUid", decode.int)
use index_uid <- decode.field("indexUid", decode.optional(decode.string))
use status_string <- decode.field("status", decode.string)
use task_type_string <- decode.field("type", decode.string)
use canceled_by <- decode.field("canceledBy", decode.optional(decode.int))
use details <- decode.field("details", task_details_decoder())
use error <- decode.field("error", decode.optional(task_error_decoder()))
use enqueued_at_string <- decode.field("enqueuedAt", decode.string)
use started_at_string <- decode.field(
"startedAt",
decode.optional(decode.string),
)
use finished_at_string <- decode.field(
"finishedAt",
decode.optional(decode.string),
)
let status = string_to_task_status(status_string)
let task_type = string_to_task_type(task_type_string)
let default_time = timestamp.from_unix_seconds(0)
let enqueued_at =
timestamp.parse_rfc3339(enqueued_at_string)
|> result.unwrap(default_time)
let started_at =
option.map(started_at_string, fn(str) {
timestamp.parse_rfc3339(str)
|> result.unwrap(default_time)
})
let finished_at =
option.map(finished_at_string, fn(str) {
timestamp.parse_rfc3339(str)
|> result.unwrap(default_time)
})
decode.success(Task(
uid:,
batch_uid:,
index_uid:,
status:,
task_type:,
canceled_by:,
details:,
error:,
enqueued_at:,
started_at:,
finished_at:,
))
}
pub type Swap {
Swap(swapped_indexes: #(String, String), rename: Bool)
}
fn task_details_decoder() -> decode.Decoder(Option(TaskDetails)) {
let document_addition_or_update = {
use received_documents <- decode.field("receivedDocuments", decode.int)
use indexed_documents <- decode.field(
"indexedDocuments",
decode.optional(decode.int),
)
decode.success(DocumentAdditionOrUpdateDetails(
received_documents:,
indexed_documents:,
))
}
let document_deletion = {
use provided_ids <- decode.field("providedIds", decode.int)
use original_filter <- decode.field(
"originalFilter",
decode.optional(decode.string),
)
use deleted_documents <- decode.field(
"deletedDocuments",
decode.optional(decode.int),
)
decode.success(DocumentDeletionDetails(
provided_ids:,
original_filter:,
deleted_documents:,
))
}
let index_creation_or_update = {
use primary_key <- decode.field(
"primaryKey",
decode.optional(decode.string),
)
decode.success(IndexCreationOrUpdateDetails(primary_key:))
}
let index_deletion = {
use deleted_documents <- decode.field(
"deletedDocuments",
decode.optional(decode.int),
)
decode.success(IndexDeletionDetails(deleted_documents:))
}
let index_swap = {
let swap_decoder = {
use indexes <- decode.field("indexes", decode.list(decode.string))
use rename <- decode.field("rename", decode.bool)
case indexes {
[first_index, second_index] -> {
decode.success(Swap(
swapped_indexes: #(first_index, second_index),
rename:,
))
}
_ -> decode.failure(Swap(#("", ""), False), "Swap")
}
}
use swaps <- decode.field("swaps", decode.list(swap_decoder))
decode.success(IndexSwapDetails(swaps:))
}
let optional_string_list = decode.optional(decode.list(decode.string))
let settings_update = {
use ranking_rules <- decode.optional_field(
"rankingRules",
None,
optional_string_list,
)
use distinct_attribute <- decode.optional_field(
"distinctAttribute",
None,
decode.optional(decode.string),
)
use searchable_attributes <- decode.optional_field(
"searchableAttributes",
None,
optional_string_list,
)
use displayed_attributes <- decode.optional_field(
"displayedAttributes",
None,
optional_string_list,
)
use sortable_attributes <- decode.optional_field(
"sortableAttributes",
None,
optional_string_list,
)
use stop_words <- decode.optional_field(
"stopWords",
None,
optional_string_list,
)
decode.success(SettingsUpdateDetails(
ranking_rules:,
distinct_attribute:,
searchable_attributes:,
displayed_attributes:,
sortable_attributes:,
stop_words:,
))
}
let dump_creation = {
use dump_uid <- decode.field("dumpUid", decode.optional(decode.string))
decode.success(DumpCreationDetails(dump_uid:))
}
let task_cancelation = {
use matched_tasks <- decode.field("matchedTasks", decode.int)
use canceled_tasks <- decode.field(
"canceledTasks",
decode.optional(decode.int),
)
use original_filter <- decode.field("originalFilter", decode.string)
decode.success(TaskCancelationDetails(
matched_tasks:,
canceled_tasks:,
original_filter:,
))
}
let task_deletion = {
use matched_tasks <- decode.field("matchedTasks", decode.int)
use deleted_tasks <- decode.field(
"deletedTasks",
decode.optional(decode.int),
)
use original_filter <- decode.field("originalFilter", decode.string)
decode.success(TaskDeletionDetails(
matched_tasks:,
deleted_tasks:,
original_filter:,
))
}
decode.optional(
decode.one_of(settings_update, [
document_deletion,
index_creation_or_update,
index_deletion,
index_swap,
document_addition_or_update,
dump_creation,
task_cancelation,
task_deletion,
]),
)
}
fn task_error_decoder() -> decode.Decoder(TaskError) {
use message <- decode.field("message", decode.string)
use code <- decode.field("code", decode.string)
use error_type <- decode.field("type", decode.string)
use link <- decode.field("link", decode.string)
decode.success(TaskError(message:, code:, error_type:, link:))
}
// ----- PRIVATE HELPER TYPES AND FNS -----
fn base_request(url: String, api_key: String) -> Request(String) {
let assert Ok(base_req) = request.to(url)
request.set_header(base_req, "Authorization", "Bearer " <> api_key)
}
const acceptable_response_codes = [200, 201, 202, 204, 205]
fn verify_response(resp: Response(String)) -> Result(String, DeweyError) {
case list.contains(acceptable_response_codes, resp.status) {
True -> Ok(resp.body)
False -> {
let decoder = {
use message <- decode.field("message", decode.string)
use code <- decode.field("code", decode.string)
use error_type <- decode.field("type", decode.string)
use link <- decode.field("link", decode.string)
decode.success(APIError(message:, code:, error_type:, link:))
}
case json.parse(resp.body, decoder) {
Ok(api_error) -> Error(api_error)
Error(_) -> Error(UnexpectedAPIError)
}
}
}
}