1. Go to this page and download the library: Download yetidevworks/yetisearch library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
yetidevworks / yetisearch example snippets
use YetiSearch\YetiSearch;
// Initialize YetiSearch with configuration
$config = [
'storage' => [
'path' => '/path/to/your/search.db'
]
];
$search = new YetiSearch($config);
// Create an index
$indexer = $search->createIndex('pages');
// Index a document
$indexer->insert([
'id' => 'doc1',
'content' => [
'title' => 'Introduction to YetiSearch',
'body' => 'YetiSearch is a powerful search engine library for PHP applications...',
'url' => 'https://example.com/intro',
'tags' => 'search php library'
]
]);
// Search for documents
$results = $search->search('pages', 'powerful search');
// Search with fuzzy matching enabled (automatic typo correction)
$fuzzyResults = $search->search('pages', 'powerfull serch', ['fuzzy' => true]);
// Automatically corrects typos: "powerfull serch" → "powerful search"
// Display results
foreach ($results['results'] as $result) {
echo $result['title'] . ' (Score: ' . $result['score'] . ")\n";
echo $result['excerpt'] . "\n\n";
}
use YetiSearch\YetiSearch;
$search = new YetiSearch([
'storage' => ['path' => './search.db']
]);
$indexer = $search->createIndex('articles');
// Index a single document
$document = [
'id' => 'article-1',
'content' => [
'title' => 'Getting Started with PHP',
'body' => 'PHP is a popular general-purpose scripting language...',
'author' => 'John Doe',
'category' => 'Programming',
'tags' => 'php programming tutorial'
],
'metadata' => [
'date' => time()
]
];
$indexer->insert($document);
// Index multiple documents
$documents = [
[
'id' => 'article-2',
'content' => [
'title' => 'Advanced PHP Techniques',
'body' => 'Let\'s explore advanced PHP programming techniques...',
'author' => 'Jane Smith',
'category' => 'Programming',
'tags' => 'php advanced tips'
]
],
[
'id' => 'article-3',
'content' => [
'title' => 'PHP Performance Optimization',
'body' => 'Optimizing PHP applications for better performance...',
'author' => 'Bob Johnson',
'category' => 'Performance',
'tags' => 'php performance optimization'
]
]
];
$indexer->insert($documents);
// Flush to ensure all documents are written
$indexer->flush();
// Update a document
$indexer->update([
'id' => 'article-1',
'content' => [
'title' => 'Getting Started with PHP 8', // Updated title
'body' => 'PHP 8 introduces many new features...',
'author' => 'John Doe',
'category' => 'Programming',
'tags' => 'php php8 programming tutorial'
]
]);
// Delete a document
$indexer->delete('article-1');
// Clear entire index
$indexer->clear();
// Get index statistics
$stats = $indexer->getStats();
echo "Total documents: " . $stats['total_documents'] . "\n";
echo "Total size: " . $stats['total_size'] . " bytes\n";
echo "Average document size: " . $stats['avg_document_size'] . " bytes\n";
// Optimize index for better performance
$indexer->optimize();
$config = [
'storage' => [
'path' => '/path/to/search.db',
'timeout' => 5000, // Connection timeout in ms
'busy_timeout' => 10000, // Busy timeout in ms
'journal_mode' => 'WAL', // Write-Ahead Logging for better concurrency
'synchronous' => 'NORMAL', // Sync mode
'cache_size' => -2000, // Cache size in KB (negative = KB)
'temp_store' => 'MEMORY' // Use memory for temp tables
],
'analyzer' => [
'min_word_length' => 2, // Minimum word length to index
'max_word_length' => 50, // Maximum word length to index
'remove_numbers' => false, // Keep numbers in index
'lowercase' => true, // Convert to lowercase
'strip_html' => true, // Remove HTML tags
'strip_punctuation' => true, // Remove punctuation
'expand_contractions' => true, // Expand contractions (e.g., don't -> do not)
'custom_stop_words' => ['example', 'custom'], // Additional stop words to exclude, in every language
'stop_words' => [ // v2.6.0+: stop words per language, replacing the built-in list of that language
'it' => ['il', 'la', 'di'] // or giving one to a language that has none
],
'disable_stop_words' => false // Set to true to disable all stop word filtering
],
'indexer' => [
'batch_size' => 100, // Documents per batch
'auto_flush' => true, // Auto-flush after batch_size
'chunk_size' => 1000, // Characters per chunk
'chunk_overlap' => 100, // Overlap between chunks
'stemming' => false, // v2.6.0+: also find words by their stems; fixed when the index is created
'language' => null, // v2.6.0+: language an index stems in when a document or query names none (null: English)
'fields' => [ // Field configuration
'title' => ['boost' => 3.0, 'store' => true],
'content' => ['boost' => 1.0, 'store' => true],
'excerpt' => ['boost' => 2.0, 'store' => true],
'tags' => ['boost' => 2.5, 'store' => true],
'category' => ['boost' => 2.0, 'store' => true],
'author' => ['boost' => 1.5, 'store' => true],
'url' => ['boost' => 1.0, 'store' => true, 'index' => false],
'route' => ['boost' => 1.0, 'store' => true, 'index' => false]
]
],
'search' => [
'min_score' => 0.0, // Minimum score threshold
'highlight_tag' => '<mark>', // Opening highlight tag
'highlight_tag_close' => '</mark>', // Closing highlight tag
'snippet_length' => 150, // Length of snippets
'max_results' => 1000, // Maximum results to return
'enable_fuzzy' => true, // Enable fuzzy search
'fuzzy_algorithm' => 'trigram', // 'trigram', 'jaro_winkler', or 'levenshtein'
'levenshtein_threshold' => 2, // Max edit distance for Levenshtein
// NEW: Query result caching (v2.2.0+)
'cache' => [
'enabled' => false, // Enable query result caching (default: false)
'ttl' => 300, // Cache time-to-live in seconds (5 minutes)
'max_size' => 1000 // Maximum cached queries per index
]
// NEW: Multi-column FTS configuration (v2.1.0+)
'multi_column_fts' => true, // Use separate FTS columns for native BM25 weighting (default: true)
// NEW: Exact match boosting (v2.1.0+)
'exact_match_boost' => 2.0, // Multiplier for exact phrase matches
'exact_terms_boost' => 1.5, // Multiplier for all exact terms present
'fuzzy_score_penalty' => 0.5, // Penalty factor for fuzzy-only matches
'stem_weight' => 0.5, // v2.6.0+: weight of a match on a word's stem, on an index that stems
// NEW: Two-pass search configuration (v2.1.0+)
'two_pass_search' => false, // Enable two-pass search for better primary field results
'primary_fields' => ['title', 'h1', 'name', 'label'], // Fields to search in first pass
'primary_field_limit' => 100, // Max results from first pass
'min_term_frequency' => 2, // Min term frequency for fuzzy matching
'max_indexed_terms' => 10000, // Max indexed terms to check
'max_fuzzy_variations' => 8, // Max fuzzy variations per term
'indexed_terms_cache_ttl' => 300, // Cache TTL for indexed terms
'enable_suggestions' => true, // Enable search suggestions
'cache_ttl' => 300, // Cache TTL in seconds
'result_fields' => [ // Fields to
$indexer = $search->createIndex('books', [
'chunk_size' => 1000, // 1000 characters per chunk
'chunk_overlap' => 100 // 100 character overlap
]);
// Index a large document - it will be automatically chunked
$indexer->insert([
'id' => 'book-1',
'title' => 'War and Peace',
'content' => $veryLongBookContent, // Will be split into chunks
'author' => 'Leo Tolstoy'
]);
// Search returns the best matching chunk by default
$results = $search->search('books', 'Napoleon');
// Get all matching chunks
$allChunks = $search->search('books', 'Napoleon', [
'unique_by_route' => false
]);
$config = [
'indexer' => [
'fields' => [
'title' => ['boost' => 3.0], // High-priority field
'name' => ['boost' => 3.0], // Another high-priority field
'description' => ['boost' => 1.0], // Standard content field
'tags' => ['boost' => 2.0], // Medium priority
]
]
];
// With this configuration:
$indexer = $search->createIndex('movies', [
'fields' => [
'title' => ['boost' => 3.0], // High-priority field
'overview' => ['boost' => 1.0] // Standard field
]
]);
// Searching for "star wars" will rank results as:
// 1. "Star Wars" (exact title match - huge bonus)
// 2. "Star Wars: Episode IV" (contains phrase but longer)
// 3. Movies with "star wars" in overview (lower boost field)
// Stemming on, in English
$search->createIndex('articles', ['stemming' => true]);
// Stemming on, in French
$search->createIndex('pages', [
'stemming' => true,
'language' => 'fr',
]);
// The same through the indexer config, for an index created by its first document
$search = new YetiSearch([
'indexer' => ['stemming' => true, 'language' => 'fr'],
]);
$search->createIndex('articles', ['stemming' => true, 'language' => 'fr-CA']);
$search->stemmingFor('articles'); // 'french'
$search->stemmingFor('posts'); // null, the index does not stem
// Index documents in different languages, in an index that stems
$indexer->insert([
'id' => 'doc-fr-1',
'content' => [
'title' => 'Introduction à PHP',
'content' => 'PHP est un langage de programmation. Des chansons sur PHP...',
],
'language' => 'fr'
]);
// Finds doc-fr-1: 'chanson' and 'chansons' have the same French stem
$results = $search->search('pages', 'chanson', [
'language' => 'fr'
]);
$search->rebuildFts('articles', ['stemming' => true]); // on, in English
$search->rebuildFts('articles', ['stemming' => true, 'language' => 'fr']); // on, in French
$search->rebuildFts('articles', ['stemming' => false]); // off
// Without options, a rebuild keeps the settings the index has and makes its stems again
$search->rebuildFts('articles');
use YetiSearch\Stemmer\StemmerFactory;
use YetiSearch\Stemmer\StemmerInterface;
class ItalianStemmer implements StemmerInterface
{
public function stem(string $word): string
{
return preg_replace('/(?:i|e|o|a)$/', '', $word);
}
public function getLanguage(): string
{
return 'it';
}
}
// The canonical name, the stemmer, and other names for the language
StemmerFactory::register('italian', ItalianStemmer::class, ['it', 'ita', 'italiano']);
$search->createIndex('articoli', ['stemming' => true, 'language' => 'it']);
// 'english', 'en' and 'eng' now use this stemmer
StemmerFactory::register('english', DomainStemmer::class);
StemmerFactory::canonical('it_IT'); // 'italian'; null for a language with no stemmer
StemmerFactory::isSupported('pt_BR'); // false until a stemmer is registered for 'pt'
StemmerFactory::getSupportedLanguages(); // ['english', 'french', 'german', 'spanish', 'italian']
StemmerFactory::create('ita'); // the ItalianStemmer
StemmerFactory::reset(); // back to the four built-in stemmers; for tests
// Configure custom stop words during initialization
$search = new YetiSearch([
'analyzer' => [
'custom_stop_words' => ['lorem', 'ipsum', 'dolor']
]
]);
// Or configure stop words at initialization
$search = new YetiSearch([
'analyzer' => [
'custom_stop_words' => ['example', 'test']
]
]);
// Disable all stop word filtering via config (not recommended)
$search = new YetiSearch([
'analyzer' => [
'disable_stop_words' => true
]
]);
$search = new YetiSearch([
'analyzer' => [
'stop_words' => [
'it' => ['il', 'la', 'di', 'che'], // a list for Italian
'french' => ['alors', 'donc', 'ainsi'], // instead of the built-in French list
],
'custom_stop_words' => ['lorem'], // still added to whichever list applies
]
]);
use YetiSearch\Geo\GeoPoint;
use YetiSearch\Geo\GeoBounds;
// Index documents with location data
$indexer->insert([
'id' => 'coffee-shop-1',
'content' => [
'title' => 'Blue Bottle Coffee',
'body' => 'Specialty coffee roaster and cafe'
],
'geo' => [
'lat' => 37.7825,
'lng' => -122.4099
]
]);
// Search within radius of a point
$searchQuery = new SearchQuery('coffee');
$searchQuery->near(new GeoPoint(37.7749, -122.4194), 5000); // 5km radius
$results = $searchEngine->search($searchQuery);
// Search within bounding box
$searchQuery = new SearchQuery('restaurant');
$searchQuery->withinBounds(37.8, 37.7, -122.3, -122.5);
// Or with a GeoBounds object:
$bounds = new GeoBounds(37.8, 37.7, -122.3, -122.5);
$searchQuery->within($bounds);
// Sort results by distance
$searchQuery = new SearchQuery('food');
$searchQuery->sortByDistance(new GeoPoint(37.7749, -122.4194), 'asc');
// Combine text search with geo filters
$searchQuery = new SearchQuery('italian restaurant')
->near(new GeoPoint(37.7749, -122.4194), 3000)
->filter('price_range', '$$')
->limit(10);
// Results
// Find results even with typos (automatic correction enabled by default)
$results = $search->search('pages', 'porgramming', [ // Note the typo
'fuzzy' => true,
'fuzziness' => 0.8 // 0.0 to 1.0 (higher = stricter)
]);
// Will still find documents about "programming" with automatic typo correction
// Configure fuzzy search algorithms
$config = [
'search' => [
'enable_fuzzy' => true,
'fuzzy_algorithm' => 'trigram', // Options: 'trigram', 'jaro_winkler', 'levenshtein'
'levenshtein_threshold' => 2, // Max edit distance for Levenshtein (default: 2)
'min_term_frequency' => 2, // Min occurrences for a term to be indexed
'max_indexed_terms' => 10000, // Max terms to check for fuzzy matches
'max_fuzzy_variations' => 8, // Max variations per search term
'fuzzy_score_penalty' => 0.4, // Score reduction for fuzzy matches (0.0-1.0)
'indexed_terms_cache_ttl' => 300 // Cache indexed terms for 5 minutes
]
];
$search = new YetiSearch($config);
// Search with advanced fuzzy matching
$results = $search->search('movies', 'Amakin Dkywalker', ['fuzzy' => true]);
// Will find "Anakin Skywalker" despite multiple typos
// Index a documentation site with proper field weighting
$search = new YetiSearch([
'search' => [
'multi_column_fts' => true, // Default - enables native field weighting
'exact_match_boost' => 2.0, // Exact "scheduler" gets 2x boost
'field_weights' => [
'title' => 10.0, // Title matches are most important
'h1' => 8.0,
'h2' => 5.0,
'content' => 1.0
]
]
]);
// Index documents with structured fields
$search->index('docs', [
'id' => 'scheduler-page',
'content' => [
'title' => 'Scheduler', // Exact match in high-weight field
'h1' => 'Task Scheduler Guide',
'content' => 'The scheduler allows you to run tasks...'
]
]);
$search->index('docs', [
'id' => 'generic-page',
'content' => [
'title' => 'Configuration Guide',
'content' => 'You can configure the scheduler here...' // Only mentions scheduler
]
]);
// Search for "scheduler"
$results = $search->search('docs', 'scheduler');
// Results ranking (v2.1.0):
// 1. scheduler-page (Score: ~150) - Exact title match + h1 match
// 2. generic-page (Score: ~20) - Only content mention
// Previous version results:
// 1. generic-page (Score: ~25) - Multiple mentions
// 2. scheduler-page (Score: ~22) - Title boost not effective enough
// For best performance (3-5ms searches)
$config = [
'search' => [
'fuzzy_algorithm' => 'trigram', // Fast algorithm
'min_term_frequency' => 5, // Skip rare terms
'max_indexed_terms' => 5000, // Check fewer terms
'indexed_terms_cache_ttl' => 600 // Cache for 10 minutes
]
];
// For best accuracy (handles more typos)
$config = [
'search' => [
'fuzzy_algorithm' => 'levenshtein',
'levenshtein_threshold' => 2, // Allow 2 edits
'min_term_frequency' => 1, // Include all terms
'max_indexed_terms' => 20000, // Check more terms
'fuzzy_score_penalty' => 0.3 // Lower penalty for fuzzy matches
]
];
// Try different algorithms to find the best fit for your use case
foreach (['trigram', 'levenshtein', 'jaro_winkler'] as $algorithm) {
$search = new YetiSearch([
'search' => ['fuzzy_algorithm' => $algorithm]
]);
// Run your test queries and measure accuracy/speed
}
use YetiSearch\YetiSearch;
use YetiSearch\Semantic\OpenAICompatibleEmbeddingProvider;
$search = new YetiSearch(['storage' => ['path' => 'search.db']]);
$search->setEmbeddingProvider(new OpenAICompatibleEmbeddingProvider([
'api_key' => getenv('OPENAI_API_KEY'),
'model' => 'text-embedding-3-small',
'dimensions' => 512,
]));
// Index as usual, then embed. Embedding is its own step so a slow or failing
// provider never holds up indexing. Each call embeds up to $limit documents;
// run it from a queue, a cron job or a CLI command until nothing is pending.
$search->indexBatch('pages', $documents);
do {
$run = $search->embedPending('pages', 200);
} while ($run['pending'] > 0 && $run['error'] === null);
$results = $search->search('pages', 'automobile');
$results['semantic']; // true when meaning took part in the ranking
// Nothing to do by default: with calibration 'auto', the embedPending() run
// that leaves nothing pending also calibrates when the stored measurement
// no longer describes the index.
$run = $search->embedPending('pages');
$run['calibrated']; // true when this run measured the gate again
// Or measure now, from a CLI command or a queue job.
$calibration = $search->calibrate('pages'); // null below 10 documents
$calibration->minMargin(); // 0.2003
$calibration->stats(); // count, min, median, mean, sd, p95, max of the probe margins
$calibration->probes(); // each probe's best, median and margin
$calibration->excluded(); // probes left out because keywords find them
$calibration->minMarginAt(1.5); // the cutoff at another strictness
$search->calibration('pages'); // what is stored, without a provider
$search->semanticGate('pages'); // ['min_margin' => 0.2003, 'source' => 'calibrated', 'configured' => 0.15, ...]
'semantic' => ['provider' => $provider, 'query_frame' => 'a {query}'],
$search->indexBatch('products', [
['id' => '12', 'type' => 'product', 'content' => ['title' => 'Shorebreak Cap', 'content' => $description, 'route' => '/p/12']],
['id' => '12#card', 'type' => 'card', 'meaning_only' => true,
'content' => ['title' => 'Product: Shorebreak Cap (Accessories)', 'content' => 'A cotton cap.', 'route' => '/p/12']],
]);
// The word "product" finds nothing; a search for "hat" can find the cap by its card.
$search->search('products', 'hat', ['unique_by_route' => true]);
$results = $search->search('products', 'laptop', [
'facets' => [
'brand' => ['limit' => 10],
'tags' => ['limit' => 5],
'price_band' => [
'field' => 'price',
'ranges' => [
['to' => 500, 'key' => 'budget'],
['from' => 500, 'to' => 1000, 'key' => 'mid-range'],
['from' => 1000, 'key' => 'premium']
]
]
]
]);
// Value facets, most common first
foreach ($results['facets']['brand'] as $brand) {
echo "{$brand['value']}: {$brand['count']} products\n";
}
// Range facets, in the order the ranges were given
foreach ($results['facets']['price_band'] as $band) {
echo "{$band['value']}: {$band['count']} products\n"; // budget: 12 products
}
use YetiSearch\Geo\GeoPoint;
use YetiSearch\Models\SearchQuery;
$engine = $search->getSearchEngine('places');
$center = new GeoPoint(40.7128, -74.0060); // NYC
$q = (new SearchQuery('coffee'))
->near($center, 5) // radius 5 km
// or pass via facade options: ['geoFilters' => ['near' => ['point'=>..., 'radius'=>5, 'units'=>'km']]]
->sortByDistance($center, 'asc'); // nearest first
$results = $engine->search($q);
foreach ($results->getResults() as $r) {
echo $r->get('title') . ' - ' . round($r->getDistance()) . " m\n";
}
$search = new YetiSearch([
'search' => [
'geo_units' => 'km', // default units for near() radius if units not specified per query
]
]);
$search = new YetiSearch([
'search' => [
// Mix distance into final score (0.0..1.0)
'distance_weight' => 0.5,
// Decay factor per km (higher = faster decay)
'distance_decay_k' => 0.01,
]
]);
// With distance_weight > 0, results incorporate both BM25 text score and proximity
// Type-ahead friendly search
$results = $search->search('movies', $query, [
'limit' => 8,
'fields' => ['title','overview','url'],
'fuzzy' => true,
'fuzzy_last_token_only' => true, // fuzz just the last term
'prefix_last_token' => true, //
$config = [
'indexer' => [
'fields' => [ // boosts become BM25 weights
'title' => ['boost' => 3.0, 'store' => true],
'overview' => ['boost' => 1.0, 'store' => true],
'tags' => ['boost' => 2.0, 'store' => true],
],
'fts' => [
'multi_column' => true, // create FTS with per-field columns
'prefix' => [2,3], // enable FTS5 prefix index (optional)
],
],
'search' => [
'prefix_last_token' => true, // use last-token prefix (needs prefix option above)
],
];
$search = new YetiSearch($config);
$indexer = $search->createIndex('movies');
// Reindex to apply schema changes; or use scripts/migrate_fts.php to migrate existing data
$suggestions = $search->suggest('movies', $query, [
'limit' => 8, // max suggestions to return
'per_variant' => 5, // results checked per fuzzy variant
'title_boost' => 100.0, // extra weight if title contains the variant
'prefix_boost' => 25.0, // extra weight if title starts with the variant
]);
// Example: display top texts
foreach ($suggestions as $s) {
echo $s['text'] . "\n";
}