Skip to content

Commit 136df8c

Browse files
authored
feat: add project, data connector counts in Search result (#1198)
1 parent 193c270 commit 136df8c

6 files changed

Lines changed: 267 additions & 26 deletions

File tree

components/renku_data_services/search/api.spec.yaml

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ components:
119119
description: The search query.
120120
type: string
121121
default: ""
122+
include_counts:
123+
description: Include counts (project_count, data_connector_count) in the results.
124+
type: boolean
125+
default: false
122126
PaginationRequest:
123127
type: object
124128
additionalProperties: false
@@ -226,7 +230,7 @@ components:
226230
type: integer
227231
format: int32
228232
SearchProject:
229-
title: Project
233+
title: SearchProject
230234
examples:
231235
- type: Project
232236
id: 01HRA7AZ2Q234CDQWGA052F8MK
@@ -302,7 +306,7 @@ components:
302306
type: string
303307
const: Project
304308
SearchDataConnector:
305-
title: DataConnector
309+
title: SearchDataConnector
306310
examples:
307311
- type: DataConnector
308312
id: 01HRA7AZ2Q234CDQWGA052F8MK
@@ -379,16 +383,16 @@ components:
379383
SearchEntity:
380384
title: SearchEntity
381385
oneOf:
382-
- $ref: '#/components/schemas/Group'
386+
- $ref: '#/components/schemas/SearchGroup'
383387
- $ref: '#/components/schemas/SearchProject'
384-
- $ref: '#/components/schemas/User'
388+
- $ref: '#/components/schemas/SearchUser'
385389
- $ref: '#/components/schemas/SearchDataConnector'
386390
discriminator:
387391
propertyName: type
388392
mapping:
389-
Group: '#/components/schemas/Group'
393+
Group: '#/components/schemas/SearchGroup'
390394
Project: '#/components/schemas/SearchProject'
391-
User: '#/components/schemas/User'
395+
User: '#/components/schemas/SearchUser'
392396
DataConnector: '#/components/schemas/SearchDataConnector'
393397
SearchResult:
394398
title: SearchResult
@@ -438,6 +442,73 @@ components:
438442
type:
439443
type: string
440444
const: User
445+
SearchUser:
446+
title: SearchUser
447+
type: object
448+
required:
449+
- id
450+
- type
451+
- path
452+
- slug
453+
properties:
454+
id:
455+
type: string
456+
path:
457+
type: string
458+
slug:
459+
type: string
460+
firstName:
461+
type: string
462+
lastName:
463+
type: string
464+
score:
465+
type: number
466+
format: double
467+
project_count:
468+
description: Number of projects with this user namespace.
469+
type: integer
470+
format: int32
471+
data_connector_count:
472+
description: Number of data connectors with this user namespace.
473+
type: integer
474+
format: int32
475+
type:
476+
type: string
477+
const: User
478+
SearchGroup:
479+
title: SearchGroup
480+
type: object
481+
required:
482+
- id
483+
- name
484+
- path
485+
- slug
486+
- type
487+
properties:
488+
id:
489+
type: string
490+
path:
491+
type: string
492+
slug:
493+
type: string
494+
name:
495+
type: string
496+
description:
497+
type: string
498+
score:
499+
type: number
500+
format: double
501+
type:
502+
type: string
503+
const: Group
504+
project_count:
505+
description: Number of projects with this group namespace.
506+
type: integer
507+
format: int32
508+
data_connector_count:
509+
description: Number of data connectors with this group namespace.
510+
type: integer
511+
format: int32
441512
UserOrGroup:
442513
title: UserOrGroup
443514
examples:

components/renku_data_services/search/apispec.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# generated by datamodel-codegen:
22
# filename: api.spec.yaml
3-
# timestamp: 2025-06-12T09:56:04+00:00
3+
# timestamp: 2026-02-03T22:16:13+00:00
44

55
from __future__ import annotations
66

@@ -59,6 +59,38 @@ class User(BaseAPISpec):
5959
type: Literal["User"] = "User"
6060

6161

62+
class SearchUser(BaseAPISpec):
63+
id: str
64+
path: str
65+
slug: str
66+
firstName: Optional[str] = None
67+
lastName: Optional[str] = None
68+
score: Optional[float] = None
69+
project_count: Optional[int] = Field(
70+
None, description="Number of projects with this user namespace."
71+
)
72+
data_connector_count: Optional[int] = Field(
73+
None, description="Number of data connectors with this user namespace."
74+
)
75+
type: Literal["User"] = "User"
76+
77+
78+
class SearchGroup(BaseAPISpec):
79+
id: str
80+
path: str
81+
slug: str
82+
name: str
83+
description: Optional[str] = None
84+
score: Optional[float] = None
85+
type: Literal["Group"] = "Group"
86+
project_count: Optional[int] = Field(
87+
None, description="Number of projects with this group namespace."
88+
)
89+
data_connector_count: Optional[int] = Field(
90+
None, description="Number of data connectors with this group namespace."
91+
)
92+
93+
6294
class UserOrGroup(RootModel[Union[Group, User]]):
6395
root: Union[Group, User] = Field(
6496
...,
@@ -108,6 +140,10 @@ class ErrorResponse(BaseAPISpec):
108140

109141
class SearchQuery(PaginationRequest):
110142
q: str = Field("", description="The search query.")
143+
include_counts: bool = Field(
144+
False,
145+
description="Include counts (project_count, data_connector_count) in the results.",
146+
)
111147

112148

113149
class FacetData(BaseAPISpec):
@@ -183,8 +219,10 @@ class SearchDataConnector(BaseAPISpec):
183219
type: Literal["DataConnector"] = "DataConnector"
184220

185221

186-
class SearchEntity(RootModel[Union[Group, SearchProject, User, SearchDataConnector]]):
187-
root: Union[Group, SearchProject, User, SearchDataConnector] = Field(
222+
class SearchEntity(
223+
RootModel[Union[SearchGroup, SearchProject, SearchUser, SearchDataConnector]]
224+
):
225+
root: Union[SearchGroup, SearchProject, SearchUser, SearchDataConnector] = Field(
188226
..., discriminator="type", title="SearchEntity"
189227
)
190228

components/renku_data_services/search/blueprints.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,14 @@ async def _query(_: Request, user: base_models.APIUser, query: SearchQuery) -> H
8585
logger.debug(f"Running search query: {query}")
8686

8787
result = await core.query(
88-
self.authz.client, self.username_resolve, self.solr_config, uq, user, per_page, offset
88+
self.authz.client,
89+
self.username_resolve,
90+
self.solr_config,
91+
uq,
92+
user,
93+
per_page,
94+
offset,
95+
include_counts=query.include_counts,
8996
)
9097
await self.metrics.search_queried(user)
9198
return json(

components/renku_data_services/search/converters.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Conversion functions."""
22

3-
from typing import cast
4-
53
from renku_data_services.authz.models import Visibility
64
from renku_data_services.search.apispec import (
75
Group as GroupApi,
@@ -14,9 +12,15 @@
1412
UserOrGroup,
1513
UserOrGroupOrProject,
1614
)
15+
from renku_data_services.search.apispec import (
16+
SearchGroup as SearchGroupApi,
17+
)
1718
from renku_data_services.search.apispec import (
1819
SearchProject as ProjectApi,
1920
)
21+
from renku_data_services.search.apispec import (
22+
SearchUser as SearchUserApi,
23+
)
2024
from renku_data_services.search.apispec import (
2125
User as UserApi,
2226
)
@@ -49,50 +53,88 @@ def from_visibility(v: Visibility) -> VisibilityApi:
4953
return VisibilityApi.private
5054

5155

52-
def from_user(user: UserDocument) -> UserApi:
56+
def from_user(user: UserDocument) -> SearchUserApi:
5357
"""Creates an apispec user from a solr user document."""
54-
return UserApi(
58+
return SearchUserApi(
5559
id=user.id,
5660
slug=user.slug.value,
5761
path=user.path,
5862
firstName=user.firstName,
5963
lastName=user.lastName,
6064
score=user.score,
65+
project_count=None,
66+
data_connector_count=None,
6167
)
6268

6369

64-
def from_group(group: GroupDocument) -> GroupApi:
70+
def from_group(group: GroupDocument) -> SearchGroupApi:
6571
"""Creates a apispec group from a solr group document."""
66-
return GroupApi(
72+
return SearchGroupApi(
6773
id=str(group.id),
6874
name=group.name,
6975
slug=group.slug.value,
7076
path=group.path,
7177
description=group.description,
7278
score=group.score,
79+
project_count=None,
80+
data_connector_count=None,
7381
)
7482

7583

7684
def __creator_details(e: ProjectDocument | DataConnectorDocument) -> UserApi | None:
7785
if e.creatorDetails is not None and e.creatorDetails.docs != []:
78-
return from_user(UserDocument.from_dict(e.creatorDetails.docs[0]))
86+
return __user_to_base(UserDocument.from_dict(e.creatorDetails.docs[0]))
7987
else:
8088
return None
8189

8290

91+
def __user_to_base(user: UserDocument) -> UserApi:
92+
"""Creates a base User (not SearchUser) from a solr user document."""
93+
return UserApi(
94+
id=user.id,
95+
slug=user.slug.value,
96+
path=user.path,
97+
firstName=user.firstName,
98+
lastName=user.lastName,
99+
score=user.score,
100+
)
101+
102+
103+
def __group_to_base(group: GroupDocument) -> GroupApi:
104+
"""Creates a base Group (not SearchGroup) from a solr group document."""
105+
return GroupApi(
106+
id=str(group.id),
107+
name=group.name,
108+
slug=group.slug.value,
109+
path=group.path,
110+
description=group.description,
111+
score=group.score,
112+
)
113+
114+
83115
def __namespace_details(d: ProjectDocument) -> UserOrGroup | None:
84116
if d.namespaceDetails is not None and d.namespaceDetails.docs != []:
85117
e = EntityDocReader.from_dict(d.namespaceDetails.docs[0])
86118
if e is not None:
87-
return UserOrGroup(cast(UserApi | GroupApi, from_entity(e).root))
119+
match e:
120+
case UserDocument() as user_doc:
121+
return UserOrGroup(__user_to_base(user_doc))
122+
case GroupDocument() as group_doc:
123+
return UserOrGroup(__group_to_base(group_doc))
88124
return None
89125

90126

91127
def __namespace_details_dc(d: DataConnectorDocument) -> UserOrGroupOrProject | None:
92128
if d.namespaceDetails is not None and d.namespaceDetails.docs != []:
93129
e = EntityDocReader.from_dict(d.namespaceDetails.docs[0])
94130
if e is not None:
95-
return UserOrGroupOrProject(cast(UserApi | GroupApi | ProjectApi, from_entity(e).root))
131+
match e:
132+
case UserDocument() as user_doc:
133+
return UserOrGroupOrProject(__user_to_base(user_doc))
134+
case GroupDocument() as group_doc:
135+
return UserOrGroupOrProject(__group_to_base(group_doc))
136+
case ProjectDocument() as project_doc:
137+
return UserOrGroupOrProject(from_project(project_doc))
96138
return None
97139

98140

0 commit comments

Comments
 (0)