Live filtering of json - javascript

This Meteor client app "mostly smartPhone usage" needs to accept input from user in the form of typed text and filter json data of about 500 bottom nodes, giving back the branches of the tree where the bottom node text contains the user input text.
{
"people": {
"honour": [
[
"family"
],
[
"friends"
]
],
"respect": [
[
"one another"
]
]
},
"animals": {
"eat": [
[
"row food"
]
]
}
}
When the user inputs 'a', the code needs to give the tree where the occurrence exists:
people, honour, family.
people, respect, one another
When the user types 'o', output should be:
people, respect, one another.
animals, eat, row food.
When the user types 'oo', output should be:
animals, eat, row food.
when the user types 'f', output should be:
people, honour, family.
people, honour, friends.
animals, eat, row food.
My options are:
Converting the json to javascript object and write the seach/find/match logic with few loops.
Use defiantjs which I never used before and have to learn.
Import the json to mongodb and filter the database.
Whatever else you suggest.
Which would be best fit for fast results and easy of maintenance? Thanks

OK this question was an excuse for me to create a generic Object method Object.prototype.paths() to get all the paths within an object. In objects there are values many paths. Some values might be the same at the end of different paths. We will generate an object with the original object's values as properties and these properties' values are going to be the paths. Each value might have several paths so an array of strings array where each strings array will contain a single path to that value.
So once we have this tool to map the object values and paths, it becomes very easy to get your result.
Object.prototype.paths = function(root = [], result = {}) {
var ok = Object.keys(this);
return ok.reduce((res,key) => { var path = root.concat(key);
typeof this[key] === "object" &&
this[key] !== null ? this[key].paths(path,res)
: res[this[key]] == 0 || res[this[key]] ? res[this[key]].push(path)
: res[this[key]] = [path];
return res;
},result);
};
var data = {"people":{"honour":[["family"],["friends"]],"respect":[["one another"],["friends"]]},"animals":{"eat":[["row food"]]}},
paths = data.paths(),
values = Object.keys(paths),
keystr = document.getElementById("keystr");
getPaths = function(str){
var valuesOfInterest = values.filter(f => f.includes(str));
return valuesOfInterest.reduce((p,c) => p.concat({[c]: paths[c]}),[]);
};
keystr.oninput = function(e){
console.log(JSON.stringify(getPaths(e.target.value),null,2))
}
<input id="keystr" placeholder = "enter some characters" value = ""/>
So when you press "o" you will get the following
[
{
"one another": [
[
"people",
"respect",
"0",
"0"
]
]
},
{
"row food": [
[
"animals",
"eat",
"0",
"0"
]
]
}
]
Which means:
The outer array has 2 object items. This means that in the original
object there are two values with "o" character/s in it. "one another" and "row food",
"one another" has only one path ["people", "respect", "0", "0"]. If "one another" was listed at multiple places like "friends" listed both under "respect" and "honour" then this array would contain two sub arrays with paths. Type "fr" and see it for yourself.
A few words of warning: We should be cautious when playing with the Object prototype. Our modification should have the descriptor enumerable = false or it will list in the for in loops and for instance jQuery will not work. (this is how silly jQuery is, since apparently they are not making a hasOwnProperty check in their for in loops) Some good reads are here and here So we have to add this Object method with Object.defineProperty() to make it enumerable = false;. But for the sake of simplicity and to stay in the scope of the question i haven't included that part in the code.

I think a few loops would work just fine. In the example below, as you type in the input, the results matching your search are logged to the console.
$("#search").on("input", function() {
var result = [];
var search = this.value;
if (search.length) {
$.each(data, function(key1, value1) {
//key1: people, animals
$.each(value1, function(key2, value2) {
//key2: honor, respect, eat
$.each(value2, function(i, leaf) {
if (leaf.length && leaf[0].indexOf(search) >= 0) {
//found a match, push it onto the result
var obj = {};
obj[key1] = {};
obj[key1][key2] = leaf;
result.push(obj);
}
});
});
});
}
console.log(result);
});
var data = {
"people": {
"honour": [
[
"family"
],
[
"friends"
]
],
"respect": [
[
"one another"
]
]
},
"animals": {
"eat": [
[
"row food"
]
]
}
};
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<input id="search" />

Use this package for meteor is awesome https://atmospherejs.com/matteodem/easy-search

Related

How do I create a mapping of all the keys to a specific value in a nested dictionary in JavaScript

I am trying to create a map to display all the keys that are needed to access a specific value inside a nested dictionary.
var example = {"parent1": {
"parent2": {},
"parent3": {},
"parent4": {
"sub_1": {},
"sub_2": {
"sub_sub2": {
"sub_sub_sub2": ["some list 1"],
"sub_sub_sub2_1": ["some list 2"]
},
"sub_3": {
"sub_sub3": ["some list 3"]
}
},
"sub_4": ["some list 4"]
},
"parent5": {},
"parent6": {},
"parent7": {},
"parent8": {
"sub_par8": {}
},
"parent9": {}
}}
I want the output to show me all the keys required to access the values that are lists. For example, if I want to know which values are lists, the output will return something like:
return_list = [
["parent4", "sub_2","sub_sub2", "sub_sub_sub2", ["some list 1"]],
["parent4", "sub_2","sub_sub2", "sub_sub_sub2_1", ["some list 2"]],
["parent4", "sub_3","sub_sub3", ["some list 3"]],
["parent4", "sub_4",["some list 4"]],
]
My thought is that I can then iterate through this nested list to take the information I need. But if you have a better idea as to how to display the information, then please let me know.
I have tried to create a function that sort of does what I'm looking for, but its broken
var sorted = []
var temp = []
function sort(dictionary){
for (var [key,value] of Object.entries(dictionary)){
temp.push(key)
if (value.constructor == Object){
sort(value)
}
else if (value.constructor == Array){
temp.push(value)
sorted.push(temp)
}
temp = []
}
}
If you run the code above, you'll notice the list sorted is not formatted correctly. Any help would be much appreciated. I've been trying to solve this for quit some time :)

Mongoose: How to extract arrays in subdocuments to a single array in the parent document

An Express server using Mongoose returns the following JSON from the database in one of its routes:
{
"gradeLevel": 12,
"classes": [
{
"className": "A",
"students": [
"student1", "student2","student3"
]
},
{
"className": "B",
"students": [
"student4", "student5","student6"
]
},
{
"className": "C",
"students": [
"student7", "student8","student9"
]
}
]
}
In the client, I'd like to be able to for example efficiently check if a student is in any of the class subdocuments in the parent document. The following are the solutions I've conceived, but am not sure which is best practice:
When a student is pushed to class subdocument, push the student to the parent's students field as well.
Add a new field in the model's toJSON transformation to compile all the students.
Use vanilla Javascript in the client to extract all students
I was at this problem too. But to tackle this you need a separate collection called allStudents for example where the schema of it is as follows
{ nameOfStudent: 'String' className: 'String' }
then aggregate it accordingly with subdoucment. So that wherever you push a new student into allStudents with the className mentioned it will be pushed to the students subdocument of the respective className.
Querying with dot notation, like
.find({"classes.students":"student8"})
will check the students field of each object in the classes array, returning the documents containing the specific student in any class.
Thank you for the answers given. Just sharing another solution using vanilla Javascript that might be of use to those in similar situations:
checkStudent = (batch, student) => {
let result = null
const inClass = (cls) => {
if (cls.students.includes(student)) {
result = cls
return true
}
return false
}
batch.classes.some(inClass)
return result
}

Getting count of products before filters are applied

I have an array of products:
[
{
"title": "ring 1",
"tags": [
"gemstone__morganite",
"material__rose-gold",
"ring-type__cocktail-rings",
"ring-type__engagement-rings"
]
},
{
"title": "ring 2",
"tags": [
"material__platinum",
"ring-type__cocktail-rings",
"ring-type__engagement-rings"
]
},
{
"title": "ring 3",
"tags": [
"gemstone__diamond",
"material__rose-gold",
"ring-type__engagement-rings",
"ring-type__etched-rings"
]
}
]
I am filtering the products using an object that looks like this:
{
"gemstone": [],
"material": [
"material__platinum",
"material__rose-gold"
],
"ring-type": [
"ring-type__cocktail-rings",
"ring-type__etched-rings"
]
}
My filters are grouped by the key and built so they look like this:
I then have a count next to each filter to indicate how many of each product will show when the filter is clicked.
This works fine on first load but after filters are applied the numbers needs to update accordingly.
The filters work so that any within the same group use 'or' logic. So, if 'material_platimun' and 'material__rose-gold' were selected it would should all products that have either one of those tags.
If you were to then select a tag from another group it uses 'and' logic. So, if 'material_platimun', 'material__rose-gold', 'ring-type__cocktail-rings' and 'ring-type__etched-rings' it would show the products above. (e.g all products with 'material_platimun' OR 'material__rose-gold' AND 'ring-type__cocktail-rings' OR 'ring-type__etched-rings')
I am am filtering the products using a function that looks like this:
if (Object.entries(filters).length !== 0 && filters.constructor === Object) {
for (let key in filters) {
if (Array.isArray(filters[key]) && filters[key].length) {
result = result.filter(object => filters[key].some(r => object.tags.includes(r)));
}
}
}
This is working fine but I can't build the expected numbers of products from the result as it is a step too late. I need to know how many products will show before the products are filtered.
I am currently building the count based off what tags are selected like so:
const tags = [];
const count = {};
Object.entries(result).forEach(([key, val]) => {
val.tags.forEach(el => {
tags.push(el);
});
})
tags.forEach(i => {
count[i] = (count[i] || 0) + 1;
});
for (let [key, value] of Object.entries(count)) {
const markup = `
<span>\u00A0(${value})</span>
`;
const el = tmpl.querySelector('[data-tag-handle=' + key + '] label');
el.insertAdjacentHTML('beforeend', markup);
}
Does anyone know how I could go about solving this problem?
The thing you are trying to make is called faceted search. A custom implementation may worth it but probably you better consider specialised database management systems. I would suggest looking at Apache Solr or Elastic Search. Both of them have this feature.

Filter data with dynamic and nested filter conditions using Lodash

I want to filter my product data with multiple as well as Dynamic filter conditions.
First my raw data format is:
[{
...,
"_source": {
...,
"categories": [
"home",
"office"
],
...,
"attributes": {
"colors": [
"Red-White-Blue",
"Orange-Red-Black"
]
},
...,
}
}]
Now, I have another dynamic object whose key-value pair is generated at run time. The object is:
{
selectedFilters: {
categories: ["home", "office", "public"],
colors: ["Red-White-Blue", "Orange-Red-Black", "Blue"],
...
}
}
Here, maybe categories array be there, or maybe not. Like wise, the keyvalue pair is generated dynamically.
Now, I want to loop through all the keys from selectedFilters object and find them in their respective array from my raw data.
Let say if selectedFilters contains categories as home and colors contain white, then all the product with home category and white colors should be returned from main product data. If there is no value in colors, then it should return all product data with category as home, likewise if color is white and categories is empty, then it should return all white color products no matter what category.
How can I achieve this with or without Lodash?
This below code worked for dynamic keys in searchFilters. And then, I'm filtering data from the product data.
let keys = Object.keys(this.selectedFilters);
keys.forEach(function(filterKey) {
let filterdProducts = _.filter(self.products, function(o) {
let flg = false;
let searchFilterKey = self.selectedFilters[filterKey];
searchFilterKey.forEach(function(sKey) {
let ind = o._source[filterKey].indexOf(sKey)
if (ind > 0)
flg = true;
})
if (flg)
return o;
});
self.filteredProducts = filterdProducts
});

Regex for parsing single key: values out of JSON in Javascript

I'm trying to see if it's possible to lookup individual keys out of a JSON string in Javascript and return it's Value with Regex. Sort of like building a JSON search tool.
Imagine the following JSON
"{
"Name": "Humpty",
"Age": "18",
"Siblings" : ["Dracula", "Snow White", "Merlin"],
"Posts": [
{
"Title": "How I fell",
"Comments": [
{
"User":"Fairy God Mother",
"Comment": "Ha, can't say I didn't see it coming"
}
]
}
]
}"
I want to be able to search through the JSON string and only pull out individual properties.
lets assume it's a function already, it would look something like.
function getPropFromJSON(prop, JSONString){
// Obviously this regex will only match Keys that have
// String Values.
var exp = new RegExp("\""+prop+"\"\:[^\,\}]*");
return JSONString.match(exp)[0].replace("\""+prop+"\":","");
}
It would return the substring of the Value for the Key.
e.g.
getPropFromJSON("Comments")
> "[
{
"User":"Fairy God Mother",
"Comment": "Ha, can't say I didn't see it coming"
}
]"
If your wondering why I want to do this instead of using JSON.parse(), I'm building a JSON document store around localStorage. localStorage only supports key/value pairs, so I'm storing a JSON string of the entire Document in a unique Key. I want to be able to run a query on the documents, ideally without the overhead of JSON.parsing() the entire Collection of Documents then recursing over the Keys/nested Keys to find a match.
I'm not the best at regex so I don't know how to do this, or if it's even possible with regex alone. This is only an experiment to find out if it's possible. Any other ideas as a solution would be appreciated.
I would strongly discourage you from doing this. JSON is not a regular language as clearly stated here: https://cstheory.stackexchange.com/questions/3987/is-json-a-regular-language
To quote from the above post:
For example, consider an array of arrays of arrays:
[ [ [ 1, 2], [2, 3] ] , [ [ 3, 4], [ 4, 5] ] ]
Clearly you couldn't parse that with true regular expressions.
I'd recommend converting your JSON to an object (JSON.parse) & implementing a find function to traverse the structure.
Other than that, you can take a look at guts of Douglas Crockford's json2.js parse method. Perhaps an altered version would allow you to search through the JSON string & just return the particular object you were looking for without converting the entire structure to an object. This is only useful if you never retrieve any other data from your JSON. If you do, you might as well have converted the whole thing to begin with.
EDIT
Just to further show how Regex breaks down, here's a regex that attempts to parse JSON
If you plug it into http://regexpal.com/ with "Dot Matches All" checked. You'll find that it can match some elements nicely like:
Regex
"Comments"[ :]+((?=\[)\[[^]]*\]|(?=\{)\{[^\}]*\}|\"[^"]*\")
JSON Matched
"Comments": [
{
"User":"Fairy God Mother",
"Comment": "Ha, can't say I didn't see it coming"
}
]
Regex
"Name"[ :]+((?=\[)\[[^]]*\]|(?=\{)\{[^\}]*\}|\"[^"]*\")
JSON Matched
"Name": "Humpty"
However as soon as you start querying for the higher structures like "Posts", which has nested arrays, you'll find that you cannot correctly return the structure since the regex does not have context of which "]" is the designated end of the structure.
Regex
"Posts"[ :]+((?=\[)\[[^]]*\]|(?=\{)\{[^\}]*\}|\"[^"]*\")
JSON Matched
"Posts": [
{
"Title": "How I fell",
"Comments": [
{
"User":"Fairy God Mother",
"Comment": "Ha, can't say I didn't see it coming"
}
]
\{|\}|\[|\]|,|:|(\\-)?\\d+(\\.\\d+)?|".+?"
You can use the following regex and iterate with a match over all tokens of a json. You can tokenize the JSON, but the parsing part has to be implemented by you.
Since you're using JavaScript as I assume from the tags, your best way to encode the JSON stays JSON.parse().
I'm almost 10 years late to the party, but I came up with this.
Not tested in crazier JSONs than this, but it solves my use cases.
const obj1 = {
id: 1,
'name.1': '123',
address: {
'address.1': 'Chicken Dinner Road, 69',
'address.2': 'Psycho lane, 666',
},
'age.1': {
'thisIsSomeCrazyJson.3': 10,
age: 50,
},
types: [
{
id: 22,
'name.name': '123',
typeOption: {
id: 1,
'whoTFWroteThisJSON.2': '123',
},
},
{
id: 32,
'name.1': '123',
},
],
};
const obj2 = {
Name: 'Humpty',
Age: '18',
Siblings: ['Dracula', 'Snow White', 'Merlin'],
Posts: [
{
Title: 'How I fell',
Comments: [
{
'User': 'Fairy God Mother',
'Comment': "Ha, can't say I didn't see it coming",
},
],
},
],
};
function matchKeyDeep(input, pattern) {
return Object.entries(input).reduce((nextInput, [key, value]) => {
const isMatch = pattern.test(key);
if (Array.isArray(value)) {
const arrValue = value;
let nextValue = arrValue.map((arrItem) => {
if (typeof arrItem === 'object') {
return matchKeyDeep(arrItem, pattern);
}
return arrItem;
});
if (!isMatch && Array.isArray(nextValue)) {
nextValue = nextValue.filter((v) => (typeof v === 'object' && v !== null));
if (nextValue.length === 0) return nextInput;
}
nextInput[key] = nextValue;
return nextInput;
}
if (typeof value === 'object') {
const recurse = matchKeyDeep(value, pattern);
if (!isMatch && Object.keys(recurse).length === 0) {
return nextInput;
}
nextInput[key] = recurse;
return nextInput;
}
if (isMatch) {
nextInput[key] = value;
}
return nextInput;
}, {});
}
const res = matchKeyDeep(obj1, /\.\d/);
const res2 = matchKeyDeep(obj2, /Comment/);
console.log(res);
console.log(res2);
First, stringify the JSON object. Then, you need to store the starts and lengths of the matched substrings. For example:
"matched".search("ch") // yields 3
For a JSON string, this works exactly the same (unless you are searching explicitly for commas and curly brackets in which case I'd recommend some prior transform of your JSON object before performing regex (i.e. think :, {, }).
Next, you need to reconstruct the JSON object. The algorithm I authored does this by detecting JSON syntax by recursively going backwards from the match index. For instance, the pseudo code might look as follows:
find the next key preceding the match index, call this theKey
then find the number of all occurrences of this key preceding theKey, call this theNumber
using the number of occurrences of all keys with same name as theKey up to position of theKey, traverse the object until keys named theKey has been discovered theNumber times
return this object called parentChain
With this information, it is possible to use regex to filter a JSON object to return the key, the value, and the parent object chain.
You can see the library and code I authored at http://json.spiritway.co/

Categories

Resources