blob: 8e3a1bd68c4295e08fbb4f41422d450b0c3da007 [file] [log] [blame]
defmodule NouveauTest do
use CouchTestCase
@moduletag :nouveau
@moduledoc """
Test search
"""
def create_search_docs(db_name) do
resp = Couch.post("/#{db_name}/_bulk_docs",
headers: ["Content-Type": "application/json"],
body: %{:docs => [
%{"_id" => "doc4", "foo" => "foo", "bar" => 42, "baz" => "hello there"},
%{"_id" => "doc3", "foo" => "bar", "bar" => 12.0, "baz" => "hello"},
%{"_id" => "doc1", "foo" => "baz", "bar" => 0, "baz" => "there"},
%{"_id" => "doc2", "foo" => "foobar", "bar" => 100, "baz" => "hi"},
]}
)
assert resp.status_code in [201]
resp
end
def create_conflicted_search_docs(db_name) do
resp = Couch.post("/#{db_name}/_bulk_docs",
headers: ["Content-Type": "application/json"],
body: %{:docs => [
# doc4: conflict between 1-a and 1-b, 1-b wins, 1-b will be purged
%{"_id" => "doc4", "foo" => "foo", "bar" => 42, "baz" => "hello there",
"_revisions" => %{:start => 1, :ids => ["a"]}
},
%{"_id" => "doc4", "foo" => "fooX", "bar" => 43, "baz" => "hello thereX",
"_revisions" => %{:start => 1, :ids => ["b"]}
},
# doc3: conflict between 1-a deleted and 2-c, 1-a is deleted,
%{"_id" => "doc3", "foo" => "bar", "bar" => 12.0, "baz" => "hello",
"_revisions" => %{:start => 1, :ids => ["a"]},
"_deleted" => true
},
%{"_id" => "doc3", "foo" => "barX", "bar" => 13.0, "baz" => "helloX",
"_revisions" => %{:start => 2, :ids => ["c", "b"]}
},
# doc1: conflict between 1-a and 2-c, 2-c is deleted
%{"_id" => "doc1", "foo" => "baz", "bar" => 0, "baz" => "there",
"_revisions" => %{:start => 1, :ids => ["a"]}
},
%{"_id" => "doc1", "foo" => "bazX", "bar" => 1, "baz" => "thereX",
"_revisions" => %{:start => 2, :ids => ["c", "b"]},
"_deleted" => true
},
# doc2: 2-b is deleted
%{"_id" => "doc2", "foo" => "foobar", "bar" => 100, "baz" => "hi",
"_revisions" => %{:start => 2, :ids => ["b", "a"]},
"_deleted" => true
}
], :new_edits => false}
)
assert resp.status_code in [201]
resp
end
def create_partitioned_search_docs(db_name) do
resp = Couch.post("/#{db_name}/_bulk_docs",
headers: ["Content-Type": "application/json"],
body: %{:docs => [
%{"_id" => "foo:doc4", "foo" => "foo", "bar" => 42},
%{"_id" => "bar:doc3", "foo" => "bar", "bar" => 12.0},
%{"_id" => "foo:doc1", "foo" => "baz", "bar" => 0},
%{"_id" => "bar:doc2", "foo" => "foobar", "bar" => 100},
]}
)
assert resp.status_code in [201]
end
def create_ddoc(db_name, opts \\ %{}) do
default_ddoc = %{
nouveau: %{
bar: %{
default_analyzer: "standard",
index: """
function (doc) {
index("string", "foo", doc.foo, {store: true});
index("double", "bar", doc.bar, {store: true});
}
"""
}
}
}
ddoc = Enum.into(opts, default_ddoc)
resp = Couch.put("/#{db_name}/_design/foo", body: ddoc)
assert resp.status_code in [201]
assert Map.has_key?(resp.body, "ok") == true
end
def create_mango_index(db_name) do
body = %{
type: "nouveau",
index: %{
fields: [
%{name: "foo", type: "string"},
%{name: "bar", type: "number"},
%{name: "baz", type: "string"},
]
}
}
resp = Couch.post("/#{db_name}/_index", body: body)
assert resp.status_code in [200]
resp.body
end
def get_ids(resp) do
%{:body => %{"hits" => hits}} = resp
Enum.map(hits, fn hit -> hit["doc"]["_id"] end)
end
def get_mango_ids(resp) do
%{:body => %{"docs" => docs}} = resp
Enum.map(docs, fn doc -> doc["_id"] end)
end
def get_bookmark(resp) do
%{:body => %{"bookmark" => bookmark}} = resp
bookmark
end
def get_total_hits(resp) do
%{:body => %{"total_hits" => total_hits}} = resp
total_hits
end
def assert_status_code(resp, code) do
assert resp.status_code == code,
"status code: #{resp.status_code}, resp body: #{:jiffy.encode(resp.body)}"
end
test "search analyze", context do
url = "/_nouveau_analyze"
resp = Couch.post(url,
headers: ["Content-Type": "application/json"],
body: %{analyzer: "standard", text: "hello there"})
assert_status_code(resp, 200)
assert resp.body == %{"tokens" => ["hello", "there"]}
end
@tag :with_db
test "search info", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_ddoc(db_name)
# query it so it builds
url = "/#{db_name}/_design/foo/_nouveau/bar"
resp = Couch.get(url, query: %{q: "*:*", include_docs: true})
assert_status_code(resp, 200)
url = "/#{db_name}/_design/foo/_nouveau_info/bar"
resp = Couch.get(url)
assert_status_code(resp, 200)
info = Map.get(resp.body, "search_index")
assert Map.get(info, "disk_size") > 0
assert Map.get(info, "num_docs") > 0
assert Map.get(info, "update_seq") > 0
end
@tag :with_db
test "search returns all items for GET", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_ddoc(db_name)
url = "/#{db_name}/_design/foo/_nouveau/bar"
resp = Couch.get(url, query: %{q: "*:*", include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
# nouveau sorts by _id as tie-breaker
assert ids == ["doc1", "doc2", "doc3", "doc4"]
end
@tag :with_db
test "search returns all items for POST", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_ddoc(db_name)
url = "/#{db_name}/_design/foo/_nouveau/bar"
resp = Couch.post(url, body: %{q: "*:*", include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
assert ids == ["doc1", "doc2", "doc3", "doc4"]
end
@tag :with_db
test "search returns all items (paginated)", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_ddoc(db_name)
url = "/#{db_name}/_design/foo/_nouveau/bar"
# page 1
resp = Couch.post(url, body: %{q: "*:*", limit: 2, include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
assert ids == ["doc1", "doc2"]
# page 2
resp = Couch.post(url, body: %{q: "*:*", limit: 2, bookmark: get_bookmark(resp), include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
assert ids == ["doc3", "doc4"]
end
@tag :with_db
test "search for foo:bar", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_ddoc(db_name)
url = "/#{db_name}/_design/foo/_nouveau/bar"
resp = Couch.post(url, body: %{q: "foo:bar", include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
assert ids == ["doc3"]
end
@tag :with_db
test "search for numeric ranges with locales", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_ddoc(db_name)
url = "/#{db_name}/_design/foo/_nouveau/bar"
resp = Couch.post(url, body: %{q: "bar:[10.0 TO 20.0]", locale: "us", include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
assert ids == ["doc3"]
url = "/#{db_name}/_design/foo/_nouveau/bar"
resp = Couch.post(url, body: %{q: "bar:[10.0 TO 20.0]", locale: "de", include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
assert ids == ["doc2"]
end
@tag :with_db
test "sort by string field (asc)", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_ddoc(db_name)
url = "/#{db_name}/_design/foo/_nouveau/bar"
resp = Couch.post(url, body: %{q: "*:*", sort: "foo<string>", include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
assert ids == ["doc3", "doc1", "doc4", "doc2"]
end
@tag :with_db
test "sort by string field (desc)", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_ddoc(db_name)
url = "/#{db_name}/_design/foo/_nouveau/bar"
resp = Couch.post(url, body: %{q: "*:*", sort: "-foo<string>", include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
assert ids == ["doc2", "doc4", "doc1", "doc3"]
end
@tag :with_db
test "sort by numeric field (asc)", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_ddoc(db_name)
url = "/#{db_name}/_design/foo/_nouveau/bar"
resp = Couch.post(url, body: %{q: "*:*", sort: "bar<double>", include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
assert ids == ["doc1", "doc3", "doc4", "doc2"]
end
@tag :with_db
test "sort by numeric field (desc)", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_ddoc(db_name)
url = "/#{db_name}/_design/foo/_nouveau/bar"
resp = Couch.post(url, body: %{q: "*:*", sort: "-bar<double>", include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
assert ids == ["doc2", "doc4", "doc3", "doc1"]
end
@tag :with_db
test "counts", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_ddoc(db_name)
url = "/#{db_name}/_design/foo/_nouveau/bar"
resp = Couch.post(url, body: %{q: "*:*", counts: ["foo"], include_docs: true})
assert_status_code(resp, 200)
%{:body => %{"counts" => counts}} = resp
assert counts == %{"foo" => %{"bar" => 1, "baz" => 1, "foo" => 1, "foobar" => 1}}
end
@tag :with_db
test "ranges", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_ddoc(db_name)
url = "/#{db_name}/_design/foo/_nouveau/bar"
resp = Couch.post(url, body: %{q: "*:*", ranges: %{bar: [
%{label: "cheap", min: 0, max: 42},
%{label: "expensive", min: 42, min_inclusive: false, max: 1000}]},
include_docs: true})
assert_status_code(resp, 200)
%{:body => %{"ranges" => ranges}} = resp
assert ranges == %{"bar" => %{"cheap" => 3, "expensive" => 1}}
end
@tag :with_db
test "ranges (open)", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_ddoc(db_name)
url = "/#{db_name}/_design/foo/_nouveau/bar"
resp = Couch.post(url, body: %{q: "*:*", ranges: %{bar: [
%{label: "cheap", max: 42},
%{label: "expensive", min: 42, min_inclusive: false}]},
include_docs: true})
assert_status_code(resp, 200)
%{:body => %{"ranges" => ranges}} = resp
assert ranges == %{"bar" => %{"cheap" => 3, "expensive" => 1}}
end
@tag :with_db
test "mango search by number", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_mango_index(db_name)
url = "/#{db_name}/_find"
resp = Couch.post(url, body: %{selector: %{bar: %{"$gt": 5}}})
assert_status_code(resp, 200)
ids = get_mango_ids(resp)
assert ids == ["doc2", "doc3", "doc4"]
end
@tag :with_db
test "mango search by string", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_mango_index(db_name)
url = "/#{db_name}/_find"
resp = Couch.post(url, body: %{selector: %{foo: %{"$eq": "foo"}}})
assert_status_code(resp, 200)
ids = get_mango_ids(resp)
assert ids == ["doc4"]
end
@tag :with_db
test "mango search by text", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_mango_index(db_name)
url = "/#{db_name}/_find"
resp = Couch.post(url, body: %{selector: %{"$text": "hello"}})
assert_status_code(resp, 200)
ids = get_mango_ids(resp)
assert ids == ["doc4", "doc3"]
end
@tag :with_db
test "mango sort by number", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_mango_index(db_name)
url = "/#{db_name}/_find"
resp = Couch.post(url, body: %{sort: [%{"bar:number": "asc"}], selector: %{bar: %{"$gt": 5}}})
assert_status_code(resp, 200)
ids = get_mango_ids(resp)
assert ids == ["doc3", "doc4", "doc2"]
end
@tag :with_db
test "mango sort by string", context do
db_name = context[:db_name]
create_search_docs(db_name)
create_mango_index(db_name)
url = "/#{db_name}/_find"
resp = Couch.post(url, body: %{sort: [%{"foo:string": "asc"}], selector: %{bar: %{"$gte": 0}}})
assert_status_code(resp, 200)
ids = get_mango_ids(resp)
assert ids == ["doc3", "doc1", "doc4", "doc2"]
end
@tag :with_partitioned_db
test "search GET (partitioned)", context do
db_name = context[:db_name]
create_partitioned_search_docs(db_name)
create_ddoc(db_name)
url = "/#{db_name}/_partition/foo/_design/foo/_nouveau/bar"
resp = Couch.get(url, query: %{q: "*:*", include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
assert ids == ["foo:doc1", "foo:doc4"]
url = "/#{db_name}/_partition/bar/_design/foo/_nouveau/bar"
resp = Couch.get(url, query: %{q: "*:*", include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
assert ids == ["bar:doc2", "bar:doc3"]
end
@tag :with_partitioned_db
test "search POST (partitioned)", context do
db_name = context[:db_name]
create_partitioned_search_docs(db_name)
create_ddoc(db_name)
url = "/#{db_name}/_partition/foo/_design/foo/_nouveau/bar"
resp = Couch.post(url, body: %{q: "*:*", include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
assert ids == ["foo:doc1", "foo:doc4"]
url = "/#{db_name}/_partition/bar/_design/foo/_nouveau/bar"
resp = Couch.post(url, body: %{q: "*:*", include_docs: true})
assert_status_code(resp, 200)
ids = get_ids(resp)
assert ids == ["bar:doc2", "bar:doc3"]
end
@tag :with_partitioned_db
test "mango (partitioned)", context do
db_name = context[:db_name]
create_partitioned_search_docs(db_name)
create_mango_index(db_name)
url = "/#{db_name}/_partition/foo/_find"
resp = Couch.post(url, body: %{selector: %{foo: %{"$eq": "foo"}}})
assert_status_code(resp, 200)
ids = get_mango_ids(resp)
assert ids == ["foo:doc4"]
url = "/#{db_name}/_partition/bar/_find"
resp = Couch.post(url, body: %{selector: %{foo: %{"$eq": "bar"}}})
assert_status_code(resp, 200)
ids = get_mango_ids(resp)
assert ids == ["bar:doc3"]
end
@tag :with_db
test "delete", context do
db_name = context[:db_name]
create_resp = create_search_docs(db_name)
create_ddoc(db_name)
search_url = "/#{db_name}/_design/foo/_nouveau/bar"
# confirm all hits
resp = Couch.get(search_url, query: %{q: "*:*", include_docs: true})
assert_status_code(resp, 200)
assert get_total_hits(resp) == 4
# delete a doc
doc = hd(create_resp.body)
resp = Couch.delete("/#{db_name}/#{doc["id"]}?rev=#{doc["rev"]}")
assert_status_code(resp, 200)
# confirm it is gone
resp = Couch.get(search_url, query: %{q: "*:*", include_docs: true})
assert_status_code(resp, 200)
assert get_total_hits(resp) == 3
resp = Couch.get("/#{db_name}/_design/foo/_nouveau_info/bar")
assert_status_code(resp, 200)
assert resp.body["search_index"]["update_seq"] == 6
assert resp.body["search_index"]["purge_seq"] == 0
end
@tag :with_db
test "purge", context do
db_name = context[:db_name]
create_resp = create_search_docs(db_name)
create_ddoc(db_name)
search_url = "/#{db_name}/_design/foo/_nouveau/bar"
# confirm all hits
resp = Couch.get(search_url, query: %{q: "*:*", include_docs: true})
assert_status_code(resp, 200)
assert get_total_hits(resp) == 4
# purge a doc
doc = hd(create_resp.body)
resp =
Couch.post("/#{db_name}/_purge",
body: %{doc["id"] => [doc["rev"]]}
)
assert_status_code(resp, 201)
# confirm it is gone
resp = Couch.get(search_url, query: %{q: "*:*", include_docs: true})
assert_status_code(resp, 200)
assert get_total_hits(resp) == 3
resp = Couch.get("/#{db_name}/_design/foo/_nouveau_info/bar")
assert_status_code(resp, 200)
assert resp.body["search_index"]["update_seq"] == 6
assert resp.body["search_index"]["purge_seq"] == 1
end
@tag :with_db
test "purge with conflicts", context do
db_name = context[:db_name]
create_resp = create_conflicted_search_docs(db_name)
create_ddoc(db_name)
search_url = "/#{db_name}/_design/foo/_nouveau/bar"
# confirm all hits
resp = Couch.get(search_url, query: %{q: "*:*", include_docs: true})
assert_status_code(resp, 200)
assert get_total_hits(resp) == 3
[hit1, hit2, hit3] = Enum.sort(resp.body["hits"])
assert hit1["doc"]["_id"] == "doc1"
assert hit1["doc"]["_rev"] == "1-a"
assert hit1["fields"] == %{"bar" => 0.0, "foo" => "baz"}
assert hit2["doc"]["_id"] == "doc3"
assert hit2["doc"]["_rev"] == "2-c"
assert hit2["fields"] == %{"bar" => 13.0, "foo" => "barX"}
assert hit3["doc"]["_id"] == "doc4"
assert hit3["doc"]["_rev"] == "1-b"
assert hit3["fields"] == %{"bar" => 43.0, "foo" => "fooX"}
# purge docs
purge_body = %{
"doc1" => ["2-c", "3-nonexistentrev"],
"doc2" => ["2-b"],
"doc3" => ["2-c"],
"doc4" => ["1-b"],
}
resp = Couch.post("/#{db_name}/_purge", body: purge_body)
assert_status_code(resp, 201)
resp = Couch.get(search_url, query: %{q: "*:*", include_docs: true})
assert_status_code(resp, 200)
hits = Enum.sort(resp.body["hits"])
assert get_total_hits(resp) == 2
[hit1, hit2] = Enum.sort(resp.body["hits"])
# doc1: 2-c deleted was purged, 1-a is still the winner
assert hit1["doc"]["_id"] == "doc1"
assert hit1["doc"]["_rev"] == "1-a"
assert hit1["fields"] == %{"bar" => 0.0, "foo" => "baz"}
# doc2: doc was deleted and now it's completely purged
# doc3: live revision is deleted, we're left with the deleted rev only
# doc4: 2-c was purged, 1-a is the new winner
assert hit2["doc"]["_id"] == "doc4"
assert hit2["doc"]["_rev"] == "1-a"
assert hit2["fields"] == %{"bar" => 42.0, "foo" => "foo"}
resp = Couch.get("/#{db_name}")
db_purge_seq = resp.body["purge_seq"]
# Double-check db purge sequence (sum of purge seqeunces on shards) is 4
assert String.starts_with?(db_purge_seq, "4-")
resp = Couch.get("/#{db_name}/_design/foo/_nouveau_info/bar")
assert_status_code(resp, 200)
assert resp.body["search_index"]["update_seq"] == 8
assert resp.body["search_index"]["purge_seq"] == 4
end
end