Autocomplete filtered with mongodb - node.js

I would like to perform autocompletion on the name but filtered on a specific city with mongoose and nodejs.
I have a mongodb collection like this :
{
"_id" : ObjectId("6007fd9d984e2507ad452cf3"),
"name" : "John",
"city" : "A",
},
{
"_id" : ObjectId("6007ff6844d9e517e1ec0976"),
"name" : "Jack",
"city" : "B",
}
What i have done so far :
I have setup MongoDB Atlas with a Search Index (with the help of search doc)
And set up the autocomplete like that :
router.get('/search', async (request, response) => {
try {
let result = await Client.aggregate([
{
"$search": {
"autocomplete": {
"query": `${request.query.term}`,
"path": "name",
"fuzzy": {
"maxEdits": 2,
"prefixLength": 3,
},
},
},
},
{
$limit: 3
},
{
$project: {
"_id": 0,
}
}
]);
response.send(result);
} catch (e) {
response.status(500).send({message: e.message});
}
});
In front-end, with autocompleteJs :
const autoCompleteJS = new autoComplete({
data: {
src: async () => {
const query = document.querySelector("#autoComplete").value;
const source = await fetch(`${window.location.origin}/search?term=${query}`);
const data = await source.json();
return data;
},
key: ["name"],
},
trigger: {
event: ["input", "focus"],
},
searchEngine: "strict",
highlight: true,
});
So far it is working well. But I don't know how to make the autocomplete result filtered based on city. It seems that the documentation does not mention this. Do you have any leads.

Use the $where pipeline stage from the aggregation pipeline after performing your search to filter out unwanted documents. So for example,
Client.aggregate([
{
"$search": {
"autocomplete": {
"query": `${request.query.term}`,
"path": "name",
"fuzzy": {
"maxEdits": 2,
"prefixLength": 3,
},
},
},
},
{
$match: { city: 'city-name' }
},
{
$limit: 3
},
{
$project: {
"_id": 0,
}
}
]);

Use a compound operator like so which lets you have more control over your results in a performant fashion:
"$search": {
"compound" : {
"filter" : [{
"text" : { path: "city", query: "New York" }
}],
"must": [{
"autocomplete": {
"query": `${request.query.term}`,
"path": "name",
"fuzzy": {
"maxEdits": 2,
"prefixLength": 3,
},
},}
}] }
using filter will filter your results, without impacting the score. must will require that the name field will also match. Also take a look at should and mustNot in the compound docs for more options.

Related

How to update the data in existing document in mongodb

I want to update the data with in existing document and not create a other json document in the database. But its not modifing log is showing { n: 0, nModified: 0, ok: 1 }..please help me thanks in advance
this is my code:
const userUpdatedPassword = await UserPreferred.updateOne({_id: userPreferredExists._id},
{
$push: {
languages: req.body.languages,
storeType: req.body.storeType,
intrestedIn: req.body.intrestedIn
}})
}
i'm passing this data in postman:
{
"languages":["korean,"Hindi"],
"storeType":["gas station"],
"intrestedIn":["sarees"]
}
I've a json document in my db.
{
"_id" : ObjectId("60e968581871a42ab43a2345"),
"languages" : [
"English"
],
"intrestedIn" : [
"clothing",
"foot wear",
"watches",
],
"storeType" : [
"department stores",
"Medical stores"
]
}
I want in this fromat:-
{
"_id" : ObjectId("60e968581871a42ab43a2345"),
"languages" : [
"English",
"korean,
"Hindi"
],
"intrestedIn" : [
"clothing",
"foot wear",
"watches",
"sarees"
],
"storeType" : [
"department stores",
"Medical stores",
"gas station"
]
}
You can use following query as your requirment, i have tried and tested at my local , One thing you have forgot to use closing quote language array for korean
const userUpdatedPassword = await UserPreferred.updateOne({_id: userPreferredExists._id},
{
$push: {
languages: {
$each: req.body.languages
},
storeType: {
$each: req.body.storeType
},
intrestedIn: {
$each: req.body.intrestedIn
}
}
})
But this code will write duplicates in the array, so if you don't want to push duplicates in the array you can use $addToSet in place of $push, like following
const userUpdatedPassword = await UserPreferred.updateOne({_id: userPreferredExists._id},
{
$addToSet: {
languages: {
$each: req.body.languages
},
storeType: {
$each: req.body.storeType
},
intrestedIn: {
$each: req.body.intrestedIn
}
}
})

Convert mongo query to aggregate query?

I am a newbie to MongoDB. I have a use case where the mongo query should be converted to aggregate query.
I have the following two collections:
items: {_id: "something", "name": "raj", "branch": "IT", subItems: ["subItemId","subItemId"]}
subItems: {_id: "something", "city": "hyd", list: ["adsf","adsf"]}
Query passed by the user is:
{
"query": { "name": "raj", "subItems.loc": "hyd" },
"sort": { "name": 1 },
"projection": { "_id": 0, "branch": 1, "subItems.loc": 1 }
}
I am able to create a dynamic query in a following way:
let itemConditions: any[] = [];
let subItemConditions: any[] = [];
let itemProjection: {[k:string]: any} = {};
let subItemProjection: {[k:string]: any} = {};
subItemConditions.push({$in: ["$_id", "$$subItems"]}); // Default condition to search files in the subItems collection
let isAnysubItemProj = false;
Object.keys(reqQuery).forEach(prop => {
let value = reqQuery[prop];
if(prop.includes('subItems.')) {
key = prop.split(".")[1];
if(key === '_id') value = new ObjectId(value);
subItemConditions.push({$eq: [`$${prop}`, value]});
return;
}
itemConditions.push({$eq: [`$${prop}`, value]});
});
if(config.projection)
Object.keys(config.projection).forEach(prop => {
if(prop.includes('subItems.')) {
isAnysubItemProj = true;
const key = prop.split(".")[1];
subItemProjection[key] = config.projection[prop];
return;
}
itemProjection[prop] = config.projection[prop];
});
if(isAnysubItemProj) itemProjection['subItems'] = 1;
let subItemPipeline: any[] = [];
subItemPipeline.push(
{ $match: {
$expr: {
$and: subItemConditions
}
}
});
if(Object.keys(subItemProjection).length)
subItemPipeline.push({$project: subItemProjection});
let query: any[] = [
{
$match: {
$expr : {
$and: itemConditions
}
}
},
{
$addFields: {
subItems: {
$map: {
input: "$subItems",
as: "id",
in: { $toObjectId: "$$id" }
}
}
}
},
{
$lookup: {
from: "subItems",
let: {subItems: "$subItems"},
pipeline: subItemPipeline,
as: "subItems"
}
}
];
if(config.sort && Object.keys(config.sort).length) query.push({$sort: config.sort});
if(Object.keys(itemProjection).length) query.push({$project: itemProjection});
const items = await collection.aggregate(query).toArray();
The above code will work only for the comparison of equality for items and subItems separately, but the user can send different types of queries like:
{
"query": { $or: [{"name": "raj"}, {subItems: {$gt: { $size: 3}}}], "subItems.city": "hyd" },
"sort": { "name": 1 },
"projection": { "_id": 0, "branch": 1, "subItems.loc": 1 }
}
{
"query": { $or: [{"name": "raj"}, {"subItems.city": {"$in" : ["hyd", "ncr"]}}], "subItems.list": {"$size": 2} },
"sort": { "name": 1 },
"projection": { "_id": 0, "branch": 1, "subItems.loc": 1 }
}
Is there any easy way to convert this normal MongoDB query into an aggregate query or is there any other approach to implement this...??
I am trying to modify the above dynamic query to work for any queries passed by the user but it is becoming difficult to handle all queries.
Is there any better approach to handle this situation like changing the query passed by the user or the way of handling it on the server-side or how I should change my code to support all types of queries passed by the user..??
Any help will be appreciated
If this is really your input
input = {
"query": { "name": "raj", "subItems.loc": "hyd" },
"sort": { "name": 1 },
"projection": { "_id": 0, "branch": 1, "subItems.loc": 1 }
}
Then the aggregation pipeline would be simply:
let pipeline = [
{$match: input.query},
{$sort: input.sort},
{$project: input.projection}
]
db.collection.aggregate(pipeline)

How do I update a field in embedded documents based on another array in MongoDB

I am trying to update an embedded document in MongoDB using mongoose in nodejs. The document is simplified and shown below (The names in friendList is assumed to be unique):
{
"_id" : ObjectId("5eb0617f3aec924ff42249cd"),
"friendList" : [
{
"name" : "Alex",
"flag" : false,
},
{
"name" : "Bob",
"flag" : false,
},
{
"name" : "Caleb",
"flag" : true,
},
{
"name" : "Debbie",
"flag" : false,
}
]
}
I would like to update this collection by:
accepting a Patch API with a request body containing a subset of friendList and
update the nested field flag.
For example, if I were to do a patch call from postman with the request body:
{
"friendList":[
{
"name":"Alex",
"flag":true
},
{
"name":"Caleb",
"flag":false
},
{
"name":"Debbie",
"flag":false
}
]
}
then I should expect my document in MongoDB to look like this:
{
"_id" : ObjectId("5eb0617f3aec924ff42249cd"),
"friendList":[
{
"name":"Alex",
"flag":true
},
{
"name":"Bob",
"flag":false
},
{
"name":"Caleb",
"flag":false
},
{
"name":"Debbie",
"flag":false
}
]
}
What I have tried on nodejs is updating the entire request body:
function updateUser(req){
User.findOneAndUpdate({'_id':req.params._id},req.body,{new:true});
}
which replaces the entire friendList array:
{
"_id" : ObjectId("5eb0617f3aec924ff42249cd"),
"friendList":[
{
"name":"Alex",
"flag":true
},
{
"name":"Caleb",
"flag":false
},
{
"name":"Debbie",
"flag":false
}
]
}
I have also tried using array operators like $:
function updateUser(req){
User.findOneAndUpdate(
{'_id':req.params._id},
{$addToSet:{
"friendList":{
$each:req.body.friendList}
}
},
{new:true}
);
}
which gave me the output:
{
"_id" : ObjectId("5eb0617f3aec924ff42249cd"),
"friendList" : [
{
"name" : "Alex",
"flag" : false,
},
{
"name" : "Bob",
"flag" : false,
},
{
"name" : "Caleb",
"flag" : true,
},
{
"name" : "Debbie",
"flag" : false,
},
{
"name" : "Alex",
"flag" : true,
},
{
"name" : "Caleb",
"flag" : false,
},
]
}
which $addToSet considers both name and flag when making a comparison to check if the values exist in the array. It might work if I am able to intercept at this comparison phase such that only the name field is checked.
I have been exploring concepts like $[<identifier>] and arrayFilter but can't seem to make it work.
Simple $addToSet does not work, because your array is not ["Alex","Caleb","Debbie"]. Your array is
[
{name: "Alex", flag: true},
{name: "Caleb", flag: false},
{name: "Debbie", flag: false}
]
Element {name:"Alex", flag: true} is different to element {name: "Alex", flag: false}, that's the reason why your approach failed. I think you have to use aggregation pipeline, e.g. this one:
db.collection.aggregate([
{ $addFields: { newFriends: friendList } },
{
$set: {
friendList: {
$map: {
input: "$friendList",
in: {
name: "$$this.name",
flag: {
$cond: [
{ $eq: [{ $indexOfArray: ["$newFriends.name", "$$this.name"] }, - 1] },
"$$this.flag",
{ $arrayElemAt: [ "$newFriends.flag", { $indexOfArray: ["$newFriends.name", "$$this.name"] } ] }
]
}
}
}
}
}
},
{ $unset: "newFriends" }
])
Or if you like to work with index variable:
db.collection.aggregate([
{ $addFields: { newFriends: friendList } },
{
$set: {
friendList: {
$map: {
input: "$friendList",
in: {
$let: {
vars: { idx: { $indexOfArray: ["$newFriends.name", "$$this.name"] } },
in: {
name: "$$this.name",
flag: {
$cond: [
{ $eq: ["$$idx", - 1] },
"$$this.flag",
{ $arrayElemAt: ["$newFriends.flag", "$$idx"] }
]
}
}
}
}
}
}
}
},
{ $unset: "newFriends" }
])
Note, this will update only existing names. New names are not added to the array, your question is not clear in this regard. If you like to add also new elements then insert
{
$set: {
friendList: { $setUnion: ["$friendList", "$newFriends"] }
}
},
just before { $unset: "newFriends" }
The aggregation pipeline can be used in an update:
User.findOneAndUpdate(
{'_id':req.params._id},
[
{ $addFields: { newFriends: req.body.friendList } },
{
$set: { ...
}
]
);

updating array object in mongodb using Nodejs driver

Here is my saved document
"_id": ObjectId("5e26be38c13b7149d0a95111"),
"isApproved": false,
"vendorOrder": [
{
"_id": ObjectId("5e26be38c13b7149d0a95113"),
"publicationCode": "TOI",
"publicationName": "Times of India",
"editionName": "chennai city",
"productCode": "TCE1",
"subscriptionCopies": 70,
"tradeCopies": 40
},
{
"_id": ObjectId("5e26be38c13b7149d0a95112"),
"publicationCode": "ET",
"publicationName": "Economic Times",
"editionName": "chennai city",
"productCode": "ECE1",
"subscriptionCopies": 20,
"tradeCopies": 100
}
],
"frequency": "TH",
"orderCreatedBy": ObjectId("5e25550a3405363bc4bf86c1"),
"submittedTo": ObjectId("5e2555363405363bc4bf86c2"),
Here is my mongo shell query:
db.orders.updateOne(
{
"_id": ObjectId("5e26be38c13b7149d0a95111"),
"submittedTo":ObjectId("5e2555363405363bc4bf86c2"),
"vendorOrder.productCode": "TCE1"
},
{ $set:{"vendorOrder.$.tradeCopies":80 }
})
But, only one element is getting updated MOREOVER this query is not working in nodejs.
Nodejs query:
const { orderId, dealerId, productCode, tradeCopies } = req.body;
try {
const orders = await Order.updateOne(
{
_id: orderId,
submittedTo: dealerId,
vendorOrder.productCode: productCode
}, {
$set: { vendorOrder.$.tradeCopies: tradeCopies }
}
)
} catch(ex) {
console.log(ex);
}
Goal is to update the vendorOrder:[{tradeCopies}] of all the elements in one go unique code is Product Code i.e i want to upadate tradeCopies in vendorOrder where productCode, orderId, dealerId would come from req.body and tradeCopies should get updated against the productCodes.
To update all elements ("tradeCopies" field) in the array matching a condition on the array element field ("productCode") you must use the $arrayFilters update option. This can be done both in Mongo Shell as well as the NodeJS driver's API method call updateOne.
Mongo Shell version:
db.collection.updateOne(
{ _id: ObjectId("5e26be38c13b7149d0a95111"), submittedTo:ObjectId("5e2555363405363bc4bf86c2") },
{ $set: { "vendorOrder.$[elem].tradeCopies" : 80 } },
{
arrayFilters: [ { "elem.productCode": "TCE1" } ]
}
)
The NodeJS version:
col.updateOne( { _id: orderId, submittedTo: dealerId, },
{ $set: { "vendorOrder.$[elem].tradeCopies" : 80 } },
{ arrayFilters: [ { "elem.productCode": "TCE1" } ] },
function( err, result ) {
console.log(result);
}
);
[ Edit - Add ]
Using the async / await syntax the query will be like this (for details see MongoDB NodeJS driver update examples):
result = await coll.updateOne( { ... }, { $set: { ... } }, { arrayFilters: [ { ...} ] } );

Elasticsearch full-text query issue from inner array

This is my object. and I want find text inside of task i.e task.name.
{
"_id" : ObjectId("55e2acd77e5e4ddc037b3ef5"),
"userId" : "123",
"username" : "user12",
"address" : "abc",
"number" : 928228828282.0,
"task" : [{
"name" : "metac",
"productCode" : "1234",
"_id" : ObjectId("55e2acd77e5e4ddc037b3ef7")
}, {
"name" : "alfa33",
"productCode" : "1234",
"_id" : ObjectId("55e2acd77e5e4ddc037b3ef6")
}],
"__v" : 0
}
so when I query for that it will return all task.
curl -XPOST 'localhost:9200/userprofiles/_search?pretty' -d '
{
"query": { "match": { "name": "alfa33" } }
}
Output:
{
"took": 51,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 0.19178301,
"hits": [
{
"_index": "userprofiles",
"_type": "userprofile",
"_id": "55e2acd77e5e4ddc037b3ef5",
"_score": 0.19178301,
"_source": {
"userId": "123",
"username": "user12",
"address": "abc",
"number": 928228828282,
"task": [
{
"name": "metac"
},
{
"name": "alfa33"
}
]
}
}
]
}
}
As you can see task return full array I want only 1 task which is selected.
I am using mongoosastic inside node it will give me problem so i tried to use request directly to Elasticsearch.
mongoosastic config - >elasticsearch search text return full array issue i tried this solution but not working.
Currently I am search my result using curl command in git bush not using search function
EDIT
FILE:- mongoose and mongoosastic.
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var medicineSchema = require('./search')
var mongoosastic = require("mongoosastic");
var UserProfileSchema = new Schema({
userId: String,
username: String,
address: String,
number: Number,
task: [{
name: {
type: String,
es_boost: 2.0 // or es_indexed:true
},
taskCode: String,
}]
});
UserProfileSchema.plugin(mongoosastic);
UserProfileSchema.plugin(mongoosastic, {
host: "localhost",
port: 9200,
// ,curlDebug: true
});
UserProfile = module.exports = mongoose.model('UserProfile', UserProfileSchema);
UserProfile.createMapping(function(err, mapping) {
if (err) {
console.log('error creating mapping (you can safely ignore this)');
console.log(err);
} else {
console.log('mapping created!');
console.log(mapping);
}
});
And my search Query:
var UserProfileSchema = require('../../app/models/user');
UserProfileSchema.search({
query_string: {
query: name
}
}, function(err, result) {
if (err) {
callback({
RESULT_CODE: '-1',
MESSAGE: 'System error'
});
} else {
callback({
RESULT_CODE: '1',
DATA: result
});
}
});
In order to treat these as separate objects, you will need to use a nested type
for the "task" field, so setting up your mappings as follows:
{
"mappings": {
"doc": {
"... all the other fields ...": {},
"task": {
"properties": {
"_id": {
"type": "string"
},
"name": {
"type": "string"
},
"productCode": {
"type": "string"
}
},
"type": "nested"
}
}
}
}
After reindexing your document. You'll need to use a nested query with an
inner-hits query to return which task matched the query:
{
"query": {
"nested": {
"path": "task",
"query": {
"match": {
"name": "alfa33"
}
},
"inner_hits": {}
}
}
}
The inner_hits portion will return the specific task that matched the query by
itself.
I've posted an example with the
full output here since it's a
little long to post in entirety here.

Resources