Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Elasticsearch implementation #658

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ config :code_corps,
postmark_project_request_template: "123",
postmark_receipt_template: "123"

# Configure elasticsearch
config :code_corps, :elasticsearch_url, "http://0.0.0.0:9200"

# If the dev environment has no CLOUDEX_API_KEY set, we want the app
# to still run, with cloudex in test API mode
if System.get_env("CLOUDEX_API_KEY") == nil do
Expand Down
3 changes: 3 additions & 0 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ config :code_corps, :analytics, CodeCorps.Analytics.SegmentAPI
config :code_corps, :stripe, Stripe
config :code_corps, :stripe_env, :prod

# Configure elasticsearch
config :code_corps, :elasticsearch_url, "http://0.0.0.0:9200"

config :sentry,
environment_name: Mix.env || :prod

Expand Down
7 changes: 7 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ config :code_corps, :stripe_env, :test

config :code_corps, :icon_color_generator, CodeCorps.RandomIconColor.TestGenerator

# Configure elasticsearch
config :code_corps, :elasticsearch_url, "http://0.0.0.0:9200"

# Set Corsica logging to output no console warning when rejecting a request
config :code_corps, :corsica_log_level, [rejected: :debug]

Expand All @@ -54,6 +57,10 @@ config :code_corps,
github_app_client_secret: System.get_env("GITHUB_TEST_APP_CLIENT_SECRET"),
github_app_pem: pem

# Configure elasticsearch
config :code_corps, :elasticsearch_url, "http://0.0.0.0:9200"
config :code_corps, :elasticsearch_index, "skills"

config :sentry,
environment_name: Mix.env || :test

Expand Down
94 changes: 94 additions & 0 deletions lib/code_corps/helpers/elastic_search_helper.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
defmodule CodeCorps.ElasticSearchHelper do
alias Elastix.Search
alias Elastix.Index
alias Elastix.Document

def delete(url, index) do
Index.delete(url, index)
end

def create_index(url, index, type) do
Index.settings(url, index, settings_map())
Index.settings(url, "#{index}/_mapping/#{type}", field_filter(type))
end

def add_documents(url, index, type, documents) when is_list(documents) do
add_documents(url, index, type, documents, [])
end

def add_documents(url, index, type, documents, query) when is_list(documents) do
Enum.each(documents, fn(x) -> add_document(url, index, type, x, query) end)
end

def add_document(url, index, type, data) do
add_document(url, index, type, data, [])
end

def add_document(url, index, type, data, query) do
Document.index_new(url, index, type, data, query)
end

def search(url, index, type, search_query) do
data = %{
query: %{
match: %{"#{type}": search_query}
}
}
Search.search(url, index, [], data) |> process_response(type)
end

def match_all(url, index, type) do
data = %{
query: %{
match_all: %{}
}
}
Search.search(url, index, [], data) |> process_response(type)
end

def process_response(%HTTPoison.Response{status_code: 200} = response, type) do
response.body["hits"]["hits"] |> Enum.map(fn(x) -> x["_source"] end)
end

def process_response(_), do: []

defp settings_map do
%{
settings: %{
number_of_shards: 5,
analysis: %{
filter: %{
autocomplete_filter: %{
type: "edge_ngram",
min_gram: 2,
max_gram: 20
}
},
analyzer: %{
autocomplete: %{
type: "custom",
tokenizer: "standard",
filter: [
"lowercase",
"autocomplete_filter"
]
}
}
}
}
}
end

def field_filter(type) do
%{
"#{type}" => %{
"properties" => %{
"#{type}" => %{
"type" => "string",
"analyzer" => "autocomplete"
}
}
}
}
end
end
8 changes: 8 additions & 0 deletions lib/code_corps_web/controllers/skill_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ defmodule CodeCorpsWeb.SkillController do
end
end

@elasticsearch_index "skills"
@elasticsearch_type "title"
@elasticsearch_url Application.get_env(:code_corps, :elasticsearch_url)

def search(_conn, %{query: query}) do
CodeCorps.ElasticSearchHelper.search(@elasticsearch_url, @elasticsearch_index, @elasticsearch_type, query)
end

@spec load_skills(map) :: list(Skill.t)
defp load_skills(%{} = params) do
Skill
Expand Down
1 change: 1 addition & 0 deletions lib/code_corps_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ defmodule CodeCorpsWeb.Router do
resources "/role-skills", RoleSkillController, only: [:create, :delete]
resources "/roles", RoleController, only: [:create]
resources "/skills", SkillController, only: [:create]
resources "/skills/search", SkillController, only: [:show]
resources "/stripe-connect-accounts", StripeConnectAccountController, only: [:show, :create, :update]
resources "/stripe-connect-plans", StripeConnectPlanController, only: [:show, :create]
resources "/stripe-connect-subscriptions", StripeConnectSubscriptionController, only: [:show, :create]
Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ defmodule CodeCorps.Mixfile do
{:sweet_xml, "~> 0.5"},
{:timber, "~> 2.0"}, # Logging
{:timex, "~> 3.0"},
{:timex_ecto, "~> 3.0"}
{:timex_ecto, "~> 3.0"},
{:elastix, git: "https://github.com/paulsullivanjr/elastix.git"} # for elastic search
]
end

Expand Down
11 changes: 6 additions & 5 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@
"earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], []},
"ecto": {:hex, :ecto, "2.2.6", "3fd1067661d6d64851a0d4db9acd9e884c00d2d1aa41cc09da687226cf894661", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]},
"ecto_ordered": {:hex, :ecto_ordered, "0.2.0-beta1", "cb066bc608f1c8913cea85af8293261720e6a88e3c99061e6877d7025352f045", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}]},
"elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [:mix], []},
"ex_aws": {:hex, :ex_aws, "1.1.4", "4bdc4fff91f8d35c7fe2355b9da54cc51f980c92f1137715d8b2d70d8e8511cc", [:mix], [{:configparser_ex, "~> 0.2.1", [hex: :configparser_ex, optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, optional: true]}]},
"ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]},
"ex_machina": {:hex, :ex_machina, "2.1.0", "4874dc9c78e7cf2d429f24dc3c4005674d4e4da6a08be961ffccc08fb528e28b", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, optional: true]}]},
"elastix": {:git, "https://github.com/paulsullivanjr/elastix.git", "72441f08d59491ec1101b8bb9afe56463a5cbd75", []},
"elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [:mix], [], "hexpm"},
"ex_aws": {:hex, :ex_aws, "1.1.5", "789173f385934f7e27f9ef36692a6c5f7dde06fd6e6f64d4cd92cda613d34bf9", [:mix], [{:configparser_ex, "~> 0.2.1", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
"ex_machina": {:hex, :ex_machina, "2.1.0", "4874dc9c78e7cf2d429f24dc3c4005674d4e4da6a08be961ffccc08fb528e28b", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.7.5", "339e433e5d3bce09400dc8de7b9040741a409c93917849916c136a0f51fdc183", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
"file_system": {:hex, :file_system, "0.2.2", "7f1e9de4746f4eb8a4ca8f2fbab582d84a4e40fa394cce7bfcb068b988625b06", [], [], "hexpm"},
"file_system": {:hex, :file_system, "0.2.2", "7f1e9de4746f4eb8a4ca8f2fbab582d84a4e40fa394cce7bfcb068b988625b06", [:mix], [], "hexpm"},
"fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []},
"gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], []},
"guardian": {:hex, :guardian, "1.0.0", "21bae2a8c0b4ed5943d9da0c6aeb16e52874c1f675de5d7920ae35471c6263f9", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}, {:uuid, ">= 1.1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm"},
Expand Down
59 changes: 59 additions & 0 deletions test/integration/skill_controller_search_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
defmodule SkillControllerSearchIntegrationTest do
use ExUnit.Case, async: true
alias CodeCorps.ElasticSearchHelper

@test_url Application.get_env(:code_corps, :elasticsearch_url)
@test_index "skills"
@type_value "title"

@elixir %{"id" => 1, "description" => "Elixir is an awesome functional language", "title" => "Elixir", "original_row" => 1}
@ruby %{"id" => 2, "description" => "Ruby is an awesome OO language", "title" => "Ruby", "original_row" => 2}
@rails %{"id" => 3, "description" => "Rails is a modern framework", "title" => "Rails", "original_row" => 3}
@css %{"id" => 4, "description" => "CSS is pretty cool too", "title" => "CSS", "original_row" => 4}
@phoenix %{"id" => 5, "description" => "Phoenix is a super framework", "title" => "Phoenix", "original_row" => 5}

setup do
ElasticSearchHelper.delete(@test_url, @test_index)
ElasticSearchHelper.create_index(@test_url, @test_index, @type_value)
init()
:ok
end

test "search partial word" do
results = ElasticSearchHelper.search(@test_url, @test_index, "title", "ru")
assert results == [@ruby]
end

test "fuzzy search partial word" do
results = ElasticSearchHelper.search(@test_url, @test_index, "title", "rj")
# Two lists can be concatenated or subtracted using the ++/2 and --/2
# see: http://elixir-lang.org/getting-started/basic-types.html#linked-lists
# This allows us to confirm the values we want regardless of the order the values are returned in.
assert results -- ["Ruby", "Rails"] == []
end

test "search whole word" do
results = ElasticSearchHelper.search(@test_url, @test_index, "title", "css")
assert results == [@css]
end

test "fuzzy search whole word" do
results = ElasticSearchHelper.search(@test_url, @test_index, "title", "csw")
assert results == [@css]
end

test "search no matches" do
results = ElasticSearchHelper.search(@test_url, @test_index, "title", "foo")
assert results == []
end

test "match all entries" do
results = ElasticSearchHelper.match_all(@test_url, @test_index, "title")
assert results -- [@elixir, @ruby, @rails, @css] == []
end

def init do
ElasticSearchHelper.add_documents(@test_url, @test_index, @type_value,
[@elixir, @css, @ruby], [refresh: true])
end
end