ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 몽고디비 필드값 다중 검색 $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 조건을 여러번 사용하여 쿼리를 실행하는 것을 방지할 수 있습니다.

     

     

     

Designed by Tistory.