Friday 17th September, 2021
By Sandy Galloway

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.

NamePositionOfficeSalary
Tiger NixonSystem ArchitectEdinburgh$320,800
Garrett WintersAccountantTokyo$170,750
Ashton CoxJunior Technical AuthorSan Francisco$86,000
Cedric KellySenior Javascript DeveloperEdinburgh$433,060
Airi SatouAccountantTokyo$162,700
Brielle WilliamsonIntegration SpecialistNew York$372,000
Herrod ChandlerSales AssistantSan Francisco$137,500
Rhona DavidsonIntegration SpecialistTokyo$327,900
Colleen HurstJavascript DeveloperSan Francisco$205,500
Sonya FrostSoftware EngineerEdinburgh$103,600
Jena GainesOffice ManagerLondon$90,560
Quinn FlynnSupport LeadEdinburgh$342,000
Charde MarshallRegional DirectorSan Francisco$470,600
Haley KennedySenior Marketing DesignerLondon$313,500
Tatyana FitzpatrickRegional DirectorLondon$385,750
Michael SilvaMarketing DesignerLondon$198,500
Paul ByrdChief Financial Officer (CFO)New York$725,000
Gloria LittleSystems AdministratorNew York$237,500
Bradley GreerSoftware EngineerLondon$132,000
Dai RiosPersonnel LeadEdinburgh$217,500
Jenette CaldwellDevelopment LeadNew York$345,000
Yuri BerryChief Marketing Officer (CMO)New York$675,000
Caesar VancePre-Sales SupportNew York$106,450
Doris WilderSales AssistantSydney$85,600
Angelica RamosChief Executive Officer (CEO)London$1,200,000
Gavin JoyceDeveloperEdinburgh$92,575
Jennifer ChangRegional DirectorSingapore$357,650
Brenden WagnerSoftware EngineerSan Francisco$206,850
Fiona GreenChief Operating Officer (COO)San Francisco$850,000
Shou ItouRegional MarketingTokyo$163,000
Michelle HouseIntegration SpecialistSydney$95,400
Suki BurksDeveloperLondon$114,500
Prescott BartlettTechnical AuthorLondon$145,000
Gavin CortezTeam LeaderSan Francisco$235,500
Martena MccrayPost-Sales supportEdinburgh$324,050
Unity ButlerMarketing DesignerSan Francisco$85,675
Howard HatfieldOffice ManagerSan Francisco$164,500
Hope FuentesSecretarySan Francisco$109,850
Vivian HarrellFinancial ControllerSan Francisco$452,500
Timothy MooneyOffice ManagerLondon$136,200
Jackson BradshawDirectorNew York$645,750
Olivia LiangSupport EngineerSingapore$234,500
Bruno NashSoftware EngineerLondon$163,500
Sakura YamamotoSupport EngineerTokyo$139,575
Thor WaltonDeveloperNew York$98,540
Finn CamachoSupport EngineerSan Francisco$87,500
Serge BaldwinData CoordinatorSingapore$138,575
Zenaida FrankSoftware EngineerNew York$125,250
Zorita SerranoSoftware EngineerSan Francisco$115,000
Jennifer AcostaJunior Javascript DeveloperEdinburgh$75,650
Cara StevensSales AssistantNew York$145,600
Hermione ButlerRegional DirectorLondon$356,250
Lael GreerSystems AdministratorLondon$103,500
Jonas AlexanderDeveloperSan Francisco$86,500
Shad DeckerRegional DirectorEdinburgh$183,000
Michael BruceJavascript DeveloperSingapore$183,000
Donna SniderCustomer SupportNew 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:

JS

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
    }
});

NamePositionOfficeSalary
Tiger NixonSystem ArchitectEdinburgh$320,800
Garrett WintersAccountantTokyo$170,750
Ashton CoxJunior Technical AuthorSan Francisco$86,000
Cedric KellySenior Javascript DeveloperEdinburgh$433,060
Airi SatouAccountantTokyo$162,700
Brielle WilliamsonIntegration SpecialistNew York$372,000
Herrod ChandlerSales AssistantSan Francisco$137,500
Rhona DavidsonIntegration SpecialistTokyo$327,900
Colleen HurstJavascript DeveloperSan Francisco$205,500
Sonya FrostSoftware EngineerEdinburgh$103,600
Jena GainesOffice ManagerLondon$90,560
Quinn FlynnSupport LeadEdinburgh$342,000
Charde MarshallRegional DirectorSan Francisco$470,600
Haley KennedySenior Marketing DesignerLondon$313,500
Tatyana FitzpatrickRegional DirectorLondon$385,750
Michael SilvaMarketing DesignerLondon$198,500
Paul ByrdChief Financial Officer (CFO)New York$725,000
Gloria LittleSystems AdministratorNew York$237,500
Bradley GreerSoftware EngineerLondon$132,000
Dai RiosPersonnel LeadEdinburgh$217,500
Jenette CaldwellDevelopment LeadNew York$345,000
Yuri BerryChief Marketing Officer (CMO)New York$675,000
Caesar VancePre-Sales SupportNew York$106,450
Doris WilderSales AssistantSydney$85,600
Angelica RamosChief Executive Officer (CEO)London$1,200,000
Gavin JoyceDeveloperEdinburgh$92,575
Jennifer ChangRegional DirectorSingapore$357,650
Brenden WagnerSoftware EngineerSan Francisco$206,850
Fiona GreenChief Operating Officer (COO)San Francisco$850,000
Shou ItouRegional MarketingTokyo$163,000
Michelle HouseIntegration SpecialistSydney$95,400
Suki BurksDeveloperLondon$114,500
Prescott BartlettTechnical AuthorLondon$145,000
Gavin CortezTeam LeaderSan Francisco$235,500
Martena MccrayPost-Sales supportEdinburgh$324,050
Unity ButlerMarketing DesignerSan Francisco$85,675
Howard HatfieldOffice ManagerSan Francisco$164,500
Hope FuentesSecretarySan Francisco$109,850
Vivian HarrellFinancial ControllerSan Francisco$452,500
Timothy MooneyOffice ManagerLondon$136,200
Jackson BradshawDirectorNew York$645,750
Olivia LiangSupport EngineerSingapore$234,500
Bruno NashSoftware EngineerLondon$163,500
Sakura YamamotoSupport EngineerTokyo$139,575
Thor WaltonDeveloperNew York$98,540
Finn CamachoSupport EngineerSan Francisco$87,500
Serge BaldwinData CoordinatorSingapore$138,575
Zenaida FrankSoftware EngineerNew York$125,250
Zorita SerranoSoftware EngineerSan Francisco$115,000
Jennifer AcostaJunior Javascript DeveloperEdinburgh$75,650
Cara StevensSales AssistantNew York$145,600
Hermione ButlerRegional DirectorLondon$356,250
Lael GreerSystems AdministratorLondon$103,500
Jonas AlexanderDeveloperSan Francisco$86,500
Shad DeckerRegional DirectorEdinburgh$183,000
Michael BruceJavascript DeveloperSingapore$183,000
Donna SniderCustomer SupportNew 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']);
});

NamePositionOfficeSalary
Tiger NixonSystem ArchitectEdinburgh$320,800
Garrett WintersAccountantTokyo$170,750
Ashton CoxJunior Technical AuthorSan Francisco$86,000
Cedric KellySenior Javascript DeveloperEdinburgh$433,060
Airi SatouAccountantTokyo$162,700
Brielle WilliamsonIntegration SpecialistNew York$372,000
Herrod ChandlerSales AssistantSan Francisco$137,500
Rhona DavidsonIntegration SpecialistTokyo$327,900
Colleen HurstJavascript DeveloperSan Francisco$205,500
Sonya FrostSoftware EngineerEdinburgh$103,600
Jena GainesOffice ManagerLondon$90,560
Quinn FlynnSupport LeadEdinburgh$342,000
Charde MarshallRegional DirectorSan Francisco$470,600
Haley KennedySenior Marketing DesignerLondon$313,500
Tatyana FitzpatrickRegional DirectorLondon$385,750
Michael SilvaMarketing DesignerLondon$198,500
Paul ByrdChief Financial Officer (CFO)New York$725,000
Gloria LittleSystems AdministratorNew York$237,500
Bradley GreerSoftware EngineerLondon$132,000
Dai RiosPersonnel LeadEdinburgh$217,500
Jenette CaldwellDevelopment LeadNew York$345,000
Yuri BerryChief Marketing Officer (CMO)New York$675,000
Caesar VancePre-Sales SupportNew York$106,450
Doris WilderSales AssistantSydney$85,600
Angelica RamosChief Executive Officer (CEO)London$1,200,000
Gavin JoyceDeveloperEdinburgh$92,575
Jennifer ChangRegional DirectorSingapore$357,650
Brenden WagnerSoftware EngineerSan Francisco$206,850
Fiona GreenChief Operating Officer (COO)San Francisco$850,000
Shou ItouRegional MarketingTokyo$163,000
Michelle HouseIntegration SpecialistSydney$95,400
Suki BurksDeveloperLondon$114,500
Prescott BartlettTechnical AuthorLondon$145,000
Gavin CortezTeam LeaderSan Francisco$235,500
Martena MccrayPost-Sales supportEdinburgh$324,050
Unity ButlerMarketing DesignerSan Francisco$85,675
Howard HatfieldOffice ManagerSan Francisco$164,500
Hope FuentesSecretarySan Francisco$109,850
Vivian HarrellFinancial ControllerSan Francisco$452,500
Timothy MooneyOffice ManagerLondon$136,200
Jackson BradshawDirectorNew York$645,750
Olivia LiangSupport EngineerSingapore$234,500
Bruno NashSoftware EngineerLondon$163,500
Sakura YamamotoSupport EngineerTokyo$139,575
Thor WaltonDeveloperNew York$98,540
Finn CamachoSupport EngineerSan Francisco$87,500
Serge BaldwinData CoordinatorSingapore$138,575
Zenaida FrankSoftware EngineerNew York$125,250
Zorita SerranoSoftware EngineerSan Francisco$115,000
Jennifer AcostaJunior Javascript DeveloperEdinburgh$75,650
Cara StevensSales AssistantNew York$145,600
Hermione ButlerRegional DirectorLondon$356,250
Lael GreerSystems AdministratorLondon$103,500
Jonas AlexanderDeveloperSan Francisco$86,500
Shad DeckerRegional DirectorEdinburgh$183,000
Michael BruceJavascript DeveloperSingapore$183,000
Donna SniderCustomer SupportNew 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 strings
  • relative - The number of steps divided by the length of the longest string
  • similarity - 1 - the value of relative

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 box
  • data The data for the row that is being processed
  • initial The fuzzySearch 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.