Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Scout Search Implementation #3082

Merged
merged 12 commits into from
Nov 4, 2023
Merged

feat: Scout Search Implementation #3082

merged 12 commits into from
Nov 4, 2023

Conversation

frknakk
Copy link
Contributor

@frknakk frknakk commented Oct 11, 2023

Fixes #3077

@yajra
What do you think? Feel free to let me know if i should change some parts.
disableOrdering js script: We could either integrate that into laravel-datatables-html or add it into some docs for people to implement on their own (if they want to). I could also leave that part out completely. What do you think?

If it's possible to somehow add custom scss/js code into laravel-datatables-html i would also do 2 more pull requests for nice optimizations that i'm using since a long time on my own projects.

Also it would be good to add an example usage into docs:

public function dataTable(QueryBuilder $query): EloquentDataTable
{
	return (new EloquentDataTable($query))
		// Enable scout search for eloquent model
		->enableScoutSearch(Product::class)
		
		// Add filters to scout search
		->scoutFilter(function (string $keyword) {
			return 'region IN ["Germany", "France"]'; // Meilisearch
                        return 'region:Germany OR region:France'; // Algolia
		})
		
		// Add filters to default search
		->filter(function (QueryBuilder $query, bool $scout_searched) {
			if (!$scout_searched)
			{
				// Filter already added for scout search
				$query->whereIn('region', ['Germany', 'France']);
			}

			// Stock is not indexed so it has to be filtered after the initial scout search
			$query->where('stock', '>', 50);
		}, true)

		->setRowId('id');
}

Disable ordering (JS)
When the scout search was performed, the code sets a disableOrdering: true flag in the ajax response. The JS part has to fetch that and disable the user ordering. Otherwise the user gets confused because the ordering is overwritten by the relevancy ordering of the search results.

$('#products-table').on('xhr.dt', function (e, settings, json, xhr) {
	if (json == null || !('disableOrdering' in json)) return;

	let table = LaravelDataTables[$(this).attr('id')];
	if (json.disableOrdering) {
		table.settings()[0].aoColumns.forEach(function(column) {
			column.bSortable = false;
			$(column.nTh).removeClass('sorting_asc sorting_desc sorting').addClass('sorting_disabled');
		});
	} else {
		let changed = false;
		table.settings()[0].aoColumns.forEach(function(column) {
			if (column.bSortable) return;
			column.bSortable = true;
			changed = true;
		});
		if (changed) {
			table.draw();
		}
	}
});

@frknakk frknakk changed the title Implemented scout searching (currently only meilisearch) Scout Search Implementation Oct 11, 2023
@frknakk frknakk marked this pull request as ready for review October 12, 2023 18:03
@yajra yajra self-requested a review October 20, 2023 06:21
Copy link
Owner

@yajra yajra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was able to test and works well 🎉. However, it seems like the scout search is failing when the global search is off but it seems intended since meilisearch will basically do the wild search for us?

		->filter(function (QueryBuilder $query, bool $scout_searched) {

		}, false)

@frknakk
Copy link
Contributor Author

frknakk commented Oct 20, 2023

Was able to test and works well 🎉. However, it seems like the scout search is failing when the global search is off but it seems intended since meilisearch will basically do the wild search for us?

		->filter(function (QueryBuilder $query, bool $scout_searched) {

		}, false)

Yes, that's intended. Adding column search for scout would make the code a mess and difficult to maintain. Additionally i don't think there is a relevant use case for that. In 99% of the cases setting searchableAttributes on meilisearch/algolia will probably be enough.

@yajra
Copy link
Owner

yajra commented Oct 21, 2023

Where do we put the disableOrdering js script?

I think we should not force disable the ordering function and let the developer handle it via javascript ordering: false.

In most of our cases, our users want a specific ordering but it will be a big boost in performance if we can delegate the searching via meilisearch.

@frknakk
Copy link
Contributor Author

frknakk commented Oct 21, 2023

Where do we put the disableOrdering js script?

I think we should not force disable the ordering function and let the developer handle it via javascript ordering: false.

In most of our cases, our users want a specific ordering but it will be a big boost in performance if we can delegate the searching via meilisearch.

I think you misunderstood the purpose of the script.

When searching via Meilisearch, it doesn't make any sense to keep the user ordering (e.g. sort by name asc), because then there will be dozens of unrelevant entries before you get the desired result. Because of that, when searching via Meilisearch the user ordering gets ignored serverside and it forces the order of meilisearch results (= entries that are matching best are shown first).

The disableOrdering js script is just a frontend script, that temporarily hides the ordering symbols when meilisearch was used. Because otherwise you can click on all columns to change ordering but nothing will change. The script also enables the ordering again when the search was deleted or meilisearch is not active.

@yajra
Copy link
Owner

yajra commented Oct 21, 2023

When searching via Meilisearch, it doesn't make any sense to keep the user ordering (e.g. sort by name asc), because then there will be dozens of unrelevant entries before you get the desired result. Because of that, when searching via Meilisearch the user ordering gets ignored serverside and it forces the order of meilisearch results (= entries that are matching best are shown first).

Good point, I was thinking only of search results (discarding relevance) but with ordering as defined by the users.

The disableOrdering js script is just a frontend script, that temporarily hides the ordering symbols when meilisearch was used. Because otherwise you can click on all columns to change ordering but nothing will change. The script also enables the ordering again when the search was deleted or meilisearch is not active.

I was trying to do a script for this and can't figure it out ^_^. Do you have a sample script for this?

@frknakk
Copy link
Contributor Author

frknakk commented Oct 21, 2023

I was trying to do a script for this and can't figure it out ^_^. Do you have a sample script for this?

Yes, it's above in my initial message 😄

@yajra
Copy link
Owner

yajra commented Oct 23, 2023

OMG! Apologies for overlooking that script 😅. The functionality makes full sense to me now.

I think it would be nice if we could have that script integrated into the HTML builder but with docs too for those not using the builder. Maybe we can add new script template for scout to integrate this?

scout.blade.php

$(function(){
    window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}=window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}||{};
    window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}["%1$s"]=$("#%1$s").DataTable(%2$s);

    $('#%1$s').on('xhr.dt', function (e, settings, json, xhr) {
        if (json == null || !('disableOrdering' in json)) return;

        let table = LaravelDataTables[$(this).attr('id')];
        if (json.disableOrdering) {
            table.settings()[0].aoColumns.forEach(function(column) {
                column.bSortable = false;
                $(column.nTh).removeClass('sorting_asc sorting_desc sorting').addClass('sorting_disabled');
            });
        } else {
            let changed = false;
            table.settings()[0].aoColumns.forEach(function(column) {
                if (column.bSortable) return;
                column.bSortable = true;
                changed = true;
            });
            if (changed) {
                table.draw();
            }
        }
    });
});

And then maybe a shortcut builder method to set the template to scout?

->setTableId('products-table')
->template('scout');

Copy link
Owner

@yajra yajra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also created a poc repo to test this. Thanks!

src/QueryDataTable.php Outdated Show resolved Hide resolved
src/QueryDataTable.php Outdated Show resolved Hide resolved
@frknakk
Copy link
Contributor Author

frknakk commented Oct 27, 2023

@yajra Do you have any more suggestions for improvement? Looks good to me :)

Last thing missing is a docs section that has a quick start with the usage examples from both this pull request and the new one in laravel-datatables-html.

Feel free to test, fix and uncomment the missing database drivers. The code that's there should work theoretically, but i couldn't test it yet.

@yajra
Copy link
Owner

yajra commented Nov 3, 2023

The changes look good! Will test on other DB drivers.

Copy link

sonarqubecloud bot commented Nov 3, 2023

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

No Coverage information No Coverage information
No Duplication information No Duplication information

@yajra yajra changed the title Scout Search Implementation feat: Scout Search Implementation Nov 3, 2023
@yajra
Copy link
Owner

yajra commented Nov 3, 2023

Confirmed working on SQLite. The CASE WHEN SQL doesn't seem to work with Oracle but it's ok.

Will also try PGSQL by tomorrow.

@yajra
Copy link
Owner

yajra commented Nov 4, 2023

PostgreSQL does not work. However was able to do a code that works for both pgsql and oracle. Can't test on sqlsrv though.

    protected function applyFixedOrderingToQuery(string $keyName, array $orderedKeys)
    {
        $connection = $this->getConnection();
        $driverName = $connection->getDriverName();

        // Escape keyName and orderedKeys
        $keyName = $connection->getQueryGrammar()->wrap($keyName);
        $orderedKeys = collect($orderedKeys)
            ->map(function ($value) use ($connection) {
                return $connection->escape($value);
            });

        switch ($driverName) {
            case 'mysql':
                $this->query->orderByRaw("FIELD($keyName, ".$orderedKeys->implode(',').')');
                return true;

            case 'pgsql':
            case 'oracle':
                $this->query->orderByRaw(
                    "CASE "
                    .
                    $orderedKeys
                        ->map(fn($value, $index) => "WHEN $keyName=$value THEN $index")
                        ->implode(' ')
                    .
                    " END"
                );
                return true;

            case 'sqlite':
            case 'sqlsrv':
                $this->query->orderByRaw(
                    "CASE $keyName "
                    .
                    $orderedKeys
                        ->map(fn($value, $index) => "WHEN $value THEN $index")
                        ->implode(' ')
                    .
                    " END"
                );
                return true;

            default:
                return false;
        }
    }

Merging this and will apply the patch in a new PR. Thanks a lot for your patience and contribution! 🍻 🎉

@yajra yajra merged commit 82127aa into yajra:master Nov 4, 2023
10 checks passed
@yajra
Copy link
Owner

yajra commented Nov 4, 2023

Released on v10.11.0 🚀 Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Scout / Meilisearch Support
2 participants