How can I improve my search function Logic? Using Lunr.js - javascript

So I am using Lunr.js for my search function. Everything is working nice and dandy BUT, when I search certain keywords like "God" and "church" I get unacceptably long search times. I mean like, 30 secs and even beyond 1 min long. I'm sure it must be the logic I am using that is causing this, at least I think it is. The aim of my Search Function is to search the users input and return only the sentences where the key word is found along with the name of the book and the page where the searchQuery is found. My goal is to have a search function that can work offline. This is a Cordova project. I have commented everything so that you can follow the logic easily. Thank you for your time and input!
function searchFunction() {
//Clears results-wrapper Html element
document.querySelector(".results-wrapper").innerHTML = "";
//Store query in localDB
searchQuery = document.querySelector("#search-id").value;
localStorage.setItem("searchQuery", searchQuery);
//Returns from the index only the books which contains the query
var results = idx.search(searchQuery);
//Run through "results" and return all the text where the query is found
results.forEach(function (entry) {
//documents contains the database of all the books from my json file
documents.find(findSearchQuery);
//Searches only the text of the doc/books found in results variable
function findSearchQuery(doc) {
//searchQuery is the users input
let re = new RegExp(searchQuery, "i");
//if the book's name matches reference in "results"
if (doc.name == entry.ref) {
//And if the searchQuery is found in Books text
if (doc.text.match(re)) {
//Break up the block of text into sentences
var sentences = doc.text.match(/[^\.!\?]+[\.!\?]+/g);
sentences.forEach(function (sentence) {
//Find the sentence inside the sentences array and return the one with the searchQuery
//Populate HTML element "results-wrapper" with the results
if (sentence.match(re)) {
var anchor = document.createElement("a");
anchor.className = "anchorSearchResult";
anchor.href = doc.href;
//Create "div" element
var div = document.createElement("div");
div.className = "div-test";
//Creates "h4" element for title
var h4 = document.createElement("h4");
var title = document.createTextNode(doc.name);
h4.className = "title-results";
//Creates "p" element for sentence
var textElement = document.createElement(p);
var searchResult = document.createTextNode(sentence);
textElement.className = "text-results";
//Creates "p" element for page
var p = document.createElement("p");
var pageResult = document.createTextNode(doc.page);
p.className = "page-results";
h4.appendChild(title);
textElement.appendChild(searchResult);
p.appendChild(pageResult);
div.appendChild(h4);
div.appendChild(textElement);
div.appendChild(p);
anchor.appendChild(div);
document.querySelector(".results-wrapper").appendChild(anchor);
anchor.addEventListener("click", returnSearchResultId);
function returnSearchResultId(e) {
//store selectorId value of document
localStorage.setItem("selectorId", doc.selectorId);
}
// Highlight Function
var instance = new Mark(
document.querySelector(".results-wrapper")
);
instance.mark(searchQuery, {
element: "span",
className: "highlight",
});
}
});
}
}
}
});
}
And here is just a snippet of my json file that is loaded in my "documents" variable.
[
{
"name": "Pre-Eleventh-Hour Extra",
"year": "1941",
"text": "Pre-\"Eleventh Hour\" Extra MYSTERY OF MYSTERIES EXPOSED!",
"page": "1TR 2",
"href": "tracks/tr1.html#page-2.subHeading",
"selectorId": "#page-2\\.subHeading"
},
{
"name": "Pre-Eleventh-Hour Extra",
"year": "1941",
"text": "In the interest of reaching every truth-seeking mind that desires to escape the path that leads to destruction of both body and soul, this tract will be distributed free of charge as long as this issue lasts.",
"page": "1TR 2",
"href": "tracks/tr1.html#page-2.1",
"selectorId": "#page-2\\.1"
},
{
"name": "Pre-Eleventh-Hour Extra",
"year": "1941",
"text": "PREFACE PERSONALLY WATCHING FOR EVERY RAY OF LIGHT.",
"page": "1TR 3",
"href": "tracks/tr1.html#page-3.preface",
"selectorId": "#page-3\\.preface"
},
{
"name": "Pre-Eleventh-Hour Extra",
"year": "1941",
"text": "One who entrusts to another the investigation of a message from the Lord, is making flesh his arm, and thus is foolishly acting as without a mind of his own. And ”the mind that depends upon the judgment of others is certain, sooner or later, to be misled. ” -- Education, p. 231.",
"page": "1TR 3",
"href": "tracks/tr1.html#page-3.1",
"selectorId": "#page-3\\.1"
},
{
"name": "Pre-Eleventh-Hour Extra",
"year": "1941",
"text": "Similarly, one who allows prejudice to bar him from a candid investigation of anything new, coming in the name of the Lord, is unwittingly an infidel.",
"page": "1TR 3",
"href": "tracks/tr1.html#page-3.2",
"selectorId": "#page-3\\.2"
},
{
"name": "Pre-Eleventh-Hour Extra",
"year": "1941",
"text": "Likewise he who is satisfied with his present attainments in the Word of God, says in effect: \"I am rich, and increased with goods, and have need of nothing.",
"page": "1TR 3",
"href": "tracks/tr1.html#page-3.3",
"selectorId": "#page-3\\.3"
},
{
"name": "Pre-Eleventh-Hour Extra",
"year": "1941",
"text": "All these, in variously acting out the part which provoked the condemnation written against the Laodiceans, thereby fulfilling the prophecy which they ought not fulfill, are preparing themselves to be spued out (Rev. 3:14-18). And if they continue in their self-satisfied attitude that they have all the truth, and so have need of nothing more, they will spurn every new claimant to truth and toss the message into the discard because it comes through an unexpected channel. Certainly, then, were this tract not the unfolding of prophecy, the fact is inevitable that when the unfoldment did come, they would treat it in like manner, and consequently toss away their salvation!",
"page": "1TR 3",
"href": "tracks/tr1.html#page-3.4",
"selectorId": "#page-3\\.4"
},
{
"name": "Pre-Eleventh-Hour Extra",
"year": "1941",
"text": "Throughout the ages, all who have put their trust in the so-called wise men, and foremost Christians of the day, all reputedly godly men, have by these very ones been bereft of the crown of eternal life, as were the Jewish laity in the days of Christ because of their failing to assume full responsibility for their own salvation. Presumptuously trusting in the wisdom of their so-called \"great men,\" they declined to believe in Christ's words \"O Father, Lord of heaven and earth,...Thou hast hid these things from the wise and prudent, and hast revealed them unto babes.\" Matt. 11:25 \"Where is the wise? where is the scribe?...hath not God made foolish the wisdom of this world?\" 1 Cor 1:20.",
"page": "1TR 4",
"href": "tracks/tr1.html#page-4.1",
"selectorId": "#page-4\\.1"
}
]

Figured it out! It was the highlight functionality that I am getting from the Mark.js library that was causing the huge delay. After commenting it out, everything is lightening fast again.

Related

.find() returns undefined using JSON data, using node.js

I'm creating a server using node and want to output JSON data, when a specific ID is clicked.
I've set up a dynamic URL which will pull the data from the clicked video using params and then compare it with the one from the JSON data, using .find()
When I log out the "req.params.id" I get the clicked ID. However, when I log out the .find() function it just gives me an undefined.
const selectedVideoData = fs.readFileSync("./data/video-details.json");
app.get("/videos/:id", (req, res) => {
const id = req.params.id;
const findClickedId = selectedVideoData.find((video) => video.id === id);
res.send(findClickedId);
});
[
{
"id": "84e96018-4022-434e-80bf-000ce4cd12b8",
"title": "BMX Rampage: 2021 Highlights",
"channel": "Red Cow",
"image": "https://i.imgur.com/l2Xfgpl.jpg",
"description": "On a gusty day in Southern Utah, a group of 25 daring mountain bikers blew the doors off what is possible on two wheels, unleashing some of the biggest moments the sport has ever seen. While mother nature only allowed for one full run before the conditions made it impossible to ride, that was all that was needed for event veteran Kyle Strait, who won the event for the second time -- eight years after his first Red Cow Rampage title",
"views": "1,001,023",
"likes": "110,985",
"duration": "4:01",
"video": "https://project-2-api.herokuapp.com/stream",
"timestamp": 1626032763000,
"comments": [
{
"id": "35bba08b-1b51-4153-ba7e-6da76b5ec1b9",
"name": "Micheal Lyons",
"comment": "They BLEW the ROOF off at their last event, once everyone started figuring out they were going. This is still simply the greatest opening of an event I have EVER witnessed.",
"likes": 0,
"timestamp": 1628522461000
},
{
"id": "091de676-61af-4ee6-90de-3a7a53af7521",
"name": "Gary Wong",
"comment": "Every time I see him shred I feel so motivated to get off my couch and hop on my board. He’s so talented! I wish I can ride like him one day so I can really enjoy myself!",
"likes": 0,
"timestamp": 1626359541000
},
{
"id": "66b7d3c7-4023-47f1-a02c-520c9ca187a6",
"name": "Theodore Duncan",
"comment": "How can someone be so good!!! You can tell he lives for this and loves to do it every day. Every time I see him I feel instantly happy! He’s definitely my favorite ever!",
"likes": 0,
"timestamp": 1626011132000
}
]
},
{
"id": "c05b9a93-8682-4ab6-aff2-92ebb4bbfc14",
"title": "Become A Travel Pro In One Easy Lesson",
"channel": "Todd Welch",
"image": "https://i.imgur.com/5qyCZrD.jpg",
"description": "Luxury is something everyone deserves from time to time. Such an indulgence can make a vacation a truly rejuvenating experience. This video will focus a lot on helping the first time or inexperienced traveler head out prepared and confident in themselves.",
"views": "2,043,765",
"likes": "400,058",
"duration": "7:26",
"video": "https://project-2-api.herokuapp.com/stream",
"timestamp": 1625158995000,
"comments": [
{
"id": "ade82e25-6c87-4403-ba35-47bdff93a51c",
"name": "Mattie Casarez",
"comment": "This is exactly the kind of advice I’ve been looking for! One minute you’re packing your bags, the next you’re dancing around in the streets without a care in the world.",
"likes": 0,
"timestamp": 1625250720000
},
{
"id": "bf704c76-cba9-462e-ac0a-166315df756c",
"name": "Taylor Jade",
"comment": "Excellent tips! Another idea is to keep all of your important belongings like your passport inside a waterproof bag. Perfect for those last minute trips to the beach, trust me.",
"likes": 0,
"timestamp": 1625238122000
},
{
"id": "ec2bec8d-ea2b-458e-9d93-b7f929a8659b",
"name": "Adnan Natt",
"comment": "Who ever knew travel could be so easy? Looking forward to getting to put this into practice when I fly away in the near future. Wish me good luck!",
"likes": 0,
"timestamp": 1625177192000
}
]
}
]
You need to parse json since fs.readFileSync return string
const rawSelectedVideoData = fs.readFileSync("./data/video-details.json");
const selectedVideoData = JSON.parse(rawSelectedVideoData)
app.get("/videos/:id", (req, res) => {
const id = req.params.id;
const findClickedId = selectedVideoData.find((video) => video.id === id);
res.send(findClickedId);
});
fs.readFileSync returns a string and an object literal, so .find shouldn't work at all.
Try instead:
const selectedVideoData = require("./data/video-details.json");
If the array you posted is the exact response you get from const selectedVideoData = fs.readFileSync("./data/video-details.json"); then make sure that id from const id = req.params.id; is a string and it should work. I have tested it on my end and it works perfectly. Though I've subbed const id = req.params.id; for const id = "84e96018-4022-434e-80bf-000ce4cd12b8"; which is the first element in your array as I don't have access to your server and it works.
Try this one:
const selectedVideoData = require("./data/video-details.json");
app.get("/videos/:id", (req, res) => {
const id = req.params.id;
const findClickedId = selectedVideoData.find((video) => video.id === id);
res.send(findClickedId);
});

How do I loop through a json array, and then display the results in an discord js embed?

Thanks for taking a peek at my question. This is my first question, so hopefully I do all the right stuff.
I am using DiscordJS/NodeJS
I have a large(ish) json file that has a list of maps/links to download the map, and an extra field.
[
{
"name": "2 Evil Eyes",
"link": "http://",
"extra": ""
},
{
"name": "25 To Life",
"link": "http://",
"extra": ""
},
{
"name": "Back To School",
"link": "http://",
"extra": ""
},
I created this file myself from a list that was given to me.
The goal of this file was to be able to display the data (maps, link, extra) in a discord embed. I know that having 70+ .addField(s) is not a great idea, but it is what was requested.
So I thought that I would just have a little loop through and display the data that way, but my issue is that it keeps posting an embed for each result it gets. Here is my code below:
fs.readFile('./maps.json', 'utf-8', function(err, data){
json = JSON.parse(data);
for(let i = 0; i < data.length; i++){
let name = data[i].name;
let link = data[i].link;
let extra = data[i].extra;
}
const mapRot = new Discord.MessageEmbed()
.setTitle("Map Details")
.setAuthor(client.user.username)
.setDescription("These are your maps for the night.")
.addField("Maps", name)
.addField("Link", link)
.addField("Extra", extra);
message.channel.send(mapRot);
Any help would be appreciated!
Welcome to StackOverflow! I'm not too familiar with the Discord API, however, it seems as though you can do something like this, where you pass to addFields a list of arguments.
So I would think you could first map through the array to transform the data, and then pass it in as it should be specified to addFields. Something along the lines of:
fs.readFile('./maps.json', 'utf-8')
.then(JSON.parse)
.then(data => data.map((entry) => {
// here, 'entry' stores an object of the form:
// {
// "name": "2 Evil Eyes",
// "link": "http://",
// "extra": ""
// }
// and per the Discord documentation, needs to return
// an object of the form:
// {
// "name": 'Inline field title',
// "value": 'Some value here'
// }
// for example:
return { name: entry.name, value: entry.value }
}))
.then((parsedEntries) => {
new Discord.MessageEmbed()
.setTitle("Map Details")
.setAuthor(client.user.username)
.setDescription("These are your maps for the night.")
.addFields(...parsedEntries)
// that last .addFields call is expecting parsedEntries to be of the form
// [{name: "field name", value: "field value"}, {name: "field2", value: "field2value"}]
})
Edit: essentially, you have a list of objects with properties that look like this:
{
"name": "2 Evil Eyes",
"link": "http://",
"extra": ""
}
But addFields takes objects with properties that look like this:
{
"name": "Field title"
"value": "Field value"
}
So you can use the JavaScript map function to map over each entry in the list, converting objects from your original format as specified in your maps.json file to the one that the Discord API requires.
Here's an article that might help introduce the essential concept of JavaScript mapping if you aren't previously familiar with it.

Is it possible to get a list of all items in an array of objects that match items in another array?

I'm working on a React project that'll allow me to search through a list of games to help me decide what to play based on what I'm in the mood for, currently I can add games to a JSON file but I'm really struggling with the searching part.
Right now, to add a new game, you'll enter the title, genre(s) and a description of the game. The genre field is a ReduxForm FieldArray object, and I think that's what's giving me the trouble. Here's my current JSON file
{
"games": [
{
"name": "Rainbow Six: Siege",
"genres": [
{
"genre": "tactical"
},
{
"genre": "shooter"
}
],
"description": "tactical team based shooter",
"id": 1
},
{
"name": "Resident Evil 2",
"genres": [
{
"genre": "horror"
},
{
"genre": "survival"
},
{
"genre": "shooter"
}
],
"description": "classic resident evil 2 remake in 2019",
"id": 2
},
{
"name": "Rocket League",
"genres": [
{
"genre": "cars"
},
{
"genre": "competition"
},
{
"genre": "one more game"
}
],
"description": "soccar!",
"id": 3
}
]
}
This is the dummy data I'm using to search:
const searchedGenres = 'horror, shooter';
const searchedList = searchedGenres.split(', ');
let foundGame = [];
Once I get the search working with this data, the plan is to allow me to just type in data on the frontend in one textbox, so "horror, shooter" would be my search term. The result from this search should only return Resident Evil 2, however I'm also receiving Rainbow Six: Siege as a result, even though it's missing one of my requested genres.
searchedList.forEach(searchedGenre => {
this.props.games.map(game => {
if (
game.genres.find(
({ genre }) =>
genre.toLowerCase() ===
searchedGenre.toLowerCase().trim()
) !== undefined
) {
foundGames.push(game);
}
});
});
I understand why I'm getting both Rainbow Six and Resident Evil as a result, because I'm not actually checking that both genres are in the games genres when I add the game to the foundGames array, but I'm completely lost on how I'd go about making sure all of the genres are in a game before I add it.
This would be a bit easier if your genres was a simple array of strings rather than objects, but still you can check pretty succinctly by leveraging some() and every() within filter() (btw filter() is a better choice than map() + push() here)
let games = [{"name": "Rainbow Six: Siege","genres": [{"genre": "tactical"},{"genre": "shooter"}],"description": "tactical team based shooter","id": 1},{"name": "Resident Evil 2","genres": [{"genre": "horror"},{"genre": "survival"},{"genre": "shooter"}],"description": "classic resident evil 2 remake in 2019","id": 2},{"name": "Rocket League","genres": [{"genre": "cars"},{"genre": "competition"},{"genre": "one more game"}],"description": "soccar!","id": 3}]
const searchedGenres = 'horror, shooter';
const searchedList = searchedGenres.split(', ');
let foundGame = games.filter(game => searchedList.every(searchItem => game.genres.some(g => g.genre == searchItem) ))
console.log(foundGame)
The filter condition basically says you want every game in searchedList to match at least one genre in the game. This will make it only return games that match every genre.

angular $watch over a particular field for all objects of an array

I have an array of halls like this, which I am getting from a $http.get().
$scope.halls = [
{
"_id": "524886d4c6d8a5a3b8949f6f",
"alias": "",
"hallId": "2",
"locationHint": "Near support team",
"name": "Conference Hall",
"bookQueue": [],
"occupancy": {
"occupied": true,
"occupiedBy": "Vignesh",
"purpose": "Sync up",
"team": "Edge",
"timeRange": {
"from": "2013-10-02T19:08:44.752Z",
"to": "2013-10-02T19:08:44.752Z"
}
},
"capabilities": [
"ceiling mic",
"room speaker",
"projector",
"Mac machine"
]
},
{
"_id": "524886fbc6d8a5a3b8949f70",
"alias": "",
"hallId": "3",
"locationHint": "Near Edge Team",
"name": "Training room",
"bookQueue": [],
"occupancy": {
"occupied": true,
"occupiedBy": "Tharma",
"purpose": "Review",
"team": "Platform",
"timeRange": {
"from": "2013-10-02T19:08:44.752Z",
"to": "2013-10-02T19:08:44.752Z"
}
},
"capabilities": [
"ceiling mic",
"room speaker",
"projector"
]
},
{
"_id": "52488794c6d8a5a3b8949f71",
"alias": "",
"hallId": "4",
"locationHint": "Near front office",
"name": "Front Office Discussion room",
"bookQueue": [],
"occupancy": {
"occupied": false,
"occupiedBy": "",
"purpose": "",
"team": "",
"timeRange": {
"from": "2013-10-02T19:08:44.752Z",
"to": "2013-10-02T19:08:44.752Z"
}
},
"capabilities": [
"ceiling mic",
"room speaker",
"TV"
]
}
]
I want to update the hall.occupancy, when the current date ( Date.now() )is greater than hall.occupancy.timeRange.to . In this case, I am not going to watch hall.occupancy.timeRange.to because, it is not the property that is going to change. Is there an angular way to do this, because it would get really messy to put a setInterval.
I am not really sure on how to go about this.
I am still in the early stages of learning angular, so it would be good if you were gentle in pointing out an efficient way.
I don't think that is something you can easily do. the $watch function watches a certain angular expression executed in the scope. You don't have a $scope.hall so it won't do anything. You could possibly watch each instance, but what if you add or remove items from the array?
$scope.$watch('halls[0].occupancy.timeRange.to', function(a, b) {
It would probably be better to just watch the halls array and loop through it to do your check on each item when anything changes. That is basically what angular does to detect changes itself.
$scope.watch('halls', function (newValue, oldValue) {
var now = Date.now();
for (var i = 0; i < newValue.length; i++) {
// check each item
}
};
You can sort your array by hall.occupancy.timeRange.to and execute setInvertal to the nearest upcoming date. Do the same again after timer event is fired taking the next item from sorted list.
If your list changes you have to cancel active timeout and set the new one using described procedure.
In my opinion $digest phase is not enough if you want to create notifications for inactive users - they leave the site with opened tab and do something else (I do that very often with Stackoverflow).
However if there is a lot of user events (clicks, playing with ng-model etc) in your application you should watch time and check the list after each 10 seconds:
var watchFn = function() {
var d = new Date();
return '' + d.getMinutes() + Math.floor(d.getSeconds()/10);
};
$scope.$watch(watchFn, function() {
console.log('User did something and $watch is fired after minimum 10s.');
});
Perhaps, I am doing this to solve this problem, but I am not sure if this is the best way.
setInterval(function(){
if($scope.halls)
{
for(var i =0 ;i< $scope.halls.length;i++){
var hall = $scope.halls[i];
if(hall.occupancy.timeRange.to){
if(Date.now() > new Date(hall.occupancy.timeRange.to.toString())){
//change occupancy.. either dequeue from blockQueue, shift to occupancy, or change occupancy.occupied to false
$scope.halls[i].occupancy = {occupied:false,occupiedBy:"",purpose:"",team:"",timeRange:{from:"",to:""}};
socket.emit('updateHall',$scope.halls[i]);
$scope.$apply();
}
}
}
}
},500);
So, basically, it checks every 500 milliseconds whether the current time is later than hall.occupancy.timeRange.to , if so, it resets the hall.occupancy field.
Let me know if you have a better solution.

Javascript looping through elements and adding to table

I'm having trouble finding a solution that will help me loop through a bunch of elements and putting the chosen values into a table. I've been able to withdraw some values but the method isn't dynamic.
Here is an example:
var Table = {
"credit": {
"link": "site link",
"logoUrl": "logo url",
"message": "message"
},
"groups": [
{
"labels": [
{
"name": "Western Conference",
"type": "conference"
},
{
"name": "Central Division",
"type": "division"
}
],
"standings": [
{
"stats": [
{
"name": "gp",
"value": 20
},
{
"name": "w",
"value": 17
},
{
"name": "l",
"value": 0
},
{
"name": "gf",
"value": 64
},
{
"name": "ga",
"value": 37
},
{
"name": "gd",
"value": 27
},
{
"name": "pts",
"value": 37
}
],
"team": {
"id": 12345,
"link": "team link",
"name": "team name",
"shortName": "team"
}
},
This is the structure of the elements. So far I've used this:
document.getElementById("sGamesPlayed").innerHTML=Table.groups[0].standings[0].stats[0].value;
to withdraw values. However there are more teams, stats and divisions so I would need some kind of loop to go through the elements and put the into a dynamic table.
I would consider you to look at http://underscorejs.org/.
it provides a bunch of utility functions that could help you,
for example, _.each() helps you loop through JSON properties.
for the sample objects you've given (after completing the missing brackets at the end),
_.each(Table.groups[0].standings[0].stats, function(stats){
console.log(stats['name']+","+stats['value'])
})
gives me:
gp,20
w,17
l,0
gf,64
ga,37
gd,27
pts,37
how it works is that you provide the object you want as the first argument and the function that you give as the second argument will be called with each element of the first argument (Assuming it is a list).
I would also urge you to look at underscore templating that you can use to render your table where i put the console.log :
http://net.tutsplus.com/tutorials/javascript-ajax/getting-cozy-with-underscore-js/
http://scriptble.com/2011/01/28/underscore-js-templates/
I guess your question is about filtering the values of the array standings. In order to do that you can use the jQuery grep function (if you want to use jQuery).
For example you can write:
var arr = $.grep(Table.groups[0].standings[0].stats, function(d){return d.value>25})
Which will give
arr = [{"name": "gf","value": 64}, {"name": "ga", "value": 37},{"name": "gd", "value": 27},{"name": "pts", "value": 37}]
If this is not what you meant, can you please create a jsFiddle with a sample of what you want?
Depending on what you want to do with the results, you can go over the object using a scheme like:
var groups, standings, stats, value;
groups = Table.groups;
// Do stuff with groups
for (var i=0, iLen=groups.length; i<iLen; i++) {
standings = groups[i].standings;
// Do stuff with standings
for (var j=0, jLen=standings.length; j<jLen; j++) {
stats = standings[j];
// Do stuff with stats
for (var k=0, kLen=stats.length; k<kLen; k++) {
value = stats[k].value;
// Do stuff with value
}
}
}
Of course I have no idea what the data is for, what the overall structure is or how you want to present it. But if you have deeply nested data, all you can do is dig into it. You might be able to write a recursive function, but it might also become very difficult to maintain if the data structure is complex.

Categories

Resources