Skip to content

Commit 5c26a76

Browse files
committed
fix(service): flag truncation at the limit that reached the wiki
- cap_list only reports a cut it made itself, but all four search operations pass the same limit to the wiki, which never returns more, so `truncated` was always False - flag when the result set is as large as the effective limit - search_entities: a `limit=` inside the ask query wins over the limit argument, so compare against that one - sparql_query unchanged; its limit applies to the response, not the query
1 parent c0304eb commit 5c26a76

2 files changed

Lines changed: 126 additions & 10 deletions

File tree

‎src/osw/service/ops/search.py‎

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,21 @@
1010
from osw.service.registry import operation
1111
from osw.service.serialization import cap_list, to_jsonable
1212
from osw.sparql_client_smw import SmwSparqlClient
13+
from osw.wiki_tools import get_query_limit
1314
from osw.wtsite import WtSite
1415

1516

17+
def _hit_limit(total: int, limit: Optional[int]) -> bool:
18+
"""Whether a result set is as large as the limit that produced it.
19+
20+
The wiki applies the limit itself, so ``cap_list`` never has to cut these
21+
results and its own flag stays False. A full result set is then the only
22+
signal left that the wiki may hold further matches. ``limit=0`` asks for
23+
no results, so meeting it says nothing about truncation.
24+
"""
25+
return bool(limit) and total >= limit
26+
27+
1628
@operation(
1729
group="search",
1830
cli_name="ask",
@@ -42,16 +54,28 @@ def search_entities(ctx: Context, ask_query: str, limit: Optional[int] = None) -
4254
``@context`` of its schema, so read it with ``osw schema get`` when a
4355
name query returns nothing.
4456
45-
``limit`` defaults to ``OSW_MAX_RESULTS`` (100 when that is unset).
57+
``limit`` defaults to ``OSW_MAX_RESULTS`` (100 when that is unset). A
58+
``limit=N`` written into the query itself wins over it.
4659
Returns ``{titles, count, truncated}``, where ``titles`` are full page
47-
names and ``count`` is how many the wiki returned.
60+
names, ``count`` is how many the wiki returned and ``truncated`` reports
61+
that further matches may exist beyond them.
4862
"""
4963
lim = ctx.limit(limit)
5064
titles = ctx.osw.site.semantic_search(
5165
WtSite.SearchParam(query=ask_query, limit=lim)
5266
)
67+
# semantic_search lets a 'limit=' written into the query win over `lim`,
68+
# so the flag has to compare against the limit that reached the wiki.
69+
# `titles` excludes hits whose page does not exist, so a result set
70+
# thinned that way reads as not truncated.
71+
query_limit = get_query_limit(ask_query)
72+
effective_limit = lim if query_limit is None else query_limit
5373
capped, total, truncated = cap_list(titles, lim)
54-
return {"titles": capped, "count": total, "truncated": truncated}
74+
return {
75+
"titles": capped,
76+
"count": total,
77+
"truncated": truncated or _hit_limit(total, effective_limit),
78+
}
5579

5680

5781
@operation(
@@ -74,12 +98,17 @@ def search_titles(ctx: Context, text: str, limit: Optional[int] = None) -> dict:
7498
7599
``limit`` defaults to ``OSW_MAX_RESULTS`` (100 when that is unset).
76100
Returns ``{titles, count, truncated}``, where ``titles`` are full page
77-
names and ``count`` is how many the wiki returned.
101+
names, ``count`` is how many the wiki returned and ``truncated`` reports
102+
that further matches may exist beyond them.
78103
"""
79104
lim = ctx.limit(limit)
80105
titles = ctx.osw.site.prefix_search(WtSite.SearchParam(query=text, limit=lim))
81106
capped, total, truncated = cap_list(titles, lim)
82-
return {"titles": capped, "count": total, "truncated": truncated}
107+
return {
108+
"titles": capped,
109+
"count": total,
110+
"truncated": truncated or _hit_limit(total, lim),
111+
}
83112

84113

85114
@operation(
@@ -98,13 +127,18 @@ def search_content(ctx: Context, text: str, limit: Optional[int] = None) -> dict
98127
99128
Returns page titles, not the matching passages. ``limit`` defaults to
100129
``OSW_MAX_RESULTS`` (100 when that is unset). Returns
101-
``{titles, count, truncated}``, where ``titles`` are full page names
102-
and ``count`` is how many the wiki returned.
130+
``{titles, count, truncated}``, where ``titles`` are full page names,
131+
``count`` is how many the wiki returned and ``truncated`` reports that
132+
further matches may exist beyond them.
103133
"""
104134
lim = ctx.limit(limit)
105135
titles = ctx.osw.site.content_search(WtSite.SearchParam(query=text, limit=lim))
106136
capped, total, truncated = cap_list(titles, lim)
107-
return {"titles": capped, "count": total, "truncated": truncated}
137+
return {
138+
"titles": capped,
139+
"count": total,
140+
"truncated": truncated or _hit_limit(total, lim),
141+
}
108142

109143

110144
@operation(
@@ -125,14 +159,19 @@ def list_instances_of_category(
125159
126160
``limit`` defaults to ``OSW_MAX_RESULTS`` (100 when that is unset).
127161
Returns ``{titles, count, truncated}``, where ``titles`` are full page
128-
names and ``count`` is how many the wiki returned.
162+
names, ``count`` is how many the wiki returned and ``truncated`` reports
163+
that further matches may exist beyond them.
129164
"""
130165
lim = ctx.limit(limit)
131166
titles = ctx.osw.query_instances(
132167
OSW.QueryInstancesParam(categories=category, limit=lim)
133168
)
134169
capped, total, truncated = cap_list(titles, lim)
135-
return {"titles": capped, "count": total, "truncated": truncated}
170+
return {
171+
"titles": capped,
172+
"count": total,
173+
"truncated": truncated or _hit_limit(total, lim),
174+
}
136175

137176

138177
@operation(

‎tests/test_service_ops_search.py‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,80 @@ def test_sparql_query_without_endpoint_raises_not_configured():
7474

7575
with pytest.raises(errors.NotConfigured):
7676
search.sparql_query(ctx, query="SELECT * WHERE {?s ?p ?o}")
77+
78+
79+
def test_search_entities_flags_truncation_at_the_requested_limit():
80+
osw = MagicMock()
81+
osw.site.semantic_search.return_value = ["Item:OSW1", "Item:OSW2"]
82+
ctx = Context(_settings(), Policy(), osw=osw)
83+
84+
result = search.search_entities(ctx, ask_query="[[Category:Item]]", limit=2)
85+
86+
assert result["count"] == 2
87+
assert result["truncated"] is True
88+
89+
90+
def test_search_entities_flags_truncation_at_a_limit_inside_the_query():
91+
"""The query's own limit reaches the wiki, so it decides truncation."""
92+
osw = MagicMock()
93+
osw.site.semantic_search.return_value = ["Item:OSW1", "Item:OSW2"]
94+
ctx = Context(_settings(), Policy(), osw=osw)
95+
96+
result = search.search_entities(
97+
ctx, ask_query="[[Category:Item]]|limit=2", limit=100
98+
)
99+
100+
assert result["truncated"] is True
101+
102+
103+
def test_search_entities_below_the_limit_is_not_truncated():
104+
osw = MagicMock()
105+
osw.site.semantic_search.return_value = ["Item:OSW1"]
106+
ctx = Context(_settings(), Policy(), osw=osw)
107+
108+
result = search.search_entities(ctx, ask_query="[[Category:Item]]", limit=2)
109+
110+
assert result["truncated"] is False
111+
112+
113+
def test_search_entities_with_limit_zero_in_the_query_is_not_truncated():
114+
"""'limit=0' asks for no results, so meeting it is not truncation."""
115+
osw = MagicMock()
116+
osw.site.semantic_search.return_value = []
117+
ctx = Context(_settings(), Policy(), osw=osw)
118+
119+
result = search.search_entities(
120+
ctx, ask_query="[[Category:Item]]|limit=0", limit=100
121+
)
122+
123+
assert result["truncated"] is False
124+
125+
126+
def test_search_titles_flags_truncation_at_the_limit():
127+
osw = MagicMock()
128+
osw.site.prefix_search.return_value = ["Item:OSW1", "Item:OSW2"]
129+
ctx = Context(_settings(), Policy(), osw=osw)
130+
131+
result = search.search_titles(ctx, text="Item", limit=2)
132+
133+
assert result["truncated"] is True
134+
135+
136+
def test_search_content_flags_truncation_at_the_limit():
137+
osw = MagicMock()
138+
osw.site.content_search.return_value = ["Item:OSW1", "Item:OSW2"]
139+
ctx = Context(_settings(), Policy(), osw=osw)
140+
141+
result = search.search_content(ctx, text="sensor", limit=2)
142+
143+
assert result["truncated"] is True
144+
145+
146+
def test_list_instances_of_category_flags_truncation_at_the_limit():
147+
osw = MagicMock()
148+
osw.query_instances.return_value = ["Item:OSW1", "Item:OSW2"]
149+
ctx = Context(_settings(), Policy(), osw=osw)
150+
151+
result = search.list_instances_of_category(ctx, category="Category:Item", limit=2)
152+
153+
assert result["truncated"] is True

0 commit comments

Comments
 (0)