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/CHANGELOG.md b/CHANGELOG.md index be8df2e..fc697e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.5.0 (2024.07.13) + +### Changed + +* `README` regarding `default_sorting_field`, where it joins the table name with "_id" (e.g. `images` is `images_id` instead of `image_id`). + +### Added + +* Function: [delete by query](https://typesense.org/docs/26.0/api/documents.html#delete-by-query). +* Function: [delete all documents](https://github.com/typesense/typesense/issues/1613#issuecomment-1994986258) in a collection. +* Collection's schema [field parameters](https://typesense.org/docs/26.0/api/collections.html#field-parameters): + - `:vec_dist` + - `:store` + - `:reference` + - `:range_index` + - `:stem` + +### Removed + +- `HttpClient.run` and `HttpClient.httpc_run` function (use `HttpClient.request`). + ## 0.4.3 (2024.07.03) ### Changed diff --git a/README.md b/README.md index 876fc75..1ae8bc8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Typesense client for Elixir with support for your Ecto schemas. -> **Note**: Breaking changes if you're upgrading from `0.3.x` to upcoming `0.5.x` version. +> **Note**: Breaking changes if you're upgrading from `0.3.x` to `0.5.x` version. ## Todo @@ -28,7 +28,7 @@ Add `:ex_typesense` to your list of dependencies in the Elixir project config fi def deps do [ # From default Hex package manager - {:ex_typesense, "~> 0.4"} + {:ex_typesense, "~> 0.5"} # Or from GitHub repository, if you want to latest greatest from main branch {:ex_typesense, git: "https://github.com/jaeyson/ex_typesense.git"} @@ -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,8 +115,9 @@ 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) - ``` Or you don't want to change the fields in your Ecto schema, thus you convert it to map: @@ -134,6 +137,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) ``` @@ -143,7 +148,10 @@ There are 2 ways to create a collection, either via [Ecto schema](https://hexdoc #### Option 1: using Ecto -In this example, we're adding `person_id` that points to the id of `persons` schema. +In this example, we're adding `persons_id` that points to the id of `persons` schema. + +> **Note**: we're using `_id`. If you have table +> e.g. named `persons`, it'll be `persons_id`. ```elixir defmodule Person do @@ -153,11 +161,11 @@ defmodule Person do defimpl Jason.Encoder, for: __MODULE__ do def encode(value, opts) do value - |> Map.take([:id, :person_id, :name, :country]) + |> Map.take([:id, :persons_id, :name, :country]) |> Enum.map(fn {key, val} -> cond do key === :id -> {key, to_string(Map.get(value, :id))} - key === :person_id -> {key, Map.get(value, :id)} + key === :persons_id -> {key, Map.get(value, :id)} true -> {key, val} end end) @@ -169,15 +177,18 @@ defmodule 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", + # Or might as well just write persons_id instead. Up to you. + default_sorting_field: primary_field, fields: [ - %{name: "person_id", type: "int32"}, + %{name: primary_field, type: "int32"}, %{name: "name", type: "string"}, %{name: "country", type: "string"} ] @@ -199,10 +210,10 @@ schema = %{ name: "companies", fields: [ %{name: "company_name", type: "string"}, - %{name: "company_id", type: "int32"}, + %{name: "companies_id", type: "int32"}, %{name: "country", type: "string"} ], - default_sorting_field: "company_id" + default_sorting_field: "companies_id" } ExTypesense.create_collection(schema) diff --git a/guides/cheatsheet.cheatmd b/guides/cheatsheet.cheatmd index 4c8b867..d9c2721 100644 --- a/guides/cheatsheet.cheatmd +++ b/guides/cheatsheet.cheatmd @@ -15,9 +15,9 @@ defmodule MyApp.Listings.Company do defimpl Jason.Encoder, for: __MODULE__ do def encode(value, opts) do value - |> Map.take([:company_id, :name, :country]) + |> Map.take([:companies_id, :name, :country]) |> Enum.map(fn {key, val} -> - if key === :company_id, do: {key, Map.get(value, :id)}, else: {key, val} + if key === :companies_id, do: {key, Map.get(value, :id)}, else: {key, val} end) |> Enum.into(%{}) |> Jason.Encode.map(opts) @@ -27,15 +27,17 @@ defmodule MyApp.Listings.Company do schema "companies" do field(:name, :string) field(:country, :string) - field(:company_id, :integer, virtual: true) + field(:companies_id, :integer, virtual: true) end @impl ExTypesense def get_field_types do + primary_field = __MODULE__.__schema__(:source) <> "_id" + %{ - default_sorting_field: "company_id", + default_sorting_field: primary_field, fields: [ - %{name: "company_id", type: "int32"}, + %{name: primary_field, type: "int32"}, %{name: "name", type: "string"}, %{name: "country", type: "string"} ] @@ -48,7 +50,7 @@ iex> ExTypesense.create_collection(Company) {:ok, %ExTypesense.Collection{ "created_at" => 1234567890, - "default_sorting_field" => "company_id", + "default_sorting_field" => "companies_id", "fields" => [...], "name" => "companies", "num_documents" => 0, @@ -67,17 +69,17 @@ iex> schema = ...> name: "companies", ...> fields: [ ...> %{name: "company_name", type: "string"}, -...> %{name: "num_employees", type: "int32"}, +...> %{name: "companies_id", type: "int32"}, ...> %{name: "country", type: "string", facet: true} ...> ], -...> default_sorting_field: "num_employees" +...> default_sorting_field: "companies_id" ...> } iex> ExTypesense.create_collection(schema) {:ok, %ExTypesense.Collection{ "created_at" => 1234567890, - "default_sorting_field" => "num_employees", + "default_sorting_field" => "companies_id", "fields" => [...], "name" => "companies", "num_documents" => 0, @@ -110,7 +112,7 @@ iex> ExTypesense.update_collection("companies", schema) %ExTypesense.Collection{ "created_at" => nil, "name" => nil, - "default_sorting_field" => nil, + "default_sorting_field" => "companies_id", "fields" => [...], "num_documents" => 0, "symbols_to_index" => [], @@ -133,7 +135,7 @@ iex> ExTypesense.create_document(post, :create) {:ok, %{ "id" => "12", - "post_id" => 12, + "posts_id" => 12, "title" => "the quick brown fox", "collection_name" => "posts" } @@ -152,7 +154,7 @@ iex> ExTypesense.update_document(post, 0) %{ "id" => "0", "collection_name" => "posts", - "post_id" => 34, + "posts_id" => 34, "title" => "test", "description" => "lorem ipsum" } @@ -170,7 +172,7 @@ iex> ExTypesense.delete_document(Post, 0) %{ "id" => "0", "collection_name" => "posts", - "post_id" => 34, + "posts_id" => 34, "title" => "test", "description" => "lorem ipsum" } @@ -179,6 +181,17 @@ iex> ExTypesense.delete_document(Post, 0) {: .wrap} +### Delete a document by query + +```elixir +iex> query = %{ +...> filter_by: "num_employees:>100", +...> batch_size: 100 +...> } +iex> ExTypesense.delete_documents_by_query(Employee, query) +{:ok, %{}} +``` + ### Indexes multiple documents ```elixir @@ -199,7 +212,7 @@ iex> ExTypesense.search(Post, params) %{ "id" => "0", "collection_name" => "posts", - "post_id" => 34, + "posts_id" => 34, "title" => "test", "description" => "lorem ipsum" } diff --git a/lib/ex_typesense.ex b/lib/ex_typesense.ex index 444b56f..2afe415 100644 --- a/lib/ex_typesense.ex +++ b/lib/ex_typesense.ex @@ -14,11 +14,11 @@ defmodule ExTypesense do defimpl Jason.Encoder, for: __MODULE__ do def encode(value, opts) do value - |> Map.take([:id, :person_id, :name, :age]) + |> Map.take([:id, :persons_id, :name, :age]) |> Enum.map(fn {key, val} -> cond do key === :id -> {key, to_string(Map.get(value, :id))} - key === :person_id -> {key, Map.get(value, :id)} + key === :persons_id -> {key, Map.get(value, :id)} true -> {key, val} end end) @@ -30,16 +30,18 @@ defmodule ExTypesense do schema "persons" do field :name, :string field :age, :integer - 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: "age", type: "integer"} ] @@ -87,8 +89,12 @@ defmodule ExTypesense do to: ExTypesense.Document defdelegate create_document(conn \\ Connection.new(), document), to: ExTypesense.Document - defdelegate delete_document(document), to: ExTypesense.Document - defdelegate delete_document(collection_name, document_id), 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 @@ -102,7 +108,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..dd5fe20 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" @@ -86,6 +96,7 @@ defmodule ExTypesense.Collection do symbols_to_index: [] ] + @typedoc since: "0.1.0" @type t() :: %__MODULE__{ created_at: integer(), name: String.t(), @@ -164,15 +175,15 @@ defmodule ExTypesense.Collection do ...> name: "companies", ...> fields: [ ...> %{name: "company_name", type: "string"}, - ...> %{name: "company_id", type: "int32"}, + ...> %{name: "companies_id", type: "int32"}, ...> %{name: "country", type: "string", facet: true} ...> ], - ...> default_sorting_field: "company_id" + ...> default_sorting_field: "companies_id" ...> } iex> ExTypesense.create_collection(schema) %ExTypesense.Collection{ created_at: 1234567890, - default_sorting_field: "company_id", + default_sorting_field: "companies_id", fields: [...], name: "companies", num_documents: 0, @@ -183,7 +194,7 @@ defmodule ExTypesense.Collection do iex> ExTypesense.create_collection(Person) %ExTypesense.Collection{ created_at: 1234567890, - default_sorting_field: "person_id", + default_sorting_field: "persons_id", fields: [...], name: "persons", num_documents: 0, @@ -244,18 +255,7 @@ defmodule ExTypesense.Collection do %ExTypesense.Collection{ created_at: 1234567890, name: companies, - default_sorting_field: "company_id", - fields: [...], - num_documents: 0, - symbols_to_index: [], - token_separators: [] - } - - iex> ExTypesense.update_collection_fields(Company, fields) - %ExTypesense.Collection{ - created_at: 1234567890, - name: companies, - default_sorting_field: "company_id", + default_sorting_field: "companies_id", fields: [...], num_documents: 0, symbols_to_index: [], @@ -294,7 +294,8 @@ defmodule ExTypesense.Collection do @doc """ Permanently drops a collection by collection name or module name. - **Note**: dropping a collection does not remove the referenced alias, only the indexed documents. + **Note**: dropping a collection does not remove the referenced + alias, only the indexed documents. """ @doc since: "0.1.0" @spec drop_collection(Connection.t(), name :: String.t() | module()) :: response() @@ -433,14 +434,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 268acbf..c5e9608 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 @@ -235,7 +235,7 @@ defmodule ExTypesense.Document do ...> %{ ...> id: "34", ...> collection_name: "posts", - ...> post_id: 34, + ...> posts_id: 34, ...> title: "the quick brown fox", ...> description: "jumps over the lazy dog" ...> } @@ -244,7 +244,7 @@ defmodule ExTypesense.Document do %{ "id" => "34", "collection_name" => "posts", - "post_id" => 34, + "posts_id" => 34, "title" => "the quick brown fox", "description" => "jumps over the lazy dog" } @@ -284,7 +284,7 @@ defmodule ExTypesense.Document do ...> %{ ...> id: "94", ...> collection_name: "posts", - ...> post_id: 94, + ...> posts_id: 94, ...> title: "the quick brown fox" ...> } iex> ExTypesense.create_document(post) @@ -292,7 +292,7 @@ defmodule ExTypesense.Document do ...> %{ ...> id: "94", ...> collection_name: "posts", - ...> post_id: 94, + ...> posts_id: 94, ...> title: "test" ...> } iex> ExTypesense.update_document(updated_post) @@ -300,8 +300,8 @@ defmodule ExTypesense.Document do %{ "id" => "94", "collection_name" => "posts", - "post_id" => 94, - "title" => "test" + "posts_id" => 94, + "title" => "sample post" } } """ @@ -372,20 +372,26 @@ defmodule ExTypesense.Document do end @doc """ - Deletes a document by struct. - """ - @doc since: "0.3.0" - @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", + "posts_id" => 1, + "title" => "our first post", + "collection_name" => "posts" + } + } + iex> schema = %{ ...> name: "posts", ...> fields: [ @@ -397,29 +403,47 @@ defmodule ExTypesense.Document do ...> %{ ...> id: "12", ...> collection_name: "posts", - ...> post_id: 22, + ...> posts_id: 22, ...> 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", - "post_id" => 22, + "posts_id" => 22, "title" => "the quick brown fox", "collection_name" => "posts" } } """ @doc since: "0.3.0" - @spec delete_document(String.t(), integer()) :: response() - def delete_document(collection_name, document_id) + @spec delete_document(Connection.t(), struct() | {String.t(), integer()}) :: response() + def delete_document(conn \\ Connection.new(), struct_or_tuple) + + 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_document(conn, {collection_name, document_id} = _tuple) when is_binary(collection_name) and is_integer(document_id) do - do_delete_document(collection_name, document_id) + 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, @@ -428,6 +452,97 @@ 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(Employee, 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 + collection_name = collection_name.__schema__(:source) + 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..684f598 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) @@ -40,6 +38,7 @@ defmodule ExTypesense.HttpClient do > function will still return the key and accessible inside > shell (assuming bad actors [pun unintended `:/`] can get in as well). """ + @doc since: "0.1.0" @spec api_key :: String.t() | nil def api_key, do: Application.get_env(:ex_typesense, :api_key) @@ -127,113 +126,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..55e7357 100644 --- a/lib/ex_typesense/result_parser.ex +++ b/lib/ex_typesense/result_parser.ex @@ -3,27 +3,22 @@ defmodule ExTypesense.ResultParser do @moduledoc false + @doc since: "0.1.0" @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..08e5f0d 100644 --- a/lib/ex_typesense/search.ex +++ b/lib/ex_typesense/search.ex @@ -13,6 +13,8 @@ defmodule ExTypesense.Search do @collections_path @root_path <> "collections" @documents_path "documents" @search_path "search" + + @typedoc since: "0.1.0" @type response :: Ecto.Query.t() | {:ok, map()} | {:error, map()} @doc """ @@ -41,23 +43,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 +71,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/lib/ex_typesense/test_schema/product.ex b/lib/ex_typesense/test_schema/product.ex new file mode 100644 index 0000000..3b8c0ff --- /dev/null +++ b/lib/ex_typesense/test_schema/product.ex @@ -0,0 +1,38 @@ +defmodule ExTypesense.TestSchema.Product do + use Ecto.Schema + @behaviour ExTypesense + + @moduledoc false + + defimpl Jason.Encoder, for: __MODULE__ do + def encode(value, opts) do + value + |> Map.take([:poducts_id, :name, :description]) + |> Enum.map(fn {key, val} -> + if key === :products_id, do: {key, Map.get(value, :id)}, else: {key, val} + end) + |> Enum.into(%{}) + |> Jason.Encode.map(opts) + end + end + + schema "products" do + field(:name, :string) + field(:description, :string) + field(:products_id, :integer, virtual: true) + end + + @impl ExTypesense + def get_field_types do + primary_field = __MODULE__.__schema__(:source) <> "_id" + + %{ + default_sorting_field: primary_field, + fields: [ + %{name: primary_field, type: "int32"}, + %{name: "name", type: "string"}, + %{name: "description", type: "string"} + ] + } + end +end diff --git a/mix.exs b/mix.exs index edebc77..d48df7f 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule ExTypesense.MixProject do use Mix.Project @source_url "https://github.com/jaeyson/ex_typesense" - @version "0.4.3" + @version "0.5.0" def project do [ diff --git a/test/collection_test.exs b/test/collection_test.exs index 836e3bd..91a1f7a 100644 --- a/test/collection_test.exs +++ b/test/collection_test.exs @@ -1,6 +1,8 @@ defmodule CollectionTest do use ExUnit.Case, async: true + alias ExTypesense.TestSchema.Product + setup_all do schema = %{ name: "collection_companies", @@ -14,22 +16,66 @@ defmodule CollectionTest do on_exit(fn -> ExTypesense.drop_collection(schema.name) + ExTypesense.drop_collection(Product) end) %{schema: schema} end test "success: create and drop collection", %{schema: schema} do - collection = ExTypesense.create_collection(schema) - assert %ExTypesense.Collection{} = collection + products = ExTypesense.create_collection(Product) + collection_companies = ExTypesense.create_collection(schema) + + assert %ExTypesense.Collection{} = products + assert %ExTypesense.Collection{} = collection_companies + + ExTypesense.drop_collection(Product) + assert {:error, "Not Found"} === ExTypesense.get_collection(Product) ExTypesense.drop_collection(schema.name) assert {:error, "Not Found"} === ExTypesense.get_collection(schema.name) end - test "error: dropping unknown collection", %{schema: schema} do - message = ~s(No collection with name `#{schema.name}` found.) - assert {:error, message} === ExTypesense.drop_collection(schema.name) + test "success: dropping collection won't affect alias", %{schema: schema} do + assert %ExTypesense.Collection{} = ExTypesense.create_collection(Product) + assert %ExTypesense.Collection{} = ExTypesense.create_collection(schema) + + assert %{"collection_name" => _collection_name, "name" => alias} = + ExTypesense.upsert_collection_alias(schema.name <> "_alias", schema.name) + + ExTypesense.drop_collection(schema.name) + ExTypesense.drop_collection(Product) + + assert {:error, "Not Found"} === ExTypesense.get_collection(schema.name) + assert {:error, "Not Found"} === ExTypesense.get_collection(Product) + + assert %{"collection_name" => _collection_name, "name" => _alias} = + ExTypesense.get_collection_alias(alias) + + assert [%{"collection_name" => _collection_name, "name" => _alias}] = + ExTypesense.list_collection_aliases() + end + + test "success: dropping collection deletes all documents", %{schema: schema} do + ExTypesense.create_collection(schema) + + multiple_documents = %{ + collection_name: "collection_companies", + documents: [ + %{company_name: "Noogle, Inc.", company_id: 56, country: "AO"}, + %{company_name: "Tikipedia", company_id: 62, country: "BD"} + ] + } + + assert {:ok, _} = ExTypesense.upsert_multiple_documents(multiple_documents) + assert %ExTypesense.Collection{} = ExTypesense.drop_collection(schema.name) + assert {:error, "Not Found"} = ExTypesense.get_document(schema.name, 1) + end + + test "error: dropping unknown collection" do + collection_name = "unknown" + message = ~s(No collection with name `#{collection_name}` found.) + assert {:error, message} === ExTypesense.drop_collection(collection_name) end test "success: get specific collection" do @@ -48,8 +94,8 @@ defmodule CollectionTest do ExTypesense.drop_collection(schema.name) end - test "error: get unknown collection", %{schema: schema} do - assert {:error, "Not Found"} === ExTypesense.get_collection(schema.name) + test "error: get unknown collection" do + assert {:error, "Not Found"} === ExTypesense.get_collection("unknown_collection_name") end test "success: update schema fields", %{schema: schema} do @@ -84,46 +130,35 @@ defmodule CollectionTest do end test "success: list aliases", %{schema: schema} do - ExTypesense.upsert_collection_alias(schema.name, schema.name) - count = length(ExTypesense.list_collection_aliases()) - assert count === 1 + assert %{"collection_name" => _collection_name, "name" => alias} = + ExTypesense.upsert_collection_alias(schema.name <> "_alias", schema.name) - ExTypesense.delete_collection_alias(schema.name) - end + refute Enum.empty?(ExTypesense.list_collection_aliases()) - test "success: create and delete alias", %{schema: schema} do - collection_alias = ExTypesense.upsert_collection_alias(schema.name, schema.name) - assert is_map(collection_alias) === true - assert Enum.empty?(collection_alias) === false + assert %{"collection_name" => _collection_name, "name" => _alias} = + ExTypesense.delete_collection_alias(alias) - ExTypesense.delete_collection_alias(schema.name) - assert {:error, "Not Found"} === ExTypesense.get_collection_alias(schema.name) + assert Enum.empty?(ExTypesense.list_collection_aliases()) end - test "success: get collection name by alias", %{schema: schema} do - %{"collection_name" => collection_name, "name" => collection_alias} = - ExTypesense.upsert_collection_alias(schema.name, schema.name) - - assert collection_name === ExTypesense.get_collection_name(collection_alias) - - ExTypesense.delete_collection_alias(schema.name) - end + test "success: create, get, delete alias", %{schema: schema} do + assert %{"collection_name" => _collection_name, "name" => alias} = + ExTypesense.upsert_collection_alias(schema.name <> "_alias", schema.name) - test "success: get specific alias", %{schema: schema} do - ExTypesense.upsert_collection_alias(schema.name, schema.name) - collection_alias = ExTypesense.get_collection_alias(schema.name) + assert %{"collection_name" => _collection_name, "name" => alias} = + ExTypesense.get_collection_alias(alias) - assert is_map(collection_alias) - assert collection_alias["name"] === schema.name + assert %{"collection_name" => _collection_name, "name" => alias} = + ExTypesense.delete_collection_alias(alias) - ExTypesense.delete_collection_alias(schema.name) + assert {:error, "Not Found"} === ExTypesense.get_collection_alias(alias) end - test "error: get unknown alias", %{schema: schema} do - assert {:error, "Not Found"} === ExTypesense.get_collection_alias(schema.name) + test "error: get unknown alias" do + assert {:error, "Not Found"} === ExTypesense.get_collection_alias("unknown_alias") end - test "error: delete unknown alias", %{schema: schema} do - assert {:error, "Not Found"} === ExTypesense.delete_collection_alias(schema.name) + test "error: delete unknown alias" do + assert {:error, "Not Found"} === ExTypesense.delete_collection_alias("unknown_alias") end end diff --git a/test/document_test.exs b/test/document_test.exs index ef63716..4d15c6f 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,6 +45,18 @@ defmodule DocumentTest do ExTypesense.drop_collection(Person) end) + %{schema: schema, document: document, multiple_documents: multiple_documents} + end + + setup %{multiple_documents: multiple_documents} do + assert {:ok, %{"num_deleted" => _}} = + ExTypesense.delete_documents_by_query(multiple_documents.collection_name, %{ + filter_by: "doc_companies_id:>=0" + }) + + assert {:ok, %{"num_deleted" => _}} = + ExTypesense.delete_documents_by_query(Person, %{filter_by: "persons_id:>=0"}) + :ok end @@ -117,13 +125,14 @@ 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" => _, "name" => "John Smith", "persons_id" => 99}} = ExTypesense.create_document(person) assert {:ok, _} = ExTypesense.delete_document(person) @@ -132,13 +141,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 +174,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 +185,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 @@ -191,4 +198,108 @@ defmodule DocumentTest do assert {:error, ~s(It should be type of map, ":documents" key should contain list of maps)} === ExTypesense.upsert_multiple_documents(persons) end + + 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" => _, "name" => "John Doe", "persons_id" => 1}} = + ExTypesense.create_document(person) + + assert {:ok, %{"num_deleted" => 1}} == ExTypesense.delete_all_documents(Person) + end + + test "success: deleting all documents won't drop the 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 + + assert %ExTypesense.Collection{name: "doc_companies"} = + ExTypesense.get_collection(multiple_documents.collection_name) + 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 + + test "success: delete documents by query (Ecto schema)" do + john_toe = %Person{id: 32, name: "John Toe", persons_id: 32, country: "Egypt"} + john_foe = %Person{id: 14, name: "John Foe", persons_id: 14, country: "Cuba"} + + assert {:ok, %{"country" => "Egypt", "id" => _, "name" => "John Toe", "persons_id" => 32}} = + ExTypesense.create_document(john_toe) + + assert {:ok, %{"country" => "Cuba", "id" => _, "name" => "John Foe", "persons_id" => 14}} = + ExTypesense.create_document(john_foe) + + assert {:ok, %{"num_deleted" => 2}} = + ExTypesense.delete_documents_by_query(Person, %{filter_by: "persons_id:>=0"}) + end + + test "success: delete documents by query (map)" do + documents = %{ + collection_name: "doc_companies", + documents: [ + %{ + company_name: "Doctor & Gamble", + doc_companies_id: 19, + country: "ES" + }, + %{ + company_name: "The Daily Bribe", + doc_companies_id: 84, + country: "PH" + } + ] + } + + assert {:ok, [%{"success" => true}, %{"success" => true}]} === + ExTypesense.index_multiple_documents(documents) + + assert {:ok, %{"num_deleted" => 2}} = + ExTypesense.delete_documents_by_query(documents.collection_name, %{ + filter_by: "doc_companies_id:>=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