Current section

Files

Jump to
lastfm_archive lib load.ex
Raw

lib/load.ex

defmodule LastfmArchive.Load do
@moduledoc """
This module provides functions for loading Lastfm data into databases and search engines.
"""
@default_data_dir "./lastfm_data/"
@doc """
Ping a Solr core/collection endpoint to check if it is running.
The endpoint can either be a URL string or an atom referring to an endpoint in configuration.
The library uses `Hui` to interact with Solr, an endpoint can be specified as below:
### Example
```
LastfmArchive.Load.ping_solr("http://solr_url...")
LastfmArchive.Load.ping_solr(:lastfm_archive) # check a configured endpoint
```
`:lastfm_archive` refers to the following Solr update endpoint in configuration:
```
config :hui, :lastfm_archive,
url: "http://solr_url..",
handler: "update",
headers: [{"Content-type", "application/json"}]
```
See `Hui.URL` module for more details.
"""
@spec ping_solr(binary|atom) :: {:ok, map} | {:error, Hui.Error.t}
def ping_solr(url) when is_atom(url), do: Application.get_env(:hui, url)[:url] |> ping_solr
def ping_solr(url) when is_binary(url) do
response = HTTPoison.get url <> "/admin/ping"
case response do
{:ok, resp} ->
if resp.status_code == 200, do: {:ok, resp.body |> Poison.decode!}, else: {:error, %Hui.Error{reason: :einval}}
{:error, %HTTPoison.Error{id: _, reason: reason}} ->
{:error, %Hui.Error{reason: reason}}
end
end
@doc """
Check a Solr core/collection to ensure it has the required Lastfm data fields.
The check currently inspects Solr schema for a list of Lastfm fields
and returns error if one or more of the fields are missing.
See `LastfmArchive.Transform.transform/3` for the list of fields.
### Example
```
LastfmArchive.Load.check_solr_schema("http://solr_url...")
LastfmArchive.Load.check_solr_schema(:lastfm_archive) # ping a configured endpoint
```
See `ping_solr/1` for more details on URL configuration.
"""
@spec check_solr_schema(binary|atom) :: {:ok, map} | {:error, Hui.Error.t}
def check_solr_schema(url) when is_atom(url) and url != nil, do: Application.get_env(:hui, url)[:url] |> check_solr_schema
def check_solr_schema(url) when is_binary(url), do: solr_schema(url) |> check_solr_schema
def check_solr_schema(nil), do: {:error, %Hui.Error{reason: :ehostunreach}}
def check_solr_schema({:error, error}), do: {:error, error}
def check_solr_schema({:ok, schema_data}) do
schema = schema_data["schema"]
fields = schema["fields"] |> Enum.map(&(&1["name"]))
{:ok, fields_s} = File.read("./solr/fields.json")
expected_fields = fields_s |> Poison.decode!
# simple check if field exists, no type checking for the time being
missing_fields = for {field, _type} <- expected_fields, do: unless Enum.member?(fields, field), do: field
missing_fields = missing_fields |> Enum.uniq |> List.delete(nil)
if length(missing_fields) > 0 do
{:error, %Hui.Error{reason: :einit}}
else
{:ok, expected_fields}
end
end
defp solr_schema(url) do
response = HTTPoison.get url <> "/schema"
case response do
{:ok, resp} ->
if resp.status_code == 200, do: {:ok, resp.body |> Poison.decode!}, else: {:error, %Hui.Error{reason: :ehostunreach}}
{:error, %HTTPoison.Error{id: _, reason: reason}} ->
{:error, %Hui.Error{reason: reason}}
end
end
@doc """
Load a TSV file data from the archive into Solr for a Lastfm user.
The function reads and converts scrobbles in a TSV file from the file
archive into a list of maps. The maps are sent to Solr for ingestion.
Use `t:Hui.URL.t/0` struct to specify the Solr endpoint.
### Example
```
# define a Solr endpoint with %Hui.URL{} struct
headers = [{"Content-type", "application/json"}]
url = %Hui.URL{url: "http://localhost:8983/solr/lastfm_archive", handler: "update", headers: headers}
# ingest data scrobbled in 2018
LastfmArchive.Load.load_solr(url, "a_lastfm_user", "tsv/2018.tsv.gz")
```
TSV files must be pre-created by transforming raw JSON Lastfm data - see
`LastfmArchive.transform_archive/2`.
"""
@spec load_solr(Hui.URL.t, binary, binary) :: {:ok, HTTPoison.Response.t()} | {:error, :enoent}
def load_solr(url, user, filename) do
{status, resp} = read(user, filename)
case status do
:ok ->
[header | scrobbles] = resp
solr_docs = for scrobble <- scrobbles, scrobble != "" do
field_names = header |> String.split("\t")
scrobble_data = scrobble |> String.split("\t")
map_fields(field_names, scrobble_data, []) |> Enum.into(%{})
end
Hui.update(url, solr_docs)
:error ->
{:error, resp}
end
end
defp map_fields(_, [], acc), do: acc
defp map_fields([field_name|field_names], [data | rest_of_data], acc) do
map_fields(field_names, rest_of_data, acc ++ [{field_name, data}])
end
@doc """
Read and parse a TSV file from the archive for a Lastfm user.
TSV files generated by transforming raw JSON Lastfm data - see
`LastfmArchive.transform_archive/2`. The file is parsed
into a list of scrobbles.
### Example
```
LastfmArchive.Load.read "a_lastfm_user", "tsv/2007.tsv.gz"
```
"""
# need to consolidate this with read functions from other modules,
# e.g. into a generic read function under a separate IO module
@spec read(binary, binary) :: {:ok, list(binary)} | {:error, :file.posix}
def read(user, filename) do
data_dir = Application.get_env(:lastfm_archive, :data_dir) || @default_data_dir
user_data_dir = Path.join data_dir, user
file_path = Path.join user_data_dir, filename
{status, file_io} = File.open(file_path, [:read, :compressed, :utf8])
resp = case status do
:ok ->
{:ok, IO.read(file_io, :all) |> String.split("\n")}
:error ->
{:error, file_io}
end
if is_pid(file_io), do: File.close(file_io)
resp
end
end