Text search
A resource declares its text search with text(). With the Contains and FullText strategies, searchable() marks the fields to search, and the text becomes one more condition of the SQL search: filters, scopes, instructions, includes, aggregates, sorts, pagination, perimeterQuery() and actions work with it as they do without it.
use Lomkit\Rest\Fields\ID;
use Lomkit\Rest\Fields\Integer;
use Lomkit\Rest\Fields\Text;
use Lomkit\Rest\Text\Contains;
use Lomkit\Rest\Text\FullText;
use Lomkit\Rest\Text\TextSearch;
public function fields(): array
{
return [
ID::make('id'),
Text::make('name')->searchable(),
Text::make('description')->searchable(),
Integer::make('price'),
];
}
public function text(): ?TextSearch
{
return FullText::make()
->index('name', 'description')
->otherwise(Contains::make());
}
{
"search": {
"text": {"value": "desk lamp"},
"filters": [{"field": "price", "operator": "<", "value": 100}]
}
}
The response is a normal search page, with the matching rows only. total counts the matching rows.
Strategies
| Strategy | Runs on | Modes | Relevance |
|---|---|---|---|
Contains::make() | Every database: ILIKE on PostgreSQL, LIKE on the others | words, phrase | No |
FullText::make() | MySQL, MariaDB and PostgreSQL | words, phrase, prefix, websearch | Yes |
ScoutEngine::make() | The search engine of a Laravel Scout model | words | The engine's order |
ScoutEngine has its own page: Search engines.
Contains
- Each word of the text must appear in one of the searchable fields, as a part of its value, in any order.
- The words after the tenth are ignored.
- The
%,_and\characters of the text match as they are typed. - Case:
ILIKEignores case on PostgreSQL. On MySQL and MariaDB,LIKEfollows the collation of the column, and the default_cicollations ignore case. On SQLite,LIKEignores the case of ASCII letters only.
FullText
index('name', 'description')declares a group of columns. Call it once per FULLTEXT index: a row matches when one of the groups matches. Withoutindex(), the searchable fields form one group.language('english')names the PostgreSQL text search configuration,englishby default: letters, digits and_only. MySQL and MariaDB ignore it.otherwise(Contains::make())runs another strategy on the other databases, such as SQLite and SQL Server. Without it, a search of the resource there throwsInvalidTextSearchException, which names the resource and the driver.otherwise()takes a strategy that adds a condition to the SQL query: aScoutEnginethere is aTypeError.- On MySQL and MariaDB, each group needs a FULLTEXT index on exactly its columns. A group without one throws
InvalidTextSearchException, which names the table and the groups. - On PostgreSQL, the search runs without index, and the same migration, with
->language(...)when the configuration is notenglish, creates the index that the search uses.
Schema::table('products', function (Blueprint $table) {
$table->fullText(['name', 'description']);
});
Declaration rules
text()returnsnullby default: the resource has no text search, andsearch.textgivesThe search.text field is prohibited.- Only
Textfields, and app types that extendText, can besearchable().searchable()throwsInvalidFieldExceptionon another type, and on avirtual()orwriteOnly()field. - A resource whose
text()returnsContainsorFullTextwithout any searchable field throwsInvalidTextSearchExceptionwhen it is searched, and so does anindex()column that is not a searchable field. - A model that uses
Laravel\Scout\Searchabledoes not give its resource a text search: declaretext().
The request
{"text": {"value": "desk lamp", "mode": "phrase"}}
textis an object.valueis a string of at most 255 characters:The search.text.value field must not be greater than 255 characters.- A blank,
nullor missingvalue, or a value without a letter or a digit, runs the search without text and returns the normal list. modeiswordsby default, a case ofLomkit\Rest\Text\TextMode. A mode that the strategy does not support givesThe selected search.text.mode is invalid.- A key other than
valueandmodegives a 422 on that key, such asThe search.text.fuzzy field is prohibited. textin an include givesThe search.includes.0.text field is prohibited.
Modes
| Mode | Matches |
|---|---|
words | The words of the text, in any order. With Contains and on PostgreSQL, a row must hold every word. On MySQL and MariaDB, a row holds one of the words, and without sorts the rows that hold more of them come first |
phrase | The words of the text, next to each other and in this order |
prefix | Each word starts a word of the row, for a search box that shows results while the user types: lam finds lamp |
websearch | Words, "quoted phrases", -excluded words, and or between two alternatives |
- FullText keeps the letters and digits of the text. The other characters, such as
+,*,:,&,|and!, separate words and have no meaning, except the quotes,-andorof thewebsearchmode. - On MySQL and MariaDB, an
_between two letters or digits stays in the word:snake_caseis one word there, and two words on PostgreSQL.
What the text narrows
- The text narrows the searched resource only. It does not narrow includes, aggregates or relation filters.
- Includes and aggregates are validated against the fields and operators of their own resource, as without text.
viewAnyis checked before the search runs, andperimeterQuery()applies as in any search.
Relevance
With a strategy that orders by relevance (FullText on MySQL, MariaDB and PostgreSQL), a search with text and without sorts returns the most relevant rows first, then follows defaultOrderBy().
The sort field @relevance places the relevance among the other sorts, desc when direction is omitted:
{
"search": {
"text": {"value": "lamp"},
"sorts": [
{"field": "price"},
{"field": "@relevance"}
]
}
}
@relevance gives The selected search.sorts.0.field is invalid. without text, in an include, and with a strategy that does not order by relevance, such as Contains or the otherwise() strategy of FullText.
Trashed rows
search.trashed reads the soft-deleted rows of a model that uses SoftDeletes, with or without text:
{
"search": {
"trashed": "with"
}
}
withadds the soft-deleted rows, andonlyreturns them alone. Another value givesThe selected search.trashed is invalid.- On a model without
SoftDeletes,trashedgivesThe search.trashed field is prohibited.It gives a 422 in an include too. search.text.trashedgivesThe search.text.trashed field is prohibited.
Actions
An action of a resource that declares text() accepts search.text and acts on the matching rows only. A blank text acts on every row that the rest of the search resolves. search.trashed applies to actions too. The action of a resource without text() refuses search.text with The search.text field is prohibited. See Actions.
Database differences
FullText on MySQL and MariaDB:
- Words are not stemmed:
lampsdoes not findlamp. - Words shorter than
innodb_ft_min_token_size(3 by default) and the InnoDB stopwords, such astheorwith, are not indexed. Thewordsmode ignores them and thephrasemode skips them, but a text made of such words only returns no row. - In the
prefixmode, each word of the text must start an indexed word of the row:landlafindlamp, butthe lammissesThe lamp, whosetheis not indexed. - In the
websearchmode each word is required, so a stopword or a short word finds no row. Lowerinnodb_ft_min_token_sizeor change the stopword list, then rebuild the indexes. - A
websearchtext made of excluded words only returns no row. - InnoDB only finds committed rows: in tests that run each test in a transaction, such as tests that use
RefreshDatabase, a FullText search finds no row that the test wrote.
FullText on PostgreSQL:
language()sets how words are read. Withenglish, words are stemmed (lampsfindslamp) and English stopwords are ignored: a text made of stopwords only returns no row. Withsimple, words are only lowercased. Give the index the same configuration,$table->fullText([...])->language('simple'), otherwise PostgreSQL does not use it.- A row whose value is
nullin one column of a group does not match through that group. Declare the columns of a groupNOT NULL, or give a nullable column its ownindex()group. - A
websearchtext made of excluded words only returns the rows without these words.
Errors
| Message | Cause |
|---|---|
The search.text field is prohibited. | The resource declares no text() |
The search.text field must be an array. | text sent as a string |
The search.text.value field must not be greater than 255 characters. | A text that is too long |
The selected search.text.mode is invalid. | A mode that the strategy does not support |
The selected search.sorts.0.field is invalid. | @relevance without relevance |
The search.trashed field is prohibited. | trashed on a model without SoftDeletes |