Skip to content

Commit 00a0767

Browse files
authored
Consider health scores in the search API (#783)
Signed-off-by: Juan Cruz Viotti <[email protected]>
1 parent dd5d964 commit 00a0767

File tree

4 files changed

+176
-34
lines changed

4 files changed

+176
-34
lines changed

src/index/explorer.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,11 @@ struct GENERATE_EXPLORER_SEARCH_INDEX {
522522
: "",
523523
directory_entry.defines("description")
524524
? directory_entry.at("description").to_string()
525-
: ""});
525+
: "",
526+
directory_entry.defines("health")
527+
? static_cast<std::uint8_t>(
528+
directory_entry.at("health").to_integer())
529+
: static_cast<std::uint8_t>(0)});
526530
}
527531
}
528532

src/search/include/sourcemeta/one/search.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ struct SearchEntry {
2222
std::string path;
2323
std::string title;
2424
std::string description;
25+
std::uint8_t health;
2526
};
2627

2728
SOURCEMETA_ONE_SEARCH_EXPORT

src/search/search.cc

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,20 @@ namespace sourcemeta::one {
1212

1313
auto make_search(std::vector<SearchEntry> &&entries)
1414
-> std::vector<std::uint8_t> {
15-
// Prioritise entries that have more metadata filled in,
16-
// then sort lexicographically by path
1715
std::ranges::sort(entries, [](const SearchEntry &left,
1816
const SearchEntry &right) {
19-
const auto left_score =
17+
const auto left_metadata =
2018
(!left.title.empty() ? 1 : 0) + (!left.description.empty() ? 1 : 0);
21-
const auto right_score =
19+
const auto right_metadata =
2220
(!right.title.empty() ? 1 : 0) + (!right.description.empty() ? 1 : 0);
23-
if (left_score != right_score) {
24-
return left_score > right_score;
21+
if (left_metadata != right_metadata) {
22+
return left_metadata > right_metadata;
23+
}
24+
25+
if (left.health != right.health) {
26+
return left.health > right.health;
2527
}
2628

27-
// TODO: Ideally we sort based on schema health too, given
28-
// lint results
2929
return left.path < right.path;
3030
});
3131

test/unit/search/search_test.cc

Lines changed: 162 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ TEST(Search, make_search_empty) {
2323

2424
TEST(Search, make_search_single_entry) {
2525
std::vector<sourcemeta::one::SearchEntry> entries{
26-
{"/foo/bar", "My Title", "A description"}};
26+
{"/foo/bar", "My Title", "A description", 80}};
2727
const auto payload{sourcemeta::one::make_search(std::move(entries))};
2828
EXPECT_FALSE(payload.empty());
2929

@@ -41,7 +41,7 @@ TEST(Search, search_empty_payload) {
4141

4242
TEST(Search, search_no_match) {
4343
std::vector<sourcemeta::one::SearchEntry> entries{
44-
{"/foo/bar", "Title", "Desc"}};
44+
{"/foo/bar", "Title", "Desc", 80}};
4545
const auto payload{sourcemeta::one::make_search(std::move(entries))};
4646
const auto result{
4747
sourcemeta::one::search(payload.data(), payload.size(), "zzzzz")};
@@ -51,7 +51,7 @@ TEST(Search, search_no_match) {
5151

5252
TEST(Search, search_match_in_path) {
5353
std::vector<sourcemeta::one::SearchEntry> entries{
54-
{"/foo/bar", "Title", "Desc"}};
54+
{"/foo/bar", "Title", "Desc", 80}};
5555
const auto payload{sourcemeta::one::make_search(std::move(entries))};
5656
const auto result{
5757
sourcemeta::one::search(payload.data(), payload.size(), "foo")};
@@ -61,7 +61,7 @@ TEST(Search, search_match_in_path) {
6161

6262
TEST(Search, search_match_in_title) {
6363
std::vector<sourcemeta::one::SearchEntry> entries{
64-
{"/foo/bar", "Special Title", "Desc"}};
64+
{"/foo/bar", "Special Title", "Desc", 80}};
6565
const auto payload{sourcemeta::one::make_search(std::move(entries))};
6666
const auto result{
6767
sourcemeta::one::search(payload.data(), payload.size(), "Special")};
@@ -71,7 +71,7 @@ TEST(Search, search_match_in_title) {
7171

7272
TEST(Search, search_match_in_description) {
7373
std::vector<sourcemeta::one::SearchEntry> entries{
74-
{"/foo/bar", "Title", "Unique description here"}};
74+
{"/foo/bar", "Title", "Unique description here", 80}};
7575
const auto payload{sourcemeta::one::make_search(std::move(entries))};
7676
const auto result{
7777
sourcemeta::one::search(payload.data(), payload.size(), "Unique")};
@@ -82,7 +82,7 @@ TEST(Search, search_match_in_description) {
8282

8383
TEST(Search, search_case_insensitive) {
8484
std::vector<sourcemeta::one::SearchEntry> entries_lower{
85-
{"/foo/bar", "Hello World", "desc"}};
85+
{"/foo/bar", "Hello World", "desc", 80}};
8686
const auto payload_lower{
8787
sourcemeta::one::make_search(std::move(entries_lower))};
8888
const auto result_lower{sourcemeta::one::search(
@@ -91,7 +91,7 @@ TEST(Search, search_case_insensitive) {
9191
EXPECT_SEARCH_RESULT(result_lower, 0, "/foo/bar", "Hello World", "desc");
9292

9393
std::vector<sourcemeta::one::SearchEntry> entries_upper{
94-
{"/foo/bar", "Hello World", "desc"}};
94+
{"/foo/bar", "Hello World", "desc", 80}};
9595
const auto payload_upper{
9696
sourcemeta::one::make_search(std::move(entries_upper))};
9797
const auto result_upper{sourcemeta::one::search(
@@ -100,7 +100,7 @@ TEST(Search, search_case_insensitive) {
100100
EXPECT_SEARCH_RESULT(result_upper, 0, "/foo/bar", "Hello World", "desc");
101101

102102
std::vector<sourcemeta::one::SearchEntry> entries_mixed{
103-
{"/foo/bar", "Hello World", "desc"}};
103+
{"/foo/bar", "Hello World", "desc", 80}};
104104
const auto payload_mixed{
105105
sourcemeta::one::make_search(std::move(entries_mixed))};
106106
const auto result_mixed{sourcemeta::one::search(
@@ -111,9 +111,9 @@ TEST(Search, search_case_insensitive) {
111111

112112
TEST(Search, search_multiple_matches) {
113113
std::vector<sourcemeta::one::SearchEntry> entries{
114-
{"/schemas/address", "Address Schema", "For addresses"},
115-
{"/schemas/person", "Person Schema", "For people"},
116-
{"/schemas/email", "Email Schema", "For emails"}};
114+
{"/schemas/address", "Address Schema", "For addresses", 80},
115+
{"/schemas/person", "Person Schema", "For people", 80},
116+
{"/schemas/email", "Email Schema", "For emails", 80}};
117117
const auto payload{sourcemeta::one::make_search(std::move(entries))};
118118
const auto result{
119119
sourcemeta::one::search(payload.data(), payload.size(), "schema")};
@@ -128,14 +128,21 @@ TEST(Search, search_multiple_matches) {
128128

129129
TEST(Search, search_limit_10) {
130130
std::vector<sourcemeta::one::SearchEntry> entries{
131-
{"/schemas/test0", "Test 0", ""}, {"/schemas/test1", "Test 1", ""},
132-
{"/schemas/test2", "Test 2", ""}, {"/schemas/test3", "Test 3", ""},
133-
{"/schemas/test4", "Test 4", ""}, {"/schemas/test5", "Test 5", ""},
134-
{"/schemas/test6", "Test 6", ""}, {"/schemas/test7", "Test 7", ""},
135-
{"/schemas/test8", "Test 8", ""}, {"/schemas/test9", "Test 9", ""},
136-
{"/schemas/test10", "Test 10", ""}, {"/schemas/test11", "Test 11", ""},
137-
{"/schemas/test12", "Test 12", ""}, {"/schemas/test13", "Test 13", ""},
138-
{"/schemas/test14", "Test 14", ""}};
131+
{"/schemas/test0", "Test 0", "", 80},
132+
{"/schemas/test1", "Test 1", "", 80},
133+
{"/schemas/test2", "Test 2", "", 80},
134+
{"/schemas/test3", "Test 3", "", 80},
135+
{"/schemas/test4", "Test 4", "", 80},
136+
{"/schemas/test5", "Test 5", "", 80},
137+
{"/schemas/test6", "Test 6", "", 80},
138+
{"/schemas/test7", "Test 7", "", 80},
139+
{"/schemas/test8", "Test 8", "", 80},
140+
{"/schemas/test9", "Test 9", "", 80},
141+
{"/schemas/test10", "Test 10", "", 80},
142+
{"/schemas/test11", "Test 11", "", 80},
143+
{"/schemas/test12", "Test 12", "", 80},
144+
{"/schemas/test13", "Test 13", "", 80},
145+
{"/schemas/test14", "Test 14", "", 80}};
139146

140147
const auto payload{sourcemeta::one::make_search(std::move(entries))};
141148
const auto result{
@@ -155,9 +162,9 @@ TEST(Search, search_limit_10) {
155162

156163
TEST(Search, search_round_trip_data_fidelity) {
157164
std::vector<sourcemeta::one::SearchEntry> entries{
158-
{"/a/b/c", "My Title", "My Description"},
159-
{"/x/y/z", "", "Only description"},
160-
{"/p/q", "Only title", ""}};
165+
{"/a/b/c", "My Title", "My Description", 80},
166+
{"/x/y/z", "", "Only description", 80},
167+
{"/p/q", "Only title", "", 80}};
161168
const auto payload{sourcemeta::one::make_search(std::move(entries))};
162169
const auto result{
163170
sourcemeta::one::search(payload.data(), payload.size(), "/")};
@@ -168,7 +175,8 @@ TEST(Search, search_round_trip_data_fidelity) {
168175
}
169176

170177
TEST(Search, search_single_entry_match) {
171-
std::vector<sourcemeta::one::SearchEntry> entries{{"/only", "One", "Entry"}};
178+
std::vector<sourcemeta::one::SearchEntry> entries{
179+
{"/only", "One", "Entry", 80}};
172180
const auto payload{sourcemeta::one::make_search(std::move(entries))};
173181
const auto result{
174182
sourcemeta::one::search(payload.data(), payload.size(), "One")};
@@ -177,18 +185,147 @@ TEST(Search, search_single_entry_match) {
177185
}
178186

179187
TEST(Search, search_single_entry_no_match) {
180-
std::vector<sourcemeta::one::SearchEntry> entries{{"/only", "One", "Entry"}};
188+
std::vector<sourcemeta::one::SearchEntry> entries{
189+
{"/only", "One", "Entry", 80}};
181190
const auto payload{sourcemeta::one::make_search(std::move(entries))};
182191
const auto result{
183192
sourcemeta::one::search(payload.data(), payload.size(), "nope")};
184193
EXPECT_EQ(result.size(), 0);
185194
}
186195

187196
TEST(Search, search_empty_title_and_description) {
188-
std::vector<sourcemeta::one::SearchEntry> entries{{"/path/only", "", ""}};
197+
std::vector<sourcemeta::one::SearchEntry> entries{{"/path/only", "", "", 80}};
189198
const auto payload{sourcemeta::one::make_search(std::move(entries))};
190199
const auto result{
191200
sourcemeta::one::search(payload.data(), payload.size(), "path")};
192201
EXPECT_EQ(result.size(), 1);
193202
EXPECT_SEARCH_RESULT(result, 0, "/path/only", "", "");
194203
}
204+
205+
TEST(Search, search_health_higher_scores_first) {
206+
std::vector<sourcemeta::one::SearchEntry> entries{
207+
{"/schemas/low", "Low Health", "Desc", 20},
208+
{"/schemas/high", "High Health", "Desc", 100},
209+
{"/schemas/mid", "Mid Health", "Desc", 60}};
210+
const auto payload{sourcemeta::one::make_search(std::move(entries))};
211+
const auto result{
212+
sourcemeta::one::search(payload.data(), payload.size(), "Health")};
213+
EXPECT_EQ(result.size(), 3);
214+
EXPECT_SEARCH_RESULT(result, 0, "/schemas/high", "High Health", "Desc");
215+
EXPECT_SEARCH_RESULT(result, 1, "/schemas/mid", "Mid Health", "Desc");
216+
EXPECT_SEARCH_RESULT(result, 2, "/schemas/low", "Low Health", "Desc");
217+
}
218+
219+
TEST(Search, search_health_100_before_50) {
220+
std::vector<sourcemeta::one::SearchEntry> entries{
221+
{"/schemas/beta", "Beta", "Desc", 50},
222+
{"/schemas/alpha", "Alpha", "Desc", 100}};
223+
const auto payload{sourcemeta::one::make_search(std::move(entries))};
224+
const auto result{
225+
sourcemeta::one::search(payload.data(), payload.size(), "schemas")};
226+
EXPECT_EQ(result.size(), 2);
227+
EXPECT_SEARCH_RESULT(result, 0, "/schemas/alpha", "Alpha", "Desc");
228+
EXPECT_SEARCH_RESULT(result, 1, "/schemas/beta", "Beta", "Desc");
229+
}
230+
231+
TEST(Search, search_health_0_ranks_last) {
232+
std::vector<sourcemeta::one::SearchEntry> entries{
233+
{"/schemas/zero", "Zero", "Desc", 0},
234+
{"/schemas/perfect", "Perfect", "Desc", 100},
235+
{"/schemas/okay", "Okay", "Desc", 50}};
236+
const auto payload{sourcemeta::one::make_search(std::move(entries))};
237+
const auto result{
238+
sourcemeta::one::search(payload.data(), payload.size(), "schemas")};
239+
EXPECT_EQ(result.size(), 3);
240+
EXPECT_SEARCH_RESULT(result, 0, "/schemas/perfect", "Perfect", "Desc");
241+
EXPECT_SEARCH_RESULT(result, 1, "/schemas/okay", "Okay", "Desc");
242+
EXPECT_SEARCH_RESULT(result, 2, "/schemas/zero", "Zero", "Desc");
243+
}
244+
245+
TEST(Search, search_health_same_score_sorts_by_path) {
246+
std::vector<sourcemeta::one::SearchEntry> entries{
247+
{"/schemas/zebra", "Zebra", "Desc", 75},
248+
{"/schemas/apple", "Apple", "Desc", 75},
249+
{"/schemas/mango", "Mango", "Desc", 75}};
250+
const auto payload{sourcemeta::one::make_search(std::move(entries))};
251+
const auto result{
252+
sourcemeta::one::search(payload.data(), payload.size(), "schemas")};
253+
EXPECT_EQ(result.size(), 3);
254+
EXPECT_SEARCH_RESULT(result, 0, "/schemas/apple", "Apple", "Desc");
255+
EXPECT_SEARCH_RESULT(result, 1, "/schemas/mango", "Mango", "Desc");
256+
EXPECT_SEARCH_RESULT(result, 2, "/schemas/zebra", "Zebra", "Desc");
257+
}
258+
259+
TEST(Search, search_metadata_score_beats_health) {
260+
std::vector<sourcemeta::one::SearchEntry> entries{
261+
{"/schemas/healthy", "", "", 100},
262+
{"/schemas/complete", "Title", "Description", 30}};
263+
const auto payload{sourcemeta::one::make_search(std::move(entries))};
264+
const auto result{
265+
sourcemeta::one::search(payload.data(), payload.size(), "schemas")};
266+
EXPECT_EQ(result.size(), 2);
267+
EXPECT_SEARCH_RESULT(result, 0, "/schemas/complete", "Title", "Description");
268+
EXPECT_SEARCH_RESULT(result, 1, "/schemas/healthy", "", "");
269+
}
270+
271+
TEST(Search, search_metadata_score_beats_health_title_only) {
272+
std::vector<sourcemeta::one::SearchEntry> entries{
273+
{"/schemas/no-meta", "", "", 100},
274+
{"/schemas/has-title", "A Title", "", 10}};
275+
const auto payload{sourcemeta::one::make_search(std::move(entries))};
276+
const auto result{
277+
sourcemeta::one::search(payload.data(), payload.size(), "schemas")};
278+
EXPECT_EQ(result.size(), 2);
279+
EXPECT_SEARCH_RESULT(result, 0, "/schemas/has-title", "A Title", "");
280+
EXPECT_SEARCH_RESULT(result, 1, "/schemas/no-meta", "", "");
281+
}
282+
283+
TEST(Search, search_health_tiebreaker_within_same_metadata) {
284+
std::vector<sourcemeta::one::SearchEntry> entries{
285+
{"/schemas/low-health", "Title", "", 25},
286+
{"/schemas/high-health", "Title", "", 90},
287+
{"/schemas/mid-health", "Title", "", 50}};
288+
const auto payload{sourcemeta::one::make_search(std::move(entries))};
289+
const auto result{
290+
sourcemeta::one::search(payload.data(), payload.size(), "schemas")};
291+
EXPECT_EQ(result.size(), 3);
292+
EXPECT_SEARCH_RESULT(result, 0, "/schemas/high-health", "Title", "");
293+
EXPECT_SEARCH_RESULT(result, 1, "/schemas/mid-health", "Title", "");
294+
EXPECT_SEARCH_RESULT(result, 2, "/schemas/low-health", "Title", "");
295+
}
296+
297+
TEST(Search, search_health_fine_grained_ordering) {
298+
std::vector<sourcemeta::one::SearchEntry> entries{
299+
{"/schemas/d", "Title", "Desc", 70},
300+
{"/schemas/a", "Title", "Desc", 100},
301+
{"/schemas/c", "Title", "Desc", 85},
302+
{"/schemas/e", "Title", "Desc", 55},
303+
{"/schemas/b", "Title", "Desc", 95}};
304+
const auto payload{sourcemeta::one::make_search(std::move(entries))};
305+
const auto result{
306+
sourcemeta::one::search(payload.data(), payload.size(), "schemas")};
307+
EXPECT_EQ(result.size(), 5);
308+
EXPECT_SEARCH_RESULT(result, 0, "/schemas/a", "Title", "Desc");
309+
EXPECT_SEARCH_RESULT(result, 1, "/schemas/b", "Title", "Desc");
310+
EXPECT_SEARCH_RESULT(result, 2, "/schemas/c", "Title", "Desc");
311+
EXPECT_SEARCH_RESULT(result, 3, "/schemas/d", "Title", "Desc");
312+
EXPECT_SEARCH_RESULT(result, 4, "/schemas/e", "Title", "Desc");
313+
}
314+
315+
TEST(Search, search_health_mixed_metadata_and_health) {
316+
std::vector<sourcemeta::one::SearchEntry> entries{
317+
{"/schemas/full-low", "Title", "Desc", 30},
318+
{"/schemas/title-high", "Title", "", 95},
319+
{"/schemas/full-high", "Title", "Desc", 90},
320+
{"/schemas/none-perfect", "", "", 100},
321+
{"/schemas/title-low", "Title", "", 40}};
322+
const auto payload{sourcemeta::one::make_search(std::move(entries))};
323+
const auto result{
324+
sourcemeta::one::search(payload.data(), payload.size(), "schemas")};
325+
EXPECT_EQ(result.size(), 5);
326+
EXPECT_SEARCH_RESULT(result, 0, "/schemas/full-high", "Title", "Desc");
327+
EXPECT_SEARCH_RESULT(result, 1, "/schemas/full-low", "Title", "Desc");
328+
EXPECT_SEARCH_RESULT(result, 2, "/schemas/title-high", "Title", "");
329+
EXPECT_SEARCH_RESULT(result, 3, "/schemas/title-low", "Title", "");
330+
EXPECT_SEARCH_RESULT(result, 4, "/schemas/none-perfect", "", "");
331+
}

0 commit comments

Comments
 (0)