I have some data in the database used for a SEARCH BAR.
In this table the field is called searchPeople (all lowercase) and contains data like:
Name##CityName
john##rome
romeu##napoli
romeu2##milan
So the user types on the SEARCH BAR some thing Rome, people that contain Rome in either their name or city. The search works well but I would like to "PRIORITIZE" the exact match String on top of the array. currently the data comes in random by the database order
{
name: 'John',
city: 'Rome'
}
Should be on top because the city matches === the string given by the user. THis can either be the name or city, I just gave an example using city match
const people = [{
name: 'Romeu',
city: 'Napoli'
},
{
name: 'John',
city: 'Rome' // this object should be first because there is a matching result
},
{
name: 'Romeu2',
city: 'Milan'
}
];
console.log(people);
// How can I sort people array with most relevant results on top?
Is there a way to sort my array to put the more correct search results on top?
You can do it like this:
sort - to sort original array
includes - to check if user input is exact match of either name or city property.
const data = [
{ name: 'Romeu', city: 'Napoli' },
{ name: 'John', city: 'Rome' },
{ name: 'Romeu2', city: 'Milan' },
];
const result = data.sort((a, b) => [a.name, a.city].includes('Rome') ? -1 : 0);
console.log('Result: ', result);
people.sort((a, b) => {
if(a.city.toLowerCase() === '*user-input*'.toLowerCase() || a.name.toLowerCase() === '*user-input*'.toLowerCase()) {
return -1;
}
return 0;
})
you can try this.
What you're speaking about is called 'relevance'. And you need somehow calculate it. And first you need to define it somehow. What is considered as more relevant in this particular search? Is name match more relevant than city match? Strict match is more relevant than partial match, that's clear.
So, strict match contributes to relevance value more that a partial match. For instance, strict match on field 'city' might give a value of 10 to a relevance score. While partial match on the beginning of a name can give value of 5. And maybe some variations of partial match contribute more than others.
For instance, search term me might contribute 1 for name Romeu and contribute 2 for city Rome (because matching the end might be defined as a more relevant than matching some center part of the word). And so on.
Does this do what you want?
Function reorder accepts the array to sort, the field to sort on, and the query string. It constructs a regular expression that looks for the exact word, delineated by word boundaries; this is then used to sort the array on the basis of matches.
If you want to sort on a second field (eg 'name'), then you could run this function again on that field.
const people = [{
name: 'Romeu',
city: 'Napoli'
}, {
name: 'John',
city: 'Rome'
}, {
name: 'Romeu2',
city: 'Milan'
}]
const reorder = ({ arr, field, q }) => {
let r = new RegExp(`\\b${q}\\b`, 'u')
return arr.sort(({ [field]: aField }, { [field]: bField }) => {
if(r.test(aField) && !r.test(bField)) return -1
if(!r.test(aField) && r.test(bField)) return 1
return 0
})
}
console.log(reorder({ arr: people, field: 'city', q: 'Rome' }))
Related
Let's say I have some code, like this:
const filter = {
address: 'India',
name: 'Aleena'
};
const users = [{
name: 'John Doe',
email: 'johndoe#mail.com',
age: 25,
address: 'USA'
},
{
name: 'Aleena',
email: 'aleena#mail.com',
age: 35,
address: 'India'
},
{
name: 'Mark Smith',
email: 'marksmith#mail.com',
age: 28,
address: 'England'
}
];
const filteredUsers = users.filter((item) => {
for (var key in filter) {
if (item[key] === undefined || item[key] != filter[key])
return false;
}
return true;
});
How can I dynamically update/change the filter object to allow users to choose which key:values to use in the filtering process? I know people normally use React for this kind of stuff, but I wondered if there was a "vanilla" way to do it.
Actually, filter does it for you already. Filter returns a new filtered array without mutating the original array.
"Users" is the original array. "FilteredUsers" is the newly created filtered array off users.
To clone/copy the original Users array above, you can do:
let clonedArray = [...Users]
The users array is a perfect candidate for this. Let's say you want to add all of the users ages together. It's pretty cool-simple.
1- function addAges(acc,ele) {return acc + ele.age}
2- Users.reduce(addAges, 0);
That'it. No console.log either. It'll returned the sum
here's a very basic example in CodeSandBox to get an idea of what to do.
I believe it meets the requirements of your question
https://codesandbox.io/s/vanila-js-dynamic-filter-6yhhe6?file=/src/index.js
Sorry for bad title, I don't really know how to phrase this and this might be trivial problem ...
The data that comes from the array looks like this, each name can have an indefinite amount of sequence, what I want to do is group them by name and put each sequence in an array
[
{
name: 'Mike',
sequence: 'AAAA',
},
{
name: 'Bob',
sequence: 'ABAB',
},
{
name: 'Bob',
sequence: 'AAAB',
},
{
name: 'Marvin',
sequence: 'AAAA',
},
{
name: 'Marvin',
sequence: 'AABA',
},
{
name: 'Marvin',
sequence: 'BBBB',
},
]
What I am looking to return for each name by using console.log(name, array) for example would be something like this
Mike ["AAAA"]
Bob ["ABAB","AAAB"]
Marvin ["AAAA","AABA","BBBB"]
Thank you very much!
As mentioned in the comments, it seems you have tried some ways to solve the problem.
You can try following solution
Use Array.reduce to convert your array into an object with keys as name and value as array of sequences
In the reduce function, check whether the name exist in the resultant object. If it exists, concat the sequence to it (using spread syntax) else add a new entry with an array with sequence.
let input = [{name:'Mike',sequence:'AAAA',},{name:'Bob',sequence:'ABAB',},{name:'Bob',sequence:'AAAB',},{name:'Marvin',sequence:'AAAA',},{name:'Marvin',sequence:'AABA',},{name:'Marvin',sequence:'BBBB',}];
let result = input.reduce((a, {name, sequence}) => Object.assign(a, {[name] : a[name] ? [...a[name], sequence]: [sequence]}), {});
console.log(result);
inputArray.reduce((acc,{name,sequence}) => {
let obj = acc.find(a => a.name === name);
obj ? obj.sequence.push(sequence)
: acc.push({name,sequence:[sequence]});
return acc;
}, [])
Forgive the title, but basically this is what I'm trying to do:
I have an array of objects, lets call them dogs for this example. Each object looks something like this:
{
name: "Poodle",
keywords: ["Fluffy", "Intelligent", "Hypo-allergenic", "Loyal"]
}
The user picks three words, and then I want to search the array of objects, and match these three words against the 'keywords' property on each array. Then I have some display logic:
If the dog matches on all three of the users chosen words, I only want to return that one dog.
If there are only dogs that match on 2 words, then I want to bring a max of 2 of these dogs.
If there is only a single 2-word dog and then multiple 1 dog matches, just show the 2 word dog.
If there are only 1-word dog matches, then show a maximum of three of these.
Basically, I'm not asking how to do the bullet points, I can figure that out, but ideally I need an array of dogs and number of matches next to each one. I can then do some filtering on this.
So I need an array of dog names, with a match total, something like this ( unless of course there is a better way then I'm all ears ):
[
["Poodle", 2],
["Labrador", 2],
["Schpitz", 1],
["Setter", 0],
["Pit Bull", 0]
]
Does this sound like the best approach, or is there a simpler/better way?
No it does not sound like the best approach, because you are saving something that is essentially an object in an array structure. A better approach would be to make an array of result objects as the outcome.
With the following snippet you get an array of objects consisting of the dog name and all the matched keywords.
You can easily get the number of keywords matched from it (by using result[i].matched.length), but I think this is more practical, as you can also show which keywords actually matched.
const data = [
{
name: "Poodle",
keywords: ["Fluffy", "Intelligent", "Hypo-allergenic", "Loyal"]
},
{
name: "Husky",
keywords: ["Fluffy", "Cute", "Stuff", "IdkMuchAboutDogs"]
}
];
const keywords = ["Fluffy", "Cute", "Intelligent"];
const result = data.map(dog => {
return {
dog: dog.name,
matches: keywords.filter(keyword => dog.keywords.indexOf(keyword) !== -1)
};
});
console.log(result);
If you want to have more logic on the keyword comparing, you need to do something like
...
matches: keywords.filter(keyword =>
dog.keywords.filter(dogkeyword =>
// Now you can compare those two keywords with more complex algorithms
dogkeyword.toLowerCase() == keyword.toLowerCase()
).length !== 0
)
...
or similar.
You can generate score by your wish:
let dogs = [
{
name: "Poodle",
keywords: ["Fluffy", "Intelligent", "Hypo-allergenic", "Loyal"]
},
{
name: "Dog2",
keywords: ["Fluffy", "Intelligent", "Hypo-allergenic",]
},
{
name: "Dog3",
keywords: ["Fluffy"]
}
]
The scoring:
let scored = words => dogs.map(dog => {
// you can add modification for 2 words term
let score = dog.keywords.filter(kw => words.includes(kw)).length
return {
name: dog.name,
score
}
})
And then sort by best score:
let sorted = dogs => dogs.sort((a,b) => b.score - a.score)
console.log(sorted(scored(['Fluffy', 'Loyal', "Intelligent"])))
{name: "Poodle", score: 3}
{name: "Dog2", score: 2}
{name: "Dog3", score: 1}
It's not optimal but you can modify it as you go.
I am pulling a database query that has the following info:
id, name, roleId, roleTitle
In the query, I am pulling for users and their roles. Each user can have 0 to N number of roles. I want to in the end have an object like this:
{
id
name
roles: [{
id
title
}]
}
What would the most efficient way of doing this be? Currently I am doing something like this:
const data = [];
arr.forEach((u) => {
const index = data.findIndex(x => x.id === u.id);
if (index >= 0) {
data[index].roles.push({ id: u.roleId, title: u.roleTitle });
} else {
data.push({
id: u.id,
name: u.name,
roles: u.roleId ? [{
id: u.roleId,
title: u.roleTitle,
}] : [],
});
}
}
This solution works correctly but wasn't sure if this was the fastest way to get this done if we scale the user numbers to 10k with an average role per user of 3 or 50k and 5 roles per user
Your best bet is actually to do this all in SQL, since you are using PostgreSQL for your database (as mentioned in comments). I don't know the exact names of your tables and columns, so you may need to tweak this, but this will get you what you want:
SELECT json_agg(t)
FROM (
SELECT
u.id,
u.name,
ro.roles
FROM "user" u
LEFT JOIN (
SELECT
ur.user_id,
json_agg(
json_build_object(
'id', r.id,
'title', r.title
)
) AS roles
FROM user_role ur
LEFT JOIN "role" r ON r.id = ur.role_id
GROUP BY ur.user_id
) ro ON ro.user_id = u.id
) t;
SQL Fiddle: http://www.sqlfiddle.com/#!17/5f6ca/11
Explanation
json_build_object will create an object using the name / value pairs specified, so:
json_build_object(
'id', r.id,
'title', r.title
)
Combines the role id and title into a JSON object like this:
{id: 1, title: "Title 1"}
json_agg aggregates multiple rows into a JSON array, so it converts the role objects above into a single column that is an array of role objects per user (thanks to the GROUP BY u.id part of the inner subquery). The inner subquery gives us a result set like this (one row per user)
| user_id | roles |
|---------|------------------------------------------------------|
| 1 | [{id: 1, title: "Role 1"}, {id: 2, title: "Role 2"}] |
Then the subquery is joined to the user table, and all of that is wrapped in another subquery so json_agg can be used on the entire result and return a single json object that is an array of users with roles.
This almost certainly isn't the most efficient possible version but is faster than what you're doing now:
const data = Object.values(arr.reduce((obj, {id, name, roleId, roleTitle}) => {
if (!(id in obj)) {
obj[id] = {
id,
name,
roles: {},
};
}
if (!obj[id].roles[roleId]) {
obj[id].roles[roleId] = {
id: roleId,
title: roleTitle,
};
}
return obj;
}, {}));
By using objects (hashes) instead of arrays, determining if the user is already there or if the user already has a role is a constant-time O(1) operation (the cost of the hashing function). But searching an array, depending on the search method used, is linear in the worst case O(n) and even the best case is O(log n).
You could go down the rabbit hole of micro-optimizations that will change with the wind, but choosing the correct data structures and algorithms will usually get you the most bang for your optimization buck.
I've used Object.values to convert back to an array at the end, if you omit this and just stick with objects it could be even faster.
Hope this helps.
var modified_array = function(xs, key) {
return xs.reduce(function(rv, x) {
obj = (rv[x[key]] = rv[x[key]] || {});
obj.id = x.id;
obj.name = x.name;
obj.roles = obj.roles || []
obj.roles.push({ id: x.roleId, title: x.roleTitle})
return rv;
}, {});
};
arr = [{id:1,name:"abd",roleId: 10,roleTitle: "hello"},
{id:1, name: "abd", roleId: 15,roleTitle: "fello"}]
console.log( Object.values(modified_array(arr, 'id')));
I have an array of objects that I want to filter for a string. So I want to check multiple properties if they contain the filter string (case insensitive).
Here's the array:
[{
id: "01234",
name: "My Object 01234",
short_name: "MO01234"
}, ...]
So all of the following filter strings should match that object: 0123, obj, mO01 etc.
Here's what I have right now:
const filterString = this.filterString.toLowerCase();
return myObjects.filter(
entry => {
return
entry.id.toLowerCase().indexOf(filterString) >= 0 ||
entry.name.toLowerCase().indexOf(filterString) >= 0 ||
entry.short_name.toLowerCase().indexOf(filterString) >= 0;
}
);
Can you think of a faster/cleaner way to do that?
I don't think that you can do it faster, but cleaner may be something like that
const filterString = this.filterString.toLowerCase();
return myObjects.filter((entry) => {
return Object.values(entry).some((value) => {
return value.toLowerCase().includes(filterString)
})
});
If you are allowed to put additional properties in your object, perhaps you could concatenate id, name and short_name (already in lowercase) into a single string and store it in the object as e.g. search_key; then you'd only have to check that.
{
id: "01234",
name: "My Object 01234",
short_name: "MO01234",
search_key: "01234:my object 01234:mo01234"
}
return myObjects.filter(
entry => entry.search_key.indexOf(filterString) >= 0
);
One thing you have to be mindful of in this case is to prevent unintended matches that may arise because e.g. the last few characters of id and the first few characters of name together produce a match. This is why I used a : delimiter here, assuming that's a character that can't appear in an ID or a short name.
let objects = [{
id: "01234",
name: "My Object 01234",
short_name: "MO01234"
},
{
id: "test",
name: "test",
short_name: "test"
}];
const filter = (collection, searchFor) => {
return collection.filter(obj => Object.values(obj).reduce((a,b) => a || String(b).toLowerCase().indexOf(searchFor.toLowerCase()) > -1, false))
}
console.log(filter(objects, "0123"));
console.log(filter(objects, "obj"));
console.log(filter(objects, "mO01"));
You could also extend this function to take a set of columns as parameter to filter on.
Another version using Regex:
const filterRegex = (collection, searchFor) => {
return collection.filter(obj => Object.values(obj).reduce((a,b) => a || String(b).match(new RegExp(searchFor, 'gi')), false))
}
console.log(filterRegex(objects, "0123"));
console.log(filterRegex(objects, "obj"));
console.log(filterRegex(objects, "mO01"));