From b13231a6801ff1b23739edb8c50d8febe07a2d0a Mon Sep 17 00:00:00 2001 From: jaeyson Date: Mon, 8 Jul 2024 14:38:00 +0800 Subject: [PATCH] fix parser to point on default_sorting_field --- .credo.exs | 217 ++++++++++++++++++++++++ .iex.exs | 1 + README.md | 6 + lib/ex_typesense.ex | 18 +- lib/ex_typesense/collection.ex | 20 ++- lib/ex_typesense/document.ex | 168 ++++++++++++++---- lib/ex_typesense/http_client.ex | 111 ------------ lib/ex_typesense/result_parser.ex | 28 ++- lib/ex_typesense/search.ex | 12 +- lib/ex_typesense/test_schema/catalog.ex | 12 +- lib/ex_typesense/test_schema/person.ex | 12 +- test/document_test.exs | 77 ++++++--- test/search_test.exs | 10 +- 13 files changed, 465 insertions(+), 227 deletions(-) create mode 100644 .credo.exs diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..784f814 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,217 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/*/lib/", + "apps/*/src/", + "apps/*/test/", + "apps/*/web/" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: false, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + {Credo.Check.Design.TagFIXME, []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, [max_nesting: 3]}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.WrongTestFileExtension, []} + ], + disabled: [ + # + # Checks scheduled for next check update (opt-in for now) + {Credo.Check.Refactor.UtcNowTruncate, []}, + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/.iex.exs b/.iex.exs index 115c75d..1ab90ce 100644 --- a/.iex.exs +++ b/.iex.exs @@ -6,3 +6,4 @@ alias ExTypesense.HttpClient alias ExTypesense.Search alias ExTypesense.TestSchema.Credential alias ExTypesense.TestSchema.Person +alias ExTypesense.TestSchema.Catalog diff --git a/README.md b/README.md index 876fc75..231fcf8 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,8 @@ conn = %{ scheme: "https" } +# NOTE: create a collection and import documents +# first before using the command below ExTypesense.search(conn, collection_name, query) ``` @@ -113,6 +115,8 @@ Or convert your struct to map, as long as the keys matches in `ExTypesense.Conne ```elixir conn = Map.from_struct(MyApp.Credential) +# NOTE: create a collection and import documents +# first before using the command below ExTypesense.search(conn, collection_name, query) ``` @@ -134,6 +138,8 @@ conn = |> Map.put(:host, conn.node) |> Map.put(:api_key, conn.secret_key) +# NOTE: create a collection and import documents +# first before using the command below ExTypesense.search(conn, collection_name, query) ``` diff --git a/lib/ex_typesense.ex b/lib/ex_typesense.ex index fb243a5..c6976eb 100644 --- a/lib/ex_typesense.ex +++ b/lib/ex_typesense.ex @@ -87,17 +87,11 @@ defmodule ExTypesense do to: ExTypesense.Document defdelegate create_document(conn \\ Connection.new(), document), to: ExTypesense.Document - # TODO: pass optional conn - defdelegate delete_document(document), to: ExTypesense.Document - # TODO: pass optional conn - defdelegate delete_document(collection_name, document_id), to: ExTypesense.Document - - defdelegate delete_all_documents( - conn \\ Connection.new(), - module_or_collection_name, - query \\ %{} - ), - to: ExTypesense.Document + defdelegate delete_document(conn \\ Connection.new(), document), to: ExTypesense.Document + defdelegate delete_documents_by_query(conn \\ Connection.new(), query), to: ExTypesense.Document + + defdelegate delete_all_documents(conn \\ Connection.new(), collection_name), + to: ExTypesense.Document defdelegate update_document(conn \\ Connection.new(), document), to: ExTypesense.Document defdelegate upsert_document(conn \\ Connection.new(), document), to: ExTypesense.Document @@ -112,7 +106,7 @@ defmodule ExTypesense do to: ExTypesense.Document # search - defdelegate search(conn \\ Connection.new(), module_or_collection_name, params), + defdelegate search(conn \\ Connection.new(), collection_name, params), to: ExTypesense.Search # geo search diff --git a/lib/ex_typesense/collection.ex b/lib/ex_typesense/collection.ex index 9abcd34..204063e 100644 --- a/lib/ex_typesense/collection.ex +++ b/lib/ex_typesense/collection.ex @@ -26,7 +26,11 @@ defmodule ExTypesense.Collection do :optional, :sort, :type, - :vec_dist + :vec_dist, + :store, + :reference, + :range_index, + :stem ] @typedoc since: "0.1.0" @@ -42,7 +46,11 @@ defmodule ExTypesense.Collection do optional: boolean(), sort: boolean(), type: field_type(), - vec_dist: String.t() + vec_dist: String.t(), + store: boolean(), + reference: String.t(), + range_index: boolean(), + stem: boolean() } @typedoc since: "0.1.0" @@ -62,6 +70,8 @@ defmodule ExTypesense.Collection do | :object | :"object[]" | :"string*" + | :image + | :auto end @collections_path "/collections" @@ -433,14 +443,12 @@ defmodule ExTypesense.Collection do collection = Map.new(collection, fn {k, v} -> if k === :fields do - Map.new(v, &to_atom/1) + Map.new(v, fn {k, v} -> {String.to_existing_atom(k), v} end) else - {String.to_atom(k), v} + {String.to_existing_atom(k), v} end end) struct(__MODULE__, collection) end - - defp to_atom({k, v}), do: {String.to_atom(k), v} end diff --git a/lib/ex_typesense/document.ex b/lib/ex_typesense/document.ex index 3f8b5eb..7d89b33 100644 --- a/lib/ex_typesense/document.ex +++ b/lib/ex_typesense/document.ex @@ -44,11 +44,11 @@ defmodule ExTypesense.Document do """ @doc since: "0.1.0" @spec get_document(Connection.t(), module() | String.t(), integer()) :: response() - def get_document(conn \\ Connection.new(), module_name, document_id) + def get_document(conn \\ Connection.new(), collection_name, document_id) - def get_document(conn, module_name, document_id) - when is_atom(module_name) and is_integer(document_id) do - collection_name = module_name.__schema__(:source) + def get_document(conn, collection_name, document_id) + when is_atom(collection_name) and is_integer(document_id) do + collection_name = collection_name.__schema__(:source) do_get_document(conn, collection_name, document_id) end @@ -372,21 +372,26 @@ defmodule ExTypesense.Document do end @doc """ - Deletes a document by struct. - """ - @doc since: "0.3.0" - # TODO: pass optional conn - @spec delete_document(struct()) :: response() - def delete_document(struct) when is_struct(struct) do - document_id = struct.id - collection_name = struct.__struct__.__schema__(:source) - do_delete_document(collection_name, document_id) - end + Deletes a document by id or struct. - @doc """ - Deletes a document by id. + > #### Deleting a document by id {: .info} + > + > If you are deleting by id, pass it as a tuple (`{"collection_name", 23}`) ## Examples + iex> ExTypesense.create_collection(Post) + iex> post = Post |> limit(1) |> Repo.one() + iex> ExTypesense.create_collection(post) + iex> ExTypesense.delete_document(post) + {:ok, + %{ + "id" => "1", + "post_id" => 1, + "title" => "our first post", + "collection_name" => "posts" + } + } + iex> schema = %{ ...> name: "posts", ...> fields: [ @@ -402,7 +407,7 @@ defmodule ExTypesense.Document do ...> title: "the quick brown fox" ...> } iex> ExTypesense.create_document(post) - iex> ExTypesense.delete_document("posts", 12) + iex> ExTypesense.delete_document({"posts", 12}) {:ok, %{ "id" => "12", @@ -413,27 +418,32 @@ defmodule ExTypesense.Document do } """ @doc since: "0.3.0" - # TODO: pass optional conn - @spec delete_document(String.t(), integer()) :: response() - def delete_document(collection_name, document_id) - when is_binary(collection_name) and is_integer(document_id) do - do_delete_document(collection_name, document_id) - end - - @doc since: "0.5.0" - @spec delete_all_documents(Connection.t(), module() | String.t(), map()) :: response() - def delete_all_documents(conn \\ Connection.new(), module_or_collection_name, query \\ %{}) + @spec delete_document(Connection.t(), struct() | {String.t(), integer()}) :: response() + def delete_document(conn \\ Connection.new(), struct_or_tuple) - def delete_all_documents(conn, collection_name, query) when is_binary(collection_name) do - %{error: %{message: "not implemented yet"}} + def delete_document(conn, struct) when is_struct(struct) do + # document_id = struct.id + collection_name = struct.__struct__.__schema__(:source) + # delete_document(conn, {collection_name, document_id}) + filter_by = + :virtual_fields + |> struct.__struct__.__schema__() + |> Enum.filter(&String.contains?(to_string(&1), "_id")) + |> Enum.map_join(" || ", fn virtual_field -> + value = Map.get(struct, virtual_field) + "#{virtual_field}:#{value}" + end) + + delete_documents_by_query(conn, collection_name, %{filter_by: filter_by}) end - def delete_all_documents(conn, module_name, query) when is_atom(module_name) do - %{error: %{message: "not implemented yet"}} + def delete_document(conn, {collection_name, document_id} = _tuple) + when is_binary(collection_name) and is_integer(document_id) do + do_delete_document(conn, collection_name, document_id) end - @deprecated "use do_delete_document/3" - defp do_delete_document(collection_name, document_id) do + @spec do_delete_document(Connection.t(), String.t(), integer()) :: response() + defp do_delete_document(conn, collection_name, document_id) do path = Path.join([ @collections_path, @@ -442,6 +452,96 @@ defmodule ExTypesense.Document do Jason.encode!(document_id) ]) - HttpClient.run(:delete, path) + opts = %{ + method: :delete, + path: path + } + + HttpClient.request(conn, opts) + end + + @doc """ + Deletes documents in a collection by query. + + > #### [Filter and batch size](https://typesense.org/docs/latest/api/documents.html#delete-by-query) {: .info} + > + > To delete all documents in a collection, you can use a filter that + > matches all documents in your collection. For eg, if you have an + > int32 field called popularity in your documents, you can use + > `filter_by: "popularity:>0"` to delete all documents. Or if you have a + > bool field called `in_stock` in your documents, you can use + > `filter_by: "in_stock:[true,false]"` to delete all documents. + > + > Use the `batch_size` to control the number of documents that should + > deleted at a time. A larger value will speed up deletions, but will + > impact performance of other operations running on the server. + > + > Filter parameters can be found here: https://typesense.org/docs/latest/api/search.html#filter-parameters + + ## Examples + iex> query = %{ + ...> filter_by: "num_employees:>100", + ...> batch_size: 100 + ...> } + iex> ExTypesense.delete_documents_by_query(query) + {:ok, %{}} + """ + @doc since: "0.5.0" + @spec delete_documents_by_query( + Connection.t(), + module() | String.t(), + %{ + filter_by: String.t(), + batch_size: integer() | nil + } + ) :: + response() + def delete_documents_by_query(conn \\ Connection.new(), collection_name, query) + + def delete_documents_by_query(conn, collection_name, %{filter_by: filter_by} = query) + when not is_nil(filter_by) and is_binary(filter_by) and is_atom(collection_name) do + path = Path.join([@collections_path, collection_name, @documents_path]) + HttpClient.request(conn, %{method: :delete, path: path, query: query}) + end + + def delete_documents_by_query(conn, collection_name, %{filter_by: filter_by} = query) + when not is_nil(filter_by) and is_binary(filter_by) and is_binary(collection_name) do + path = Path.join([@collections_path, collection_name, @documents_path]) + HttpClient.request(conn, %{method: :delete, path: path, query: query}) end + + @doc """ + Deletes all documents in a collection. + + > #### On using this function {: .info} + > As of this writing (v0.5.0), there's no built-in way of deleting + > all documents via [Typesense docs](https://github.com/typesense/typesense/issues/1613#issuecomment-1994986258). + > This function uses `delete_by_query` under the hood. + """ + @doc since: "0.5.0" + @spec delete_all_documents(Connection.t(), module() | String.t()) :: response() + def delete_all_documents(conn \\ Connection.new(), collection_name) + + def delete_all_documents(conn, collection_name) when is_binary(collection_name) do + delete_documents_by_query(conn, collection_name, %{filter_by: "#{collection_name}_id:>=0"}) + end + + def delete_all_documents(conn, collection_name) when is_atom(collection_name) do + name = collection_name.__schema__(:source) + + virtual_field = + :virtual_fields + |> collection_name.__schema__() + |> Enum.filter(&String.contains?(to_string(&1), "_id")) + |> hd() + |> to_string + + delete_documents_by_query(conn, name, %{filter_by: "#{virtual_field}:>0"}) + end + + # @spec do_delete_all_documents(Connection.t(), String.t()) :: response() + # defp do_delete_all_documents(conn, collection_name) do + # path = Path.join([ @collections_path, collection_name, @documents_path ]) + # HttpClient.request(conn, %{method: :delete, path: path}) + # end end diff --git a/lib/ex_typesense/http_client.ex b/lib/ex_typesense/http_client.ex index 138e129..7473e1c 100644 --- a/lib/ex_typesense/http_client.ex +++ b/lib/ex_typesense/http_client.ex @@ -15,8 +15,6 @@ defmodule ExTypesense.HttpClient do @typedoc since: "0.1.0" @type request_path() :: String.t() - @api_header_name ~c"X-TYPESENSE-API-KEY" - @doc since: "0.1.0" @spec get_host :: String.t() | nil def get_host, do: Application.get_env(:ex_typesense, :host) @@ -127,113 +125,4 @@ defmodule ExTypesense.HttpClient do {:error, Jason.decode!(response.body)["message"]} end end - - @doc """ - Req client. - - ## Examples - iex> HttpClient.run(:get, "/collections") - {:ok, - [%{ - "created_at" => 123456789, - "default_sorting_field" => "num_employees", - "fields" => [...], - "name" => "companies", - "num_documents" => 0, - "symbols_to_index" => [], - "token_separators" => [] - }] - } - """ - @doc since: "0.1.0" - @deprecated "Use request/2 instead" - @spec run(request_method(), request_path(), request_body(), map()) :: - {:ok, map()} | {:error, map()} - def run(request_method, request_path, body \\ nil, query \\ %{}) do - url = %URI{ - scheme: get_scheme() || "https", - host: get_host(), - port: get_port() || 443, - path: request_path, - query: URI.encode_query(query) - } - - response = - %Req.Request{ - body: body, - method: request_method, - url: url - } - |> Req.Request.put_header("x-typesense-api-key", api_key()) - |> Req.Request.append_error_steps(retry: &Req.Steps.retry/1) - |> Req.Steps.encode_body() - |> Req.Request.run!() - - case response.status in 200..299 do - true -> - {:ok, Jason.decode!(response.body)} - - false -> - {:error, Jason.decode!(response.body)["message"]} - end - end - - @doc since: "0.3.0" - @deprecated "Use request/2 instead" - @spec httpc_run(URI.t(), atom(), String.t(), list()) :: {:ok, map()} | {:error, map()} - def httpc_run(uri, method, payload, content_type \\ ~c"application/json") do - uri = %URI{ - scheme: get_scheme(), - host: get_host(), - port: get_port(), - path: uri.path, - query: uri.query - } - - api_key = String.to_charlist(api_key()) - - headers = [{@api_header_name, api_key}] - - request = { - URI.to_string(uri), - headers, - content_type, - payload - } - - :ok = :ssl.start() - - http_opts = [ - ssl: [ - {:versions, [:"tlsv1.2"]}, - verify: :verify_peer, - cacerts: :public_key.cacerts_get(), - customize_hostname_check: [ - match_fun: :public_key.pkix_verify_hostname_match_fun(:https) - ] - ], - timeout: 3_600, - connect_timeout: 3_600 - ] - - case :httpc.request(method, request, http_opts, []) do - {:ok, {_status_code, _headers, message}} -> - case Jason.decode(message) do - {:ok, message} -> - {:ok, message} - - {:error, %Jason.DecodeError{data: data}} -> - message = - data - |> String.split("\n", trim: true) - |> Stream.map(&Jason.decode!/1) - |> Enum.to_list() - - {:ok, message} - end - - {:error, reason} -> - {:error, reason} - end - end end diff --git a/lib/ex_typesense/result_parser.ex b/lib/ex_typesense/result_parser.ex index 1cd3417..64df763 100644 --- a/lib/ex_typesense/result_parser.ex +++ b/lib/ex_typesense/result_parser.ex @@ -5,25 +5,19 @@ defmodule ExTypesense.ResultParser do @spec hits_to_query(Enum.t(), module()) :: Ecto.Query.t() def hits_to_query(hits, module_name) do - case Enum.empty?(hits) do - true -> - module_name - |> where([i], i.id in []) + if Enum.empty?(hits) do + module_name + |> where([i], i.id in []) + else + primary_field = module_name.__schema__(:source) <> "_id" - false -> - # this assumes the fk pointing to primary, e.g. post_id - first_virtual_field = hd(module_name.__schema__(:virtual_fields)) + values = + Enum.map(hits, fn %{"document" => document} -> + document[primary_field] + end) - # this assumes the pk, e.g. posts' "id" that matches fk above - primary_key = hd(module_name.__schema__(:primary_key)) - - values = - Enum.map(hits, fn %{"document" => document} -> - document[to_string(first_virtual_field)] - end) - - module_name - |> where([i], field(i, ^primary_key) in ^values) + module_name + |> where([i], i.id in ^values) end end end diff --git a/lib/ex_typesense/search.ex b/lib/ex_typesense/search.ex index 1ae7412..cac3c81 100644 --- a/lib/ex_typesense/search.ex +++ b/lib/ex_typesense/search.ex @@ -41,23 +41,22 @@ defmodule ExTypesense.Search do """ @doc since: "0.1.0" @spec search(Connection.t(), module() | String.t(), map()) :: response() - def search(conn \\ Connection.new(), module_or_collection_name, params) + def search(conn \\ Connection.new(), collection_name, params) - def search(conn, module_name, params) when is_atom(module_name) and is_map(params) do - collection_name = module_name.__schema__(:source) + def search(conn, collection_name, params) when is_atom(collection_name) and is_map(params) do + collection = collection_name.__schema__(:source) path = Path.join([ @collections_path, - collection_name, + collection, @documents_path, @search_path ]) {:ok, result} = HttpClient.request(conn, %{method: :get, path: path, query: params}) - # {:ok, result} = HttpClient.run(:get, path, nil, params) - ResultParser.hits_to_query(result["hits"], module_name) + ResultParser.hits_to_query(result["hits"], collection_name) end def search(conn, collection_name, params) when is_binary(collection_name) and is_map(params) do @@ -70,6 +69,5 @@ defmodule ExTypesense.Search do ]) HttpClient.request(conn, %{method: :get, path: path, query: params}) - # HttpClient.run(:get, path, nil, params) end end diff --git a/lib/ex_typesense/test_schema/catalog.ex b/lib/ex_typesense/test_schema/catalog.ex index 03f28a9..5e12960 100644 --- a/lib/ex_typesense/test_schema/catalog.ex +++ b/lib/ex_typesense/test_schema/catalog.ex @@ -7,9 +7,9 @@ defmodule ExTypesense.TestSchema.Catalog do defimpl Jason.Encoder, for: __MODULE__ do def encode(value, opts) do value - |> Map.take([:catalog_id, :name, :description]) + |> Map.take([:catalogs_id, :name, :description]) |> Enum.map(fn {key, val} -> - if key === :catalog_id, do: {key, Map.get(value, :id)}, else: {key, val} + if key === :catalogs_id, do: {key, Map.get(value, :id)}, else: {key, val} end) |> Enum.into(%{}) |> Jason.Encode.map(opts) @@ -19,15 +19,17 @@ defmodule ExTypesense.TestSchema.Catalog do schema "catalogs" do field(:name, :string) field(:description, :string) - field(:catalog_id, :integer, virtual: true) + field(:catalogs_id, :integer, virtual: true) end @impl ExTypesense def get_field_types do + primary_field = __MODULE__.__schema__(:source) <> "_id" + %{ - default_sorting_field: "catalog_id", + default_sorting_field: primary_field, fields: [ - %{name: "catalog_id", type: "int32"}, + %{name: primary_field, type: "int32"}, %{name: "name", type: "string"}, %{name: "description", type: "string"} ] diff --git a/lib/ex_typesense/test_schema/person.ex b/lib/ex_typesense/test_schema/person.ex index db468e2..4a4e725 100644 --- a/lib/ex_typesense/test_schema/person.ex +++ b/lib/ex_typesense/test_schema/person.ex @@ -7,9 +7,9 @@ defmodule ExTypesense.TestSchema.Person do defimpl Jason.Encoder, for: __MODULE__ do def encode(value, opts) do value - |> Map.take([:person_id, :name, :country]) + |> Map.take([:persons_id, :name, :country]) |> Enum.map(fn {key, val} -> - if key === :person_id, do: {key, Map.get(value, :id)}, else: {key, val} + if key === :persons_id, do: {key, Map.get(value, :id)}, else: {key, val} end) |> Enum.into(%{}) |> Jason.Encode.map(opts) @@ -19,15 +19,17 @@ defmodule ExTypesense.TestSchema.Person do schema "persons" do field(:name, :string) field(:country, :string) - field(:person_id, :integer, virtual: true) + field(:persons_id, :integer, virtual: true) end @impl ExTypesense def get_field_types do + primary_field = __MODULE__.__schema__(:source) <> "_id" + %{ - default_sorting_field: "person_id", + default_sorting_field: primary_field, fields: [ - %{name: "person_id", type: "int32"}, + %{name: primary_field, type: "int32"}, %{name: "name", type: "string"}, %{name: "country", type: "string"} ] diff --git a/test/document_test.exs b/test/document_test.exs index 49c409c..6cb2733 100644 --- a/test/document_test.exs +++ b/test/document_test.exs @@ -8,16 +8,16 @@ defmodule DocumentTest do name: "doc_companies", fields: [ %{name: "company_name", type: "string"}, - %{name: "company_id", type: "int32"}, + %{name: "doc_companies_id", type: "int32"}, %{name: "country", type: "string"} ], - default_sorting_field: "company_id" + default_sorting_field: "doc_companies_id" } document = %{ collection_name: "doc_companies", company_name: "Test", - company_id: 1001, + doc_companies_id: 1001, country: "US" } @@ -26,21 +26,17 @@ defmodule DocumentTest do documents: [ %{ company_name: "Industrial Mills, Co.", - company_id: 990, + doc_companies_id: 990, country: "US" }, %{ company_name: "Washing Machine, Inc.", - company_id: 10, + doc_companies_id: 10, country: "US" } ] } - %{schema: schema, document: document, multiple_documents: multiple_documents} - end - - setup %{schema: schema} do ExTypesense.create_collection(schema) ExTypesense.create_collection(Person) @@ -49,7 +45,7 @@ defmodule DocumentTest do ExTypesense.drop_collection(Person) end) - :ok + %{schema: schema, document: document, multiple_documents: multiple_documents} end test "error: get unknown document", %{schema: schema} do @@ -117,13 +113,15 @@ defmodule DocumentTest do test "success: deletes a document using map", %{document: document} do {:ok, %{"id" => id}} = ExTypesense.create_document(document) - assert {:ok, _} = ExTypesense.delete_document(document.collection_name, String.to_integer(id)) + assert {:ok, _} = + ExTypesense.delete_document({document.collection_name, String.to_integer(id)}) end test "success: deletes a document by struct" do - person = %Person{id: 0, name: "John Smith", person_id: 0, country: "Brazil"} + person = %Person{id: 99, name: "John Smith", persons_id: 99, country: "Brazil"} - assert {:ok, %{"country" => "Brazil", "id" => "0", "name" => "John Smith", "person_id" => 0}} = + assert {:ok, + %{"country" => "Brazil", "id" => "1", "name" => "John Smith", "persons_id" => 99}} = ExTypesense.create_document(person) assert {:ok, _} = ExTypesense.delete_document(person) @@ -132,13 +130,13 @@ defmodule DocumentTest do test "success: deletes a document by id", %{document: document} do {:ok, %{"id" => id}} = ExTypesense.create_document(document) - assert {:ok, _} = ExTypesense.delete_document(document.collection_name, String.to_integer(id)) + assert {:ok, _} = + ExTypesense.delete_document({document.collection_name, String.to_integer(id)}) end test "success: index multiple documents", %{multiple_documents: multiple_documents} do - ExTypesense.index_multiple_documents(multiple_documents) - |> Kernel.===({:ok, [%{"success" => true}, %{"success" => true}]}) - |> assert() + assert {:ok, [%{"success" => true}, %{"success" => true}]} === + ExTypesense.index_multiple_documents(multiple_documents) end test "success: update multiple documents", %{ @@ -165,9 +163,8 @@ defmodule DocumentTest do multiple_documents = Map.put(multiple_documents, :documents, [update_1, update_2]) - ExTypesense.update_multiple_documents(multiple_documents) - |> Kernel.===({:ok, [%{"success" => true}, %{"success" => true}]}) - |> assert() + assert {:ok, [%{"success" => true}, %{"success" => true}]} === + ExTypesense.update_multiple_documents(multiple_documents) {:ok, first} = ExTypesense.get_document(schema.name, String.to_integer(first_id)) {:ok, second} = ExTypesense.get_document(schema.name, String.to_integer(second_id)) @@ -177,9 +174,8 @@ defmodule DocumentTest do end test "success: upsert multiple documents", %{multiple_documents: multiple_documents} do - ExTypesense.upsert_multiple_documents(multiple_documents) - |> Kernel.===({:ok, [%{"success" => true}, %{"success" => true}]}) - |> assert() + assert {:ok, [%{"success" => true}, %{"success" => true}]} === + ExTypesense.upsert_multiple_documents(multiple_documents) end test "error: upsert multiple documents with struct type" do @@ -192,7 +188,38 @@ defmodule DocumentTest do ExTypesense.upsert_multiple_documents(persons) end - test "success: delete all documents in a collection", %{schema: schema} do - assert %{ok: %{}} == ExTypesense.delete_all_documents(schema.name) + test "success: delete all documents using Ecto schema module" do + person = %Person{id: 1, name: "John Doe", persons_id: 1, country: "Scotland"} + + assert {:ok, %{"country" => "Scotland", "id" => "0", "name" => "John Doe", "persons_id" => 1}} = + ExTypesense.create_document(person) + + assert {:ok, %{"num_deleted" => 1}} == ExTypesense.delete_all_documents(Person) + end + + test "success: delete all documents in a collection" do + multiple_documents = %{ + collection_name: "doc_companies", + documents: [ + %{ + company_name: "Boca Cola", + doc_companies_id: 227, + country: "SG" + }, + %{ + company_name: "Motor, Inc.", + doc_companies_id: 99, + country: "TH" + } + ] + } + + assert {:ok, [%{"success" => true}, %{"success" => true}]} === + ExTypesense.index_multiple_documents(multiple_documents) + + {:ok, %{"num_deleted" => documents_deleted}} = + ExTypesense.delete_all_documents(multiple_documents.collection_name) + + assert documents_deleted > 0 end end diff --git a/test/search_test.exs b/test/search_test.exs index a5577ef..39c26d2 100644 --- a/test/search_test.exs +++ b/test/search_test.exs @@ -9,16 +9,16 @@ defmodule SearchTest do name: "search_companies", fields: [ %{name: "company_name", type: "string"}, - %{name: "company_id", type: "int32"}, + %{name: "search_companies_id", type: "int32"}, %{name: "country", type: "string"} ], - default_sorting_field: "company_id" + default_sorting_field: "search_companies_id" } document = %{ collection_name: "search_companies", company_name: "Test", - company_id: 1001, + search_companies_id: 1001, country: "US" } @@ -26,7 +26,7 @@ defmodule SearchTest do id: 1002, name: "Rubber Ducky", description: "A tool by articulating a problem in spoken or written natural language.", - catalog_id: 1002 + catalogs_id: 1002 } with %ExTypesense.Collection{} <- ExTypesense.create_collection(schema) do @@ -60,7 +60,7 @@ defmodule SearchTest do query_by: "name" } - assert %Ecto.Query{} = Catalog |> where([p], p.id in ^[catalog.catalog_id]) + assert %Ecto.Query{} = Catalog |> where([p], p.id in ^[catalog.catalogs_id]) assert %Ecto.Query{} = ExTypesense.search(Catalog, params) end