ArangoSearch view module
Defines a schema for a view to help create and manage Arango Search views.
Creation
Like for collections, the same static vs dynamic system is in place.
If in dynamic mode and a collection that a view links to doesn't exist when querying,
it will be created automatically. If in static mode an error will be raised if the the
appropriate migration has not been made. Check out the ArangoXEcto.Migration
module for
more info.
Like for collections, there is no check if a view exists when use the
ArangoXEcto.aql_query/4
function and will error, so make sure the view exists first.
Example
defmodule Something.UserSearch do
use ArangoXEcto.View
alias ArangoXEcto.View.Link
view "user_search" do
primary_sort :created_at, :desc
primary_sort :name
store_value [:email], :lz4
store_value [:first_name, :last_name], :none
link MyApp.Users, %Link{
includeAllFields: true,
fields: %{
name: %Link{
analyzers: [:text_en]
}
}
}
options [
primarySortCompression: :lz4
]
end
end
Defining the analyzer module
When in dynamic mode the analyzer module needs to be passed to the view so that
the analyzers will be automatically created. This option will be ignored in static mode.
You can pass the analyzer module by passing the module to the :analyzer_module
option on
the use statement.
use ArangoXEcto.View, analyzer_module: Something.Analyzers
Querying
Views essentially operate as virtual wrappers of Ecto Schemas. You can use them just like
other schemas to query on and all the fields available are a culmination of the fields on the
link schemas. This is how it works in the ArangoDB so to keep it true to function, it
functions the same here.
Querying is done exactly the same as a normal schema except the result will not be a struct. This is
explained further below.
iex> Repo.all(MyApp.UsersView)
[%{first_name: "John", last_name: "Smith"}, _]
Ecto Query Search
Since the Arango Search function heavily relies on the AQL SEARCH
operation it only makes
sense for this to also work in Ecto Queries. You can find more info about this under ArangoXEcto.Query.search/3
and ArangoXEcto.Query.or_search/3
.
The search operation functions the same as the Ecto.Query.where/3
clause so that can be used for more reference.
You can also use fragments to use special analyzers, an example is given below.
Note
You must import the ArangoXEcto.Query function to use the search query macro.
iex> MyApp.UsersView |> search(gender: :male) |> Repo.all()
[%{gender: :male}, ...]
iex> from(MyApp.UsersView) |> search([uv], uv.gender == :female) |> Repo.all()
[%{gender: :female}, ...]
iex> UsersView |> search([uv], fragment("ANALYZER(? == ?, "identity")", uv.first_name, "John")) |> Repo.all()
[%{first_name: "John"}]
Unfortunately this won't work as an argument to the Ecto Query from
macro (e.g. below) and will
only work as above.
# DO NOT USE THIS, IT WILL NOT WORK
iex> from(uv in MyApp.UsersView, search: uv.first_name == "John")
ArgumentError
Of course you can also use AQL queries. Noting that you need to ensure the view is created already
because it will not be checked.
iex> ArangoXEcto.aqlquery(Repo,
...> "FOR uv IN @@view SEARCH ANALYZER(uv.firstname == @first_name, "identity") RETURN uv",
...> "@view": UsersView.__view(:name),
...> first_name: "John"
...> )
{:ok, [%{"first_name" => "John"}]}
Sorting
Since views work with ecto queries as per usual, you can use functions such as sort in Ecto queries.
If you want to sort based on the BM25 score for example, you can use fragments like below.
iex> UsersView
...> |> search(gender: :male)
...> |> order_by([uv], fragment("BM25(?)", uv))
...> |> select([uv], {uv.first_name, fragment("BM25(?)", uv)})
[{"John", 1.8}, {"Bob", 1.8}]
Loading results
Due to how views function and the possibility of multiple types of returns, the result of a query
cannot be automatically loaded to a struct with Ecto so you will have to use the ArangoXEcto.load/2
function.
It does still load the values using the field loaders to the correct type, so if you don't need to load
it into a struct you can skip this.
iex> Repo.all(MyApp.UsersView) |> ArangoXEcto.load(MyApp.User)
[%User{first_name: "John", last_name: "Smith"}, _]
Since you can have multiple different schemas linked, the ArangoXEcto.load/2
function supports
passing multiple module options that will match against the arango _id
to load the correct module.
iex> Repo.all(MyApp.UsersView) |> ArangoXEcto.load([MyApp.User, MyApp.Post])
[%User{first_name: "John", last_name: "Smith"}, %Post{name: "abc"}]