Skip to the content.

Complex Queries

This example demonstrates how to build complex, multi-level queries for advanced search scenarios.

Advanced Product Search with User Preferences

This example shows a complex search that combines various query types with weighted preferences:

<?php
use Zvonchuk\Elastic\Client;
use Zvonchuk\Elastic\Core\SearchRequest;
use Zvonchuk\Elastic\Search\Builder\SearchSourceBuilder;
use Zvonchuk\Elastic\Query\QueryBuilders;
use Zvonchuk\Elastic\Search\Sort\SortBuilders;

$client = Client::getInstance(['localhost:9200']);

// User parameters
$searchTerm = 'wireless headphones';
$userLocation = [
    'lat' => 40.7128,
    'lon' => -74.0060
];
$preferredBrands = ['Sony', 'Bose', 'Sennheiser'];
$priceRange = [50, 350];
$minRating = 4.0;

// User preferences weighting
$preferNewArrivals = true;
$preferLocalStock = true;

// Build the main query
$mainQuery = QueryBuilders::boolQuery();

// 1. Must conditions (required matches)
if (!empty($searchTerm)) {
    // Match title with higher weight
    $mainQuery->should(
        QueryBuilders::matchQuery('title', $searchTerm)
            ->operator('AND')
            ->fuzziness('AUTO')
    );
    
    // Match description with lower weight
    $mainQuery->should(
        QueryBuilders::matchQuery('description', $searchTerm)
            ->operator('OR')
    );
    
    // Require at least one should clause to match
    $mainQuery->minimumShouldMatch(1);
}

// 2. Filter conditions (don't affect score, but must match)
// Price range filter
if (!empty($priceRange)) {
    $mainQuery->filter(
        QueryBuilders::rangeQuery('price')
            ->gte((string)$priceRange[0])
            ->lte((string)$priceRange[1])
    );
}

// Rating filter
if ($minRating > 0) {
    $mainQuery->filter(
        QueryBuilders::rangeQuery('rating')
            ->gte((string)$minRating)
    );
}

// 3. Should conditions (boost score if matched)
// Preferred brands (boost score)
if (!empty($preferredBrands)) {
    $mainQuery->should(
        QueryBuilders::termsQuery('brand', $preferredBrands)
    );
}

// Prefer new arrivals
if ($preferNewArrivals) {
    // Last 30 days items get a boost
    $thirtyDaysAgo = date('Y-m-d', strtotime('-30 days'));
    $mainQuery->should(
        QueryBuilders::rangeQuery('created_at')
            ->gte($thirtyDaysAgo)
    );
}

// Prefer local stock if user location provided
if ($preferLocalStock && !empty($userLocation)) {
    // Boost items available in stores within 10 miles
    $mainQuery->should(
        QueryBuilders::GeoDistanceQuery('store_location')
            ->point($userLocation['lat'], $userLocation['lon'])
            ->distance('10mi')
    );
}

// Set up the search
$searchSource = new SearchSourceBuilder();
$searchSource->query($mainQuery);
$searchSource->size(20);

// Add sorting (by relevance, then by distance if location provided)
if (!empty($userLocation)) {
    $searchSource->sort(
        SortBuilders::geoDistanceSort('store_location', $userLocation['lat'], $userLocation['lon'])
            ->unit('mi')
    );
}

// Execute the search
$request = new SearchRequest('products');
$request->source($searchSource);
$response = $client->search($request);

// Process results
$hits = $response->getHits();
echo "Found " . count($hits) . " matching products:\n\n";

foreach ($hits as $hit) {
    $product = $hit['_source'];
    $score = $hit['_score'];
    $distance = isset($hit['sort'][0]) ? round($hit['sort'][0], 1) . ' miles' : 'N/A';
    
    echo "- {$product['title']} by {$product['brand']}\n";
    echo "  Price: \${$product['price']}, Rating: {$product['rating']}/5\n";
    echo "  Relevance Score: $score, Distance to nearest store: $distance\n";
    echo "  {$product['description']}\n\n";
}

E-commerce Search with Learning to Rank Features

This example demonstrates a complex search that could be used with a learning-to-rank system:

<?php
use Zvonchuk\Elastic\Client;
use Zvonchuk\Elastic\Core\SearchRequest;
use Zvonchuk\Elastic\Search\Builder\SearchSourceBuilder;
use Zvonchuk\Elastic\Query\QueryBuilders;

$client = Client::getInstance(['localhost:9200']);

// User input and context
$query = 'lightweight laptop';
$userId = '12345';
$userPreferences = [
    'brands' => ['Apple', 'Dell', 'Lenovo'],
    'priceRange' => [800, 2000],
    'categories' => ['laptops', 'ultrabooks']
];
$userHistory = [
    'recent_views' => ['product_123', 'product_456'],
    'recent_purchases' => ['product_789']
];
$seasonalBoosts = [
    'isBackToSchool' => true,
    'isHolidaySeason' => false
];

// Build the main query
$mainQuery = QueryBuilders::boolQuery();

// 1. Text matching component (full-text search)
$textMatchQuery = QueryBuilders::boolQuery();

// Title matching with high boost
$textMatchQuery->should(
    QueryBuilders::matchQuery('title', $query)
        ->operator('OR')
        ->fuzziness('AUTO')
);

// Description matching with medium boost
$textMatchQuery->should(
    QueryBuilders::matchQuery('description', $query)
        ->operator('OR')
);

// Keywords matching
$textMatchQuery->should(
    QueryBuilders::matchQuery('keywords', $query)
        ->operator('OR')
);

// Add the text matching component to main query
$mainQuery->must($textMatchQuery);

// 2. Filtering component (non-scoring)
// Price range filter
if (!empty($userPreferences['priceRange'])) {
    $mainQuery->filter(
        QueryBuilders::rangeQuery('price')
            ->gte((string)$userPreferences['priceRange'][0])
            ->lte((string)$userPreferences['priceRange'][1])
    );
}

// Only show in-stock items
$mainQuery->filter(
    QueryBuilders::termQuery('in_stock', true)
);

// 3. Personalization component (boosting)
// Boost preferred brands
if (!empty($userPreferences['brands'])) {
    $mainQuery->should(
        QueryBuilders::termsQuery('brand', $userPreferences['brands'])
    );
}

// Boost preferred categories
if (!empty($userPreferences['categories'])) {
    $mainQuery->should(
        QueryBuilders::termsQuery('category', $userPreferences['categories'])
    );
}

// Boost recently viewed products (except ones already purchased)
if (!empty($userHistory['recent_views'])) {
    $recentViewsQuery = QueryBuilders::boolQuery()
        ->should(QueryBuilders::termsQuery('product_id', $userHistory['recent_views']));
    
    // Exclude recently purchased items
    if (!empty($userHistory['recent_purchases'])) {
        $recentViewsQuery->mustNot(
            QueryBuilders::termsQuery('product_id', $userHistory['recent_purchases'])
        );
    }
    
    $mainQuery->should($recentViewsQuery);
}

// 4. Business rules component
// Boost new arrivals
$thirtyDaysAgo = date('Y-m-d', strtotime('-30 days'));
$mainQuery->should(
    QueryBuilders::rangeQuery('created_at')
        ->gte($thirtyDaysAgo)
);

// Boost sale items
$mainQuery->should(
    QueryBuilders::termQuery('on_sale', true)
);

// Seasonal boosts
if ($seasonalBoosts['isBackToSchool']) {
    $mainQuery->should(
        QueryBuilders::termQuery('tags', 'back_to_school')
    );
}

if ($seasonalBoosts['isHolidaySeason']) {
    $mainQuery->should(
        QueryBuilders::termQuery('tags', 'holiday_deal')
    );
}

// 5. Social proof component
// Boost highly rated products
$mainQuery->should(
    QueryBuilders::rangeQuery('rating')
        ->gte('4.5')
);

// Boost frequently purchased products
$mainQuery->should(
    QueryBuilders::rangeQuery('purchase_count')
        ->gte('100')
);

// Set up the search with explain to see scoring details
$searchSource = new SearchSourceBuilder();
$searchSource->query($mainQuery);
$searchSource->size(20);
$searchSource->explain(true);  // Include score explanation

// Execute the search
$request = new SearchRequest('products');
$request->source($searchSource);
$response = $client->search($request);

// Process results
$hits = $response->getHits();
echo "Search results for '$query':\n\n";

foreach ($hits as $hit) {
    $product = $hit['_source'];
    $score = $hit['_score'];
    
    echo "- {$product['title']} ({$product['brand']})\n";
    echo "  Price: \${$product['price']}, Rating: {$product['rating']}/5\n";
    echo "  Score: $score\n";
    
    if (isset($hit['_explanation'])) {
        echo "  Top scoring factors:\n";
        printTopScoringFactors($hit['_explanation'], 2);
    }
    
    echo "\n";
}

// Helper function to print top scoring factors
function printTopScoringFactors($explanation, $maxDepth, $depth = 0) {
    if ($depth >= $maxDepth) return;
    
    if (isset($explanation['description'])) {
        $indent = str_repeat("  ", $depth + 1);
        echo $indent . "- " . $explanation['description'] . " (score: {$explanation['value']})\n";
    }
    
    if (isset($explanation['details']) && is_array($explanation['details'])) {
        // Sort details by value (score) in descending order
        usort($explanation['details'], function($a, $b) {
            return $b['value'] <=> $a['value'];
        });
        
        // Print top 3 details
        $topDetails = array_slice($explanation['details'], 0, 3);
        foreach ($topDetails as $detail) {
            printTopScoringFactors($detail, $maxDepth, $depth + 1);
        }
    }
}