Download the PHP package yetidevworks/yetisearch without Composer

On this page you can find all versions of the php package yetidevworks/yetisearch. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package yetisearch

YetiSearch

CI PHP Version Latest Stable Version

A powerful, pure-PHP search engine library with advanced full-text search capabilities, designed for modern PHP applications.

Important: Requires SQLite FTS5 (full‑text search) support in your PHP’s SQLite library. See “Requirements” for a quick check.

Table of Contents

Features

Requirements

Important: SQLite FTS5 required

Installation

Install YetiSearch via Composer:

Quick Start

Example Applications

The examples/ directory contains fully working demonstrations of YetiSearch features:

🏢 Apartment Search Tutorial

Complete real-world example of a property search application:

Run it:

🔍 Other Examples

Usage Examples

Basic Indexing

Advanced Indexing

Search Examples

Multi-Index Search

Search across multiple indexes simultaneously:

Document Management

Configuration

Full Configuration Example

Storage Schema: External-Content (Default)

YetiSearch defaults to an efficient external-content FTS5 schema:

Legacy indices (string id primary key with id_map) continue to work. You can migrate any legacy index to the new schema:

To explicitly create an index with/without external-content:

Advanced Features

Document Chunking

YetiSearch supports both automatic and manual (pre-chunked) document chunking:

Automatic Chunking

Large documents are automatically split into smaller chunks for better search performance:

Pre-chunked Documents (Custom Chunking)

NEW: You can provide your own chunks for better semantic boundaries:

Benefits of pre-chunked documents:

See examples/pre-chunked-indexing.php for a complete example.

Writing a document again replaces its chunks: the chunks of the earlier version that the new one does not have ({id}#chunk{N} for a number N) are removed in the same transaction, so a document that became shorter, or that is no longer chunked, is not found by the words of its old text. Write a document and its chunks in the same call, as the indexer does, because writing a document without a chunk that is already in the index removes that chunk. A batch that names a document more than once writes the last of them and its chunks only. A document and its chunks are written to the index in one transaction, so they are stored together or not at all. A write made while you have a transaction open on the storage's connection joins it with a savepoint: it neither begins nor commits one, and if it fails only its own changes are undone. Content or metadata that cannot be encoded as JSON (NAN, INF, a resource, nesting deeper than 511 levels) is refused with a StorageException before anything of the call is written, a chunk's included. Separate batches are separate transactions: with more than batch_size documents in one call, or with auto_flush off, an earlier batch stays stored if the storage itself fails during a later one.

Field Boosting and Exact Match Scoring

YetiSearch provides intelligent field-weighted scoring with special handling for exact matches in high-priority fields:

How Field Boosting Works:

  1. Basic Boost Values: Each field's boost value multiplies its relevance score
  2. High-Priority Fields (boost ≥ 2.5): Get special exact match handling:

    • Exact field match: +50 point bonus (e.g., searching "Star Wars" finds a movie titled exactly "Star Wars")
    • Near-exact match: +30 point bonus (ignoring punctuation)
    • Length penalty: Shorter exact matches score higher than longer titles containing the phrase
  3. Phrase Matching: Exact phrases get 15x boost over individual word matches

Example:

This intelligent scoring ensures the most relevant results appear first, with exact matches in important fields (like titles or names) getting priority over partial matches in longer text.

Enhanced Result Ranking (v1.0.3):

For more detailed information about scoring and configuration options, see the Field Boosting and Scoring Guide.

For comprehensive fuzzy search documentation, see the Fuzzy Search Guide.

Multi-language Support

Stemming

YetiSearch can find a word by its stem. With stemming on, connect finds "connected" and "connection", runs finds "running", and chanson finds "chansons". Stemming is off by default and is chosen for an index when the index is created:

Both are fixed when the index is created: creating an index that already exists again, with other options, does not change whether or how it stems. An index that does not stem is searched exactly as before.

To ask an index what it does, call stemmingFor(). It returns the canonical name of the language the index stems in (english, french, german, spanish; a language with no stemmer comes back as it was given, in lower case), or null when the index does not stem or does not exist:

It follows rebuildFts(): after rebuildFts('articles', ['stemming' => false]) it returns null.

An index that stems needs the FTS5 option detail at full, its default: a stem with a hyphen in it is searched as a phrase, and column and none keep no positions to search one with. Creating a stemming index with 'fts' => ['detail' => 'column'] (or 'none'), or switching an index that has one with rebuildFts(), throws an InvalidArgumentException.

A document is stemmed in its own language, else the language of the index. A search is stemmed in its language option, else the language of the index. The language option of a search also keeps only the documents indexed with that same language.

Languages are known by name, by code and by locale. The built-in stemmers are:

Language Names
English (default) english, en, eng
French french, fr, fra, francais
German german, de, deu, deutsch
Spanish spanish, es, spa, espanol

A locale such as en_US, en-GB or fr-CA is the language before the underscore or hyphen, unless that locale is registered as it is. Case and surrounding spaces do not matter. Stop words are found the same way: fr and fr_FR use the French list.

A language with no stemmer, such as Italian until you register one, is not stemmed. Its documents get no stems and its words match as typed; they are never stemmed with the English stemmer.

What changes on an index that stems:

The stems are kept in the index, in a _stems column of the FTS table (and of the table that holds the documents, for external content), so a field cannot be named _stems on an index that stems. They are made when a document is written, with the stemmer registered at that time.

To switch an existing index to stemming, or off it, without indexing its documents again, rebuild its full-text index with the new settings. The stems are made from the documents the index stores:

Rebuild an index after you register a different stemmer for its language, as its stems are still the ones the earlier stemmer made.

A rebuild makes the FTS table again with the options it has, its prefix indexes and its detail, read from the table itself, so an index created with its own fts options keeps them.

An external-content index (the default) that YetiSearch 2.5.x ran deleteByIdPrefix() on holds the names of the fields (title, content, route, ...) as words of every document, and 2.6.0 could leave such an index failing with database disk image is malformed when it was searched for one of those words after a delete or an update. YetiSearch 2.6.1 records in the index's {index}_meta table, under fts_built_by, the version that built its FTS table, and the first write to an external-content index without it (index(), indexBatch(), update(), delete(), deleteByIdPrefix()) makes the FTS table again from the stored documents, with the index's own settings and stems, before the write goes on. You do not need to call rebuildFts() after a prefix delete or after upgrading. The rebuild takes about 0.3 seconds per 32,000 documents (1.6 seconds with stemming) and happens once; searches never start it, and a write whose rebuild fails throws and changes nothing. An index created by 2.6.0 has no mark either and is rebuilt once, needlessly but harmlessly.

What stemming costs

Stemming makes the index bigger and indexing slower, as every document is stemmed when it is written, and a search that also matches by stems matches more documents. On the 31,944 movies of benchmarks/movies.json (title, overview and genres in an external-content index, PHP 8.4.25, batches of 250, medians of five fresh processes per configuration), indexing took 2.16 seconds with stemming on and 0.84 seconds with it off, and the database was 35.9 MiB against 23.7 MiB. The benchmark's 45 first-page queries × five repetitions, with caching bypassed, took a median of 1.71 ms with stemming on against 0.86 ms without it; movies then matches 2,990 documents instead of 184. The analyzer remembers the stems of up to 50,000 words (about 6 MiB) only for the four exact built-in stemmer classes; registered custom stemmers and subclasses are called for every word.

A text search with no filter, language, location or sort ranks on the FTS table alone only for the first page (offset 0), then reads that page's documents. A missing document inside the ranked page triggers a fallback to the join; positive offsets always use the join so an orphaned match before the offset cannot shift the page. An SQLite built without its math functions (PHP on Windows, for one) joins the spatial table to every search, so there every search uses the join. Analyzer subclasses keep their overridden stem() and getStopWords() behavior, and a stemmer registered inside a callback applies to the next token.

benchmarks/stemming-benchmark.php repeats these measurements on your own machine, with stemming on or off, and prints the median of several runs:

Registering stemmers

StemmerFactory::register() adds a stemmer for a language that has none, or replaces a built-in one:

The stemmer is a class name implementing StemmerInterface, an instance of one, or a callable that returns one. A callable is called once, the first time the language is needed. Something else, a class that does not exist or one that does not implement the interface, throws an InvalidArgumentException.

Registering the name of a built-in language replaces its stemmer, and its other names keep pointing at it:

An alias that already belongs to another language moves to the one registered. Registration lasts for the process, so register your stemmers where you build YetiSearch, before indexing or searching, on every request.

Custom Stop Words

You can add custom stop words to exclude specific terms from being indexed:

Custom stop words are applied in addition to the default language-specific stop words. They are case-insensitive and apply across all languages.

Stop words are looked up by the language of the text or query, by name, code or locale: french, fr and fr_FR are the same list. English, French, German and Spanish have built-in lists. A language with no list, such as Italian, has no stop words, so nothing is removed from its text until you give it a list. The stop_words option gives a list per language, replacing the built-in list of that language or providing one for a language that has none:

The keys are language names, codes or locales, and the words are case-insensitive. With no language, English is used.

Geo-Spatial Search

YetiSearch supports location-based searching using SQLite's R-tree spatial indexing:

Geo Utilities:

Indexing with Bounds:

Search Result Deduplication

By default, YetiSearch deduplicates results to show only the best matching chunk per document:

Highlighting

Search results can include highlighted matches:

Fuzzy Search

Enable fuzzy matching for typo tolerance:

Advanced Fuzzy Search Algorithms

YetiSearch supports multiple fuzzy matching algorithms for different use cases:

Available Fuzzy Algorithms:

  1. Trigram (Default) - Best overall accuracy and performance

    • Breaks words into 3-character sequences for matching
    • Excellent for most use cases
    • Good balance of speed and accuracy
  2. Jaro-Winkler - Optimized for short strings

    • Great for names, titles, and short text
    • Favors matches with common prefixes
    • Very fast performance
  3. Levenshtein - Edit distance algorithm
    • Counts insertions, deletions, and substitutions
    • Most flexible but requires term indexing
    • Best for handling complex typos

Configuration Options:

Performance Considerations:

Different algorithms have different performance characteristics:

Term indexing is only performed when fuzzy_algorithm is set to 'levenshtein'. For most use cases, 'trigram' provides the best balance of accuracy and performance.

Enhanced Fuzzy Search with Modern Typo Correction

YetiSearch 2.2+ includes enhanced fuzzy search with automatic typo correction, behaving like modern search engines (Google, Elasticsearch):

Multi-Algorithm Consensus Scoring:

The enhanced fuzzy search uses 5 different algorithms and combines their scores:

  1. Trigram Similarity (25%) - Overall character sequence similarity
  2. Levenshtein Distance (20%) - Edit distance (insertions, deletions, substitutions)
  3. Jaro-Winkler Similarity (25%) - Optimized for short strings and prefixes
  4. Phonetic Matching (15%) - Sound-alike typos using Metaphone
  5. Keyboard Proximity (15%) - Fat-finger errors based on QWERTY layout

Typo Correction Examples:

Enhanced "Did You Mean?" Suggestions:

Configuration Options:

The enhanced fuzzy search provides significantly better user experience by automatically correcting common typos while maintaining high precision through consensus scoring.

Understanding Fuzzy Search vs. Spelling Suggestions

YetiSearch provides two distinct but complementary features for handling typos:

1. Fuzzy Search - Returns documents with typo-tolerant matching

2. Spelling Suggestions - Shows "Did you mean?" corrections

Key Configuration:

Critical Requirement: enable_fuzzy must be true for suggestions to work, even if you don't want fuzzy search results. This is because the spelling correction algorithm uses the fuzzy matching infrastructure.

Interaction Matrix:

enable_fuzzy Query fuzzy option enable_suggestions Behavior
true false true Exact search; shows suggestion if 0 results
true true true Fuzzy search; returns results (no suggestion shown)
true true false Fuzzy search; returns results
false N/A true ⚠️ Suggestions won't work - requires enable_fuzzy

Usage Pattern:

Per-Index Default:

You can set the default fuzzy option for an index:

Recommendation: For optimal user experience, do an exact search first (with fuzzy: false). If there are no results and a suggestion is returned, either:

  1. Display "Did you mean: [suggestion]?" to the user
  2. Automatically retry with the suggested term
  3. Automatically retry with fuzzy: true to find similar matches

Query Result Caching

YetiSearch includes built-in query result caching to dramatically improve performance for repeated searches:

Cache Features:

Cache Management:

Performance Impact:

Best Practices:

Multi-Column FTS and Field Weighting

YetiSearch now supports multi-column FTS indexing for superior field weighting and performance:

Benefits of Multi-Column FTS:

Two-Pass Search Strategy

For maximum precision, enable the optional two-pass search:

When to Use Two-Pass Search:

Migration Guide for v2.1.0

Upgrading Existing Indexes

Existing indexes continue to work but won't benefit from multi-column FTS. To upgrade:

Configuration Changes

Update your configuration to use new defaults:

Performance Comparison

Based on A/B testing with real-world data:

Configuration Avg Query Time Relevance Notes
Single-column (legacy) 7.09ms Good Original implementation
Multi-column FTS 6.76ms Excellent Default in v2.1.0
Two-pass search 16.36ms Best Optional for precision
Combined 16.86ms Best Maximum precision

Recommendation: Use the default multi-column FTS for best balance of performance and relevance. Enable two-pass search only when title/heading matches are critical.

Real-World Example: Documentation Search

Here's how the v2.1.0 improvements solve the "scheduler" ranking problem:

Key Improvements Demonstrated:

Performance Optimization Tips:

Algorithm Comparison:

You can compare fuzzy algorithm performance by configuring different algorithms and testing with your data:

Semantic Search (Hybrid)

Keyword search finds documents that contain the words you typed. Semantic search also finds documents that mean the same thing: a search for automobile finds a page about cars, and can't log in finds the page titled "Reset your password". YetiSearch runs both and merges the two rankings with reciprocal rank fusion, so a document that matches on keywords and meaning ranks highest, and one found by meaning alone still makes it into the results.

It is off until you give YetiSearch an embedding provider. Without one, nothing changes: same queries, same results, no network calls.

Providers. OpenAICompatibleEmbeddingProvider speaks the OpenAI /embeddings API, which most embedding services copy. Point base_url elsewhere for the rest:

Service Settings
OpenAI api_key, model: text-embedding-3-small, dimensions: 512
OpenRouter base_url: https://openrouter.ai/api/v1, api_key, model: openai/text-embedding-3-small, dimensions: 512
Ollama (self-hosted) base_url: http://localhost:11434/v1, model: nomic-embed-text, query_prefix: "search_query: ", document_prefix: "search_document: "
Voyage base_url: https://api.voyageai.com/v1, api_key, model: voyage-3.5-lite, input_type: true
Mistral base_url: https://api.mistral.ai/v1, api_key, model: mistral-embed
LM Studio base_url: http://localhost:1234/v1, model: <loaded model>

Anything else can implement YetiSearch\Contracts\EmbeddingProviderInterface: embed(array $texts, string $purpose), dimensions() and modelId(). $purpose is PURPOSE_QUERY or PURPOSE_DOCUMENT for models that embed the two differently.

Semantic search needs an embedding model somewhere. On shared PHP hosting that means a hosted API; running a model inside PHP isn't practical. Ollama or another local server is the self-hosted route.

What gets embedded. Each document row is embedded as its title and content fields, as plain text, cut to 6000 characters. Long documents are chunked by the indexer, and their chunks are embedded instead of the whole document. A hash of the text is stored with each vector, so embedPending() only sends documents whose text changed, and re-indexing unchanged content costs nothing. Changing the model or its dimensions (anything in modelId()) marks every document as pending; until they are re-embedded, searches on that index use keywords only rather than compare vectors from two different models.

Filters and fallbacks. The vector side applies the same filters and language as the keyword side. Geo searches and searches sorted by anything other than relevance keep their own ordering and skip semantic ranking. If the provider fails or times out (query_timeout, 5 seconds by default), the search falls back to keywords, logs a warning, and returns 'semantic' => false. Query embeddings are cached in the database, so a repeated search makes no provider call. Pass 'semantic' => false in the search options to turn it off for one query, or 'semantic_weight' to change the balance for one query.

Options (under 'semantic' in the config, or as the second argument to setEmbeddingProvider()):

Option Default What it does
provider null An EmbeddingProviderInterface. Semantic search is off without one.
weight 0.5 Share of the ranking from meaning: 0 is keyword search, 1 is semantic alone.
min_similarity 0.25 Documents less similar than this never enter the results on meaning alone. Depends on the model.
min_margin 0.15 How far the best match must stand above the median document before meaning adds anything. Stops a query that means nothing in particular (asdf) from returning whatever happens to be closest. Applies from 10 documents up. With calibration: auto, a measured value replaces it once there is one (see "Noise calibration" below).
relative_similarity 0.6 Documents must be at least this share as similar as the best match, which trims loosely related results.
candidates 100 Results each side contributes before fusion.
rrf_k 60 Reciprocal rank fusion constant.
fields ['title', 'content'] Document fields that make up the embedded text.
max_chars 6000 Characters of text embedded per document.
batch_size 32 Documents per provider call in embedPending().
query_cache_size 5000 Query embeddings kept in the database.
calibration 'auto' 'auto' gates on the min_margin measured for the index while that measurement still describes it; 'off' always uses the configured min_margin.
calibration_strictness 2.0 Standard deviations above the mean probe margin at which a calibrated gate sits. Lower keeps more borderline real searches and lets more nonsense through. Changing it needs no new calibration.
gate_filters [] Filters that pick the documents the noise gate compares a query with, such as [['field' => 'type', 'value' => 'card']]. Every document is still ranked. Empty means all of them.
query_frame null A sentence a query is put in before it is embedded, holding {query}, such as 'a {query}'. Documents are not framed.
calibration_store null A YetiSearch\Semantic\CalibrationStore to keep calibrations and probe vectors in, instead of the index's own database.

Other calls: embeddingStats($index) returns total, embedded, pending and model; clearEmbeddings($index) forgets every vector (and the index's noise calibration) so the next run starts over.

Noise calibration

min_margin asks how far the best match stands above the median document, and what nonsense scores on that question depends on the index as much as on the model: the more documents there are, the more likely one of them lands near any string at all. Measured with text-embedding-3-small at 512 dimensions:

Index Nonsense margin Real query margin Calibrated min_margin
17 product cards up to 0.060 0.141 and up 0.119
4,415 product cards 0.124 to 0.196 0.200 to 0.362 0.200
1,013 documentation pages and chunks 0.12 to 0.35 0.16 to 0.52 0.231

No single number serves all of them, so each index measures its own. calibrate() embeds a fixed set of 88 probes (made-up words, keyboard mashes, placeholder Latin, everyday phrases nobody searches for) exactly as a search is embedded, asks each the gate's question against the index, and sets min_margin at the mean probe margin plus two standard deviations. Only the margin is calibrated; gating on the best similarity turned away real queries.

A probe that keywords find in the index is left out of the measurement: just testing is not noise on a site full of test pages, and a search that keywords answer does not depend on the gate anyway. On the documentation site above, 13 probes were keyword hits (just testing, placeholder text, error 404 and some everyday phrases), and leaving them out moved the cutoff from 0.244 to 0.231, enough for logo branding (0.236) to find the design services page while asdf, qwerty, xyzzy, zxcv and hjkl still find nothing. On the two stores 5 probes were keyword hits and the cutoff barely moved. excluded() lists them. If fewer than 44 probes would be left, every probe is measured.

Two standard deviations is a default, not a law. Where real searches and nonsense overlap (on the documentation site, frobnitz stands 0.293 above the median and the real search case studies 0.162), no cutoff keeps every real search and blocks every piece of nonsense. calibration_strictness sets how many deviations to use: 1.5 on that site gives 0.211 and still blocks the nonsense above, while on the 4,415-product store it lets qwerty through. The gate works it out from the stored probe margins, so changing it takes effect on the next search. The median plus a multiple of the median absolute deviation and the 90th percentile were both tried on all three indexes, and neither separated real searches from nonsense as well.

The probes' vectors are cached per model and query frame in yetisearch_probe_embeddings, apart from the query cache so its eviction never drops them, and shared by every index in the database. Only the first calibration with a model calls the provider (one request of 88 short strings); every one after that is free.

A calibration is stored per index in yetisearch_calibration, and the gate uses it while it was measured with the current model id, dimensions, query frame, gate_filters and probe set, over a number of documents within 25% of today's. Otherwise, and always with calibration: 'off', the gate uses the configured min_margin, exactly as before. Changing the model, clearEmbeddings() and dropIndex() all invalidate it, and the next embedPending() run that finishes the index measures it again. The 25% is loose on purpose: the noise ceiling grows slowly with the number of documents (from 0.119 to 0.200 across a 260-fold difference above), so a quarter more documents moves it by far less than the gap between noise and real queries, and a store that adds a few products a day is not re-measured every day.

A calibrated min_margin is the noise ceiling, so a query must stand above it; a configured one is a floor to reach, as it always was.

Measuring the gate over a subset

Where some documents say what a thing is and others are long prose (a product's short card beside its full description, a knowledge-base article's summary beside its chunks), the prose can sit close to nonsense and pull the median around. gate_filters has the gate take its best match and median over the documents that pass those filters alone, while every document is still ranked. Both come out of the same pass over the vectors. With a subset, its best match must also clear min_similarity. When no candidate of a search is in the subset, the margin does not apply, as below 10 documents.

calibrate() measures over the same subset, and a calibration only applies to the gate_filters it was measured with.

Query frame

A one-word query embedded on its own is read as a word, not a thing. On a 17-product demo store, hat scored 0.20 against the cap's document, fifth of seventeen; a hat scored 0.39, first. query_frame puts every query in a sentence before it is embedded:

Only queries are framed, so changing the frame never re-embeds a document. The query cache, the probe cache and the calibration are all keyed on the frame, so a vector framed one way is never compared as if it were framed another.

Meaning-only documents

A document marked meaning_only is embedded and ranked by meaning, and never found by keywords: it gets no full-text entry, takes no part in BM25 statistics and never matches a word. Use it for text written for the model rather than for readers, such as a short card that says what a product is:

The flag is stored as _meaning_only in the document's metadata, is inherited by its chunks, and survives rebuildFts() and deleteByIdPrefix(). Index the document again without the flag and keywords find it again. With unique_by_route, a card and its product share a route and come back as one result.

Scale. Vectors are stored in the index's own SQLite file ({index}_vectors), and a search compares the query with every candidate vector in PHP. On an M-series Mac with PHP 8.4 that takes about 110 ms for 10,000 chunks at 512 dimensions, 55 ms at 256, and grows linearly from there. Filters cut the work in proportion. Past a few tens of thousands of chunks, use fewer dimensions or keep semantic search off for that index.

Faceted Search

Get counts for categories, tags, price bands, etc:

A facet counts a content field or a metadata field of the matching documents. Its name is the key it comes back under, and field counts a different field than the name (price_band above counts price).

Facets are counted over the first 1,000 matching documents. When no matching document has the facet's field at all, the search logs a notice naming the field, which usually means a misspelled field or a missing field option.

Architecture

See the architecture overview diagram and component notes in docs/architecture-overview.md.

Geo Search

YetiSearch supports location filtering and sorting with SQLite R-tree and accurate distances:

Units

Example (PHP):

Note

Without R-Tree

R-Tree is an optional SQLite module, and some PHP builds ship a SQLite without it. YetiSearch checks for it when it first needs it. If it is missing, the index's <index>_spatial table is a plain table with the same minimum and maximum latitude and longitude columns, and the same queries run against it. Your code does not change: near(), within(), sortByDistance(), the distance facet and nearest take the same arguments and return the same documents, in both storage schemas (external_content on or off).

To see whether your PHP has R-Tree, run php scripts/check_sqlite_features.php. Look for RTREE: OK under Runtime Probes (the compile options line reads RTREE=yes or RTREE=no). If it says FAIL, you are on the plain table.

How near() works, with or without R-Tree:

What differs without R-Tree:

Without SQL math functions

Computing a haversine distance in SQL needs SQLite's math functions (sin, cos, asin and so on), which are a compile-time option of SQLite and arrived in version 3.35. If your SQLite lacks them, with R-Tree or without, YetiSearch keeps the box in SQL and runs the radius test in PHP on the rows that come back. Results are still exact: near() returns only documents inside the radius, with the right distance, nearest measures and sorts its candidates in PHP before it takes the k nearest, and within() is unchanged. The Windows builds of PHP we test on have no math functions, so this is the path they take. Two things differ:

Global Units & Composite Scoring

Guidance

Distance Facets

Bucket results by distance from a point to power UI filters.

Documents with no geo are left out of the buckets.

k‑Nearest Neighbors (k‑NN)

Return the k nearest documents by distance, optionally clamped by max distance:

The same on a SearchQuery is (new SearchQuery(''))->sortByDistance($point)->nearest(5)->maxDistance(10)->geoUnits('km'). The total it reports is not capped at k: it counts every matching document within max_distance.

YetiSearch follows a modular architecture with clear separation of concerns:

Key Components

Testing

YetiSearch includes comprehensive test coverage. Run tests using various commands:

Basic Testing

Coverage Reports

Filtered Testing

Advanced Testing

Static Analysis

API Reference

YetiSearch Class

Document Structure

Documents are represented as associative arrays with the following structure:

Content vs Metadata

Understanding the distinction between content and metadata fields:

Content Fields:

Metadata Fields:

When to use metadata:

This separation improves performance (less data to index), prevents false matches (searching "42" won't find products with 42 in stock), and keeps your search index focused on actual searchable content.

SearchQuery Model

Result Structure

Search results are returned as an associative array:

Performance Tips

  1. Index Configuration

    • Use appropriate field boosts - don't over-boost
    • Only index fields you need to search
    • Use metadata for non-searchable data
    • Configure reasonable chunk sizes (default 1000 chars works well)
  2. Search Optimization

    • Use field-specific searches when possible: inFields(['title'])
    • Enable unique_by_route (default) to avoid duplicate documents
    • Use filters instead of text queries for exact matches
    • Limit results with reasonable page sizes
  3. Storage Optimization
    • Run optimize() periodically on large indexes
    • Use WAL mode for better concurrency (default)
    • Consider separate indexes for different content types

Error Handling

Performance

YetiSearch is designed for high performance with minimal resource usage. Here are real-world benchmarks and performance characteristics.

Benchmark Results

Tested on M4 MacBook Pro with PHP 8.3, using a dataset of 32,000 movies:

Indexing Performance

Operation Performance Details
Document Indexing ~4,360 docs/sec Without fuzzy term indexing
With Levenshtein ~1,770 docs/sec With term indexing for fuzzy search
Batch Processing 250 docs/batch Optimal batch size
Memory Usage ~60MB For 32k documents

Search Performance

Query Type Response Time Details
Simple Search 2-5ms Single term, no fuzzy
Phrase Search 3-8ms Multi-word queries
Fuzzy Search (Trigram) 5-15ms Default algorithm
Fuzzy Search (Levenshtein) 10-30ms Most accurate
Complex Queries 15-50ms With filters, facets, geo

Real-World Example

From the movie database benchmark:

Performance Characteristics

1. Linear Scalability

2. Memory Efficiency

3. Disk I/O Optimization

Performance Tuning

For Maximum Indexing Speed

For Fastest Searches

For Best Accuracy

Bottlenecks and Solutions

Bottleneck Impact Solution
Large documents Slow indexing Increase chunk_size
Many small documents I/O overhead Increase batch_size
Complex queries Slow searches Add specific indexes
Fuzzy search CPU intensive Use trigram or basic algorithm
High concurrency Lock contention Enable WAL mode

Comparison with Other Solutions

Feature YetiSearch Elasticsearch MeiliSearch TNTSearch
Setup Time < 1 min 10-30 min 5-10 min < 1 min
Memory Usage 50-200MB 1-4GB 200MB-1GB 100-500MB
Dependencies PHP only Java + Service Binary/Docker PHP only
Index Speed 4,500/sec 10,000/sec 5,000/sec 2,000/sec
Search Speed 1-30ms 5-50ms 10-100ms 5-40ms

Best Practices for Performance

  1. Index Design

    • Create separate indexes for different content types
    • Use appropriate field boosts
    • Only index searchable content
  2. Query Optimization

    • Use field-specific searches when possible
    • Limit results appropriately
    • Enable result caching for repeated queries
  3. Maintenance

    • Run optimize() during low-traffic periods
    • Monitor index size and split if needed
    • Clear old cache entries periodically
  4. Hardware Considerations
    • SSD storage recommended for large indexes
    • More RAM allows larger caches
    • Multi-core CPUs benefit batch operations

Type-Ahead Setup

For as-you-type search, enable fuzzy matching and (optionally) last-token prefixing. Debounce input by 200–300ms on the client.

CLI Demo

Try an interactive demonstration that seeds a small dataset, prints suggestions, and shows as‑you‑type results:

Or run a single query:

Notes

Weighted FTS and Prefix (Optional)

You can enable multi-column FTS5 and weighted BM25 to boost important fields (e.g., title, tags). Prefix indexing improves strict prefix matches for type-ahead.

Migration helper:

Suggestions

Use suggest(index, term, options) to power a dropdown for type‑ahead. Suggestions are ranked by frequency across fuzzy variants and boosted when the title contains or starts with the variant.

Tips

Synonyms

Enable query‑time synonyms expansion to improve recall for known aliases and abbreviations.

Config (array or JSON file):

Behavior

DSL (Domain Specific Language)

YetiSearch now supports a powerful DSL for building complex queries with multiple syntaxes. For comprehensive documentation with migration guide and advanced examples, see docs/DSL.md.

Natural Language Query Syntax

Write queries using SQL-like syntax:

JSON API-Compliant URL Parameters

Support for standard REST API query patterns:

Example URL: ?filter[category][eq]=tech&filter[tags][in]=go,php&sort=-date&page[limit]=10

Fluent PHP Interface

Build queries programmatically:

DSL Features

Metadata Fields

YetiSearch distinguishes between content (searchable text) and metadata (filterable attributes):

Common metadata fields like author, status, price, views, etc. are automatically recognized. See docs/DSL.md for complete documentation.

CLI

A simple CLI is included for quick testing of search, suggestions, geo nearest, and distance facets.

Install deps if you haven't:

Examples

Synonyms example

Common flags

Future Feature Ideas

The following features are ideas for future releases:

Index Management Enhancements

Language and Analysis

Search Enhancements

Performance and Scalability

Integration Features

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Run tests (composer test:verbose)
  4. Commit your changes (git commit -m 'Add amazing feature')
  5. Push to the branch (git push origin feature/amazing-feature)
  6. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Credits

YetiSearch is maintained by the YetiSearch Team and contributors.

Special thanks to:


All versions of yetisearch with dependencies

PHP Build Version
Package Version
Requires php Version ^7.4|^8.0|^8.1|^8.2|^8.3|^8.4
ext-json Version *
ext-mbstring Version *
ext-pdo Version *
ext-sqlite3 Version *
lib-pdo_sqlite-sqlite Version >=3.35.0
psr/log Version ^1.1 || ^2.0 || ^3.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package yetidevworks/yetisearch contains the following files

Loading the files please wait ...