Skip to content

Commit 8077cac

Browse files
search: add support for alternate sorting
By default the unified sort endpoint will continue to return the results without additional sorting. However, for usability tests for the search results sort order, we want to expose a query param that will allow the caller to specify an alternative sorting algorithm. To keep it simple, we are exposing only two options for now: - `alphabetic`: A-Z on the title attribute. - `weighted_index`: custom algorithm to give more weight to different types of matches - this algorithm is a work-in-progress. This code was intentionally made to be easily reverted as we may not want to expose sorting options in the final implementation. CMK-24563 Co-authored-by: Benjamin Knapp <benjamin.knapp@checkmk.com> Change-Id: I25d1cb5120b58ea6eb69caf1ef79687ba4e91379
1 parent dd4de6a commit 8077cac

6 files changed

Lines changed: 123 additions & 2 deletions

File tree

cmk/gui/search/pages.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from .engines.monitoring import MonitoringSearchEngine
88
from .engines.setup import SetupSearchEngine
9-
from .type_defs import Provider
9+
from .type_defs import Provider, SortType
1010
from .unified import UnifiedSearch
1111

1212

@@ -15,12 +15,18 @@ class PageUnifiedSearch(AjaxPage):
1515
def page(self, config: Config) -> PageResult:
1616
query = request.get_str_input_mandatory("q")
1717
provider = self._parse_provider_query_param()
18+
sort_type = self._parse_sort_query_param()
1819

1920
setup_engine = SetupSearchEngine()
2021
monitoring_engine = MonitoringSearchEngine()
2122
unified_search_engine = UnifiedSearch(setup_engine, monitoring_engine)
2223

23-
response = unified_search_engine.search(query, config=config, provider=provider)
24+
response = unified_search_engine.search(
25+
query,
26+
config=config,
27+
provider=provider,
28+
sort_type=sort_type,
29+
)
2430

2531
return {
2632
"url": request.url,
@@ -34,3 +40,9 @@ def _parse_provider_query_param(self) -> Provider | None:
3440
return None
3541

3642
return cast(Provider, provider) if provider in get_args(Provider) else None
43+
44+
def _parse_sort_query_param(self) -> SortType | None:
45+
if (sort_type := request.get_str_input("sort")) is None:
46+
return None
47+
48+
return cast(SortType, sort_type) if sort_type in get_args(SortType) else None

cmk/gui/search/sorting.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env python3
2+
# Copyright (C) 2025 Checkmk GmbH - License: GNU General Public License v2
3+
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
4+
# conditions defined in the file COPYING, which is part of this source code package.
5+
6+
from collections.abc import Callable
7+
8+
from .type_defs import SortType, UnifiedSearchResultItem
9+
10+
type Sorter = Callable[[list[UnifiedSearchResultItem]], None]
11+
12+
13+
def get_sorter(sort_type: SortType | None, query: str = "") -> Sorter:
14+
match sort_type:
15+
case "alphabetic":
16+
return _get_alphabetical_sorter
17+
case "weighted_index":
18+
return _get_weighted_index_sorter(query)
19+
case _:
20+
return _get_no_op_sorter
21+
22+
23+
def _get_no_op_sorter(_: list[UnifiedSearchResultItem]) -> None:
24+
return None
25+
26+
27+
def _get_alphabetical_sorter(items: list[UnifiedSearchResultItem]) -> None:
28+
items.sort(key=lambda item: item.title)
29+
30+
31+
def _get_weighted_index_sorter(query: str) -> Sorter:
32+
def algorithm(item: UnifiedSearchResultItem) -> int:
33+
weighting = 5
34+
if (title_idx := item.title.lower().find(query)) >= 0:
35+
weighting = 1
36+
if len(item.title) == len(query):
37+
weighting = 0
38+
39+
if title_idx > 0:
40+
weighting = 2
41+
else:
42+
title_idx = len(item.title)
43+
44+
return weighting * title_idx
45+
46+
def sorter(items: list[UnifiedSearchResultItem]) -> None:
47+
items.sort(key=lambda item: algorithm(item))
48+
49+
return sorter

cmk/gui/search/type_defs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# NOTE: intentionally not using the `type` syntax because it's not possible to use
1010
# `typing.get_args` to dynamically check if string is type at runtime.
1111
Provider = Literal["setup", "monitoring"]
12+
SortType = Literal["alphabetic", "weighted_index"]
1213

1314

1415
class UnifiedSearchResultItemSerialized(TypedDict):

cmk/gui/search/unified.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111

1212
from .engines.monitoring import SupportsMonitoringSearchEngine
1313
from .engines.setup import SupportsSetupSearchEngine
14+
from .sorting import get_sorter
1415
from .type_defs import (
1516
Provider,
17+
SortType,
1618
UnifiedSearchResult,
1719
UnifiedSearchResultCounts,
1820
UnifiedSearchResultItem,
@@ -37,6 +39,7 @@ def search(
3739
*,
3840
config: Config,
3941
provider: Provider | None = None,
42+
sort_type: SortType | None = None,
4043
) -> UnifiedSearchResult:
4144
setup_results_by_topic: SearchResultsByTopic = []
4245
monitoring_results_by_topic: SearchResultsByTopic = []
@@ -63,6 +66,7 @@ def search(
6366
)
6467
)
6568
search_results = [*setup_results, *monitoring_results]
69+
get_sorter(sort_type, query)(search_results)
6670

6771
result_counts = UnifiedSearchResultCounts(
6872
total=len(search_results),
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env python3
2+
# Copyright (C) 2025 Checkmk GmbH - License: GNU General Public License v2
3+
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
4+
# conditions defined in the file COPYING, which is part of this source code package.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env python3
2+
# Copyright (C) 2025 Checkmk GmbH - License: GNU General Public License v2
3+
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
4+
# conditions defined in the file COPYING, which is part of this source code package.
5+
6+
from cmk.gui.search.sorting import get_sorter
7+
from cmk.gui.search.type_defs import UnifiedSearchResultItem
8+
9+
10+
def get_unsorted_results() -> list[UnifiedSearchResultItem]:
11+
return [
12+
UnifiedSearchResultItem(title="Beta", url="/beta", provider="setup", topic="Code"),
13+
UnifiedSearchResultItem(title="Charlie", url="/charlie", provider="setup", topic="Code"),
14+
UnifiedSearchResultItem(title="Alpha", url="/alpha", provider="setup", topic="Code"),
15+
]
16+
17+
18+
def test_no_op_sorter() -> None:
19+
results = get_unsorted_results()
20+
sorter = get_sorter(None)
21+
sorter(results)
22+
23+
assert results == get_unsorted_results()
24+
25+
26+
def test_alphabetical_sorter() -> None:
27+
results = get_unsorted_results()
28+
sorter = get_sorter("alphabetic")
29+
sorter(results)
30+
31+
expected = [
32+
UnifiedSearchResultItem(title="Alpha", url="/alpha", provider="setup", topic="Code"),
33+
UnifiedSearchResultItem(title="Beta", url="/beta", provider="setup", topic="Code"),
34+
UnifiedSearchResultItem(title="Charlie", url="/charlie", provider="setup", topic="Code"),
35+
]
36+
37+
assert results == expected
38+
39+
40+
def test_weighted_sorter() -> None:
41+
results = get_unsorted_results()
42+
sorter = get_sorter("weighted_index", query="bet")
43+
sorter(results)
44+
45+
expected = [
46+
UnifiedSearchResultItem(title="Beta", url="/beta", provider="setup", topic="Code"),
47+
UnifiedSearchResultItem(title="Alpha", url="/alpha", provider="setup", topic="Code"),
48+
UnifiedSearchResultItem(title="Charlie", url="/charlie", provider="setup", topic="Code"),
49+
]
50+
51+
assert results == expected

0 commit comments

Comments
 (0)