Skip to content

Commit ddafb8f

Browse files
authored
Fix/pictique search (#342)
* fix: search * chore: data formats
1 parent f543b20 commit ddafb8f

File tree

3 files changed

+190
-38
lines changed

3 files changed

+190
-38
lines changed

platforms/pictique-api/src/controllers/UserController.ts

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -65,34 +65,8 @@ export class UserController {
6565
// Validate sort parameter
6666
const validSortOptions = ["relevance", "name", "verified", "newest"];
6767
const sortOption = validSortOptions.includes(sort as string) ? sort as string : "relevance";
68-
69-
// Get users and count in parallel
70-
const [users, total] = await Promise.all([
71-
this.userService.searchUsers(q, pageNum, limitNum, verifiedOnly, sortOption),
72-
this.userService.getSearchUsersCount(q, verifiedOnly)
73-
]);
74-
75-
// Calculate pagination metadata
76-
const totalPages = Math.ceil(total / limitNum);
77-
const hasNextPage = pageNum < totalPages;
78-
const hasPrevPage = pageNum > 1;
79-
80-
res.json({
81-
users,
82-
pagination: {
83-
page: pageNum,
84-
limit: limitNum,
85-
total,
86-
totalPages,
87-
hasNextPage,
88-
hasPrevPage
89-
},
90-
searchInfo: {
91-
query: q,
92-
verifiedOnly,
93-
sortBy: sortOption
94-
}
95-
});
68+
const users = await this.userService.searchUsers(q, pageNum, limitNum, verifiedOnly, sortOption);
69+
res.json(users);
9670
} catch (error) {
9771
console.error("Error searching users:", error);
9872
res.status(500).json({ error: "Internal server error" });
@@ -135,6 +109,44 @@ export class UserController {
135109
}
136110
};
137111

112+
searchByEnameOrName = async (req: Request, res: Response) => {
113+
try {
114+
const { q, page = "1", limit = "10", verified } = req.query;
115+
116+
if (!q || typeof q !== "string") {
117+
return res
118+
.status(400)
119+
.json({ error: "Search query is required" });
120+
}
121+
122+
// Validate search query length
123+
if (q.trim().length < 2) {
124+
return res
125+
.status(400)
126+
.json({ error: "Search query must be at least 2 characters long" });
127+
}
128+
129+
// Parse and validate pagination parameters
130+
const pageNum = parseInt(page as string) || 1;
131+
const limitNum = Math.min(parseInt(limit as string) || 10, 50); // Cap at 50 results
132+
133+
if (pageNum < 1 || limitNum < 1) {
134+
return res
135+
.status(400)
136+
.json({ error: "Invalid pagination parameters" });
137+
}
138+
139+
// Parse verified filter
140+
const verifiedOnly = verified === "true";
141+
142+
const users = await this.userService.searchUsersByEnameOrName(q, pageNum, limitNum, verifiedOnly);
143+
res.json(users);
144+
} catch (error) {
145+
console.error("Error searching users by ename or name:", error);
146+
res.status(500).json({ error: "Internal server error" });
147+
}
148+
};
149+
138150
follow = async (req: Request, res: Response) => {
139151
try {
140152
const followerId = req.user?.id;

platforms/pictique-api/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ app.get(
128128
// User routes
129129
app.get("/api/users", userController.currentUser);
130130
app.get("/api/users/search", userController.search);
131+
app.get("/api/users/search/ename-name", userController.searchByEnameOrName);
131132
app.get("/api/users/suggestions", userController.getSearchSuggestions);
132133
app.get("/api/users/popular", userController.getPopularSearches);
133134
app.post("/api/users/:id/follow", authGuard, userController.follow);

platforms/pictique-api/src/services/UserService.ts

Lines changed: 149 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,37 @@ export class UserService {
5858
return [];
5959
}
6060

61-
// Use query builder for more complex queries
61+
// Use query builder for more complex queries with relevance scoring
6262
const queryBuilder = this.userRepository
6363
.createQueryBuilder("user")
6464
.select([
6565
"user.id",
6666
"user.handle",
6767
"user.name",
68+
"user.ename",
6869
"user.description",
6970
"user.avatarUrl",
7071
"user.isVerified"
7172
])
73+
.addSelect(`
74+
CASE
75+
WHEN user.ename ILIKE :exactQuery THEN 100
76+
WHEN user.name ILIKE :exactQuery THEN 90
77+
WHEN user.handle ILIKE :exactQuery THEN 80
78+
WHEN user.ename ILIKE :query THEN 70
79+
WHEN user.name ILIKE :query THEN 60
80+
WHEN user.handle ILIKE :query THEN 50
81+
WHEN user.description ILIKE :query THEN 30
82+
WHEN user.ename ILIKE :fuzzyQuery THEN 40
83+
WHEN user.name ILIKE :fuzzyQuery THEN 35
84+
WHEN user.handle ILIKE :fuzzyQuery THEN 30
85+
ELSE 0
86+
END`, 'relevance_score')
7287
.where(
73-
"user.name ILIKE :query OR user.ename ILIKE :query OR user.handle ILIKE :query OR user.description ILIKE :query OR user.name ILIKE :fuzzyQuery OR user.ename ILIKE :fuzzyQuery OR user.handle ILIKE :fuzzyQuery",
88+
"user.name ILIKE :query OR user.ename ILIKE :query OR user.handle ILIKE :query OR user.description ILIKE :query OR user.ename ILIKE :fuzzyQuery OR user.name ILIKE :fuzzyQuery OR user.handle ILIKE :fuzzyQuery",
7489
{
7590
query: `%${searchQuery}%`,
91+
exactQuery: searchQuery, // Exact match for highest priority
7692
fuzzyQuery: `%${searchQuery.split('').join('%')}%` // Fuzzy search with wildcards between characters
7793
}
7894
);
@@ -98,8 +114,10 @@ export class UserService {
98114
break;
99115
case "relevance":
100116
default:
101-
// Default relevance sorting: verified first, then by name
102-
queryBuilder.orderBy("user.isVerified", "DESC").addOrderBy("user.name", "ASC");
117+
// Default relevance sorting: relevance score first, then verified status, then by name
118+
queryBuilder.orderBy("relevance_score", "DESC")
119+
.addOrderBy("user.isVerified", "DESC")
120+
.addOrderBy("user.name", "ASC");
103121
break;
104122
}
105123

@@ -121,13 +139,14 @@ export class UserService {
121139
return 0;
122140
}
123141

124-
// Use query builder for count
142+
// Use query builder for count with same search logic
125143
const queryBuilder = this.userRepository
126144
.createQueryBuilder("user")
127145
.where(
128-
"user.name ILIKE :query OR user.ename ILIKE :query OR user.handle ILIKE :query OR user.description ILIKE :query OR user.name ILIKE :fuzzyQuery OR user.ename ILIKE :fuzzyQuery OR user.handle ILIKE :fuzzyQuery",
146+
"user.name ILIKE :query OR user.ename ILIKE :query OR user.handle ILIKE :query OR user.description ILIKE :query OR user.ename ILIKE :fuzzyQuery OR user.name ILIKE :fuzzyQuery OR user.handle ILIKE :fuzzyQuery",
129147
{
130148
query: `%${searchQuery}%`,
149+
exactQuery: searchQuery, // Exact match for highest priority
131150
fuzzyQuery: `%${searchQuery.split('').join('%')}%` // Fuzzy search with wildcards between characters
132151
}
133152
);
@@ -152,7 +171,7 @@ export class UserService {
152171
return [];
153172
}
154173

155-
// Use query builder for suggestions
174+
// Use query builder for suggestions with relevance scoring
156175
const queryBuilder = this.userRepository
157176
.createQueryBuilder("user")
158177
.select([
@@ -161,18 +180,138 @@ export class UserService {
161180
"user.name",
162181
"user.ename"
163182
])
183+
.addSelect(`
184+
CASE
185+
WHEN user.ename ILIKE :exactQuery THEN 100
186+
WHEN user.name ILIKE :exactQuery THEN 90
187+
WHEN user.ename ILIKE :query THEN 70
188+
WHEN user.name ILIKE :query THEN 60
189+
WHEN user.handle ILIKE :query THEN 50
190+
WHEN user.ename ILIKE :fuzzyQuery THEN 40
191+
WHEN user.name ILIKE :fuzzyQuery THEN 35
192+
WHEN user.handle ILIKE :fuzzyQuery THEN 30
193+
ELSE 0
194+
END`, 'relevance_score')
164195
.where(
165-
"user.name ILIKE :query OR user.ename ILIKE :query OR user.handle ILIKE :query",
166-
{ query: `%${searchQuery}%` }
196+
"user.name ILIKE :query OR user.ename ILIKE :query OR user.handle ILIKE :query OR user.ename ILIKE :fuzzyQuery OR user.name ILIKE :fuzzyQuery OR user.handle ILIKE :fuzzyQuery",
197+
{
198+
query: `%${searchQuery}%`,
199+
exactQuery: searchQuery, // Exact match for highest priority
200+
fuzzyQuery: `%${searchQuery.split('').join('%')}%` // Fuzzy search with wildcards between characters
201+
}
167202
)
168203
.andWhere("user.isArchived = :archived", { archived: false })
169-
.orderBy("user.isVerified", "DESC")
204+
.orderBy("relevance_score", "DESC")
205+
.addOrderBy("user.isVerified", "DESC")
170206
.addOrderBy("user.name", "ASC")
171207
.take(limit);
172208

173209
return queryBuilder.getMany();
174210
};
175211

212+
searchUsersByEnameOrName = async (
213+
query: string,
214+
page: number = 1,
215+
limit: number = 10,
216+
verifiedOnly: boolean = false
217+
) => {
218+
// Sanitize and trim the search query
219+
const searchQuery = query.trim();
220+
221+
// Return empty array if query is too short or empty
222+
if (searchQuery.length < 2) {
223+
return [];
224+
}
225+
226+
// Validate pagination parameters
227+
if (page < 1 || limit < 1 || limit > 100) {
228+
return [];
229+
}
230+
231+
// Specialized search focusing only on ename and name with high priority
232+
const queryBuilder = this.userRepository
233+
.createQueryBuilder("user")
234+
.select([
235+
"user.id",
236+
"user.handle",
237+
"user.name",
238+
"user.ename",
239+
"user.description",
240+
"user.avatarUrl",
241+
"user.isVerified"
242+
])
243+
.addSelect(`
244+
CASE
245+
WHEN user.ename ILIKE :exactQuery THEN 100
246+
WHEN user.name ILIKE :exactQuery THEN 95
247+
WHEN user.ename ILIKE :query THEN 80
248+
WHEN user.name ILIKE :query THEN 75
249+
WHEN user.ename ILIKE :fuzzyQuery THEN 60
250+
WHEN user.name ILIKE :fuzzyQuery THEN 55
251+
ELSE 0
252+
END`, 'relevance_score')
253+
.where(
254+
"user.ename ILIKE :query OR user.name ILIKE :query OR user.ename ILIKE :fuzzyQuery OR user.name ILIKE :fuzzyQuery",
255+
{
256+
query: `%${searchQuery}%`,
257+
exactQuery: searchQuery, // Exact match for highest priority
258+
fuzzyQuery: `%${searchQuery.split('').join('%')}%` // Fuzzy search with wildcards between characters
259+
}
260+
);
261+
262+
// Add verified filter if requested
263+
if (verifiedOnly) {
264+
queryBuilder.andWhere("user.isVerified = :verified", { verified: true });
265+
}
266+
267+
// Add additional filters for better results
268+
queryBuilder.andWhere("user.isArchived = :archived", { archived: false });
269+
270+
// Order by relevance score (ename and name matches first)
271+
return queryBuilder
272+
.orderBy("relevance_score", "DESC")
273+
.addOrderBy("user.isVerified", "DESC")
274+
.addOrderBy("user.name", "ASC")
275+
.skip((page - 1) * limit)
276+
.take(limit)
277+
.getMany();
278+
};
279+
280+
getSearchUsersByEnameOrNameCount = async (
281+
query: string,
282+
verifiedOnly: boolean = false
283+
) => {
284+
// Sanitize and trim the search query
285+
const searchQuery = query.trim();
286+
287+
// Return 0 if query is too short or empty
288+
if (searchQuery.length < 2) {
289+
return 0;
290+
}
291+
292+
// Use query builder for count with same specialized search logic
293+
const queryBuilder = this.userRepository
294+
.createQueryBuilder("user")
295+
.where(
296+
"user.ename ILIKE :query OR user.name ILIKE :query OR user.ename ILIKE :fuzzyQuery OR user.name ILIKE :fuzzyQuery",
297+
{
298+
query: `%${searchQuery}%`,
299+
exactQuery: searchQuery, // Exact match for highest priority
300+
fuzzyQuery: `%${searchQuery.split('').join('%')}%` // Fuzzy search with wildcards between characters
301+
}
302+
);
303+
304+
// Add verified filter if requested
305+
if (verifiedOnly) {
306+
queryBuilder.andWhere("user.isVerified = :verified", { verified: true });
307+
}
308+
309+
// Add additional filters for consistency
310+
queryBuilder.andWhere("user.isArchived = :archived", { archived: false });
311+
312+
return queryBuilder.getCount();
313+
};
314+
176315
getPopularSearches = async (limit: number = 10) => {
177316
// This could be enhanced with actual search analytics in the future
178317
// For now, return some sample popular searches based on verified users

0 commit comments

Comments
 (0)