-
-
Notifications
You must be signed in to change notification settings - Fork 3
Added search endpoint using rudimentary search algorithm to calculate results based on a simple weighted score system. #412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 2 commits
c6ef1ef
ceeb71f
3a241f8
c022944
e80e654
aef359f
c55cc3f
5837aba
73c5e6b
4fc6453
2b0d00a
338aeb5
e81a751
06f7794
5ba70f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export interface SearchTermInterface { | ||
| field: string; | ||
| term: string; | ||
| options: string; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| import {Controller, Get, Inject, Optional, Query} from '@nestjs/common'; | ||
| import MongoOperationsService from '../../../services/mongo/mongo.operations.service'; | ||
| import {ApiOperation, ApiResponse, ApiTags} from '@nestjs/swagger'; | ||
| import {ApiImplicitQueries} from 'nestjs-swagger-api-implicit-queries-decorator'; | ||
| import {PAGINATION_IMPLICIT_QUERIES} from './common/rest.pagination.queries'; | ||
| import GlobalCharacterAggregateEntity from '../../data/entities/aggregate/global/global.character.aggregate.entity'; | ||
| import GlobalOutfitAggregateEntity from '../../data/entities/aggregate/global/global.outfit.aggregate.entity'; | ||
| import Pagination from '../../../services/mongo/pagination'; | ||
| import {SearchTermInterface} from '../../../interfaces/SearchTermInterface'; | ||
|
|
||
| @ApiTags('Search') | ||
| @Controller('search') | ||
| export default class RestSearchController { | ||
| constructor( | ||
| @Inject(MongoOperationsService) private readonly mongoOperationsService: MongoOperationsService, | ||
| ) {} | ||
|
|
||
| @Get() | ||
| @ApiOperation({summary: 'Searches GlobalCharacterAggregateEntity and GlobalOutfitAggregateEntity for a term'}) | ||
| @ApiImplicitQueries([...PAGINATION_IMPLICIT_QUERIES, { | ||
| name: 'type', | ||
| required: false, | ||
| description: 'The type of the data to be searched, either "characters" or "outfits". If not specified, both types will be searched.', | ||
| type: String, | ||
| enum: ['characters', 'outfits'], | ||
| }]) | ||
| @ApiResponse({ | ||
| status: 200, | ||
| description: 'The list of GlobalCharacterAggregateEntity and GlobalOutfitAggregateEntity for a search term', | ||
| type: Object, | ||
| isArray: false, | ||
| }) | ||
| async search( | ||
| @Query('searchTerm') searchTerm: string, | ||
| @Query('type') @Optional() type?: string, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would probably split this endpoint into two, one for characters and one for outfits. I also would use an actual enum as the type. Also you might want to look at the NestJS Swagger CLI plugin. |
||
| @Query('sortBy') sortBy?: string, | ||
| @Query('order') order?: string, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems odd parameter to have for a search. You want the best result at the top?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good spot, oversight |
||
| ): Promise<Array<GlobalCharacterAggregateEntity | GlobalOutfitAggregateEntity>> { | ||
| let characterResults: GlobalCharacterAggregateEntity[] = []; | ||
| let outfitResults: GlobalOutfitAggregateEntity[] = []; | ||
|
|
||
| const pagination = new Pagination({sortBy, order, page: 0, pageSize: 10}, false); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this conflicts with your search algorithm, as the subset might ignore better results. |
||
|
|
||
| if (type === 'characters' || type === undefined) { | ||
| const characterSearchTerm: SearchTermInterface = { | ||
| field: 'character.name', | ||
| term: searchTerm, | ||
| options: 'i', | ||
| }; | ||
| characterResults = await this.mongoOperationsService.searchText( | ||
| GlobalCharacterAggregateEntity, | ||
| characterSearchTerm, | ||
| {$and: [{bracket: 0}]}, | ||
| pagination, | ||
| ); | ||
| } | ||
|
|
||
| if (type === 'outfits' || type === undefined) { | ||
| const outfitNameSearchTerm: SearchTermInterface = { | ||
| field: 'outfit.name', | ||
| term: searchTerm, | ||
| options: 'i', | ||
| }; | ||
| const outfitTagSearchTerm: SearchTermInterface = { | ||
| field: 'outfit.tag', | ||
| term: searchTerm, | ||
| options: 'i', | ||
| }; | ||
|
|
||
| // First, search for outfits by tag | ||
| const outfitTagResults = await this.mongoOperationsService.searchText( | ||
| GlobalOutfitAggregateEntity, | ||
| outfitTagSearchTerm, | ||
| {$and: [{bracket: 0}]}, | ||
| pagination, | ||
| ); | ||
|
|
||
| const outfitNameResults = await this.mongoOperationsService.searchText( | ||
| GlobalOutfitAggregateEntity, | ||
| outfitNameSearchTerm, | ||
| {$and: [{bracket: 0}]}, | ||
| pagination, | ||
| ); | ||
|
|
||
| // Combine both arrays, ensuring that the tag results appear first | ||
| outfitResults = [...outfitTagResults, ...outfitNameResults]; | ||
|
|
||
| // Deduplicate outfits based on name | ||
| const outfitsMap = new Map(outfitResults.map((outfit) => [outfit.outfit.name, outfit])); | ||
| outfitResults = Array.from(outfitsMap.values()); | ||
| } | ||
|
|
||
| const searchTermLower = searchTerm.toLowerCase(); | ||
|
|
||
| // For outfits | ||
| outfitResults.forEach((outfit) => { | ||
| let score = 0; | ||
|
|
||
| // Higher weight for exact matches on tag and name | ||
| if (outfit.outfit.tag?.toLowerCase() === searchTermLower || outfit.outfit.name.toLowerCase() === searchTermLower) { | ||
| score += 100; | ||
| } else { | ||
| // Lower weight for partial matches | ||
| const searchTermRegex = new RegExp(searchTerm, 'i'); | ||
|
|
||
| if (outfit.outfit.tag?.match(searchTermRegex)) { | ||
| score += 20; | ||
| } else if (outfit.outfit.name.match(searchTermRegex)) { | ||
| score += 10; | ||
| } | ||
| } | ||
|
|
||
| outfit.searchScore = score; | ||
| outfit.searchResultType = 'outfit'; // added type field | ||
| }); | ||
|
|
||
| // For characters | ||
| characterResults.forEach((character) => { | ||
| let score = 0; | ||
|
|
||
| // Higher weight for exact matches | ||
| if (character.character.name.toLowerCase() === searchTermLower) { | ||
| score += 100; | ||
| } else { | ||
| // Lower weight for partial matches | ||
| const searchTermRegex = new RegExp(searchTerm, 'i'); | ||
|
|
||
| if (character.character.name.match(searchTermRegex)) { | ||
| score += 5; | ||
| } | ||
| } | ||
|
|
||
| character.searchScore = score; | ||
| character.searchResultType = 'character'; // added type field | ||
| }); | ||
|
|
||
| // Sort by score | ||
| characterResults.sort((a, b) => this.searchScores(a.searchScore, b.searchScore)); | ||
|
|
||
| // Combine both arrays and return combined array sorted by score | ||
| return [...characterResults, ...outfitResults].sort((a, b) => this.searchScores(a.searchScore, b.searchScore)); | ||
| } | ||
|
|
||
| private searchScores(a: number | undefined, b: number | undefined): number { | ||
| if (a && b) { | ||
| return b - a; | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevents this being persisted to the entity but allows us to add it to objects in results