JavaScript Styling Search Results

Okay, so I've written a searching application, it works great, back end is 100% fine. HOWEVER now that the back end is done, I'm working on the front end. I'm just trying to include some highlighting features, and it works, however, right now, due to a lack of sleep, my ability to think with boolean logic isn't great. I've been having a lot of brain farts today.

All I'm working on is this piece of code:

/* UPDATE INCLUDES TESTING VARIABLES */
$(document).ready(function(){
     var start = new Date().getTime();
     console.log("Started at: " + start);
     var search = $('#keyword').text();	
     var listItems = $('#search-results'); 
     

           listItems.find('.index').each(function(){
                let current = $(this).text();

                for(let i = 0; i < s.length; i++){
                    for(let j = 0; j < current.length; j++){
                        let currChar = current.charAt(j);
                        let sChar = s.charAt(i);
                        
                        if(currChar == sChar){
                                $('.search-index').highlight(current.charAt(j));
                                break;
                        }
                    }
                }
          });

         var end = new Date().getTime();
         console.log("Ended at : " + end);
         var dur = (end - start) / 1000;
         console.log("Time taken to execute: " + dur + ".sec");
});

Now like I said, it works no problem, however I think it would be best to only highlight characters IF both j & i match twice in a row rather than just the once. I only say that because let's say you've searched for something like Forest Gump and there's a tonne of results. Let's just say the server returns Forever Young (random - don't ask, just accept it). It will currently highlight every character in the returned result if character 'z' is within the searched term.

That's not really an issue, I mean I kinda wanted that to be honest. But I would also like to calm it down a little. As for the highlighting method, I'm just being lazy and using a tool someone else made.

To be exact:- http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html


From what you've read, can you suggest any better ways in which I could include such a feature? - I've also notices if I allow a silly number of results to return, it'll take a noticeable amount of time for the function to do it's thing, I know nested loops with a lot of iterations aren't exactly computationally friendly, but right now it's all I can come up with. I am pretty much brain dead right now.


Just to test how effective my front end implementation is, I decided to place a ridiculous amount of data into the algorithm. Long story short, it's actually embarrassing how awful it is in terms of efficiency..... I mean I did decide to pass in 1,000 different pieces of data into it, but still. I console logged the time my script started, and ended, and on my test of 1,000 different inputs, it ended up taking... [DRUM ROLL].... 203.652 SECONDS... That's not good by anyone's standard... :joy:


Maybe someone can see a way where I don't have to use nested loops or quite as many loops? - The only way I can see around this is that I'd have to use nested loops, but then again, I'm a junior web developer, what do I know aye? :wink:

But at the same time, I do need to loop through each class called index. I then need to collect or loop through each character in the specific class of index. I then need to loop/compare each letter in the searched term. I mean I can't really think of a way around this without seriously sacrificing on the code's readability.


Not that I thought it would've made much difference, but I thought I'd try breaking it up into functions, and if anything, I think that made it slower to be honest. With it all being within one function, it takes 0.763 seconds with a realistic amount of data passed into the function, when I tried breaking it up, it went to over double that.

So you want to highlight only certain characters withing a string, like only "est" in Forest Gump?

Instead of using multiple loops have you tried using a regular expression?

I haven't if I'm honest. But I'm not 100% sure how I'd implement such a feature, I know that WOULD be much quicker. But I'm trying to think of how I'd go about implementing that, and I'm not 100% sure? :joy:

Here's a rudimentary example using a regular expression

var searchResults = [
	"hey forest",
	"a quick forest gump jumped over the fence",
	"foo podcast"
];

// loop through search results
for (var i = 0; i < searchResults.length; i++) {
	
	if (/est/i.test(searchResults[i]))
		// match is found/style text accordingly
	
}

-the first two indicies of searchResults match

So I'd have to still loop through the word.. And I'm not sure how I'd carry out the if statement to be honest, simply because of the fact that I wouldn't be wanting to highlight stuff that contains 'est' all the time, as an example.

Plus I'd only want to highlight specific characters, not entire strings if that makes sense?

I mean I'm heavily sleep deprived, at this rate I swear I have insomnia or something... :joy: ... But right now, I cannot focus on anything! :stuck_out_tongue:


I may be totally wrong, but that method, as far as I can see, that would only add an if statement, nothing more nothing less? I may be wrong, but I can't see how that would make it quicker. Plus your example loses some functionality, but I'm sure that's just because it's simply an example.

I was just showing you how to apply logic using a regular expression, and it wasn't necessarily applicable to your exact needs.

I understand that you want something highlighted based on a match of some sort, but that is what I'm still a little confused on. If you can please provide a pseudo example of the scenario. Input parameters? Search criteria? Results returned from search? What you want done to the results? The results that should not receive any formatting, and so on.

I'm thinking out loud here...

If you're using jquery - if it's dynamic (like the google search "early search results drop down) could you assign the value of the text field to a var on keyup- and if var.length > 1 then

Run

if(results.indexOf($var) >=0)
   $('#searchResults).highlight($var);

Otherwise just have an event trigger the results to be assigned to variable and if greater than two do that if statement. Maybe that might work?

If the results are and array maybe do

 (for i = 0; i < results.length; i++;)
{
   if (results[$i].indexOf($var)>=0)
    $ ( '#searchResults).highlight($var);
 }

As far as I can tell, that solution is kinda the same as to what I have in place now. Only I'm not using an array.

I mean CURRENTLY I'm looping through a list, then for each li within the list, I'm looping through the word, while looping through the word, I'm also looping through the searched term, comparing each individual character. Then if each character matches aka char x = char y then highlight.

I may just be missing something in your example.


As for an actual example @semnon. Here goes.

The user can search for the word: forest gump 123

The backend returns the following results (it returns as many results as possible, it's a deliberate design):

  1. Forget me not
  2. 12x10
  3. Single Door
    ...

It then applies the styling to each char in each result, IF the char exists in the searched term, as you can see below, I've copied the source code that's produced:

  1. <span>Forget me</span> n<span>ot</span>
  2. <span>12</span>x<span>1</span>0
  3. <span>S</span>in<span>g</span>l<span>e</span> D<span>oor</span>
    ...

The produced HTML doesn't look pretty I know, and if anything I've trimmed it down so that each span doesn't have the class attribute or the value of zed class. I just apply a class called highlight and then use some CSS to apply some styling to the class highlight. I mean if anything that may not be what it actually outputs, I hand wrote that because of how disgusting the source code looks when you run this code.


Example 2:

Search just the char s.

Returns from backend:

  1. Simple Shed
    ...

Front end result:

1.<span>S</span>imple <span>S</span>hed
...

Just look at my second example to get the picture, the first one IS in more depth, BUT the output code looks f***ing foul.


With my current implementation it's lightning fast, even on s***y mobile devices when I return no more than n number of results. I mean this is NOT urgent, I mean if I can't make it faster than 0.5 seconds, then I'm not gonna lose sleep over it. I'm mostly doing this to LEARN more than anything, if I'm honest. :wink: ... I'm just trying to make it more efficient because I know that at some point in the future, I PROBABLY will need a faster algorithm with similar functionality. :slight_smile:


I also tried the following:

@cotton

(for i = 0; i &lt; results.length; i++;){                            
      if (results[$i].indexOf($var)>=0)
      $ ( '#searchResults).highlight($var);
}

It didn't quite work as expected, it applied some highlighting, but on the wrong characters, like if you searched the letter s, somehow, it would end up highlighting numbers and some non alphabetic symbols.

When I change the code to do something HIGHLY similar, it doesn't seem to work any quicker than when I use nested loops too. I'm assuming it's either that the .highlight function isn't entirely efficient or it's due to the fact that I'm still using 2 nested loops rather than 3?


Here's exactly what I tried:

for(let i = 0; i < searched.length; i++){
    let currChar = searched.charAt(i);

    if(current.indexOf(currChar) >= 0){
    	$('.search-index').highlight(currChar);
    	// break;
        //KINDA NEED A BREAK OTHERWISE IT'S **STUPIDLY SLOW.**
    }
}

NEWS FLASH

I honestly don't know how this works. BUT it does. I've managed to increase the speed of the algorithm dramatically, now it doesn't seem to matter on the size of the data that's input into the function.

Take a look and please, if you can tell me why or how this works, then you're a freaking star. It now takes 0.05 seconds worst case scenario AND IT STILL WORKS!!!!!!!!. :smiley:

So here's the code anyways:

	let start = new Date().getTime();

	$(document).ready(function(){
		let searched = $('#search-keyword').text().toLowerCase();
		let listItems = $('#search-results-list');
		let ext = 0;

		listItems.find('.search-index').each(function(){
			let current = $(this).text().toLowerCase();
			ext ++;

			if(ext  > 1){
				return;
			}

			for(let i = 0; i < searched.length; i++){
				for(let j = 0; j < current.length; j++){
					let currChar = current.charAt(j);
					let sChar = searched.charAt(i);
					
					if(currChar == sChar){
						$('.search-index').highlight(current.charAt(j));
						break;
					}
				}
			}
		});

		let end = new Date().getTime();
		let dur = (end - start) / 1000;
		console.log("It took this lon to process:" + dur + " sec");
	});

Just by including the variable ext it seems to run stupidly fast. Anyone got any ideas how this is working? I mean I would've thought that with that functionality included it would ONLY run ANY of the code below on the first list item/result from the search. Turns out that it still manages to apply highlighting to ALL list items that are returned from the search result, even if 1,000 results pop up, the javascript now only takes > 0.06 seconds to execute.

The ONLY thing I've added as I've implied above is the ext variable. Which also implies that I've added the if statement that checks to see if ext is greater than 1.

I honestly am mind blown by the fact that this works, let alone being mind blown by how it's DRAMATICALLY faster. With my previous implementation, I managed to find out that worst case scenario it would take 8 seconds, provided I returned no more than 100 results.

PLEASE, PLEASE tell me how this has managed to work? :joy::joy::joy::joy:

@semnon @cotton


I genuinely swear down, I can even provide evidence. I'm currently running the code when it was dramatically slower, and I'm gonna take screenshots of the slow implementation. And now I'm gonna take a screenshot of the fast implementation, even if you don't believe me, the numbers don't lie! :wink: ...

Just bear with me, it may take a while for the slow implementation to finish running on the slower implementation.

Slow Implementation

As you can see, that kinda time for ANYTHING to execute is NOT acceptable.


Fast Implementation

Now yeah, it took longer than 0.06 seconds as I've previously mentioned. But it's still an INSANE PERFORMANCE INCREASE...... Seriously, how? Just how does this work?


@semnon, @cotton

UPDATE

I think I MAY have worked out why it still works, I think it's to do with how the highlight jquery plug in works... I THINK.... As I'm feeding the highlight function the class of search-index rather than $(this), it's able to highlight all of the classes 'search-index'....

Minor Issue

I think that if let's say you searched pimp les and it returns:
1. pimp
2. pimp les

I think that it may not apply the highlight to the les output... I mean don't ask me why I picked that as an example, but I just did, because pimps.


The Truth

I'm spot on, I just typed in something random, looked at the results, and then I typed in a space and 1 additional character that occurs on result number 2 but doesn't appear in result number 1. That additional character didn't get highlighted. No big deal in my opinion, I don't think that's essential, I mean it doesn't work 100% correctly now, HOWEVER it still works kinda okay and it's still stupidly much faster.

I know you're doing this purposely, but just out of curiosity why would you want to include negated strings in your results?

Perhaps because you're breaking out of your logic too early? Are you running your algorithm after the user finishes typing, or after each keystroke?

Albeit the minor issue it seems like your content with what you have. If you want to experiment with a different methodology you could try using regular expressions. Here's another example that provides you the index of the match

// data on your DB
var strings = 
	[
		"a quick forest gump",
		"forest gump",
		"a big green forest"
	];

// search criteria entered by user
var inputString = "forest gump";

// build your dynamic regular expression
var search = new RegExp(inputString, "i");

// search for/return matches
for (var i = 0; i < strings.length; i++) {
	
	var temp = search.exec(strings[i]);
	
	if (temp)
		console.log(temp.index);
}

The negated strings were just a silly example... That's it...

I WAS breaking out of the logic too early, you're exactly right, it now only looks at the top 10 results and I have yet to run into any issues with that logic.

The way it now works, there's really no need to use a regular expression, I mean I could use a regex, but it actually works just fine the way it is now. The logic I'm using now is 'if it ain't broken...' Plus it runs quickly now too, so I fail to see why bother with an alternative methodology! :wink:


BUT I'd like to just thank you for both your time and your suggestions, I have no doubt that using a regex would without a doubt be a perfectly valid approach. :slight_smile: