Skip to content

Commit 110843f

Browse files
paulsullivanjrjoshsmith
authored andcommitted
Initial elasticsearch changes
- Removed duplicate entry - Moved some code and a couple small refactors of the helper module - Updated to not pass the document index when creating - cleaning up some unneeded changes - Updated search function - Changed elixir version back to 1.3.4 - Updates per comments, added add_documents() function to helper - added some convenience functions - Removing unnecessary code, eliminated 1.4 warning messages so it's done when we upgrade - Added tests for fuzzy search, updates some map formats - Added match all function - Added search endpoint for elasticsearch, updated to return maps - Added comment about using -- with lists
1 parent 065a300 commit 110843f

File tree

9 files changed

+183
-6
lines changed

9 files changed

+183
-6
lines changed

config/dev.exs

+3
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ config :code_corps,
6565
postmark_project_request_template: "123",
6666
postmark_receipt_template: "123"
6767

68+
# Configure elasticsearch
69+
config :code_corps, :elasticsearch_url, "http://0.0.0.0:9200"
70+
6871
# If the dev environment has no CLOUDEX_API_KEY set, we want the app
6972
# to still run, with cloudex in test API mode
7073
if System.get_env("CLOUDEX_API_KEY") == nil do

config/prod.exs

+3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ config :code_corps, :analytics, CodeCorps.Analytics.SegmentAPI
5252
config :code_corps, :stripe, Stripe
5353
config :code_corps, :stripe_env, :prod
5454

55+
# Configure elasticsearch
56+
config :code_corps, :elasticsearch_url, "http://0.0.0.0:9200"
57+
5558
config :sentry,
5659
environment_name: Mix.env || :prod
5760

config/test.exs

+7
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ config :code_corps, :stripe_env, :test
3838

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

41+
# Configure elasticsearch
42+
config :code_corps, :elasticsearch_url, "http://0.0.0.0:9200"
43+
4144
# Set Corsica logging to output no console warning when rejecting a request
4245
config :code_corps, :corsica_log_level, [rejected: :debug]
4346

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

60+
# Configure elasticsearch
61+
config :code_corps, :elasticsearch_url, "http://0.0.0.0:9200"
62+
config :code_corps, :elasticsearch_index, "skills"
63+
5764
config :sentry,
5865
environment_name: Mix.env || :test
5966

lib/code_corps_web/controllers/skill_controller.ex

+8
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ defmodule CodeCorpsWeb.SkillController do
3333
end
3434
end
3535

36+
@elasticsearch_index "skills"
37+
@elasticsearch_type "title"
38+
@elasticsearch_url Application.get_env(:code_corps, :elasticsearch_url)
39+
40+
def search(_conn, params) do
41+
CodeCorps.ElasticSearchHelper.search(@elasticsearch_url, @elasticsearch_index, @elasticsearch_type, query)
42+
end
43+
3644
@spec load_skills(map) :: list(Skill.t)
3745
defp load_skills(%{} = params) do
3846
Skill

lib/code_corps_web/router.ex

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ defmodule CodeCorpsWeb.Router do
8282
resources "/role-skills", RoleSkillController, only: [:create, :delete]
8383
resources "/roles", RoleController, only: [:create]
8484
resources "/skills", SkillController, only: [:create]
85+
resources "/skills/search", SkillController, only: [:show]
8586
resources "/stripe-connect-accounts", StripeConnectAccountController, only: [:show, :create, :update]
8687
resources "/stripe-connect-plans", StripeConnectPlanController, only: [:show, :create]
8788
resources "/stripe-connect-subscriptions", StripeConnectSubscriptionController, only: [:show, :create]

mix.exs

+2-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ defmodule CodeCorps.Mixfile do
8181
{:sweet_xml, "~> 0.5"},
8282
{:timber, "~> 2.0"}, # Logging
8383
{:timex, "~> 3.0"},
84-
{:timex_ecto, "~> 3.0"}
84+
{:timex_ecto, "~> 3.0"},
85+
{:elastix, git: "https://github.com/paulsullivanjr/elastix.git"} # for elastic search
8586
]
8687
end
8788

mix.lock

+6-5
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@
2020
"earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], []},
2121
"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]}]},
2222
"ecto_ordered": {:hex, :ecto_ordered, "0.2.0-beta1", "cb066bc608f1c8913cea85af8293261720e6a88e3c99061e6877d7025352f045", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}]},
23-
"elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [:mix], []},
24-
"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]}]},
25-
"ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]},
26-
"ex_machina": {:hex, :ex_machina, "2.1.0", "4874dc9c78e7cf2d429f24dc3c4005674d4e4da6a08be961ffccc08fb528e28b", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, optional: true]}]},
23+
"elastix": {:git, "https://github.com/paulsullivanjr/elastix.git", "72441f08d59491ec1101b8bb9afe56463a5cbd75", []},
24+
"elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [:mix], [], "hexpm"},
25+
"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"},
26+
"ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
27+
"ex_machina": {:hex, :ex_machina, "2.1.0", "4874dc9c78e7cf2d429f24dc3c4005674d4e4da6a08be961ffccc08fb528e28b", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
2728
"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"},
2829
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
29-
"file_system": {:hex, :file_system, "0.2.2", "7f1e9de4746f4eb8a4ca8f2fbab582d84a4e40fa394cce7bfcb068b988625b06", [], [], "hexpm"},
30+
"file_system": {:hex, :file_system, "0.2.2", "7f1e9de4746f4eb8a4ca8f2fbab582d84a4e40fa394cce7bfcb068b988625b06", [:mix], [], "hexpm"},
3031
"fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []},
3132
"gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], []},
3233
"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"},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
defmodule SkillControllerSearchIntegrationTest do
2+
use ExUnit.Case, async: true
3+
alias CodeCorps.ElasticSearchHelper
4+
5+
@test_url Application.get_env(:code_corps, :elasticsearch_url)
6+
@test_index "skills"
7+
@type_value "title"
8+
9+
@elixir %{"id" => 1, "description" => "Elixir is an awesome functional language", "title" => "Elixir", "original_row" => 1}
10+
@ruby %{"id" => 2, "description" => "Ruby is an awesome OO language", "title" => "Ruby", "original_row" => 2}
11+
@rails %{"id" => 3, "description" => "Rails is a modern framework", "title" => "Rails", "original_row" => 3}
12+
@css %{"id" => 4, "description" => "CSS is pretty cool too", "title" => "CSS", "original_row" => 4}
13+
@phoenix %{"id" => 5, "description" => "Phoenix is a super framework", "title" => "Phoenix", "original_row" => 5}
14+
15+
setup do
16+
ElasticSearchHelper.delete(@test_url, @test_index)
17+
ElasticSearchHelper.create_index(@test_url, @test_index, @type_value)
18+
init()
19+
:ok
20+
end
21+
22+
test "search partial word" do
23+
results = ElasticSearchHelper.search(@test_url, @test_index, "title", "ru")
24+
assert results == [@ruby]
25+
end
26+
27+
test "fuzzy search partial word" do
28+
results = ElasticSearchHelper.search(@test_url, @test_index, "title", "rj")
29+
# Two lists can be concatenated or subtracted using the ++/2 and --/2
30+
# see: http://elixir-lang.org/getting-started/basic-types.html#linked-lists
31+
# This allows us to confirm the values we want regardless of the order the values are returned in.
32+
assert results -- ["Ruby", "Rails"] == []
33+
end
34+
35+
test "search whole word" do
36+
results = ElasticSearchHelper.search(@test_url, @test_index, "title", "css")
37+
assert results == [@css]
38+
end
39+
40+
test "fuzzy search whole word" do
41+
results = ElasticSearchHelper.search(@test_url, @test_index, "title", "csw")
42+
assert results == [@css]
43+
end
44+
45+
test "search no matches" do
46+
results = ElasticSearchHelper.search(@test_url, @test_index, "title", "foo")
47+
assert results == []
48+
end
49+
50+
test "match all entries" do
51+
results = ElasticSearchHelper.match_all(@test_url, @test_index, "title")
52+
assert results -- [@elixir, @ruby, @rails, @css] == []
53+
end
54+
55+
def init do
56+
ElasticSearchHelper.add_documents(@test_url, @test_index, @type_value,
57+
[@elixir, @css, @ruby], [refresh: true])
58+
end
59+
end

web/helpers/elastic_search_helper.ex

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
defmodule CodeCorps.ElasticSearchHelper do
2+
alias Elastix.Search
3+
alias Elastix.Index
4+
alias Elastix.Document
5+
6+
def delete(url, index) do
7+
Index.delete(url, index)
8+
end
9+
10+
def create_index(url, index, type) do
11+
Index.settings(url, index, settings_map())
12+
Index.settings(url, "#{index}/_mapping/#{type}", field_filter(type))
13+
end
14+
15+
def add_documents(url, index, type, documents) when is_list(documents) do
16+
add_documents(url, index, type, documents, [])
17+
end
18+
19+
def add_documents(url, index, type, documents, query) when is_list(documents) do
20+
Enum.each(documents, fn(x) -> add_document(url, index, type, x, query) end)
21+
end
22+
23+
def add_document(url, index, type, data) do
24+
add_document(url, index, type, data, [])
25+
end
26+
27+
def add_document(url, index, type, data, query) do
28+
Document.index_new(url, index, type, data, query)
29+
end
30+
31+
def search(url, index, type, search_query) do
32+
data = %{
33+
query: %{
34+
match: %{"#{type}": search_query}
35+
}
36+
}
37+
Search.search(url, index, [], data) |> process_response(type)
38+
end
39+
40+
def match_all(url, index, type) do
41+
data = %{
42+
query: %{
43+
match_all: %{}
44+
}
45+
}
46+
Search.search(url, index, [], data) |> process_response(type)
47+
end
48+
49+
def process_response(%HTTPoison.Response{status_code: 200} = response, type) do
50+
response.body["hits"]["hits"] |> Enum.map(fn(x) -> x["_source"] end)
51+
end
52+
53+
def process_response(_), do: []
54+
55+
defp settings_map do
56+
%{
57+
settings: %{
58+
number_of_shards: 5,
59+
analysis: %{
60+
filter: %{
61+
autocomplete_filter: %{
62+
type: "edge_ngram",
63+
min_gram: 2,
64+
max_gram: 20
65+
}
66+
},
67+
analyzer: %{
68+
autocomplete: %{
69+
type: "custom",
70+
tokenizer: "standard",
71+
filter: [
72+
"lowercase",
73+
"autocomplete_filter"
74+
]
75+
}
76+
}
77+
}
78+
}
79+
}
80+
end
81+
82+
def field_filter(type) do
83+
%{
84+
"#{type}" => %{
85+
"properties" => %{
86+
"#{type}" => %{
87+
"type" => "string",
88+
"analyzer" => "autocomplete"
89+
}
90+
}
91+
}
92+
}
93+
end
94+
end

0 commit comments

Comments
 (0)