How to make fuzzy search using Lodash java-script library? - javascript

I want to make fuzzy search from array of objects using query string .
Search may be nested in search object like the following example ;
var data = [
{
"id":"1",
"name":"Ali",
"BOD":"29/10/2055",
"type":"primary",
"email":"b#v.com",
"mobile":"0100000000",
"notes":["note1" ,"note2" ,"note3"]
},
{
"id":"2",
"name":"Tie",
"BOD":"29/10/2055",
"type":"primary",
"email":"b#v.net",
"mobile":"0100000000",
"notes":["note4" ,"note5" ,"note6"]
}
];
function search(query){
// search code go here
}
// search examples
search('.net'); //expected data[1]
search('ali'); //expected data[0]
search('0110'); //expected data[0],data[1]

I made simple solution it is not that optimal but it works .
this done without score of fuzzy search for nested searching .
var data = [
{
"id":"1",
"name":"Ali",
"BOD":"29/10/2055",
"type":"primary",
"email":null,
"mobile":"010100000000",
"notes":["note1" ,"note2.nett" ,"note3"]
},
{
"id":"2",
"name":"Tie",
"BOD":"29/10/2055",
"type":"primary",
"email":"b#v.net",
"mobile":"0100000000",
"notes":["note4" ,"note5" ,"note6"]
}
];
/**
* query: query string to match with
* dataArray: data array variable, array or opject to search it
**/
function search(query,dataArray){
// search code go here
//console.log(query);
var matched = [];
//init null values
if(!dataArray)dataArray={};
if(!query)query='';
dataArray.forEach(function(obj,index){
for(var key in obj){
if(!obj.hasOwnProperty(key) || !obj[key]) continue;
if(obj[key].toString().indexOf(query) !== -1)
{
matched.push(obj );
}
}
});
return matched ;
}
// search examples .
console.log("found",search('.net',data).length);//expected data[0] data[1]
console.log("found",search('Ali',data).length);//expected data[0]
console.log("found",search('0116',data).length);//expected data[0],data[1]

Related

How to get inner array in javascript

I'm trying to get all the contents from an array.
This is the function that extracts the data for display via innerHTML:
window.location.href = 'gonative://contacts/getAll?callback=contacts_callback';
function contacts_callback(obj) {
var contactinfo = obj.contacts.map(({givenName}) => givenName) + " " +
obj.contacts.map(({familyName}) => familyName) + " " + " (" +
obj.contacts.map(({organizationName}) => organizationName) + ") " +
obj.contacts.map(({phoneNumbers.phoneNumber}) => phoneNumbers.phoneNumber) + "<br>";
document.getElementById("demo").innerHTML = contactinfo;
}
This is an example of what the input looks like when there are only 2 contacts:
{"success":true,"contacts":[
{
"emailAddresses":[],
"phoneNumbers":
[
{
"label":"unknown",
"phoneNumber":"XXX-XXXXXXX"
}
],
"givenName":"John",
"organizationName":"Apple",
"familyName":"Appleseed",
},
{
"emailAddresses":[],
"phoneNumbers":
[
{
"label":"unknown",
"phoneNumber":"XXX-XXXXXXX"
}
],
"givenName":"John",
"organizationName":"Apple",
"familyName":"Appleseed",
},
]
}
I just want the result to be listed as:
John Appleseed (Apple) XXX-XXXXXXX
John Appleseed (Apple) XXX-XXXXXXX
Two issues:
You are displaying all given names, then all family names, ...etc, each with a separate .map() call. Instead only perform one .map() call on the array and then display the properties for each iterated object.
phoneNumbers.phoneNumber is not a correct reference. phoneNumbers is an array, so you should iterate it.
Also:
Template literals make it maybe a bit easier to build the string
You can use .join("<br>") to glue the lines together with line breaks.
Here is a corrected version:
function contacts_callback(obj) {
var contactinfo = obj.contacts.map(o =>
`${o.givenName} ${o.familyName} (${o.organizationName}) ${
o.phoneNumbers.map(n => n.phoneNumber)
}`)
.join("<br>");
document.getElementById("demo").innerHTML = contactinfo;
}
// Demo
var obj = {"success":true,"contacts":[{"emailAddresses":[],"phoneNumbers":[{"label":"unknown","phoneNumber":"XXX-XXXXXXX"}],"givenName":"John","organizationName":"Apple","familyName":"Appleseed",},{"emailAddresses":[],"phoneNumbers":[{"label":"unknown","phoneNumber":"XXX-XXXXXXX"}],"givenName":"John","organizationName":"Apple","familyName":"Appleseed",},]};
contacts_callback(obj);
<div id="demo"></div>
It is hard to give an answer since is hard to tell what you can have in your phoneNumbers array and if you will also display one line for each phone number in that array.
I'll do something like this:
function contacts_callback(obj) {
let arrayContacts = [];
// Iterate over all your contacts
obj.contacts.forEach(item => {
// Iterate over each contact's phone numbers
item.phoneNumbers.forEach(phone => {
// Building your string with string interpolation and pushing to result array
// You could also add <br> or other tags needed here
arrayContacts.push(`${item.givenName} ${item.familyName} (${item.organizationName}) ${phone.phoneNumber}`);
});
});
// Return your array, use it in your innerHTNL, etc.
return arrayContacts;
}
If Your obj is called "obj", than:
const result = obj.contacts.map(contact =>{
return `${contact.givenName} ${contact.familyName} (${contact.organizationName}) ${contact.phoneNumbers[0].phoneNumber}`
}
this code will give back an array of informations that U asked, but if user has more than 1 phone number, it will take only first from the list

optimizing JSON querying performance in javascript

I have a 10MB JSON file of the following structure (10k entries):
{
entry_1: {
description: "...",
offset: "...",
value: "...",
fields: {
field_1: {
offset: "...",
description: "...",
},
field_2: {
offset: "...",
description: "...",
}
}
},
entry_2:
...
...
...
}
I want to implement an autocomplete input field that will fetch suggestions from this file, as fast as possible while searching multiple attributes.
For example, finding all entry names,field names and descriptions that contain some substring.
Method 1:
I tried to flatten the nesting into an array of strings:
"entry_1|descrption|offset|value|field1|offset|description",
"entry_1|descrption|offset|value|field2|offset|description",
"entry2|..."
and perform case insensitive partial string match, query took about 900ms.
Method 2
I tried Xpath-based JSON querying (using defiant.js).
var snapshot = Defiant.getSnapshot(DATA);
found = JSON.search(snapshot, '//*[contains(fields, "substring")]');
query took about 600ms (just for a single attribute, fields).
Are there other options that will get me to sub 100ms? I have control of the file format so I can turn it into XML or any other format, the only requirement is speed.
Since you are trying to search for a substring of values it is not a good idea to use indexeddb as suggested. You can try flattening the values of the fields to text where fields seperated by :: and each key in the object is a line in the text file:
{
key1:{
one:"one",
two:"two",
three:"three"
},
key2:{
one:"one 2",
two:"two 2",
three:"three 2"
}
}
Will be:
key1::one::two::three
key2::one 2::two 2::three
Then use regexp to search for text after the keyN:: part and store all keys that match. Then map all those keys to the objects. So if key1 is the only match you'd return [data.key1]
Here is an example with sample data of 10000 keys (search on laptop takes couple of milliseconds but have not tested when throttling to mobile):
//array of words, used as value for data.rowN
const wordArray = ["actions","also","amd","analytics","and","angularjs","another","any","api","apis","application","applications","are","arrays","assertion","asynchronous","authentication","available","babel","beautiful","been","between","both","browser","build","building","but","calls","can","chakra","clean","client","clone","closure","code","coherent","collection","common","compiler","compiles","concept","cordova","could","created","creating","creation","currying","data","dates","definition","design","determined","developed","developers","development","difference","direct","dispatches","distinct","documentations","dynamic","easy","ecmascript","ecosystem","efficient","encapsulates","engine","engineered","engines","errors","eslint","eventually","extend","extension","falcor","fast","feature","featured","fetching","for","format","framework","fully","function","functional","functionality","functions","furthermore","game","glossary","graphics","grunt","hapi","has","having","help","helps","hoisting","host","how","html","http","hybrid","imperative","include","incomplete","individual","interact","interactive","interchange","interface","interpreter","into","its","javascript","jquery","jscs","json","kept","known","language","languages","library","lightweight","like","linked","loads","logic","majority","management","middleware","mobile","modular","module","moment","most","multi","multiple","mvc","native","neutral","new","newer","nightmare","node","not","number","object","objects","only","optimizer","oriented","outside","own","page","paradigm","part","patterns","personalization","plugins","popular","powerful","practical","private","problem","produce","programming","promise","pure","refresh","replace","representing","requests","resolved","resources","retaining","rhino","rich","run","rxjs","services","side","simple","software","specification","specifying","standardized","styles","such","support","supporting","syntax","text","that","the","their","they","toolkit","top","tracking","transformation","type","underlying","universal","until","use","used","user","using","value","vuejs","was","way","web","when","which","while","wide","will","with","within","without","writing","xml","yandex"];
//get random number
const rand = (min,max) =>
Math.floor(
(Math.random()*(max-min))+min
)
;
//return object: {one:"one random word from wordArray",two:"one rand...",three,"one r..."}
const threeMembers = () =>
["one","two","three"].reduce(
(acc,item)=>{
acc[item] = wordArray[rand(0,wordArray.length)];
return acc;
}
,{}
)
;
var i = -1;
data = {};
//create data: {row0:threeMembers(),row1:threeMembers()...row9999:threeMembers()}
while(++i<10000){
data[`row${i}`] = threeMembers();
}
//convert the data object to string "row0::word::word::word\nrow1::...\nrow9999..."
const dataText = Object.keys(data)
.map(x=>`${x}::${data[x].one}::${data[x].two}::${data[x].three}`)
.join("\n")
;
//search for someting (example searching for "script" will match javascript and ecmascript)
// i in the regexp "igm" means case insensitive
//return array of data[matched key]
window.searchFor = search => {
const r = new RegExp(`(^[^:]*).*${search}`,"igm")
,ret=[];
var result = r.exec(dataText);
while(result !== null){
ret.push(result[1]);
result = r.exec(dataText);
}
return ret.map(x=>data[x]);
};
//example search for "script"
console.log(searchFor("script"));

Lodash help on filtering using a search term and multiple property names

I wish to search on multiple columns however all the code I could find on the internet was restricted on a single search term that would search multiple columns. I wish to filter on multiple columns by multiple search terms
data:
var propertynames = ['firstName','lastName'];
var data = [
{
"city":"Irwin town",
"address":"1399 Cecil Drive",
"lastName":"Auer",
"firstName":"Wanda"
},
{
"city":"Howell haven"
"address":"168 Arnoldo Light"
"lastName":"Balistreri",
"firstName":"Renee"
}
];
var searchTerm = 'Wanda Auer';
Should result in an array that filtered out the 2nd object.
Thanks!
I've created two solutions for your question. The first one is do exactly what you need: filters collection by two fields. The second one is more flexible, because it allows filter by any multiple fields.
First solution:
function filterByTwoFields(coll, searchFilter) {
return _.filter(coll, function(item) {
return (item.firstName + ' ' + item.lastName) === searchTerm;
});
}
var data = [
{
"city":"Irwin town",
"address":"1399 Cecil Drive",
"lastName":"Auer",
"firstName":"Wanda"
},
{
"city":"Howell haven",
"address":"168 Arnoldo Light",
"lastName":"Balistreri",
"firstName":"Renee"
}
];
var searchTerm = 'Wanda Auer';
var result = filterByTwoFields(data, searchTerm);
alert(JSON.stringify(result));
<script src="https://raw.githubusercontent.com/lodash/lodash/master/dist/lodash.min.js"></script>
Second solution:
function filterByMultipleFields(coll, filter) {
var filterKeys = _.keys(filter);
return _.filter(coll, function(item) {
return _.every(filterKeys, function(key) {
return item[key] === filter[key];
});
});
}
var data = [
{
"city":"Irwin town",
"address":"1399 Cecil Drive",
"lastName":"Auer",
"firstName":"Wanda"
},
{
"city":"Howell haven",
"address":"168 Arnoldo Light",
"lastName":"Balistreri",
"firstName":"Renee"
}
];
var filter = {
firstName: 'Wanda',
lastName: 'Auer'
}
var result = filterByMultipleFields(data, filter);
alert(JSON.stringify(result));
<script src="https://raw.githubusercontent.com/lodash/lodash/master/dist/lodash.min.js"></script>
Not the most efficent but it does the job. You might want to make an non case sensitive comparison on the property values:
var searchTerm = 'Wanda Auer',
splitted = searchTerm.split(' ');
var result = data.filter(function(item){
return window.Object.keys(item).some(function(prop){
if(propertynames.indexOf(prop) === -1)
return;
return splitted.some(function(term){
return item[prop] === term;
});
});
});
https://jsfiddle.net/3g626fb8/1/
Edit: Just noticed the Lodash tag. If you want to use it, the framework is using the same function names as the array prototype, i.e. _.filter and _.some
Here is a pretty straightforward lodash version:
var matchingRecords = _.filter(data, function (object) {
return _(object)
.pick(propertynames)
.values()
.intersection(searchTerm.split(' '))
.size() > 0;
})
It filters the objects based on if any of the chosen property name values intersect with the search term tokens.

How to return a result from JSON with case insensitive search using javascript?

My JSON contains 'products' that each have multiple values (name, manufacturer etc.)
I have the following code that allows me to pull 'products' from my JSON based on a search result acquired from a query string. It works fine, but only if the search entry is formatted exactly how it is written in the string.
How can I allow case insensitive searches yield the desired result (and ideally, partial searches)?
I believe that regular expressions are the way to go, but I've exhausted my knowledge and can't seem to get anywhere with it.
I tried to put together a fiddle but couldn't get a proper demo working, so instead I've tried to consolidate the code and comment it for clarity. Any help would be greatly appreciated :)
Parse the JSON:
var prodobjects = JSON.parse(prods);
Code to get products based on specific value:
function getData(array, type, val) {
return array.filter(function (el) {
return el[type] === val;
});
}
Code to retrieve query string:
function gup( name ){
name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
var regexS = "[\\?&]"+name+"=([^&#]*)";
var regex = new RegExp( regexS );
var results = regex.exec( window.location.href );
if( results == null ) return "";
else return results[1];}
Section of query string I want to use:
var prodresult = gup( 'search' );
Remove plusses and replace with spaces:
var removeplus = prodresult.replace(/\+/g, ' ');
Compile list of products with their 'Prodname' matching the 'search' query:
var searchname = getData(prodobjects.products, 'Prodname', removeplus);
And as requested here is a sample of the JSON. It's still in development so the null values etc. are currently being worked out (it's received through an api). This is just one of the products, but the actual string contains many in the same format (but within "products"):
var prods = JSON.stringify({"products": [
{
"Prodname": null,
"Oem": "Example OEM",
"Snippet": "Example Snippet",
"Linkto": "www.linkhere.com",
"Imagesource": "image.png",
"Category": "Category",
"Tagline": "Tagline goes here",
"Longdescription": [
{
"Paragraph": "<p>First description of the paragraph</p>"
},
null,
null,
null
],
"Features": null,
"Company": false,
"Subscribed": false,
"Tariffs": [
{
"Tarname": "Tariff one",
"Tarpaysched": "Monthly per User",
"Tarcost": "£1"
},
null,
null,
null,
null,
null
],
"Extratariffs": null
}
]
});
---UPDATE---
I managed to get it working to support partial searches and case insensitivity with the following:
function getData(array, type, val) {
return array.filter(function (el) {
if (el[type]!=null && val!=null) {
var seeker = val.toLowerCase();
var rooted = el[type].toLowerCase();
var boxfresh = rooted.indexOf(seeker);
if (boxfresh!=-1) {
return rooted
}
}
});
}
You can convert two strings to lowercase (or uppercase) to make the comparison case-insensitive.
function getData(array, type, val) {
return array.filter(function (el) {
return el[type].toLowerCase() === val.toLowerCase();
});
}
For better searching, you might want to look into fuzzy comparison, which is "a search against data to determine likely mispellings and approximate string matching".

How to get/filter all results from JSON file with underscore.js

I have this (shortened for question) JSON file:
[
{
"product":"aardappelen gebakken",
"quantity":"100gr",
"carbohydrates":"19,3"
},
{
"product":"aardappelen pikant",
"quantity":"100gr",
"carbohydrates":"3"
},
{
"product":"aardappelmeel",
"quantity":"100gr",
"carbohydrates":"80"
}
]
Wat i want to do is:
search for a product, and result all of its content, like when i would search for "aardappelmeel", i get product, quantity and carbohydrates key value, i do this with this code:
the search term is hardcoded for the moment, this will be a var later one.
$(function() {
$.getJSON( "js/data.json").fail(function(jqxhr, textStatus, error) {
var err = textStatus + ", " + error;
console.log( "Request Failed: " + err );
}).done(function(data) {
var carbohydratesResult = getCarbohydrates(data, 'aardappelmeel');
console.log(carbohydratesResult);
});
});
function getCarbohydrates(arr, searchTerm){
var result;
if(searchTerm === '') {
result = 'No searchterm';
}
else {
result = _.where(arr, {product: searchTerm});
}
return result;
}
This gets 1 result:
Question: When i search for "aardappelen", i get no result, and it should be 2, because there are 2 products that contain the name "aardappelen". How do i do this?
I use jQuery, Underscore. If Underscore is not needed for this, fine by me, please show me how i modify my code to get more then one result when "product" value contains the search term.
You need _.filter along with indexOf to do a substring search:
result = _.filter(arr, function(item) {
return item.product && item.product.indexOf(searchTerm) != -1;
});
Note that _.where performs an exact match.
I would probably say underscore is not needed for this (not optimal to include an entire js library just use use a single function). Searching for text simply in JavaScript is never fun. You're probably best writing some regex function that loops over your result set and tries to match some text.
If possible, I would try to implement the searching functionality on the server side, and return those results in an ajax request.
Rough example of a JS Solution...
var searchText = 'blah',
matches = [];
for(var i = 0; i < results.length; i++) {
var reg = new RegExp(searchText);
if(results[i].product.match(reg)) matches.push(results[i]);
}
return matches;

Categories