Skip to content

Commit 0074f71

Browse files
authored
Merge pull request #153 from alphagov/filter-timestamp
Implement timestamp filtering
2 parents f983c62 + 09924a2 commit 0074f71

File tree

3 files changed

+95
-26
lines changed

3 files changed

+95
-26
lines changed

app/services/discovery_engine/query/filter_expression_helpers.rb

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,44 @@
11
module DiscoveryEngine::Query
22
module FilterExpressionHelpers
3+
TIMESTAMP_VALUE_REGEX = /\A(?:from:(?<from>\d{4}-\d{2}-\d{2}))?(?:,)?(?:to:(?<to>\d{4}-\d{2}-\d{2}))?\z/
4+
35
# Creates a filter expression for documents where string_or_array_field contains any of the
46
# values in string_value_or_values
5-
def any_string(string_or_array_field, string_value_or_values)
7+
def filter_any_string(string_or_array_field, string_value_or_values)
68
Array(string_value_or_values)
79
.map { escape_and_quote(_1) }
810
.join(",")
911
.then { "#{string_or_array_field}: ANY(#{_1})" }
1012
end
1113

1214
# Creates a filter expression for documents where array_field contains all of the values in string_value_or_values
13-
def all_string(array_field, string_value_or_values)
15+
def filter_all_string(array_field, string_value_or_values)
1416
Array(string_value_or_values)
15-
.map { any_string(array_field, _1) }
16-
.then { conjunction(_1) }
17+
.map { filter_any_string(array_field, _1) }
18+
.then { filter_conjunction(_1) }
1719
end
1820

1921
# Creates a filter expression for documents where string_or_array_field does not contain any of the values in
2022
# string_value_or_values
21-
def not_string(string_or_array_field, string_value_or_values)
22-
any_string(string_or_array_field, string_value_or_values)
23-
.then { negate(_1) }
23+
def filter_not_string(string_or_array_field, string_value_or_values)
24+
filter_any_string(string_or_array_field, string_value_or_values)
25+
.then { filter_negate(_1) }
26+
end
27+
28+
# Creates a filter expression for documents where timestamp_field is between the dates in
29+
# timestamp_value
30+
def filter_timestamp(timestamp_field, timestamp_value)
31+
match = timestamp_value.match(TIMESTAMP_VALUE_REGEX)
32+
return nil unless match && (match[:from] || match[:to])
33+
34+
from = match[:from] ? Date.parse(match[:from]).beginning_of_day.to_i : "*"
35+
to = match[:to] ? Date.parse(match[:to]).end_of_day.to_i : "*"
36+
37+
"#{timestamp_field}: IN(#{from},#{to})"
2438
end
2539

2640
# Creates a filter expression from several expressions where all must be true
27-
def conjunction(expression_or_expressions)
41+
def filter_conjunction(expression_or_expressions)
2842
expressions = Array(expression_or_expressions).compact_blank
2943
return expressions.first if expressions.one?
3044

@@ -36,7 +50,7 @@ def conjunction(expression_or_expressions)
3650

3751
private
3852

39-
def negate(expression)
53+
def filter_negate(expression)
4054
"NOT #{expression}"
4155
end
4256

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
module DiscoveryEngine::Query
22
class Filters
3-
FILTERABLE_FIELDS = %i[content_purpose_supergroup link part_of_taxonomy_tree].freeze
3+
FILTER_PARAM_KEY_REGEX = /\A(filter_all|filter|reject)_(.+)\z/
4+
5+
FILTERABLE_STRING_FIELDS = %w[content_purpose_supergroup link part_of_taxonomy_tree].freeze
6+
FILTERABLE_TIMESTAMP_FIELDS = %w[public_timestamp].freeze
47

58
include FilterExpressionHelpers
69

@@ -9,24 +12,37 @@ def initialize(query_params)
912
end
1013

1114
def filter_expression
12-
expressions = [
13-
*query_params_of_type(:reject).map { not_string(_1, _2) },
14-
*query_params_of_type(:filter).map { any_string(_1, _2) },
15-
*query_params_of_type(:filter_all).map { all_string(_1, _2) },
16-
].compact
17-
18-
conjunction(expressions)
15+
query_params
16+
.map { parse_param(_1, _2) }
17+
.compact_blank
18+
.then { filter_conjunction(_1) }
1919
end
2020

2121
private
2222

2323
attr_reader :query_params
2424

25-
def query_params_of_type(type)
26-
FILTERABLE_FIELDS
27-
.filter_map { [_1, query_params["#{type}_#{_1}".to_sym]] }
28-
.to_h
29-
.compact_blank
25+
def parse_param(key, value)
26+
filter_type, filter_field = key.match(FILTER_PARAM_KEY_REGEX)&.captures
27+
return nil unless filter_type && value.present?
28+
29+
case filter_field
30+
when *FILTERABLE_STRING_FIELDS
31+
string_filter_expression(filter_type, filter_field, value)
32+
when *FILTERABLE_TIMESTAMP_FIELDS
33+
filter_timestamp(filter_field, value)
34+
end
35+
end
36+
37+
def string_filter_expression(filter_type, filter_field, value)
38+
case filter_type
39+
when "filter"
40+
filter_any_string(filter_field, value)
41+
when "filter_all"
42+
filter_all_string(filter_field, value)
43+
when "reject"
44+
filter_not_string(filter_field, value)
45+
end
3046
end
3147
end
3248
end

spec/services/discovery_engine/query/filters_spec.rb

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
it { is_expected.to be_nil }
99
end
1010

11-
context "with a reject filter" do
11+
context "with a reject string filter" do
1212
context "with an empty parameter" do
1313
let(:query_params) { { q: "garden centres", reject_link: "" } }
1414

@@ -28,7 +28,7 @@
2828
end
2929
end
3030

31-
context "with an 'any' filter" do
31+
context "with an 'any' string filter" do
3232
context "with an empty parameter" do
3333
let(:query_params) { { q: "garden centres", filter_content_purpose_supergroup: "" } }
3434

@@ -52,7 +52,7 @@
5252
end
5353
end
5454

55-
context "with an 'all' filter" do
55+
context "with an 'all' string filter" do
5656
context "with an empty parameter" do
5757
let(:query_params) { { q: "garden centres", filter_all_part_of_taxonomy_tree: "" } }
5858

@@ -74,17 +74,56 @@
7474
end
7575
end
7676

77+
context "with a timestamp filter" do
78+
context "with an empty parameter" do
79+
let(:query_params) { { q: "garden centres", filter_public_timestamp: "" } }
80+
81+
it { is_expected.to be_nil }
82+
end
83+
84+
context "with a from parameter" do
85+
let(:query_params) { { q: "garden centres", filter_public_timestamp: "from:1989-12-13" } }
86+
87+
it { is_expected.to eq("public_timestamp: IN(629510400,*)") }
88+
end
89+
90+
context "with a to parameter" do
91+
let(:query_params) { { q: "garden centres", filter_public_timestamp: "to:1989-12-13" } }
92+
93+
it { is_expected.to eq("public_timestamp: IN(*,629596799)") }
94+
end
95+
96+
context "with both from and to parameters" do
97+
let(:query_params) { { q: "garden centres", filter_public_timestamp: "from:1989-12-13,to:1989-12-13" } }
98+
99+
it { is_expected.to eq("public_timestamp: IN(629510400,629596799)") }
100+
end
101+
102+
context "with an invalid from parameter" do
103+
let(:query_params) { { q: "garden centres", filter_public_timestamp: "from:1989" } }
104+
105+
it { is_expected.to be_nil }
106+
end
107+
108+
context "with an invalid to parameter" do
109+
let(:query_params) { { q: "garden centres", filter_public_timestamp: "to:12-13" } }
110+
111+
it { is_expected.to be_nil }
112+
end
113+
end
114+
77115
context "with several filters specified" do
78116
let(:query_params) do
79117
{
80118
q: "garden centres",
81119
reject_link: "/foo",
82120
filter_content_purpose_supergroup: "services",
83121
filter_all_part_of_taxonomy_tree: %w[cafe-1234 face-5678],
122+
filter_public_timestamp: "from:1989-12-13,to:1989-12-13",
84123
}
85124
end
86125

87-
it { is_expected.to eq('(NOT link: ANY("/foo")) AND (content_purpose_supergroup: ANY("services")) AND ((part_of_taxonomy_tree: ANY("cafe-1234")) AND (part_of_taxonomy_tree: ANY("face-5678")))') }
126+
it { is_expected.to eq('(NOT link: ANY("/foo")) AND (content_purpose_supergroup: ANY("services")) AND ((part_of_taxonomy_tree: ANY("cafe-1234")) AND (part_of_taxonomy_tree: ANY("face-5678"))) AND (public_timestamp: IN(629510400,629596799))') }
88127
end
89128

90129
context "with filters containing escapable characters" do

0 commit comments

Comments
 (0)