Fuzzy Search Plug-in
Fuzzy searching is used in search engines and databases to perform searches that will match results which are similar, but not necessarily exactly the same as the search term. This allows spelling mistakes and typos to be accounted for. It also allows small changes in dialect not to affect search results. A commonly used example is for surname searching; "Smith" and "Smythe" are pronounced the same way, but when using exact match searching typing "Smith" would not return "Smythe".
This plug-in adds fuzzy search functionality to DataTables. It does this through a combination of exact matching and the Damerau-Levenshtein algorithm.
Before we dive any deeper, here is a preview of what you can expect from this plug-in. The below example initialises the plugin in it's most simple form - replacing the exact searching that is standard with DataTables and replacing it with the new fuzzy search algorithm.
Name | Position | Office | Salary |
---|---|---|---|
Tiger Nixon | System Architect | Edinburgh | $320,800 |
Garrett Winters | Accountant | Tokyo | $170,750 |
Ashton Cox | Junior Technical Author | San Francisco | $86,000 |
Cedric Kelly | Senior Javascript Developer | Edinburgh | $433,060 |
Airi Satou | Accountant | Tokyo | $162,700 |
Brielle Williamson | Integration Specialist | New York | $372,000 |
Herrod Chandler | Sales Assistant | San Francisco | $137,500 |
Rhona Davidson | Integration Specialist | Tokyo | $327,900 |
Colleen Hurst | Javascript Developer | San Francisco | $205,500 |
Sonya Frost | Software Engineer | Edinburgh | $103,600 |
Jena Gaines | Office Manager | London | $90,560 |
Quinn Flynn | Support Lead | Edinburgh | $342,000 |
Charde Marshall | Regional Director | San Francisco | $470,600 |
Haley Kennedy | Senior Marketing Designer | London | $313,500 |
Tatyana Fitzpatrick | Regional Director | London | $385,750 |
Michael Silva | Marketing Designer | London | $198,500 |
Paul Byrd | Chief Financial Officer (CFO) | New York | $725,000 |
Gloria Little | Systems Administrator | New York | $237,500 |
Bradley Greer | Software Engineer | London | $132,000 |
Dai Rios | Personnel Lead | Edinburgh | $217,500 |
Jenette Caldwell | Development Lead | New York | $345,000 |
Yuri Berry | Chief Marketing Officer (CMO) | New York | $675,000 |
Caesar Vance | Pre-Sales Support | New York | $106,450 |
Doris Wilder | Sales Assistant | Sydney | $85,600 |
Angelica Ramos | Chief Executive Officer (CEO) | London | $1,200,000 |
Gavin Joyce | Developer | Edinburgh | $92,575 |
Jennifer Chang | Regional Director | Singapore | $357,650 |
Brenden Wagner | Software Engineer | San Francisco | $206,850 |
Fiona Green | Chief Operating Officer (COO) | San Francisco | $850,000 |
Shou Itou | Regional Marketing | Tokyo | $163,000 |
Michelle House | Integration Specialist | Sydney | $95,400 |
Suki Burks | Developer | London | $114,500 |
Prescott Bartlett | Technical Author | London | $145,000 |
Gavin Cortez | Team Leader | San Francisco | $235,500 |
Martena Mccray | Post-Sales support | Edinburgh | $324,050 |
Unity Butler | Marketing Designer | San Francisco | $85,675 |
Howard Hatfield | Office Manager | San Francisco | $164,500 |
Hope Fuentes | Secretary | San Francisco | $109,850 |
Vivian Harrell | Financial Controller | San Francisco | $452,500 |
Timothy Mooney | Office Manager | London | $136,200 |
Jackson Bradshaw | Director | New York | $645,750 |
Olivia Liang | Support Engineer | Singapore | $234,500 |
Bruno Nash | Software Engineer | London | $163,500 |
Sakura Yamamoto | Support Engineer | Tokyo | $139,575 |
Thor Walton | Developer | New York | $98,540 |
Finn Camacho | Support Engineer | San Francisco | $87,500 |
Serge Baldwin | Data Coordinator | Singapore | $138,575 |
Zenaida Frank | Software Engineer | New York | $125,250 |
Zorita Serrano | Software Engineer | San Francisco | $115,000 |
Jennifer Acosta | Junior Javascript Developer | Edinburgh | $75,650 |
Cara Stevens | Sales Assistant | New York | $145,600 |
Hermione Butler | Regional Director | London | $356,250 |
Lael Greer | Systems Administrator | London | $103,500 |
Jonas Alexander | Developer | San Francisco | $86,500 |
Shad Decker | Regional Director | Edinburgh | $183,000 |
Michael Bruce | Javascript Developer | Singapore | $183,000 |
Donna Snider | Customer Support | New York | $112,000 |
Quick start
If you'd like to use the fuzzy search plug-in on your DataTable, you can do so by simply including the following javascript on your page in a script tag:
Finally, (yes it is that easy!) you will need to set the fuzzySearch
initialisation option to be true
- e.g.:
$('#myTable').DataTable({
fuzzySearch: true
})
From here you will find that fuzzy searching is enabled and spelling mistakes or typos will not force the removal of records from the table. Visually nothing will change when initialising this option as a boolean, but there is scope for extra functionality by using the other options.
Options
Type | Option | Description |
---|---|---|
boolean or object |
fuzzySearch |
Enable fuzzy search on the table. |
boolean |
fuzzySearch.toggleSmart |
Allow the search mode to be toggled - simply hover over the input element and select your desired search mode from the tooltip. |
column-selector |
fuzzySearch.rankColumn |
Defines a column to be used for displaying the similarity between the search term and the match value. |
number |
fuzzySearch.threshold |
Set the matching threashold from the Damerau-Levenshtein algorithim. Values between 0 and 1. Lower numbers mean less exact matching. Default is 0.5. |
Examples
With the fuzzySearch.toggleSmart
option enabled, the end user can switch between DataTables normal smart search and fuzzy search, with an indicator showing which search mode they are in:
$('#fuzzy-toggle').DataTable({
fuzzySearch: {
toggleSmart: true
}
});
Name | Position | Office | Salary |
---|---|---|---|
Tiger Nixon | System Architect | Edinburgh | $320,800 |
Garrett Winters | Accountant | Tokyo | $170,750 |
Ashton Cox | Junior Technical Author | San Francisco | $86,000 |
Cedric Kelly | Senior Javascript Developer | Edinburgh | $433,060 |
Airi Satou | Accountant | Tokyo | $162,700 |
Brielle Williamson | Integration Specialist | New York | $372,000 |
Herrod Chandler | Sales Assistant | San Francisco | $137,500 |
Rhona Davidson | Integration Specialist | Tokyo | $327,900 |
Colleen Hurst | Javascript Developer | San Francisco | $205,500 |
Sonya Frost | Software Engineer | Edinburgh | $103,600 |
Jena Gaines | Office Manager | London | $90,560 |
Quinn Flynn | Support Lead | Edinburgh | $342,000 |
Charde Marshall | Regional Director | San Francisco | $470,600 |
Haley Kennedy | Senior Marketing Designer | London | $313,500 |
Tatyana Fitzpatrick | Regional Director | London | $385,750 |
Michael Silva | Marketing Designer | London | $198,500 |
Paul Byrd | Chief Financial Officer (CFO) | New York | $725,000 |
Gloria Little | Systems Administrator | New York | $237,500 |
Bradley Greer | Software Engineer | London | $132,000 |
Dai Rios | Personnel Lead | Edinburgh | $217,500 |
Jenette Caldwell | Development Lead | New York | $345,000 |
Yuri Berry | Chief Marketing Officer (CMO) | New York | $675,000 |
Caesar Vance | Pre-Sales Support | New York | $106,450 |
Doris Wilder | Sales Assistant | Sydney | $85,600 |
Angelica Ramos | Chief Executive Officer (CEO) | London | $1,200,000 |
Gavin Joyce | Developer | Edinburgh | $92,575 |
Jennifer Chang | Regional Director | Singapore | $357,650 |
Brenden Wagner | Software Engineer | San Francisco | $206,850 |
Fiona Green | Chief Operating Officer (COO) | San Francisco | $850,000 |
Shou Itou | Regional Marketing | Tokyo | $163,000 |
Michelle House | Integration Specialist | Sydney | $95,400 |
Suki Burks | Developer | London | $114,500 |
Prescott Bartlett | Technical Author | London | $145,000 |
Gavin Cortez | Team Leader | San Francisco | $235,500 |
Martena Mccray | Post-Sales support | Edinburgh | $324,050 |
Unity Butler | Marketing Designer | San Francisco | $85,675 |
Howard Hatfield | Office Manager | San Francisco | $164,500 |
Hope Fuentes | Secretary | San Francisco | $109,850 |
Vivian Harrell | Financial Controller | San Francisco | $452,500 |
Timothy Mooney | Office Manager | London | $136,200 |
Jackson Bradshaw | Director | New York | $645,750 |
Olivia Liang | Support Engineer | Singapore | $234,500 |
Bruno Nash | Software Engineer | London | $163,500 |
Sakura Yamamoto | Support Engineer | Tokyo | $139,575 |
Thor Walton | Developer | New York | $98,540 |
Finn Camacho | Support Engineer | San Francisco | $87,500 |
Serge Baldwin | Data Coordinator | Singapore | $138,575 |
Zenaida Frank | Software Engineer | New York | $125,250 |
Zorita Serrano | Software Engineer | San Francisco | $115,000 |
Jennifer Acosta | Junior Javascript Developer | Edinburgh | $75,650 |
Cara Stevens | Sales Assistant | New York | $145,600 |
Hermione Butler | Regional Director | London | $356,250 |
Lael Greer | Systems Administrator | London | $103,500 |
Jonas Alexander | Developer | San Francisco | $86,500 |
Shad Decker | Regional Director | Edinburgh | $183,000 |
Michael Bruce | Javascript Developer | Singapore | $183,000 |
Donna Snider | Customer Support | New York | $112,000 |
The next example adds a column for the similarity to be displayed in by initialising using the fuzzySearch.rankColumn
option, which the table orders by to give an output that might expected from a search engine:
var fsrco = $('#fuzzy-ranking').DataTable({
fuzzySearch: {
rankColumn: 3
},
sort: [[3, 'desc']]
});
fsrco.on('draw', function(){
fsrco.order([3, 'desc']);
});
Name | Position | Office | Salary |
---|---|---|---|
Tiger Nixon | System Architect | Edinburgh | $320,800 |
Garrett Winters | Accountant | Tokyo | $170,750 |
Ashton Cox | Junior Technical Author | San Francisco | $86,000 |
Cedric Kelly | Senior Javascript Developer | Edinburgh | $433,060 |
Airi Satou | Accountant | Tokyo | $162,700 |
Brielle Williamson | Integration Specialist | New York | $372,000 |
Herrod Chandler | Sales Assistant | San Francisco | $137,500 |
Rhona Davidson | Integration Specialist | Tokyo | $327,900 |
Colleen Hurst | Javascript Developer | San Francisco | $205,500 |
Sonya Frost | Software Engineer | Edinburgh | $103,600 |
Jena Gaines | Office Manager | London | $90,560 |
Quinn Flynn | Support Lead | Edinburgh | $342,000 |
Charde Marshall | Regional Director | San Francisco | $470,600 |
Haley Kennedy | Senior Marketing Designer | London | $313,500 |
Tatyana Fitzpatrick | Regional Director | London | $385,750 |
Michael Silva | Marketing Designer | London | $198,500 |
Paul Byrd | Chief Financial Officer (CFO) | New York | $725,000 |
Gloria Little | Systems Administrator | New York | $237,500 |
Bradley Greer | Software Engineer | London | $132,000 |
Dai Rios | Personnel Lead | Edinburgh | $217,500 |
Jenette Caldwell | Development Lead | New York | $345,000 |
Yuri Berry | Chief Marketing Officer (CMO) | New York | $675,000 |
Caesar Vance | Pre-Sales Support | New York | $106,450 |
Doris Wilder | Sales Assistant | Sydney | $85,600 |
Angelica Ramos | Chief Executive Officer (CEO) | London | $1,200,000 |
Gavin Joyce | Developer | Edinburgh | $92,575 |
Jennifer Chang | Regional Director | Singapore | $357,650 |
Brenden Wagner | Software Engineer | San Francisco | $206,850 |
Fiona Green | Chief Operating Officer (COO) | San Francisco | $850,000 |
Shou Itou | Regional Marketing | Tokyo | $163,000 |
Michelle House | Integration Specialist | Sydney | $95,400 |
Suki Burks | Developer | London | $114,500 |
Prescott Bartlett | Technical Author | London | $145,000 |
Gavin Cortez | Team Leader | San Francisco | $235,500 |
Martena Mccray | Post-Sales support | Edinburgh | $324,050 |
Unity Butler | Marketing Designer | San Francisco | $85,675 |
Howard Hatfield | Office Manager | San Francisco | $164,500 |
Hope Fuentes | Secretary | San Francisco | $109,850 |
Vivian Harrell | Financial Controller | San Francisco | $452,500 |
Timothy Mooney | Office Manager | London | $136,200 |
Jackson Bradshaw | Director | New York | $645,750 |
Olivia Liang | Support Engineer | Singapore | $234,500 |
Bruno Nash | Software Engineer | London | $163,500 |
Sakura Yamamoto | Support Engineer | Tokyo | $139,575 |
Thor Walton | Developer | New York | $98,540 |
Finn Camacho | Support Engineer | San Francisco | $87,500 |
Serge Baldwin | Data Coordinator | Singapore | $138,575 |
Zenaida Frank | Software Engineer | New York | $125,250 |
Zorita Serrano | Software Engineer | San Francisco | $115,000 |
Jennifer Acosta | Junior Javascript Developer | Edinburgh | $75,650 |
Cara Stevens | Sales Assistant | New York | $145,600 |
Hermione Butler | Regional Director | London | $356,250 |
Lael Greer | Systems Administrator | London | $103,500 |
Jonas Alexander | Developer | San Francisco | $86,500 |
Shad Decker | Regional Director | Edinburgh | $183,000 |
Michael Bruce | Javascript Developer | Singapore | $183,000 |
Donna Snider | Customer Support | New York | $112,000 |
Deep dive - Building the plug-in
Using our FuzzySearch plug-in is easy enough, so if you are interested in the implementation details, let's take a deep dive into how it all works, and we can investigate how to create custom row based filtering plug-ins.
The Damerau-Levenshtein Algorithm
The Damerau-Levenshtein Algorithm is used to measure the edit distance between two sequences. This algorithm is often used in search engines, databases and spell checkers to better improve their abilities to identify potential mistakes in the input. We aren't going to go into this algorithm in great depth here, all we need to know is that it has been tried and tested in many applications before ours!
Another big plus is that it is available on npm, making it perfect for our use case.
The npm module provides a function (levenstein()
) that takes two string arguments and returns an object with three values as follows.
steps
- the Damerau-Levenshtein distance between the two stringsrelative
- The number of steps divided by the length of the longest stringsimilarity
- 1 - the value ofrelative
Creating the Specification for this Plug-in
Before creating the plug-in it was important to carefully consider what features we wanted to provide.
The first clearly was the fuzzy search capability. Given that DataTables already has a searchbox this plugin should reuse that when searching the table. This means less UI changes for the end users and keeps the interface nice and simple.
It may also be useful for users to be able to toggle between exact searching and fuzzy searching. To do this an icon should be appended to the search box that is capable of indicating the search mode. When hovering over the search box a tooltip should be displayed. This tooltip should contain two buttons that toggle the search mode appropriately. This shouldn't be the default, but the user should be able to enable it by using a fuzzySearch.toggleSmart
initialisation option.
Another cool feature would be to have a column in the table that shows how similar the input string is to the data in that row. This shouldn't be the default, but the user should be able to point to which column to use for this by using a fuzzySearch.rankColumn
initialisation option.
Searching on return is something that often comes up in the forums. In DataTables 1.11 we added the search.return
initialisation option. Given there may not be any matches initially the plugin should also integrate with this initialisation option and delay searching until the enter key is pressed. This will not be default behaviour.
The search function will use the similarity
property that is returned from the levenshtein()
function to decide whether to display the row or not. The threshold for this comparison should be able to be set by using a fuzzySearch.threshold
initialisation option.
Another useful feature would be to add an api method that can get and set the search value for fuzzy searching.
Finally, the search mode should be saved and reset when the stateSave
initialisation option is enabled.
Creating the Fuzzy Search Code
Having the npm module is great, but given that the code within it is fairly simple and short (66 lines) the first thing we are going to do is lift it out and place it within our own file. This will save us bundling code to release in the plguins.
Now we can start writing our own code. Let's start by writing a fuzzySearch()
function that will return a boolean value pass
and a score
for a given row. The pass
value indicates whether the row should be included in the search results or not. The score
value is the value that should be displayed within the similarity column indicated by rankColumn
(if enabled).
This function takes 3 parameters.
searchVal
The value that has been entered into the search boxdata
The data for the row that is being processedinitial
ThefuzzySearch
initialisation options that were used
The very first check to perform is whether a searchVal
has been defined or not. If not, we want all of the rows to display so we return true and a blank score.
function fuzzySearch(searchVal, data, initial) {
// If no searchVal has been defined then return all rows.
if(searchVal === undefined || searchVal.length === 0) {
return {
pass: true,
score: ''
}
}
...
}
Our search algorithm is going to compare each word in the search term with each word in the row data. If at least one combination for each search word is above the threshold then the row should be displayed. To do this we split the search term and declare and populate an array that contains the scores and whether it is a pass or not for each word. Initially these values are {pass: false, score: 0}
. If there are any empty words after the split we don't want to consider them so those are removed from the array.
...
// Split the searchVal into individual words.
var splitSearch = searchVal.split(/[^(a-z|A-Z|0-9)]/g);
// Array to keep scores in
var highestCollated = [];
// Remove any empty words or spaces
for(var x = 0; x < splitSearch.length; x++) {
if (splitSearch[x].length === 0 || splitSearch[x] === ' ') {
splitSearch.splice(x, 1);
x--;
}
// Aside - Add to the score collection if not done so yet for this search word
else if (highestCollated.length < splitSearch.length) {
highestCollated.push({pass: false, score: 0});
}
}
...
Next we want to do some very similar operations to the row data, going over each cell.
...
// Going to check each cell for potential matches
for(var i = 0; i < data.length; i++) {
// Convert all data points to lower case fo insensitive sorting
data[i] = data[i].toLowerCase();
// Split the data into individual words
var splitData = data[i].split(/[^(a-z|A-Z|0-9)]/g);
// Remove any empty words or spaces
for (var y = 0; y < splitData.length; y++){
if(splitData[y].length === 0 || splitData[y] === ' ') {
splitData.splice(y, 1);
x--;
}
}
...
Within the same for loop shown above we are going to do some comparisons between the words from the search box and the words that we have just identified for this cell. The code for the comparison that takes place is shown below.
...
// Check each search term word
for(var x = 0; x < splitSearch.length; x++) {
// Reset highest score
var highest = {
pass: undefined,
score: 0
};
// Against each word in the cell
for (var y = 0; y < splitData.length; y++){
// If this search Term word is the beginning of the word in
// the cell we want to pass this word
if(splitData[y].indexOf(splitSearch[x]) === 0){
var newScore =
splitSearch[x].length / splitData[y].length;
highest = {
pass: true,
score: highest.score < newScore ?
newScore :
highest.score
};
}
// Get the levenshtein similarity score for the two words
var steps =
levenshtein(splitSearch[x], splitData[y]).similarity;
// If the levenshtein similarity score is better than a
// previous one for the search word then let's store it
if(steps > highest.score) {
highest.score = steps;
}
}
// If this cell has a higher scoring word than previously found
// to the search term in the row, store it
if(highestCollated[x].score < highest.score || highest.pass) {
highestCollated[x] = {
pass: highest.pass || highestCollated.pass ?
true :
highest.score > threshold,
score: highest.score
};
}
}
}
...
Lastly, we check if the search words have been passed at some point throughout the row.
// Check that all of the search words have passed
for(var i = 0; i < highestCollated.length; i++) {
if(!highestCollated[i].pass) {
return {
pass: false,
score: Math.round(((highestCollated.reduce((a,b) => a+b.score, 0) / highestCollated.length) * 100)) + "%"
};
}
}
// If we get to here, all scores greater than 0.5 so display the row
return {
pass: true,
score: Math.round(((highestCollated.reduce((a,b) => a+b.score, 0) / highestCollated.length) * 100)) + "%"
};
}
Because of the rankColumn
option which will display the score in one of the tables columns, it is not possible to populate the columns within the search function because of the order that DataTables performs it's operations. Instead we have to set a listener for the init
event.
Within the listener for this event we create an api and from that we get the initialisation options for fuzzy searching and the DataTables initialisation object. If fuzzySearch
has not been defined in the initialisation options then we can bail at this point, otherwise we will carry on and identify the input element for this table.
$(document).on('init.dt', function(e, settings) {
var api = new $.fn.dataTable.Api(settings);
var initial = api.init();
var initialFuzzy = initial.fuzzySearch;
// If this is not set then fuzzy searching is not enabled on the table so return.
if(!initialFuzzy) {
return;
}
// Find the input element
var input = $('div.dataTables_filter input', api.table().container())
...
Next we are going to remove DataTables default searching events and turn the listener off on the input element that was identified for this table. We then define our own function that should trigger on a input or a keydown.
// Turn off the default DataTables searching events
$(settings.nTable).off('search.dt.DT');
var fuzzySearchVal = ''; // Storage for the most recent fuzzy search value - ui or api set
var searchVal = ''; // Storage for the most recent exact search value - ui or api set
// The function that we want to run on search
var triggerSearchFunction = function(event){
...
}
input.off();
// Always add this event no matter if toggling is enabled
input.on('input keydown', triggerSearchFunction);
The triggerSearchFunction()
function runs the fuzzySearch()
function for each row and stores the result on that row's internal DataTables property. We must at this point stress that this is not recommended when creating your own search plug-ins. The draw()
function is then called to trigger a search.
// Get the value from the input element and convert to lower case
fuzzySearchVal = input.val();
searchVal = fuzzySearchVal; // Overwrite the value for search as ui interaction
if (fuzzySearchVal !== undefined && fuzzySearchVal.length === 0) {
fuzzySearchVal = fuzzySearchVal.toLowerCase();
}
// For each row call the fuzzy search function to get result
api.rows().iterator('row', function(settings, rowIdx) {
settings.aoData[rowIdx]._fuzzySearch = fuzzySearch(fuzzySearchVal, settings.aoData[rowIdx]._aFilterData, initialFuzzy)
});
// Empty the DataTables search and replace it with our own
api.search("");
input.val(fuzzySearchVal);
api.draw();
Now we can write the function that gets called on a draw. This is much more similar to the other searching plug-ins.
If the internal _fuzzySearch
property has been defined then the searching is done based on the pass value within it. If rankColumn
has been defined then it is populated with the score for that row. If no internal property is set for _fuzzySearch
then the html is not set and all the rows return true.
$.fn.dataTable.ext.search.push(
function( settings, data, dataIndex ) {
var initial = settings.oInit.fuzzySearch;
// If fuzzy searching has not been implemented then pass all rows for this function
if (settings.aoData[dataIndex]._fuzzySearch !== undefined) {
// Read score to set the cell content and sort data
var score = settings.aoData[dataIndex]._fuzzySearch.score;
settings.aoData[dataIndex].anCells[initial.rankColumn].innerHTML = score;
// Remove '%' from the end of the score so can sort on a number
settings.aoData[dataIndex]._aSortData[initial.rankColumn] = +score.substring(0, score.length - 1);
// Return the value for the pass as decided by the fuzzySearch function
return settings.aoData[dataIndex]._fuzzySearch.pass;
}
settings.aoData[dataIndex].anCells[initial.rankColumn].innerHTML = '';
settings.aoData[dataIndex]._aSortData[initial.rankColumn] = '';
return true;
}
);
Next we want to integrate the functionality that will allow fuzzy searching to be toggled on and off. This requires a bit of dom manipulation which is dealt with within the init
listener that we set earlier, immediately after the identification of the input element.
var fontBold = {
'font-weight': '600',
'background-color': 'rgba(255,255,255,0.1)'
};
var fontNormal = {
'font-weight': '500',
'background-color': 'transparent'
};
var toggleDataTables = {
'border': 'none',
'background': 'none',
'font-size': '100%',
'width': '50%',
'display': 'inline-block',
'color': 'white',
'cursor': 'pointer',
'padding': '0.5em'
}
// Only going to set the toggle if it is enabled
var toggle, tooltip, exact, fuzzy, label;
if(initialFuzzy.toggleSmart) {
toggle =$('<button class="toggleSearch">Abc</button>')
.insertAfter(input)
.css({
'border': 'none',
'background': 'none',
'position': 'absolute',
'right': '0px',
'top': '4px',
'cursor': 'pointer',
'color': '#3b5e99',
'margin-top': '1px'
});
exact =$('<button class="toggleSearch">Exact</button>')
.insertAfter(input)
.css(toggleCSS)
.css(fontBold)
.attr('highlighted', true);
fuzzy =$('<button class="toggleSearch">Fuzzy</button>')
.insertAfter(input)
.css(toggleCSS);
input.css({
'padding-right': '30px'
});
label = $('<div>Search Type<div>').css({'padding-bottom': '0.5em', 'font-size': '0.8em'})
tooltip = $('<div class="fuzzyToolTip"></div>')
.css({
'position': 'absolute',
'right': '0px',
'top': '2em',
'background': 'white',
'border-radius': '4px',
'text-align': 'center',
'padding': '0.5em',
'background-color': '#16232a',
'box-shadow': '4px 4px 4px rgba(0, 0, 0, 0.5)',
'color': 'white',
'transition': 'opacity 0.25s',
'z-index': '30001'
})
.width(input.outerWidth() - 3)
.append(label).append(exact).append(fuzzy);
}
This inserts the icons, buttons and labels into the correct locations ready to be used. The CSS is defined within the plug-in so that there is no need for a seperate CSS file.
Next a function is defined that will toggle which button is highlighted within the tooltip. This is done by adding a custom 'highlighted' attribute and also some additional CSS that was declared above. The icon is also blurred to indicate the search is in fuzzy mode. At the end of the function a search is triggered using our triggerSearchFunction()
call. We want this to happen when a toggle occurs as often it will result in different data being displayed.
We can now add to our triggerSearchFunction()
function so that the mode of the search can be checked before running the search.
var searchVal = '';
// If the toggle is set and isn't checkd then perform a normal search
if(toggle && !toggle.attr('blurred')) {
api.rows().iterator('row', function(settings, rowIdx) {
settings.aoData[rowIdx]._fuzzySearch = undefined;
})
api.search(input.val())
}
// Otherwise perform a fuzzy search
else {
// Get the value from the input element and convert to lower case
searchVal = input.val();
if (searchVal !== undefined && searchVal.length === 0) {
searchVal = searchVal.toLowerCase();
}
// For each row call the fuzzy search function to get result
api.rows().iterator('row', function(settings, rowIdx) {
settings.aoData[rowIdx]._fuzzySearch = fuzzySearch(searchVal, settings.aoData[rowIdx]._aFilterData, initialFuzzy)
});
// Empty the DataTables search and replace it with our own
api.search("");
input.val(searchVal);
}
api.draw();
Now we want to add some event listeners to our new dom elements. In the spirit of minimising code, first we will define three functions.
The first, toggleFuzzy()
, changes whether the search mode is fuzzy or exact by toggling the states of the buttons and triggering a search function.
function toggleFuzzy() {
if(toggle.attr('blurred')) {
toggle.css({'filter': 'blur(0px)'}).removeAttr('blurred');
fuzzy.removeAttr('highlighted').css(fontNormal);
exact.attr('highlighted', true).css(fontBold);
}
else {
toggle.css({'filter': 'blur(1px)'}).attr('blurred', true);
exact.removeAttr('highlighted').css(fontNormal);
fuzzy.attr('highlighted', true).css(fontBold);
}
// Whenever the search mode is changed we need to re-search
triggerSearchFunction();
}
The second, highlightButton()
, takes one parameter, the button to be highlighted. If it is not highlighted then the toggleFuzzy function is called.
// Highlights one of the buttons in the tooltip and un-highlights the other
function highlightButton(toHighlight) {
if(!toHighlight.attr('highlighted')){
toggleFuzzy()
}
}
The third, removeToolTip()
removes the tooltip from the page.
// Removes the tooltip element
function removeToolTip() {
tooltip.remove();
}
The toggle icon has three event listeners. The first is on the click
event and simply calls toggleFuzzy. This means that when the toggle icon is clicked the search mode will be changed and the results updated. The second is the mouseenter
event. When this happens the following function is called.
function() {
tooltip
.insertAfter(toggle)
.on('mouseleave', removeToolTip);
exact.on('click', () => highlightButton(exact, fuzzy));
fuzzy.on('click', () => highlightButton(fuzzy, exact));
}
This inserts the tooltip, setting an event listener to remove itself when the mouse leaves. It then also sets the highlightButton function to run when one of the toggle buttons is clicked.
The last event listener on the toggle icon is for mouseleave
when this occurs the tooltip will be removed.
The search box has two event listeners. The first is for the mouseenter
event and is the same as the toggle icon. The second is for mouseleave
- this is slightly differnet to before.
function() {
var inToolTip = false;
tooltip.on('mouseenter', () => inToolTip = true);
toggle.on('mouseenter', () => inToolTip = true);
setTimeout(function(){
if(!inToolTip) {
removeToolTip();
}
}, 50)
}
This function sets event listeners for mouseenter on the toggle icon and the tooltip. If the mouse enters either of these within 50 milliseconds then the tooltip will not be removed. Otherwise the tooltip is indeed hidden.
The last additions here is to deal with stateSave
. First the loaded state is fetched using state.loaded()
. A listener is then set for stateSaveParams
so that the current state of the search mode can be saved in future. The current state is then checked to see if the _fuzzySearch
property is set to true. If so, the toggle button is clicked to change to fuzzy searching.
var state = api.state.loaded();
api.on('stateSaveParams', function(e, settings, data) {
data._fuzzySearch = toggle.attr('blurred');
})
if(state !== null && state._fuzzySearch === 'true') {
toggle.click();
}
Next we can add the functionality for the search.return
initialisation option. This involves one final change to the triggerSearchFunction to check which key has been pressed. This is a small change resulting in the following function.
// The function that we want to run on search
var triggerSearchFunction = function(event){
// If the search is only to be triggered on return wait for that
if (!initial.search.return || event.key === "Enter") {
var searchVal = '';
// If the toggle is set and isn't checkd then perform a normal search
if(toggle && !toggle.attr('blurred')) {
api.rows().iterator('row', function(settings, rowIdx) {
settings.aoData[rowIdx]._fuzzySearch = undefined;
})
api.search(input.val())
}
// Otherwise perform a fuzzy search
else {
// Get the value from the input element and convert to lower case
searchVal = input.val();
if (searchVal !== undefined && searchVal.length === 0) {
searchVal = searchVal.toLowerCase();
}
// For each row call the fuzzy search function to get result
api.rows().iterator('row', function(settings, rowIdx) {
settings.aoData[rowIdx]._fuzzySearch = fuzzySearch(searchVal, settings.aoData[rowIdx]._aFilterData, initialFuzzy)
});
// Empty the DataTables search and replace it with our own
api.search("");
input.val(searchVal);
}
api.draw();
}
}
The final step is to implement the api method that will get or set the fuzzy search value. Again we are going to do this inside our init listener. We do this by accessing the Api register function. This function takes two arguments. The first is the path that should be taken within an api instance to access the api method. The second is the action that should be invoked when the Api method is called.
var apiRegister = $.fn.dataTable.Api.register;
apiRegister('search.fuzzy()', function(value) {
...
})
We then want to add behaviour for the retrieving the fuzzy search value. If the parameter passed in is undefined then this is that path that should be taken.
var apiRegister = $.fn.dataTable.Api.register;
apiRegister('search.fuzzy()', function(value) {
if(value === undefined) {
return fuzzySearchVal;
}
...
})
Otherwise the value is to be set so a slightly different path is followed. The fuzzy search value is inputted into the input box and recorded, along with the current search value. An iterator is then used to search all of the fuzzy search details based on the new value.
var apiRegister = $.fn.dataTable.Api.register;
apiRegister('search.fuzzy()', function(value) {
if(value === undefined) {
return fuzzySearchVal;
}
else {
fuzzySearchVal = value.toLowerCase();
searchVal = api.search();
input.val(fuzzySearchVal);
// For each row call the fuzzy search function to get result
api.rows().iterator('row', function(settings, rowIdx) {
settings.aoData[rowIdx]._fuzzySearch = fuzzySearch(fuzzySearchVal, settings.aoData[rowIdx]._aFilterData, initialFuzzy)
});
return this;
}
})
The final piece of the puzzle is to add the most recent search value to the input element. This is done by setting a listener for the search
.
api.on('search', function(){
if(!fromPlugin) {
input.val(api.search() !== searchVal ? api.search() : fuzzySearchVal);
}
})
The boolean flag, fromPlugin
is used to prevent infinite loops when it is the plugin that is causing the search. This flag is set within the triggerSearchFunction()
function, simply setting the value to true
before each search
/draw
and false
after. The input value is then set based on the current search value that DataTables is storing, the last search value when a fuzzy search occured and the last fuzzy search value.
If the current search value stored within DataTables does not match the last search value we saw, then it must have been updated since and is therefore more recent. If the two are equal then the fuzzy search value is more recent so it should be displayed.
And that's it. Everything that is required to create a complex row based searching plug-in. The full file is available on the [cdn] so that you can see the full flow and all of the parts integrated together.
Limitations
As the filtering performed by FuzzySearch is all done on the client-side, this plug-in does not support server-side processing.
Feedback
As always, we are keen to hear how you are using DataTable. Please drop us a message in the forum with how you are getting on with our software, or if you have run into any problems, or have ideas for future enhancements. We would love to know if people are able to integrate fuzzy searching into their projects and your customer feedback.