Skip to content

Query String Queries

Query String Queries leverage Elasticsearch’s powerful query_string syntax to perform advanced full-text and structured searches directly on your Laravel models

Version 5.2+
  • searchQueryString($query, $fields = null, $options = [])
  • orSearchQueryString($query, $fields = null, $options = [])
  • searchNotQueryString($query, $fields = null, $options = [])
  • orSearchNotQueryString($query, $fields = null, $options = [])
Book::searchQueryString('Eric OR (Lean AND Startup)')->get();
{
"index": "books",
"body": {
"query": {
"query_string": {
"query": "Eric OR (Lean AND Startup)"
}
},
"size": 1000
}
}

You can combine searchQueryString with other eloquent methods, for example:

Book::searchQueryString('Eric OR (Lean AND Startup)')
->orderByDesc('sales')->get();
{
"index": "books",
"body": {
"query": {
"query_string": {
"query": "Eric OR (Lean AND Startup)"
}
},
"sort": [
{
"sales": {
"order": "desc"
}
}
],
"size": 1000
}
}
Book::searchQueryString('Eric OR (Lean AND Startup)')
->where('sales', '>', 100)->get();
{
"index": "books",
"body": {
"query": {
"bool": {
"must": [
{
"query_string": {
"query": "Eric OR (Lean AND Startup)"
}
},
{
"range": {
"sales": {
"gt": 100
}
}
}
]
}
},
"size": 1000
}
}
Book::searchQueryString('Eric OR (Lean AND Startup)')
->where('sales', '>=', 100)->limit(10)->skip(3)->get();
{
"index": "books",
"body": {
"query": {
"bool": {
"must": [
{
"query_string": {
"query": "Eric OR (Lean AND Startup)"
}
},
{
"range": {
"sales": {
"gte": 100
}
}
}
]
}
},
"from": 3,
"size": 10
}
}
Book::searchQueryString('Eric OR (Lean AND Startup)')
->where('published', true)->paginate(10);
Book::searchQueryString('Eric OR (Lean AND Startup)')
->where('published', true)->count()

By default, all fields will be searched through; you can specify which to search through as well as optionally boost certain fields using a caret ^:

People::searchQueryString('John',['name^3','description^2','friends.name'])->get();

Search through people for ‘John’ in the name field, description field, and friends.name field. The name field is boosted by 3, and the description field is boosted by 2, which will affect the relevance score used for sorting.

{
"index": "people",
"body": {
"query": {
"query_string": {
"query": "John",
"fields": [
"name^3",
"description^2",
"friends.name"
]
}
},
"size": 1000
}
}

Allows you to set any options for the query_string clause to use, ex:

  • type
  • best_fields (Default), most_fields, cross_fields, phrase, phrase_prefix, bool_prefix
  • allow_leading_wildcard
  • analyze_wildcard
  • analyzer
  • auto_generate_synonyms_phrase_query
  • boost
  • default_operator
  • fuzziness
  • fuzzy_max_expansions
  • fuzzy_prefix_length
  • fuzzy_transpositions
  • fuzzy_rewrite
  • lenient
  • max_determinized_states
  • minimum_should_match
  • quote_analyzer
  • phrase_slop
  • quote_field_suffix
  • rewrite
  • time_zone
use PDPhilip\Elasticsearch\Query\Options\QueryStringOptions;
Product::searchQueryString('espreso tme', function (QueryStringOptions $options) {
// Match behavior and field resolution
$options->type('most_fields'); // Default: best_fields
$options->analyzer('my_custom_analyzer');
// Fuzziness and tolerance
$options->fuzziness(2); // Allow up to 2 edit distances
$options->fuzzyPrefixLength(1);
$options->fuzzyTranspositions(true);
$options->fuzzyMaxExpansions(50);
$options->rewrite('constant_score');
// Scoring and matching
$options->boost(2.0);
$options->defaultOperator('OR'); // Default: OR
$options->minimumShouldMatch('2');
$options->autoGenerateSynonymsPhraseQuery(true);
// Wildcards and parsing
$options->allowLeadingWildcard(false);
$options->analyzeWildcard(true);
$options->lenient(true);
// Phrase matching
$options->phraseSlop(2);
$options->quoteFieldSuffix('.exact');
// Misc
$options->timeZone('UTC');
})->get();
{
"index": "products",
"body": {
"query": {
"query_string": {
"query": "espreso tme",
"type": "most_fields",
"analyzer": "my_custom_analyzer",
"fuzziness": 2,
"fuzzy_prefix_length": 1,
"fuzzy_transpositions": true,
"fuzzy_max_expansions": 50,
"rewrite": "constant_score",
"boost": 2,
"default_operator": "OR",
"minimum_should_match": "2",
"auto_generate_synonyms_phrase_query": true,
"allow_leading_wildcard": false,
"analyze_wildcard": true,
"lenient": true,
"phrase_slop": 2,
"quote_field_suffix": ".exact",
"time_zone": "UTC"
}
},
"size": 1000
}
}
User::searchQueryString('age:35')->get();
User::searchQueryString('age:>=35')->get();
User::searchQueryString('age:<=18')->get();
User::searchQueryString('age:(NOT 35)')->get();
// AND
User::searchQueryString('(age:35) AND (title:admin)')->get();
// OR
User::searchQueryString('name:(doe OR toe)')->get();
// NOT
User::searchQueryString('name:(NOT doe)')->get();
// AND NOT & OR NOT (composed with helpers)
User::searchNotQueryString('name:doe')->searchNotQueryString('name:toe')->get();
User::searchNotQueryString('name:doe')->orSearchNotQueryString('age:35')->get();

3. Search Scope (All fields vs Specific fields)

Section titled “3. Search Scope (All fields vs Specific fields)”
// All fields
Product::searchQueryString('sweet')->get();
Product::searchQueryString('NOT sweet')->get();
// Specific field
Product::searchQueryString('sweet', 'details.type')->get();
Product::searchQueryString('false', 'details.gluten_free')->get();
// Fielded in query
Product::searchQueryString('details.gluten_free:false')->get();
use PDPhilip\Elasticsearch\Query\Options\QueryStringOptions;
// Default fuzziness (~2 edits)
Product::searchQueryString('esprso~')->get();
// Custom fuzziness settings
Product::searchQueryString('espro~', function (QueryStringOptions $options) {
$options->fuzziness(3); // more tolerant
})->get();
Product::searchQueryString('espreso~', function (QueryStringOptions $options) {
$options->fuzziness(1); // tighter match, fewer results
})->get();

5. Default Operator & Minimum Should Match

Section titled “5. Default Operator & Minimum Should Match”
use PDPhilip\Elasticsearch\Query\Options\QueryStringOptions;
// Default operator (space = OR)
Product::searchQueryString('sweet soda')->get();
Product::searchQueryString('sweet soda', function (QueryStringOptions $options) {
$options->defaultOperator('AND'); // require both terms
})->get();
// Minimum should match on OR group
Product::searchQueryString('drink OR water OR soda')->get();
Product::searchQueryString('drink OR water OR soda', function (QueryStringOptions $options) {
$options->minimumShouldMatch(2); // must match at least two terms
})->get();
// Allow gaps between terms
Product::searchQueryString('"fresh juice"', fn($o) => $o->phraseSlop(1))->get();
// Exact adjacency only
Product::searchQueryString('"fresh juice"', fn($o) => $o->phraseSlop(0))->get();
use PDPhilip\Elasticsearch\Query\Options\QueryStringOptions;
// Leading wildcard disabled → throws (not allowed by default)
Product::searchQueryString('*pro', function (QueryStringOptions $options) {
$options->allowLeadingWildcard(false);
})->get();
// Leading wildcard enabled → allowed
Product::searchQueryString('*pro', function (QueryStringOptions $options) {
$options->allowLeadingWildcard(true);
})->get();
// Strict → throws when querying numeric field with text
Product::searchQueryString('ABC', 'price')->get();
// Lenient → ignores type errors, returns 0 matches
Product::searchQueryString('ABC', 'price', fn($o) => $o->lenient(true))->get();

9. Query Types (best_fields, cross_fields, phrase, etc.)

Section titled “9. Query Types (best_fields, cross_fields, phrase, etc.)”
use PDPhilip\Elasticsearch\Query\Options\QueryStringOptions;
// phrase vs phrase_prefix (single field)
Product::searchQueryString('club sand', 'name', function (QueryStringOptions $options) {
$options->type('phrase'); // requires exact phrase match
})->get();
Product::searchQueryString('club sand', 'name', function (QueryStringOptions $options) {
$options->type('phrase_prefix'); // allows partial phrase match ("Club sandwich deluxe")
})->get();
// best_fields vs cross_fields (multi-field resolution)
Product::searchQueryString('vanilla pizza', ['name', 'details.product'], function (QueryStringOptions $options) {
$options->defaultOperator('AND')->type('best_fields'); // both terms must appear in the same field
})->get();
Product::searchQueryString('vanilla pizza', ['name', 'details.product'], function (QueryStringOptions $options) {
$options->defaultOperator('AND')->type('cross_fields'); // terms can appear across multiple fields
})->get();
// bool_prefix (autocomplete-style)
Product::searchQueryString('cheese piz', 'name', function (QueryStringOptions $options) {
$options->type('bool_prefix'); // prefix matching for partial terms
})->get();
use PDPhilip\Elasticsearch\Query\Options\QueryStringOptions;
// Boost name vs product and compare first() result
Product::searchQueryString('coffee', ['name^3', 'details.product'], function (QueryStringOptions $options) {
$options->type('cross_fields'); // boosts name field more heavily
})->first();
Product::searchQueryString('coffee', ['name', 'details.product^3'], function (QueryStringOptions $options) {
$options->type('cross_fields'); // boosts product field more heavily
})->first();
User::searchQueryString('name:/joh?n(ath[oa]n)/')->get();

12. Numeric Ranges (inclusive vs exclusive)

Section titled “12. Numeric Ranges (inclusive vs exclusive)”
// Inclusive upper bound
Product::searchQueryString('price:[5 TO 19]')->get();
// Exclusive upper bound (right brace)
Product::searchQueryString('price:[5 TO 19}')->get();

13. Mixed Boolean Operators (must/forbid/optional)

Section titled “13. Mixed Boolean Operators (must/forbid/optional)”
// vanilla optional, +pizza required, -ice forbidden
Product::searchQueryString('vanilla +pizza -ice', fn($o) => $o->type('cross_fields'))->get();