defmodule Hui do
@moduledoc """
Hui 辉 ("shine" in Chinese) is an [Elixir](https://elixir-lang.org) client and library for
[Solr enterprise search platform](http://lucene.apache.org/solr/).
### Usage
- Searching Solr: `search/3`
- Updating: `update/3`, `delete/3`, `delete_by_query/3`, `commit/2`
- Other: `suggest/2`, `suggest/5`
- Admin: `metrics/2`, `ping/1`
- [README](https://hexdocs.pm/hui/readme.html#usage)
"""
alias Hui.Http
alias Hui.Query
@client Hui.Http.Client.impl()
@type endpoint :: Http.endpoint()
@type query :: Http.query()
@type update_query :: Http.update_query()
@type http_response :: Http.response()
@doc """
Issue a keyword list or structured query to a Solr endpoint.
### Example - parameters
```
url = "http://localhost:8983/solr/collection"
# a keyword list of arbitrary parameters
Hui.search(url, q: "edinburgh", rows: 10)
# supply a list of Hui structs for more complex query e.g. DisMax
alias Hui.Query
x = %Query.DisMax{q: "run", qf: "description^2.3 title", mm: "2<-25% 9<-3"}
y = %Query.Common{rows: 10, start: 10, fq: ["edited:true"]}
z = %Query.Facet{field: ["cat", "author_str"], mincount: 1}
Hui.search(url, [x, y, z])
# SolrCloud query
x = %Query.DisMax{q: "john"}
y = %Query.Common{collection: "library,commons", rows: 10, distrib: true, "shards.tolerant": true, "shards.info": true}
Hui.search(url, [x,y])
# With results highlighting (snippets)
x = %Query.Standard{q: "features:photo"}
y = %Query.Highlight{fl: "features", usePhraseHighlighter: true, fragsize: 250, snippets: 3}
Hui.search(url, [x, y])
```
### Example - faceting
```
alias Hui.Query
range1 = %Query.FacetRange{range: "price", start: 0, end: 100, gap: 10, per_field: true}
range2 = %Query.FacetRange{range: "popularity", start: 0, end: 5, gap: 1, per_field: true}
x = %Query.DisMax{q: "ivan"}
y = %Query.Facet{field: ["cat", "author_str"], mincount: 1, range: [range1, range2]}
Hui.search(:default, [x, y])
```
The above `Hui.search(:default, [x, y])` example issues a request that resulted in
the following Solr response header showing the corresponding generated and encoded parameters.
```json
"responseHeader" => %{
"QTime" => 106,
"params" => %{
"f.popularity.facet.range.end" => "5",
"f.popularity.facet.range.gap" => "1",
"f.popularity.facet.range.start" => "0",
"f.price.facet.range.end" => "100",
"f.price.facet.range.gap" => "10",
"f.price.facet.range.start" => "0",
"facet" => "true",
"facet.field" => ["cat", "author_str"],
"facet.mincount" => "1",
"facet.range" => ["price", "popularity"],
"q" => "ivan"
},
"status" => 0,
"zkConnected" => true
}
```
### URLs, Headers, Options
HTTP headers and client options for a specific endpoint may also be
included in the a `{url, headers, options}` tuple where:
- `url` is a typical Solr endpoint that includes a request handler
- `headers`: a tuple list of [HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)
- `options`: a keyword list of configured http client options such as [Erlang httpc](https://erlang.org/doc/man/httpc.html#request-4),
[HTTPoison](https://hexdocs.pm/httpoison/HTTPoison.Request.html), e.g.
`timeout`, `recv_timeout`, `max_redirect`
If `HTTPoison` is used, advanced HTTP options such as the use of connection pools
may also be specified via `options`.
"""
@spec search(endpoint, query, module) :: http_response
defdelegate search(endpoint, query, client), to: Http, as: :get
@doc """
Issue a structured suggest query to a specified Solr endpoint.
### Example
```
# :library is a configured endpoint
suggest_query = %Hui.Query.Suggest{q: "ha", count: 10, dictionary: "name_infix"}
Hui.suggest(:library, suggest_query)
```
"""
@spec suggest(endpoint, Query.Suggest.t()) :: http_response
defdelegate suggest(endpoint, query), to: Hui.Suggest
@doc """
Convenience function for issuing a suggester query to a specified Solr endpoint.
### Example
```
# :autocomplete is a configured endpoint
Hui.suggest(:autocomplete, "t")
Hui.suggest(:autocomplete, "bo", 5, ["name_infix", "ln_prefix", "fn_prefix"], "1939")
```
"""
@spec suggest(endpoint, binary, nil | integer, nil | binary | list(binary), nil | binary) :: http_response
defdelegate suggest(endpoint, q, count, dictionaries, context), to: Hui.Suggest
@doc """
Updates or adds Solr documents to an index or collection.
This function accepts documents as map (single or a list) and commits the docs
to the index immediately by default - set `commit` to `false` for manual or
auto commits later.
It can also operate in update struct and binary modes,
the former uses the `t:Hui.Query.Update.t/0` struct
while the latter acepts text containing any valid Solr update data or commands.
An index/update handler endpoint should be specified through a URL string or
{url, headers, options} tuple for headers and HTTP client options specification.
A "content-type" request header is required so that Solr knows the
incoming data format (JSON, XML etc.) and can process data accordingly.
### Example - map, list and binary data
```
# Index handler for JSON-formatted update
headers = [{"content-type", "application/json"}]
endpoint = {"http://localhost:8983/solr/collection/update", headers}
# Solr docs in maps
doc1 = %{
"actors" => ["Ingrid Bergman", "Liv Ullmann", "Lena Nyman", "Halvar Björk"],
"desc" => "A married daughter who longs for her mother's love is visited by the latter, a successful concert pianist.",
"directed_by" => ["Ingmar Bergman"],
"genre" => ["Drama", "Music"],
"id" => "tt0077711",
"initial_release_date" => "1978-10-08",
"name" => "Autumn Sonata"
}
doc2 = %{
"actors" => ["Bibi Andersson", "Liv Ullmann", "Margaretha Krook"],
"desc" => "A nurse is put in charge of a mute actress and finds that their personas are melding together.",
"directed_by" => ["Ingmar Bergman"],
"genre" => ["Drama", "Thriller"],
"id" => "tt0060827",
"initial_release_date" => "1967-09-21",
"name" => "Persona"
}
Hui.update(endpoint, doc1) # add a single doc
Hui.update(endpoint, [doc1, doc2]) # add a list of docs
# Don't commit the docs e.g. mass ingestion when index handler is setup for autocommit.
Hui.update(endpoint, [doc1, doc2], false)
# Send to a configured endpoint
Hui.update(:updater, [doc1, doc2])
# Binary mode, add and commit a doc
Hui.update(endpoint, "{\\\"add\\\":{\\\"doc\\\":{\\\"name\\\":\\\"Blade Runner\\\",\\\"id\\\":\\\"tt0083658\\\",..}},\\\"commit\\\":{}}")
# Binary mode, delete a doc via XML
headers = [{"content-type", "application/xml"}]
endpoint = {"http://localhost:8983/solr/collection/update", headers}
Hui.update(endpoint, "<delete><id>9780141981727</id></delete>")
```
### Example - `t:Hui.Query.Update.t/0` and other update options
```
# endpoint, doc1, doc2 from the above example
...
# Hui.Query.Update struct command for updating and committing the docs to Solr within 5 seconds
alias Hui.Query
x = %Query.Update{doc: [doc1, doc2], commitWithin: 5000, overwrite: true}
{status, resp} = Hui.update(endpoint, x)
# Delete the docs by IDs, with a URL key from configuration
{status, resp} = Hui.update(:library_update, %Query.Update{delete_id: ["tt1316540", "tt1650453"]})
# Commit and optimise index, keep max index segments at 10
{status, resp} = Hui.update(endpoint, %Query.Update{commit: true, waitSearcher: true, optimize: true, maxSegments: 10})
# Commit index, expunge deleted docs
{status, resp} = Hui.update(endpoint, %Query.Update{commit: true, expungeDeletes: true})
```
"""
@spec update(endpoint, update_query) :: http_response
defdelegate update(endpoint, query), to: Http, as: :post
@spec update(endpoint, update_query, boolean, module) :: http_response
defdelegate update(endpoint, query, commit, client), to: Http, as: :post
@doc """
Deletes Solr documents.
This function accepts a single or list of IDs and immediately delete the corresponding
documents from the Solr index (commit by default).
An index/update handler endpoint should be specified through a URL string
or {url, headers, options} tuple.
A JSON "content-type" request header is required so that Solr knows the
incoming data format and can process data accordingly.
### Example
```
# Index handler for JSON-formatted update
headers = [{"content-type", "application/json"}]
endpoint = {"http://localhost:8983/solr/collection/update", headers}
Hui.delete_by_id(endpoint, "tt2358891") # delete a single doc
Hui.delete_by_id(endpoint, ["tt2358891", "tt1602620"]) # delete a list of docs
Hui.delete_by_id(endpoint, ["tt2358891", "tt1602620"], false) # delete without immediate commit
```
"""
@spec delete_by_id(endpoint, binary | list(binary), boolean, module) :: http_response
def delete_by_id(endpoint, ids, commit \\ true, client \\ @client) when is_binary(ids) or is_list(ids) do
Http.post(endpoint, %Query.Update{delete_id: ids, commit: commit}, commit, client)
end
# coveralls-ignore-start
@deprecated "Use delete_by_id/3 instead"
def delete(endpoint, ids, commit \\ true) when is_binary(ids) or is_list(ids) do
Http.post(endpoint, %Query.Update{delete_id: ids, commit: commit})
end
# coveralls-ignore-stop
@doc """
Deletes Solr documents by filter queries.
This function accepts a single or list of filter queries and immediately delete the corresponding
documents from the Solr index (commit by default).
An index/update handler endpoint should be specified through a URL string
or {url, headers, options} tuple.
A JSON "content-type" request header is required so that Solr knows the
incoming data format and can process data accordingly.
### Example
```
# Index handler for JSON-formatted update
headers = [{"content-type", "application/json"}]
endpoint = {"http://localhost:8983/solr/collection", headers}
Hui.delete_by_query(endpoint, "name:Persona") # delete with a single filter
Hui.delete_by_query(endpoint, ["genre:Drama", "name:Persona"]) # delete with a list of filters
```
"""
@spec delete_by_query(endpoint, binary | list(binary), boolean) :: http_response
def delete_by_query(endpoint, q, commit \\ true, client \\ @client) when is_binary(q) or is_list(q) do
Http.post(endpoint, %Query.Update{delete_query: q, commit: commit}, commit, client)
end
@doc """
Commit any added or deleted Solr documents to the index.
This provides a (separate) mechanism to commit previously added or deleted documents to
Solr index for different updating and index maintenance scenarios. By default, the commit
waits for a new Solr searcher to be regenerated, so that the commit result is made available
for search.
An index/update handler endpoint should be specified through a URL string
or {url, headers, options} tuple.
A JSON "content-type" request header is required so that Solr knows the
incoming data format and can process data accordingly.
### Example
```
# Index handler for JSON-formatted update
headers = [{"content-type", "application/json"}]
endpoint = {"http://localhost:8983/solr/collection", headers}
Hui.commit(endpoint) # commits, make new docs available for search
Hui.commit(endpoint, false) # commits op only, new docs to be made available later
```
Use `t:Hui.Query.Update.t/0` struct for other types of commit and index optimisation, e.g. expunge deleted docs to
physically remove docs from the index, which could be a system-intensive operation.
"""
@spec commit(endpoint, boolean) :: http_response
def commit(endpoint, wait_searcher \\ true, client \\ @client) do
Http.post(endpoint, %Query.Update{commit: true, waitSearcher: wait_searcher}, true, client)
end
@doc """
Retrieves metrics data from the Solr admin API.
### Example
```
endpoint = {"http://localhost:8983/solr/admin/metrics", [{"content-type", "application/json"}]}
Hui.metrics(endpoint, group: "core", type: "timer", property: ["mean_ms", "max_ms", "p99_ms"])
```
"""
@spec metrics(endpoint, keyword) :: http_response
defdelegate metrics(endpoint, options), to: Hui.Admin
@doc """
Ping a given endpoint.
### Example
```
# ping a configured atomic endpoint
Hui.ping(:gettingstarted)
# directly ping a binary URL
Hui.ping("http://localhost:8983/solr/gettingstarted/admin/ping")
```
Successful ping returns a `{:pong, qtime}` tuple, whereas failure gets a `:pang` response.
"""
@spec ping(binary() | atom()) :: {:pong, integer} | :pang
defdelegate ping(endpoint), to: Hui.Admin
@doc """
Ping a given endpoint with options.
Raw HTTP response is returned when options such as `wt`, `distrib` is provided:
```
Hui.ping(:gettingstarted, wt: "xml", distrib: false)
# -> returns {:ok, %Hui.HTTP{body: "raw HTTP response", status: 200, ..}}
```
"""
@spec ping(binary() | atom(), keyword) :: http_response
defdelegate ping(endpoint, options), to: Hui.Admin
end