-
몽고디비 필드값 다중 검색 $or 사용 및 향상Project/NYAM 2023. 2. 24. 21:39
앱 마이무라의 다음 버전의 들어가는 가게 검색 api을 만든 것을 정리 하는 글입니다.
사용기술 : NestJS , MongoDB, Regex()
다음 과 같은 필드에서 가게 이름 ("store_name": "돈카츠앤우동돼랑이우랑이") 또는 가게의 메뉴를 입력 받았을때 모든 가게를 찾는 검색 쿼리를 만들려고합니다.
"store_name": "돈카츠앤우동돼랑이우랑이", "store_thumbnail_image": "store/1672723948155.jfif", "store_call": "02-969-1999", "category": { "category_name": "일식", "category_id": "63aef68a0ef2e5098a34e48d" }, "sp_categorys": [ { "category_name": "미정", "category_id": "63a9b548810d87cd6c65d581", "category_image": "test/1660831266494.jpg" } ], "store_facility": [], "store_menus": [ { "name": "메뉴판", "price": 0, "image": "store/1673416624615.jpg" }, { "name": "안심카츠세트", "price": 12000, "image": "store/1673416624779.jpg" }
처음 작성 한 코드는 다음과 같습니다. class 안의 query() 로 시작 하는 부분을 보시면 됩니다.
export class StoreUserSearchQuery { campus_id: Types.ObjectId; search: string; sort: string; constructor(data) { this.campus_id = data.campus_id; this.search = data.search.trim(); //정렬 switch (data.sort) { case 'view': this.sort = 'store_view'; break; case 'total_view': this.sort = 'store_total_view'; break; case 'rating': this.sort = 'store_rating'; break; case 'review_count': this.sort = 'store_review_count'; break; case 'store_event_active_count': this.sort = 'store_event_active_count'; break; default: this.sort = 'total_view'; break; } } query() { const query = { campus_id: this.campus_id, store_active: true, store_block: false, }; if (this.campus_id) query['campus_id'] = this.campus_id; if (this.search) { const searchOrigin = new RegExp( this.search + '|' + this.search.replace(/ /g, ''), 'g', ); const searchSplit = new RegExp(this.search.split('').join('\\s*'), 'g'); const searchSplitAll = new RegExp('.*' + this.search + '.*', 'i'); query['$or'] = [ { store_name: { $in: [searchOrigin, searchSplit, searchSplitAll] } }, { 'store_menus.name': { $in: [searchOrigin, searchSplit, searchSplitAll] } } ]; } console.log(query); return query; } }
$or 연산자를 이용해 store_name과 store_menus.name 두 필드 중 하나라도 검색어가 있는 경우를 찾을 수 있도록 작성되었습니다. 'store_name'과 'store_menus.name' 두 가지 필드에서 검색어를 찾게 됩니다.
검색어의 원본, 공백을 제거한 문자열, 그리고 해당 문자열을 포함하는 문자열을 찾습니다.
이후 $or 연산자를 사용하여 두 필드 중 어느 하나에서라도 검색어를 찾으면 결과를 반환합니다
저번 블로그에 포스팅 하지 않는 const searchSplitAll = new RegExp('.*' + this.search + '.*', 'i') 은 정규식 패턴을 생성하는 코드입니다.
코드 내부에서 사용되는 RegExp() 함수는 문자열을 정규식 패턴으로 컴파일하여 해당 패턴을 사용하여 문자열을 검색할 수 있습니다.
이 코드에서는 '.*'와 같은 특수 문자를 사용하여, 입력한 검색어(this.search)를 문자열 내에서 모든 위치에서 매칭할 수 있도록 하였습니다. 따라서 searchSplitAll은 검색어를 포함하고 있는 모든 문자열을 매칭할 수 있습니다.
'g' 플래그와 'i' 플래그의 차이점은 정규식에서 g 플래그는 전역 검색을 의미합니다.
즉, 대상 문자열에서 모든 패턴과 일치하는 모든 부분을 찾아서 반환합니다.
반면 i 플래그는 대소문자를 무시하는 검색을 의미합니다.
즉, 패턴에 상관없이 대소문자를 구별하지 않고 일치하는 모든 부분을 찾아서 반환합니다.
따라서, new RegExp('.*' + this.search + '.*', 'i')에서는 search 변수의 값을 포함하는 문자열을 대소문자를 구분하지 않고 찾습니다.
문자열 "ABC"가 주어졌을 때:
- RegExp('A').test('ABC')는 true를 반환합니다.
- RegExp('A').test('BCD')는 false를 반환합니다.
그러나 g 플래그를 사용하면, 이전 검색에서 찾은 문자열 이후부터 다시 검색을 수행합니다.
예를 들어:
- RegExp('A', 'g').test('ABC')는 true를 반환합니다. 그리고 다시 호출하면 RegExp('A', 'g').test('ABC')는 false를 반환합니다.
- RegExp('A', 'g').test('BCD')는 false를 반환합니다.
즉, g 플래그를 사용하면 하나의 문자열에서 여러 번 검색을 수행할 수 있습니다.
this.search 문자열을 포함하는 문자열이라면 어디에 있더라도 매칭이 되는 표현을 작성하는 방식은 다음과 같습니다.
new RegExp('.*' + this.search + '.*', 'i')
위의 코드에서 * 메타 문자는 0번 이상 반복을 나타냅니다 즉 ' .* ' 패턴은 아무 문자나 0번 이상 반복되는 패턴을 나타냅니다.
'.*' + this.search + '.*'은 this.search 문자열이 아무 위치에 있더라도 포함된 모든 문자열을 매칭하는 정규표현식 패턴을 생성합니다.
즉, 이 패턴은 this.search 문자열을 포함하는 문자열이라면 어디에 있더라도 매칭이 가능합니다.
ex) this.search가 'abc' 라면, 'xyzabc'나 'defgabc'와 같은 문자열도 매칭됩니다.
이 패턴을 사용하여 검색 쿼리를 생성하면 상대적으로 느린 성능이 발생할 수 있습니다.
몽고디비에서 g 플래그는 정규 표현식에서 전역 매칭(Global matching)을 나타내는 플래그입니다. 이 플래그를 사용하면 문자열 전체에서 정규 표현식과 일치하는 모든 부분을 찾아 반환합니다.
예를 들어, 다음과 같은 restaurants 컬렉션이 있다고 가정해보겠습니다.
[ { "name": "Pizza Place", "address": { "street": "123 Main St", "city": "Anytown", "state": "CA", "zip": "12345" } }, { "name": "Burger Joint", "address": { "street": "456 Elm St", "city": "Anytown", "state": "CA", "zip": "12345" } }, { "name": "Taco Stand", "address": { "street": "789 Maple St", "city": "Anytown", "state": "CA", "zip": "12345" } } ]
다음 몽고디비 쿼리는 name 필드에서 "Pizza"라는 문자열을 찾아 반환합니다.
db.restaurants.find({ "name": /Pizza/g })
db.restaurants.find({ "name": Pizza}) 라고 검색을 할시 결과는 나오지 않습니다.
하지만, 이 g 플래그는 일치하는 모든 패턴을 찾을 때까지 문자열 전체를 검색하므로 성능에 영향을 미칠 수 있습니다.
특히 큰 문자열에서는 이러한 성능 문제가 더욱 심각해질 수 있습니다.
따라서, g 플래그를 사용할 때는 이러한 성능 문제를 고려하고 적절한 시기와 방법으로 사용해야 합니다.
위의 코드에 성능을 올린 코드는 다음과 같이 작성을 할 수 가 있습니다.
query() { const query = { campus_id: this.campus_id, store_active: true, store_block: false, }; if (this.campus_id) query['campus_id'] = this.campus_id; if (this.search) { const search = this.search.trim(); const searchOrigin = new RegExp(search, 'gi'); const searchSplit = new RegExp(search.split('').join('\\s*'), 'gi'); query['$or'] = [ { store_name: searchOrigin }, { 'store_menus.name': searchOrigin }, { store_name: searchSplit }, { 'store_menus.name': searchSplit }, ]; } return query; }
수정된 코드에서는 검색어의 앞뒤 공백을 제거한 뒤 검색에 사용하므로 불필요한 검색을 제거할 수 있습니다.
searchOrigin과 searchSplit를 $or 조건으로 모두 사용하여 하나의 조건으로 쿼리를 수행할 수 있도록 변경하였습니다.
이렇게 함으로써 OR 조건을 여러번 사용하여 쿼리를 실행하는 것을 방지할 수 있습니다.
'Project > NYAM' 카테고리의 다른 글
매장 리뷰수 계산 api 성능 문제 (0) 2023.02.28 "아니 디도스 공격을 받았다고요 ???" (0) 2023.02.25 MongoDB export errorPath collision at ~ remaining portion ~ (0) 2023.02.19 MongoDB,NestJs 공백 제거, 공백포함 문자열 찾기 [검색 기능 고도화] (0) 2023.02.19 "냠" 아 이제는 "마이무라" 유저 160명 돌파 그치만 서버가.. (0) 2023.02.18