timjohnbancroft / constructor-laravel
Laravel integration for Constructor.io - AI-powered search, recommendations, and shopping agents
Package info
github.com/timjohnbancroft/constructor-laravel
pkg:composer/timjohnbancroft/constructor-laravel
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0
Suggests
- laravel/scout: Required for Scout integration (^10.0)
README
A Laravel package for Constructor.io - AI-powered product discovery.
Features
- Product Search - Full-text search with filters, sorting, pagination
- Category Browse - Browse products by category or facet
- Autocomplete - Search suggestions and product previews with zero-state support
- AI Shopping Agent - Natural language product discovery ("I need a gift for my mom")
- Product Insights Agent - AI-powered Q&A on product detail pages
- Recommendations - Personalized product recommendations
- Collections - Browse curated product collections
- Catalog Management - Bulk catalog uploads (CSV, JSONL)
- Backend Integration - Automatic forwarding of user context headers and cookies for server-side API calls
- Laravel Scout - Full Scout engine integration
Architecture Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ Your Laravel Application │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌────────────────────────────────────────┐ │
│ │ Constructor Facade │───▶│ ConstructorSandboxSearch │ │
│ │ (Search/Browse) │ │ - search(), browse(), autocomplete() │ │
│ └─────────────────────┘ │ - getRecommendations() │ │
│ │ - getBrowseGroups(), getCollections() │ │
│ └──────────────┬─────────────────────────┘ │
│ │ │
│ ┌─────────────────────┐ ▼ │
│ │ ConstructorService │ ┌────────────────────────────────────────┐ │
│ │ (Catalog Management)│───▶│ ac.cnstrc.com (Search API) │ │
│ │ - uploadCatalog() │ └────────────────────────────────────────┘ │
│ │ - getTaskStatus() │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ ┌────────────────────────────────────────┐ │
│ │ ConstructorAgent │───▶│ agent.cnstrc.com (Agent API) │ │
│ │ Service │ │ - AI Shopping Agent │ │
│ │ - askShoppingAgent()│ │ - Product Insights Agent │ │
│ │ - askProductQuestion│ └────────────────────────────────────────┘ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Requirements
- PHP 8.1+
- Laravel 10.x, 11.x, or 12.x
- Constructor.io account (constructor.io)
Installation
1. Install via Composer
composer require timjohnbancroft/constructor-laravel
2. Publish Configuration
php artisan vendor:publish --provider="ConstructorIO\Laravel\ConstructorServiceProvider"
3. Configure Environment
Add to your .env file:
CONSTRUCTOR_API_KEY=your-api-key CONSTRUCTOR_API_TOKEN=your-api-token CONSTRUCTOR_AGENT_DOMAIN=your-agent-domain # Optional: for AI Shopping Agent # Optional: Backend integration (for server-side calls on behalf of browser users) CONSTRUCTOR_BACKEND_TOKEN=your-backend-token # Falls back to API_TOKEN if not set CONSTRUCTOR_CLIENT_IDENTIFIER=cio-be-laravel-your-company # Auto-generates if not set
Get your credentials from the Constructor.io Dashboard.
Credential Types:
- API Key (public): Identifies your index, used for search/browse/autocomplete requests
- API Token (secret): Used for authenticated operations like catalog uploads
Quick Start
Search Products
use ConstructorIO\Laravel\Facades\Constructor; // Basic search $results = Constructor::search('blue shoes'); // With filters and options $results = Constructor::search('shoes', [ 'brand' => ['Nike', 'Adidas'], 'color' => ['blue'], ], [ 'page' => 1, 'per_page' => 24, 'sort_by' => 'price', 'sort_order' => 'ascending', ]); // Access results foreach ($results->products as $product) { echo $product['name']; echo $product['price']; echo $product['image_url']; } // Pagination echo "Page {$results->page} of {$results->totalPages()}"; echo "Total: {$results->total} products";
Browse by Category
use ConstructorIO\Laravel\Facades\Constructor; // Browse by category $results = Constructor::browse('group_id', 'mens-clothing'); // Browse by brand $results = Constructor::browse('brand', 'Nike', [], [ 'page' => 1, 'per_page' => 24, ]); // Get category hierarchy $categories = Constructor::getBrowseGroups([ 'max_items' => 10, 'max_children' => 5, 'with_images' => true, ]);
Autocomplete
use ConstructorIO\Laravel\Facades\Constructor; // Get autocomplete suggestions $results = Constructor::autocomplete('blu', [ 'sections' => [ 'suggestions' => ['enabled' => true, 'limit' => 5], 'products' => ['enabled' => true, 'limit' => 6], ], ]); foreach ($results->suggestions as $suggestion) { echo $suggestion['term']; } // Zero-state (search box focused but empty) $zeroState = Constructor::getZeroStateData([ 'show_top_categories' => true, 'show_popular_products' => true, 'recommendation_pod_id' => 'hp-bestsellers', ]);
Recommendations
use ConstructorIO\Laravel\Facades\Constructor; // Home page recommendations $recs = Constructor::getRecommendations('home-page-1', [ 'num_results' => 8, ]); // Product page recommendations (requires item_id) $similar = Constructor::getItemRecommendations('pdp-similar', 'PRODUCT-123');
Collections
use ConstructorIO\Laravel\Facades\Constructor; // Get all collections $collections = Constructor::getCollections(['max_items' => 10]); // Get collection metadata $collection = Constructor::getCollection('summer-essentials'); // Browse products in a collection $results = Constructor::browseCollection('summer-essentials', [], [ 'page' => 1, 'per_page' => 24, ]);
AI Shopping Agent
use ConstructorIO\Laravel\Services\ConstructorAgentService; $agent = app(ConstructorAgentService::class); // Natural language query $response = $agent->askShoppingAgent('I need a gift for my mom who likes gardening'); echo $response['message']; foreach ($response['products'] as $product) { echo $product['name']; } // Continue conversation $followUp = $agent->askShoppingAgent( 'Something under $50', $response['thread_id'] );
Product Insights Agent
use ConstructorIO\Laravel\Services\ConstructorAgentService; $agent = app(ConstructorAgentService::class); // Get suggested questions for a product $questions = $agent->getProductQuestions('PRODUCT-123'); // Ask a question about a product $answer = $agent->askProductQuestion( question: 'Is this true to size?', itemId: 'PRODUCT-123' ); echo $answer['answer'];
Catalog Management
use ConstructorIO\Laravel\Services\ConstructorService; $constructor = app(ConstructorService::class); // Upload catalog file (creates or replaces all items) $result = $constructor->uploadCatalog( storage_path('app/catalog/items.csv'), 'create_or_replace' ); // Or patch (update only specified items) $result = $constructor->uploadCatalog( storage_path('app/catalog/updates.csv'), 'patch' ); // Wait for completion $status = $constructor->waitForTaskCompletion($result['task_id']); if ($status['successful']) { echo "Catalog uploaded!"; }
Recipes (If Configured)
use ConstructorIO\Laravel\Facades\Constructor; // Check if recipes are supported if (Constructor::supportsRecipes()) { // Search recipes $results = Constructor::searchRecipes('chicken pasta'); // Browse recipes by category $results = Constructor::browseRecipes('meal_type', 'dinner'); // Get a single recipe $recipe = Constructor::getRecipe('recipe-123'); }
Data Transfer Objects (DTOs)
All search operations return strongly-typed DTOs with convenient methods.
SearchResults
Returned by search(), browse(), and browseCollection().
$results = Constructor::search('shoes'); // Properties $results->products // array - Product data $results->total // int - Total matching results $results->page // int - Current page (1-indexed) $results->perPage // int - Results per page $results->facets // array - Available filters with counts $results->groups // array - Category hierarchy (for browse) $results->metadata // array - Request ID, result ID, etc. // Methods $results->hasMore() // bool - More pages available? $results->totalPages() // int - Calculate total pages $results->isEmpty() // bool - No products? $results->count() // int - Products on this page $results->getOffset() // int - Offset for "Showing X-Y of Z" $results->nextPageNumber() // int - Next page number or 0 $results->toArray() // array - For JSON serialization
Facets Structure:
// $results->facets [ 'brand' => [ 'name' => 'Brand', // Display name 'values' => [ // Value => count 'Nike' => 45, 'Adidas' => 32, ], 'type' => 'checkbox_list', // single_select, checkbox_list, or range ], 'price' => [ 'name' => 'Price', 'values' => [...], 'type' => 'range', 'min' => 25.00, 'max' => 299.99, ], ]
AutocompleteResults
Returned by autocomplete() and getZeroStateData().
$results = Constructor::autocomplete('blu'); // Properties $results->suggestions // array - Search term suggestions $results->products // array - Product previews $results->categories // array - Category suggestions $results->trending // array - Trending searches (zero-state) $results->popularProducts // array - Popular products (zero-state) $results->topCategories // array - Top categories (zero-state) $results->metadata // array - Request metadata // Methods $results->hasSuggestions() // bool $results->hasProducts() // bool $results->hasCategories() // bool $results->hasZeroStateData() // bool $results->hasTrending() // bool $results->hasPopularProducts() // bool $results->hasTopCategories() // bool $results->isEmpty() // bool $results->isAutocompleteEmpty() // bool - Ignores zero-state $results->toArray() // array
RecommendationResults
Returned by getRecommendations() and getItemRecommendations().
$recs = Constructor::getRecommendations('home-bestsellers'); // Properties $recs->podId // string - The pod ID requested $recs->title // string - Pod display name $recs->products // array - Recommended products $recs->total // int - Total recommendations available $recs->metadata // array - Request metadata, pod info // Methods $recs->isEmpty() // bool $recs->hasRecommendations() // bool $recs->count() // int - Number of products returned $recs->toArray() // array
Complete Facade Methods Reference
use ConstructorIO\Laravel\Facades\Constructor; // Search & Browse Constructor::search(string $query, array $filters = [], array $options = []): SearchResults; Constructor::browse(string $filterName, string $filterValue, array $filters = [], array $options = []): SearchResults; Constructor::autocomplete(string $query, array $options = []): AutocompleteResults; Constructor::getZeroStateData(array $options = []): AutocompleteResults; // Recommendations Constructor::getRecommendations(string $podId, array $options = []): RecommendationResults; Constructor::getItemRecommendations(string $podId, string $itemId, array $options = []): RecommendationResults; // Categories & Groups Constructor::getBrowseGroups(array $options = []): array; // Collections Constructor::getCollections(array $options = []): array; Constructor::getCollection(string $collectionId): ?array; Constructor::browseCollection(string $collectionId, array $filters = [], array $options = []): SearchResults; Constructor::getFirstProductImageFromCollection(string $collectionId): ?string; // Facets Constructor::getFacets(string $query, array $filters = []): array; Constructor::getAvailableFacets(): array; Constructor::getFacetValuesWithImages(string $facetName, int $maxItems = 10): array; // Recipes Constructor::searchRecipes(string $query, array $filters = [], array $options = []): SearchResults; Constructor::browseRecipes(string $filterName, string $filterValue, array $filters = [], array $options = []): SearchResults; Constructor::getRecipe(string $recipeId): ?array; // Feature Detection Constructor::supportsZeroState(): bool; Constructor::supportsRecommendations(): bool; Constructor::supportsBrowseGroups(): bool; Constructor::supportsCollections(): bool; Constructor::supportsRecipes(): bool; Constructor::getProviderName(): string;
Search Options Reference
Options available for search() and browse():
$options = [ // Pagination 'page' => 1, // Page number (1-indexed) 'per_page' => 24, // Results per page (max 100) // Sorting 'sort_by' => 'price', // Field to sort by 'sort_order' => 'ascending', // 'ascending' or 'descending' // Section 'section' => 'Products', // Constructor section name // Range Filters 'range_filters' => [ 'price' => ['min' => 50, 'max' => 200], ], // User Context (for personalization) 'user_id' => 'user-123', 'session_id' => 'session-abc', ];
Laravel Scout Integration
Register the Engine
The package automatically registers the constructor Scout driver. Configure in config/scout.php:
return [ 'driver' => env('SCOUT_DRIVER', 'constructor'), 'constructor' => [ 'api_key' => env('CONSTRUCTOR_API_KEY'), 'api_token' => env('CONSTRUCTOR_API_TOKEN'), ], ];
Make Models Searchable
use Illuminate\Database\Eloquent\Model; use Laravel\Scout\Searchable; class Product extends Model { use Searchable; /** * Get the Constructor section for this model. */ public function getConstructorSection(): string { return 'Products'; } /** * Get the indexable data array for the model. */ public function toSearchableArray(): array { return [ 'id' => $this->sku, 'name' => $this->name, 'url' => route('products.show', $this), 'image_url' => $this->image_url, 'price' => $this->price, 'brand' => $this->brand, 'categories' => $this->categories->pluck('name')->toArray(), ]; } }
Search with Scout
// Basic search $products = Product::search('blue shirt')->get(); // With filters $products = Product::search('shirt') ->where('brand', 'Nike') ->paginate(24);
Configuration Reference
config/constructor.php
return [ // API Credentials 'api_key' => env('CONSTRUCTOR_API_KEY'), 'api_token' => env('CONSTRUCTOR_API_TOKEN'), // API Endpoints 'search_base_url' => env('CONSTRUCTOR_SEARCH_BASE_URL', 'https://ac.cnstrc.com'), 'agent_base_url' => env('CONSTRUCTOR_AGENT_BASE_URL', 'https://agent.cnstrc.com'), // AI Agent Settings 'agent_domain' => env('CONSTRUCTOR_AGENT_DOMAIN'), 'agent_guard' => env('CONSTRUCTOR_AGENT_GUARD', true), 'agent_num_result_events' => env('CONSTRUCTOR_AGENT_NUM_RESULT_EVENTS', 5), 'agent_num_results_per_event' => env('CONSTRUCTOR_AGENT_NUM_RESULTS_PER_EVENT', 4), // Backend Integration (server-side calls on behalf of browser users) 'backend_token' => env('CONSTRUCTOR_BACKEND_TOKEN'), // Falls back to api_token 'client_identifier' => env('CONSTRUCTOR_CLIENT_IDENTIFIER'), // Auto-generates if null // HTTP Client 'timeout' => env('CONSTRUCTOR_TIMEOUT', 30), 'retry_times' => env('CONSTRUCTOR_RETRY_TIMES', 2), 'retry_sleep' => env('CONSTRUCTOR_RETRY_SLEEP', 100), ];
Available Services
| Service | Description |
|---|---|
ConstructorSandboxSearch |
Search, browse, autocomplete, recommendations, collections |
ConstructorAgentService |
AI Shopping Agent and Product Insights Agent |
ConstructorService |
Catalog uploads, task monitoring, admin operations |
ConstructorEngine |
Laravel Scout engine implementation |
Error Handling
The package handles errors gracefully:
- Search errors return empty
SearchResults(logged at error level) - Recommendation errors return empty
RecommendationResults(logged at error level) - Agent errors throw exceptions (for UI error handling)
- Catalog errors throw exceptions (for background job handling)
// Search - gracefully returns empty on error $results = Constructor::search('query'); if ($results->isEmpty()) { // Handle no results (could be error or just no matches) } // Agent - throws on error try { $response = $agent->askShoppingAgent($query); } catch (\Exception $e) { // Handle error: rate limit, auth failure, network error Log::error('Shopping Agent error: ' . $e->getMessage()); }
Troubleshooting
"Constructor.io configuration not properly set"
Ensure all required environment variables are set:
CONSTRUCTOR_API_KEY=your-api-key CONSTRUCTOR_API_TOKEN=your-api-token
Empty results when products should exist
- Check API key: Verify the API key matches your Constructor index
- Check section name: Default is 'Products', ensure your index uses this
- Check filters: Some filters may be too restrictive
- View logs: Check
storage/logs/laravel.logfor API errors
Facets not returning
- Facets must be configured in the Constructor.io dashboard
- Not all indexes have facets enabled
- Try a broader search query to see available facets
Recommendations returning empty
- Verify the pod ID exists in your Constructor account
- Some pods require
item_id- usegetItemRecommendations()instead - Check that the pod has been trained with data
Agent authentication failures
Constructor Agent API authentication failed. Check your API key and domain configuration.
- Ensure
CONSTRUCTOR_AGENT_DOMAINis set correctly - The agent domain is separate from the search API key
Rate limit errors
Rate limit exceeded. Please try again later.
- Implement caching for repeated requests
- Contact Constructor.io to increase limits for production
Testing Your Integration
Artisan Test Command
# Test search functionality php artisan tinker >>> Constructor::search('test')->toArray() # Test recommendations >>> Constructor::getRecommendations('your-pod-id')->toArray() # Test agent (if configured) >>> app(ConstructorAgentService::class)->getProductQuestions('product-id')
Unit Testing
Mock the facade for testing:
use ConstructorIO\Laravel\Facades\Constructor; use ConstructorIO\Laravel\DataTransferObjects\SearchResults; public function test_search_page_displays_products() { Constructor::shouldReceive('search') ->once() ->with('shoes', [], \Mockery::any()) ->andReturn(new SearchResults( products: [['id' => '1', 'name' => 'Nike Shoes']], total: 1, page: 1, perPage: 24 )); $response = $this->get('/search?q=shoes'); $response->assertSee('Nike Shoes'); }
Best Practices
Caching Recommendations
use Illuminate\Support\Facades\Cache; $recommendations = Cache::remember( "recs:home-page:{$userId}", now()->addMinutes(15), fn() => Constructor::getRecommendations('home-page-1') );
User Personalization
Pass user identifiers for personalized results:
$results = Constructor::search('shoes', [], [ 'user_id' => auth()->id(), 'session_id' => session()->getId(), ]);
Handling Zero-State Gracefully
public function autocomplete(Request $request) { $query = trim($request->input('q', '')); if (empty($query)) { return Constructor::getZeroStateData([ 'show_top_categories' => true, 'recommendation_pod_id' => 'autocomplete-bestsellers', ])->toArray(); } return Constructor::autocomplete($query)->toArray(); }
Documentation
- Constructor.io Documentation
- Search API Reference
- Browse API Reference
- Autocomplete API Reference
- Recommendations API Reference
- AI Shopping Agent API Reference
License
MIT License. See LICENSE for details.