'Boost search score from data in another collection

I use Atlas Search to return a list of documents (using Mongoose):

const searchResults = await Resource.aggregate()
   .search({
       text: {
           query: searchQuery,
           path: ["title", "tags", "link", "creatorName"], 
       },
   }
   )
   .match({ approved: true })
   .addFields({
       score: { $meta: "searchScore" }
   })
   .exec();

These resources can be up and downvoted by users (like questions on Stackoverflow). I want to boost the search score depending on these votes.

I can use the boost operator for that.

Problem: The votes are not a property of the Resource document. Instead, they are stored in a separate collection:

const resourceVoteSchema = mongoose.Schema({
    _id: { type: String },
    userId: { type: mongoose.Types.ObjectId, required: true },
    resourceId: { type: mongoose.Types.ObjectId, required: true },
    upDown: { type: String, required: true },

After I get my search results above, I fetch the votes separately and add them to each search result:

for (const resource of searchResults) {
    const resourceVotes = await ResourceVote.find({ resourceId: resource._id }).exec();
    resource.votes = resourceVotes
}

I then subtract the downvotes from the upvotes on the client and show the final number in the UI.

How can I incorporate this vote points value into the score of the search results? Do I have to reorder them on the client?

Edit:

Here is my updated code. The only part that's missing is letting the resource votes boost the search score, while at the same time keeping all resource-votes documents in the votes field so that I can access them later. I'm using Mongoose syntax but an answer with normal MongoDB syntax will work for me:

const searchResults = await Resource.aggregate()
            .search({
                compound: {
                    should: [
                        {
                            wildcard: {
                                query: queryStringSegmented,
                                path: ["title", "link", "creatorName"],
                                allowAnalyzedField: true,
                            }
                        },
                        {
                            wildcard: {
                                query: queryStringSegmented,
                                path: ["topics"],
                                allowAnalyzedField: true,
                                score: { boost: { value: 2 } },
                            }
                        }
                        ,
                        {
                            wildcard: {
                                query: queryStringSegmented,
                                path: ["description"],
                                allowAnalyzedField: true,
                                score: { boost: { value: .2 } },
                            }
                        }
                    ]
                }
            }
            )
            .lookup({
                from: "resourcevotes",
                localField: "_id",
                foreignField: "resourceId",
                as: "votes",
            })
            .addFields({
                searchScore: { $meta: "searchScore" },
            })
            .facet({
                approved: [
                    { $match: matchFilter },
                    { $skip: (page - 1) * pageSize },
                    { $limit: pageSize },
                ],
                resultCount: [
                    { $match: matchFilter },
                    { $group: { _id: null, count: { $sum: 1 } } }
                ],
                uniqueLanguages: [{ $group: { _id: null, all: { $addToSet: "$language" } } }],
            })
            .exec();


Solution 1:[1]

It could be done with one query only, looking similar to:

Resource.aggregate([
  {
    $search: {
      text: {
        query: "searchQuery",
        path: ["title", "tags", "link", "creatorName"]
      }
    }
  },
  {$match: {approved: true}},
  {$addFields: {score: {$meta: "searchScore"}}},
  {
    $lookup: {
      from: "ResourceVote",
      localField: "_id",
      foreignField: "resourceId",
      as: "votes"
    }
  }
])

Using the $lookup step to get the votes from the ResourceVote collection

If you want to use the votes to boost the score, you can replace the above $lookup step with something like:

{
    $lookup: {
      from: "resourceVote",
      let: {resourceId: "$_id"},
      pipeline: [
        {
          $match: {$expr: {$eq: ["$resourceId", "$$resourceId"]}}
        },
        {
          $group: {
            _id: 0,
            sum: {$sum: {$cond: [{$eq: ["$upDown", "up"]}, 1, -1]}}
          }
        }
      ],
      as: "votes"
    }
  },
  {$addFields: { votes: {$arrayElemAt: ["$votes", 0]}}},
  {
    $project: {
      "wScore": {
        $ifNull: [
          {$multiply: ["$score", "$votes.sum"]},
          "$score"
        ]
      },
      createdAt: 1,
      score: 1
    }
  }

As you can see on this playground example

EDIT: If you want to keep the votes on the results, you can do something like:

db.searchResults.aggregate([
   {
    $lookup: {
      from: "ResourceVote",
      localField: "_id",
      foreignField: "resourceId",
      as: "votes"
    }
  },
  {
    "$addFields": {
      "votesCount": {
        $reduce: {
          input: "$votes",
          initialValue: 0,
          in: {$add: ["$$value", {$cond: [{$eq: ["$$this.upDown", "up"]}, 1, -1]}]}
        }
      }
    }
  },
  {
    $addFields: {
      "wScore": {
        $add: [{$multiply: ["$votesCount", 0.1]}, "$score"]
      }
    }
  }
])

As can be seen here

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1