티스토리 뷰

728x90
반응형

목표사항

 

상품검색

mysql like 처럼 상품명 전부 입력하지 않는다하더라도

일부만 입력한  일부를 입력하면

일부라도 갖고있다면 검색결과에 나오도록 구현하는 것을 목표로했다.

 

 

국가별 검색/ 카테고리 검색 

국가와 카테고리 검색은 옵션이다. 

 

최신순

- 상품을 등록한 날짜가 최신일수록 가장 먼저 나오도록 구현하는 것을 목표로 했다.

- 꼭 query에 넣지 않아도 최신순 검색을 기본으로 하고 있다.

 

주문날짜순

- 상품의 주문마감일이 현재와 가까울 수록 가장 먼저나오도록 구현하는 것을 목표로 했다.

- 단, 주문마감일은 지나지 않은 데이터만을 나타내도록 했다.

 

typeORM은 검색이 쉬우나, 몽고디비에서는 생각보다 쉽지 않았다.

검색하려는 상품명이  null 일 수 있고

마찬가지로 주문날짜순, 국가별검색, 카테고리 검색조건이 없어도 모두 null일 수있다.

 

그때 내 판단은 검색조건이 Null 인지 아닌지 체킹해야되는지를 따져야되는 $cond(if, then, else) 를 넣어서 검색해보자고 판단했다.

그러나 검색조건에 맞지 않은 데이터 전부를 응답한.. 내가 생각하는 방향성과 다른 결과가 나왔다..😔


고민사항

  • populate 시킨 이후에 populate 된 데이터를 가지고 정렬을 할 수 있을까?
  • populate를 시킨 이후에  populate 된 데이터를 가지고 검색을 할 수 없을까?

 

1) populate 이후에 populate 된 데이터를 가지고 정렬하기 (populate after sort)

const result = await this.marketModel
	.find()
	.populate('product', 
		['_id', 'name', 'price', 'createdAt' ], 
		null, 
		{ sort: { 
			createdAt: search.createdAt, // 등록일(createdAt)을 기준으로 정렬 (값 존재시: 내림차순)
			closeDate: search.closeDate  // 주문마감일(closeDate)을 기준으로 정렬 (값 존재시: 오름차순)
		});

[참고]

 

How to sort a populated document in find request?

I would like to sort a populated Document from the collection i fetch, i get an error requesting it. Let's admit a Document Group (Group) and 'Member' (Group.Members) Group .find({}) .populate('

stackoverflow.com

 

[발생에러]

populate() 는 참조키 필드로 다른 다큐먼트 불러올수 있어도 참조필드를 통해 불러온 다큐먼트의 데이터를 이용할 수 없다.

- 1차 시도

const result = await this.marketModel
	.find({ product.name: search.name }) // <- 오류 발생 (eslint 오류)
	.populate('product', ['_id', 'name', 'price'])

 

- 2차 시도

const result = await this.marketModel
	.find({ product: { name: { $eq: search.name }}}) // <- 오류 발생
	.populate('product', ['_id', 'name', 'price'])

[Nest] 64364 - 2022. 11. 15. 오후 7:17:29 ERROR [ExceptionsHandler] Cast to ObjectId failed for value "{ name: { '$eq': 'black padding' } }" (type Object) at path "product" for model "Market" CastError: Cast to ObjectId failed for value "{ name: { '$eq': 'black padding' } }" (type Object) at path "product" for model "Market"

 

 

 

2) populate 이후 검색조건이 맞는 데이터를 추출

- 그러나 검색조건이 맞지 않은 데이터의 경우에는 상품정보가 null 이 나왔다.

- sql의 join 과 다르게 검색조건에 맞지않아도, product 영역만 null일뿐 모든 데이터가 검색결과에 나와버렸다.

내가 의도한 방향과 다른 방향의 결과였다 😢

const results = await this.marketModel
	.find()
	.populate({
    	path: 'product',
        select: [ '_id', 'name', 'price', 'createdAt', 'closeDate' ],
		match: {
        	name: { $eq: search.name }, // 상품명 검색
			sort: {
            		createdAt: search.createdAt,
                	closeDate: search.closeDate  
                }
		}
	})

 

상품명과 페이지만 쿼리에 포함하여 아래 와 같이 요청한 결과가 아래 사진 이다.

[GET] /api/markets?page=1&name=black%20padding

- 요청데이터

{ page = 1, name = 'black padding' }

 

- 응답데이터 

상품명이 black padding 이 아닌 데이터는 위의 사진 주황색 박스처럼 product 가 null 로 표기되어있다.

내가 원한 방향은 상품명이 black padding인 데이터만 응답데이터 로 나타내야하는거다.

그래서 populate를 사용하지 않고 다른 방법을 모색하기로 했다.

mongoose에서 join과 같은 역할이 populate 라고는 하지만

populate가 아닌 다른 방법도 있다.

몽고디비의 꽃 aggregate 로 해보기로 했다.


aggregate

구글링 결과 aggregate로도 참조 키가 있다면 콜렉션을 조인할 수 있다.

아래 코드는 2개의 콜렉션을 join 한 결과이다.

const result = await this.marketModel.aggregate([
      {
        $lookup: {
          from: 'users', // join할 콜렉션이름
          localField: 'seller', // 현재콜렉션의 외래키
          foreignField: '_id', // join할 콜렉션의 primary key
          as: 'seller', // join 이후 user 데이터를 나타내는 key값
        },
      },
    ]);

 

그런데 여기서는 검색필터없이 전체 데이터가 뿌려지는 결과이다.

필자가 목표하는 바는 '검색 상품명' , '검색 카테고리', '검색국가'  조건에 맞는 데이터를 뿌려야한다.

검색하려는 '상품명', '카테고리', '국가' 는 선택적인 옵션이다.

 

- 선택적으로 검색하기 (very hard!!!)

[시도1]

3개의 콜렉션을 aggregate 후 검색조건에 맞는 데이터만 추출되도록 $cond 를 추가했으나 linter error(코드문법에러)가 떴다.

async searchMarket(search: SearchMarket) {

    const query = this.marketModel.aggregate([
      {
        $lookup: {
          from: 'users', // user 콜렉션과 join
          localField: 'seller',
          foreignField: '_id',
          as: 'seller',
        },
      },
      {
        $lookup: {
          from: 'products', // product 데이터과 join
          localField: 'product',
          foreignField: '_id',
          as: 'product',
        },
      },
      {
        $project: {
          product: 1, // product 정보를 모두 구한다.
          seller: {
            // 판매자의 _id, name, sellerNickname 컬럼만 추출한다. (일부컬럼 추출)
            _id: 1,
            name: 1,
            sellerNickname: 1,
          },
        },
      },
      {
        // 상품명 파라미터가 null 이 아니라면 검색 가능
        // liter error 발생지점
        // $cond 영역에서 다음과 같은 'PipelineStage 형식에 할당할 수 없습니다.' 라는 에러가 나온다.
        $cond: {
          if: { 'search.name': { $ne: null } },
          then: {
            $match: {
              'product.name': { $regex: '.*' + search.name + '.*' },
            },
          },
          else: null,
        },
      },
    ]);
    return await query
      .limit(PER_PAGE) // 한 페이지당 20개
      .skip(PER_PAGE * (search.page - 1))
      .exec();
  }

 

[시도2] $match 안에 $cond 를 넣어서 선택적으로 나타내려고 했으나... unknown top level operator 에러가 발생했다.

const query = this.marketModel.aggregate([
      {
        $lookup: {
          from: 'users', // user 콜렉션과 join
          localField: 'seller',
          foreignField: '_id',
          as: 'seller',
        },
      },
      {
        $lookup: {
          from: 'products', // product 데이터과 join
          localField: 'product',
          foreignField: '_id',
          as: 'product',
        },
      },
      {
        $project: {
          product: 1, // product 정보를 모두 구한다.
          seller: {
            // 판매자의 _id, name, sellerNickname 컬럼만 추출한다. (일부컬럼 추출)
            _id: 1,
            name: 1,
            sellerNickname: 1,
          },
        },
      },
      {
        $match: {
          // 조건1(필수): 상품은 삭제되지 않은 상태이다.(ok)
          'product.deletedAt': null,
          $or: [
            // 조건2(선택): 상품명(search.name) 가 null 이 아니라면 검색 가능
            {
              $cond: {
                if: { $exists: { 'search.name': { $not: null } } },
                then: {
                  // search.name like 검색 
                  'product.name': { $regex: '.*' + search.name + '.*' },
                },
                else: {},
              },
            },
            // 조건3(선택): 상품 카테고리(search.category)가 null이 아니라면 검색 조건 추가
            // 조건4(선택): 상품 구매국가(search.buyCountry)가 null 이 아니라면 검색조건 추가
          ],
        },
      },
    ]);


return await query.sort({'product.createdAt': -1}).exec()
[에러메시지]

ERROR [ExceptionsHandler] unknown top level operator: $cond. 
If you have a field name that starts with a '$' symbol, consider using $getField or $setField.

 

 

아래 stackoverflow 질문자 글도, $match 블록안에 $cond를 넣어서 조건에 따라 검색을 시도하다가 에러발생하는 필자와 같은 상황을 겪었다. 물론 $expr 도 사용해서 시도해봤지만 에러가 발생했다.

$cond 옵션은  $project 옵션 블록에서는 가능하지만

하지만 $match 옵션 블록 내에서는 $cond를 이용하여 선택적인 검색을 할 수 없다고 한다.

- 필자가 사용하는 몽고디비(Mongodb atlas)의 버젼은 5.0.13 이다.

 

this.marketModel.aggregate([
  $match: {
    '필드명': {
      $cond: {
        if: { 조건 },
        then: { 조건이 true 일때 },
        else: { 조건이 false 일때 }
      }
    }
  }
])

 

this.marketModel.aggregate([
  $match: {
    $expr:{
        '필드명': {
          $cond: {
            if: { 조건 },
            then: { 조건이 true 일때 },
            else: { 조건이 false 일때 }
          }
        }
    }
  }
]);

 

 

Use $cond within $match in mongoDB aggregation

i've tried to use $cond within $match in one of the stages of an aggregation as shown below : { "$match" : { "field1" : { "$cond" : { "if" : { "$eq" : [ "$id...

stackoverflow.com

 

 

How should I put $cond into $match?

Hi @vid_Proli, If your objective is to find: Then you don’t need to add a $project stage. The most efficient and straightforward approach for this would be to use a $match stage and then count the number of documents in the next stage. Also, the followin

www.mongodb.com

 

 

공식다큐먼트 예제에서도 $cond는 aggregate 연산할때 $project 내부블록에서 사용할 수 있다.

$project는 $aggregate 이후 나타내고 싶은 컬럼을 숫자로 지정하여 결과로 나타내게 해주는 옵션이다.

예를들면 aggregate연산이후에 이름(name) 컬럼에  대한 데이터가 꼭 필요하다면

  name: 1  로 지정하면

결과데이터에는 name필드가 포함되어있다. 자세한 예시는 아래 공식다큐먼트의 예제를 보고 이해하면된다.

 

$cond (aggregation) — MongoDB Manual

Docs Home → MongoDB Manual $condEvaluates a boolean expression to return one of the two specified return expressions.The $cond expression has one of two syntaxes:{ $cond: { if: , then: , else: } }Or:{ $cond: [ , , ] }$cond requires all three arguments (i

www.mongodb.com


aggregate 연산순서

몽고디비의 aggregate 연산을 수행할 때  처리 단계가 존재한다.

collection > $project > $match > $group > $sort > $skip > $limit > $unwind > $out

 

[참고]

 

MongoDB Aggregate

오늘은 MongoDB의 아주 강력한 툴인 aggregate에 대해서 배워보고자 한다. 간단한 예시로 시작을 해보자...

blog.naver.com


해결코드

aggregate를 이용하여, 2개이상의 콜렉션을 join시켜서 조건검색에 맞는 데이터 조회를 마쳤다!

특히 선택적으로 검색하는 과정이 어려웠다. 해결방안을 찾아보고 적용하다가 에러가 계속발생하고 또 해결방안찾다가...

결국 생각한건 if문을 추가하여 초기값(null)을 변경하여 쿼리문에 적용하는 방식으로 했다.

(+ 더 좋은 쿼리문이 있다면 댓글로 공유부탁드립니다!! 🙇🏻‍♀️)

 

- searchNameQuery(상품명 검색), searchCategoryQuery(상품 카테고리 검색), searchBuyCountryQuery(상품 구매국가검색)  값을  null  로 초기화 했다.

- 만일 상품명 검색이 존재한다면, searchNameQuery 값을 null 이 아닌 다른 값으로 변경했다.

- sortCloseDateQuery(주문마감일 조건)이 존재한다면, searchNameQuery와 비슷한 방식으로 해결했다.

  async searchMarket(search: SearchMarket) {
    // (선택) 상품명(search.name) 가 null 이 아니라면 검색 쿼리 추가
    let searchNameQuery = null;
    if (search.name) {
      searchNameQuery = {
        'product.name': { $regex: '.*' + search.name + '.*' },
      };
    }

    // (선택) 상품 카테고리(search.category)가 null이 아니라면 검색 쿼리 추가
    let searchCategoryQuery = null;
    if (search.category) {
      searchCategoryQuery = {
        'product.category': { $in: [search.category] },
      };
    }

    // (선택) 상품 구매국가(search.buyCountry)가 null 이 아니라면 검색조건 추가
    let searchBuyCountryQuery = null;
    if (search.buyCountry) {
      searchBuyCountryQuery = {
        'product.buyCountry': { $in: [search.buyCountry] },
      };
    }

    const query = this.marketModel.aggregate([
      {
        $lookup: {
          from: 'users', // user 콜렉션과 join
          localField: 'seller',
          foreignField: '_id',
          as: 'seller',
        },
      },
      {
        $lookup: {
          from: 'products', // product 데이터과 join
          localField: 'product',
          foreignField: '_id',
          as: 'product',
        },
      },
      {
        $project: {
          product: 1, // product 데이터를 갖는다.
          seller: {
            // 판매자의 _id, name, sellerNickname 컬럼만 추출한다. (일부컬럼 추출)
            _id: 1,
            name: 1,
            sellerNickname: 1,
          },
        },
      },
      {
        $match: {
          // 상품은 삭제되지 않은 상태 (product.deletedAt !== null)
          'product.deletedAt': null,
        },
      },
      {
        $match: {
          // 상품명 검색 쿼리
          ...searchNameQuery,

          // 상품 카테고리 검색 쿼리
          ...searchCategoryQuery,

          // 상품 구매국가 검색쿼리
          ...searchBuyCountryQuery,
        },
      },
    ]);

    // 페이지네이션 추가
    query
      .limit(PER_PAGE) // 한 페이지당 20개
      .skip(PER_PAGE * (search.page - 1));

    // 날짜 sort 옵션 추가
    let sortCloseDateQuery = null;
    if (search.closeDate) {
      sortCloseDateQuery = { 'product.closeDate': 1 };
    }

    return await query
      .sort({ 'product.createdAt': -1, ...sortCloseDateQuery }) // 주문마감일 조건쿼리
      .exec();
  }

 

[참고]

- aggregate를 이용하여 2개 콜렉션을 join 하기

 

How to aggregate two collections in Mongoose/MongoDB?

On my study server. I've got two collections with users and their races. Races has userId property in order to know what user made this race. I need to make consolidated object of all users and all...

stackoverflow.com

 

Mongoose $lookup in same collection doesn't work

I have the following dataset, where I have two types of objects discriminate by the field lType that can be either base or extra. extra has a ref to a base location. [ { ...

stackoverflow.com

 

- aggregate 를 이용하여 3개 콜렉션을 join 하기

 

MongoDb aggregate query three collection

I am really new to MongoDB query and practicing mongoDb query. I am using this official mongoDb-package. I am following this doc .I have three collections of data. One is bags, one is sellers and last one is cloths. Our data architecture is When seller sen

www.mongodb.com

 

- aggregate 이후 원하는 조건의 데이터를 추가하도록, 조건옵션 추가하기

 

NestJs Aggregate & $lookup & $match & Pipeline

Sample:

khajehossini.medium.com

 

 

mongodb - how to find and then aggregate

I have collection that contains documents with below schema. I want to filter/find all documents that contain the gender female and aggregate the sum of brainscore. I tried the below statement and it

stackoverflow.com

 

728x90
반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함