Skip to content

Commit c001d3f

Browse files
authored
Merge pull request #72 from GeneralMagicio/add-search-functionality
Add semantic search functionality
2 parents 6740851 + b483a75 commit c001d3f

File tree

7 files changed

+112
-41
lines changed

7 files changed

+112
-41
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-- Add search vector column
2+
ALTER TABLE "Poll" ADD COLUMN "searchVector" tsvector;
3+
4+
-- Create function to update search vector
5+
CREATE OR REPLACE FUNCTION poll_search_vector_update() RETURNS trigger AS $$
6+
BEGIN
7+
NEW."searchVector" :=
8+
setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') ||
9+
setweight(to_tsvector('english', COALESCE(NEW.description, '')), 'B') ||
10+
setweight(to_tsvector('english', array_to_string(NEW.tags, ' ')), 'C');
11+
RETURN NEW;
12+
END
13+
$$ LANGUAGE plpgsql;
14+
15+
-- Create trigger to update search vector
16+
CREATE TRIGGER poll_search_vector_trigger
17+
BEFORE INSERT OR UPDATE ON "Poll"
18+
FOR EACH ROW
19+
EXECUTE FUNCTION poll_search_vector_update();
20+
21+
-- Update existing records
22+
UPDATE "Poll" SET "searchVector" = NULL;

prisma/schema.prisma

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,12 @@ model Poll {
4949
isAnonymous Boolean @default(false)
5050
participantCount Int @default(0)
5151
voteResults Json
52+
searchVector Unsupported("tsvector")? @db.TsVector
5253
author User @relation("PollAuthor", fields: [authorUserId], references: [id])
5354
votes Vote[]
5455
userAction UserAction[]
56+
57+
@@index([searchVector], type: Gin)
5558
}
5659

5760
model Vote {

src/poll/Poll.dto.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ export class GetPollsDto {
5252
@IsOptional()
5353
worldID?: string;
5454

55+
@IsString()
56+
@IsOptional()
57+
search?: string;
58+
5559
@IsOptional()
5660
@Type(() => Number)
5761
@IsInt()

src/poll/poll.service.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BadRequestException, Injectable } from '@nestjs/common';
2-
import { ActionType } from '@prisma/client';
2+
import { ActionType, Prisma } from '@prisma/client';
33
import { DatabaseService } from 'src/database/database.service';
44
import {
55
PollNotFoundException,
@@ -12,6 +12,21 @@ import { CreatePollDto, DeletePollDto, GetPollsDto } from './Poll.dto';
1212
export class PollService {
1313
constructor(private readonly databaseService: DatabaseService) {}
1414

15+
private async searchPolls(searchTerm: string): Promise<number[]> {
16+
const searchQuery = searchTerm
17+
.split(' ')
18+
.map((word) => `${word}:*`)
19+
.join(' & ');
20+
const searchResults = await this.databaseService.$queryRaw<
21+
{ pollId: number }[]
22+
>`
23+
SELECT "pollId" FROM "Poll"
24+
WHERE "searchVector" @@ to_tsquery('english', ${searchQuery})
25+
ORDER BY ts_rank("searchVector", to_tsquery('english', ${searchQuery})) DESC
26+
`;
27+
return searchResults.map((result) => result.pollId);
28+
}
29+
1530
async createPoll(createPollDto: CreatePollDto) {
1631
const user = await this.databaseService.user.findUnique({
1732
where: { worldID: createPollDto.worldID },
@@ -72,12 +87,13 @@ export class PollService {
7287
isActive,
7388
userVoted,
7489
userCreated,
90+
search,
7591
sortBy = 'endDate',
7692
sortOrder = 'asc',
7793
} = query;
7894
const skip = (page - 1) * limit;
7995
const now = new Date();
80-
const filters: any = {};
96+
const filters: Prisma.PollWhereInput = {};
8197
let userId: number | undefined;
8298

8399
if (isActive) {
@@ -88,6 +104,7 @@ export class PollService {
88104
if (isActive === false) {
89105
filters.OR = [{ startDate: { gt: now } }, { endDate: { lte: now } }];
90106
}
107+
91108
if ((userCreated || userVoted) && query.worldID) {
92109
const user = await this.databaseService.user.findUnique({
93110
where: { worldID: query.worldID },
@@ -99,25 +116,21 @@ export class PollService {
99116
}
100117
userId = user.id;
101118
} else if (userCreated || userVoted) {
102-
throw new Error('worldId Not Provided');
119+
throw new BadRequestException('worldId Not Provided');
103120
}
104121

105122
if (userCreated && userVoted) {
106-
// Get polls user voted in
107123
const userVotes = await this.databaseService.vote.findMany({
108124
where: { userId },
109125
select: { pollId: true },
110126
});
111127
const votedPollIds = userVotes.map((v) => v.pollId);
112128

113-
// Use OR condition to get polls where user voted OR user created
114129
filters.OR = [{ authorUserId: userId }, { pollId: { in: votedPollIds } }];
115130
} else {
116131
if (userCreated) {
117132
filters.authorUserId = userId;
118133
}
119-
120-
// Get polls user voted in
121134
let votedPollIds: number[] = [];
122135
if (userVoted) {
123136
const userVotes = await this.databaseService.vote.findMany({
@@ -129,15 +142,22 @@ export class PollService {
129142
}
130143
}
131144

132-
// Sorting options
133-
const orderBy: any = {};
145+
if (search) {
146+
const pollIds = await this.searchPolls(search);
147+
if (Object.keys(filters).length > 0) {
148+
filters.AND = [filters, { pollId: { in: pollIds } }];
149+
} else {
150+
filters.pollId = { in: pollIds };
151+
}
152+
}
153+
154+
const orderBy: Prisma.PollOrderByWithRelationInput = {};
134155
if (sortBy) {
135156
orderBy[sortBy] = sortOrder;
136157
} else {
137158
orderBy.endDate = 'asc';
138159
}
139160

140-
// Fetch polls with pagination
141161
const [polls, total] = await this.databaseService.$transaction([
142162
this.databaseService.poll.findMany({
143163
where: filters,

src/user/user.dto.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
import { ActionType } from '@prisma/client';
2+
import { Transform, Type } from 'class-transformer';
13
import {
2-
IsDate,
4+
IsArray,
5+
IsBoolean,
6+
IsDateString,
37
IsEnum,
8+
IsInt,
49
IsNotEmpty,
510
IsNumber,
611
IsOptional,
712
IsString,
813
Validate,
914
} from 'class-validator';
10-
import { ActionType } from '@prisma/client';
11-
import { Transform } from 'class-transformer';
1215
import { IsPositiveInteger, IsRecordStringNumber } from '../common/validators';
1316

1417
export class GetUserDataDto {
@@ -43,48 +46,47 @@ export class GetUserActivitiesDto {
4346
@IsEnum(['active', 'inactive', 'created', 'participated'])
4447
@IsOptional()
4548
filter?: 'active' | 'inactive' | 'created' | 'participated';
49+
50+
@IsString()
51+
@IsOptional()
52+
search?: string;
4653
}
4754

4855
export class UserActionDto {
49-
@IsNumber()
50-
@IsNotEmpty()
56+
@IsInt()
5157
id: number;
5258

5359
@IsEnum([ActionType.CREATED, ActionType.VOTED])
54-
@IsNotEmpty()
5560
type: ActionType;
5661

57-
@IsNumber()
58-
@IsNotEmpty()
62+
@IsInt()
5963
pollId: number;
6064

6165
@IsString()
62-
@IsNotEmpty()
6366
pollTitle: string;
6467

6568
@IsString()
66-
@IsNotEmpty()
6769
pollDescription: string;
6870

69-
@IsDate()
70-
@IsNotEmpty()
71-
endDate: Date;
71+
@IsDateString()
72+
endDate: string;
7273

73-
@IsNumber()
74-
@IsNotEmpty()
74+
@IsBoolean()
75+
isActive: boolean;
76+
77+
@IsInt()
7578
votersParticipated: number;
7679

7780
@IsString()
78-
@IsNotEmpty()
7981
authorWorldId: string;
8082

81-
@IsDate()
82-
@IsNotEmpty()
83-
createdAt: Date;
83+
@IsDateString()
84+
createdAt: string;
8485
}
8586

8687
export class UserActivitiesResponseDto {
87-
@IsNotEmpty()
88+
@IsArray()
89+
@Type(() => UserActionDto)
8890
userActions: UserActionDto[];
8991
}
9092

src/user/user.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Module } from '@nestjs/common';
2+
import { PollService } from '../poll/poll.service';
23
import { UserController } from './user.controller';
34
import { UserService } from './user.service';
45

56
@Module({
67
controllers: [UserController],
7-
providers: [UserService],
8+
providers: [UserService, PollService],
89
})
910
export class UserModule {}

src/user/user.service.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
VoteOptionException,
1313
} from '../common/exceptions';
1414
import { DatabaseService } from '../database/database.service';
15+
import { PollService } from '../poll/poll.service';
1516
import {
1617
CreateUserDto,
1718
CreateUserResponseDto,
@@ -36,12 +37,18 @@ type UserActionFilters = {
3637
gte?: Date;
3738
lt?: Date;
3839
};
40+
pollId?: {
41+
in?: number[];
42+
};
3943
};
4044
};
4145

4246
@Injectable()
4347
export class UserService {
44-
constructor(private readonly databaseService: DatabaseService) {}
48+
constructor(
49+
private readonly databaseService: DatabaseService,
50+
private readonly pollService: PollService,
51+
) {}
4552

4653
private validateWeightDistribution(
4754
weightDistribution: Record<string, number>,
@@ -124,6 +131,20 @@ export class UserService {
124131
} else if (dto.filter === 'participated') {
125132
filters.type = ActionType.VOTED;
126133
}
134+
let pollIds: number[] | undefined;
135+
if (dto.search) {
136+
pollIds = await this.pollService['searchPolls'](dto.search);
137+
}
138+
if (pollIds && pollIds.length > 0) {
139+
if (filters.poll) {
140+
filters.poll.pollId = { in: pollIds };
141+
} else {
142+
filters.poll = { pollId: { in: pollIds } };
143+
}
144+
} else if (pollIds && pollIds.length === 0) {
145+
return { userActions: [] };
146+
}
147+
127148
const userActions = await this.databaseService.userAction.findMany({
128149
where: filters,
129150
orderBy: { createdAt: 'desc' },
@@ -138,17 +159,15 @@ export class UserService {
138159
description: true,
139160
endDate: true,
140161
authorUserId: true,
162+
participantCount: true,
141163
},
142164
},
143165
},
144166
});
145167
const actions: UserActionDto[] = await Promise.all(
146-
// TODO: it's a temporary work around, should add count to Poll and User and authorWorldId to UserAction later
168+
// TODO: it's a temporary work around, should add authorWorldId to UserAction later
147169
userActions.map(async (action) => {
148-
const participantCount = await this.databaseService.userAction.count({
149-
where: { pollId: action.poll.pollId, type: ActionType.VOTED },
150-
});
151-
const authorWorldId = await this.databaseService.user.findUnique({
170+
const author = await this.databaseService.user.findUnique({
152171
where: { id: action.poll.authorUserId },
153172
select: { worldID: true },
154173
});
@@ -158,11 +177,11 @@ export class UserService {
158177
pollId: action.poll.pollId,
159178
pollTitle: action.poll.title,
160179
pollDescription: action.poll.description ?? '',
161-
endDate: action.poll.endDate,
180+
endDate: action.poll.endDate.toISOString(),
162181
isActive: action.poll.endDate >= now,
163-
votersParticipated: participantCount,
164-
authorWorldId: authorWorldId?.worldID || '',
165-
createdAt: action.createdAt,
182+
votersParticipated: action.poll.participantCount,
183+
authorWorldId: author?.worldID || '',
184+
createdAt: action.createdAt.toISOString(),
166185
};
167186
}),
168187
);

0 commit comments

Comments
 (0)