From a5da4c3307f31a89ff85b5fcb7f8ce3965bde893 Mon Sep 17 00:00:00 2001 From: woxxy Date: Sat, 28 Feb 2026 01:57:47 +0000 Subject: [PATCH] docs: fully refresh docs with extensive real examples --- README.md | 815 ++++++++------------------ docs/conf.py | 2 +- docs/config.rst | 69 ++- docs/contribute.rst | 33 +- docs/features/facet.rst | 87 +++ docs/features/match-builder.rst | 74 +++ docs/features/multi-query-builder.rst | 73 ++- docs/features/percolate.rst | 83 +++ docs/helper.rst | 274 ++++++--- docs/index.rst | 35 +- docs/intro.rst | 79 ++- docs/migrating.rst | 69 +++ docs/query-builder.rst | 432 ++++++++++++-- 13 files changed, 1354 insertions(+), 771 deletions(-) create mode 100644 docs/features/match-builder.rst create mode 100644 docs/features/percolate.rst create mode 100644 docs/migrating.rst diff --git a/README.md b/README.md index ed514f1b..25340f35 100755 --- a/README.md +++ b/README.md @@ -1,691 +1,382 @@ -Query Builder for SphinxQL -========================== +# SphinxQL Query Builder [![CI](https://github.com/FoolCode/SphinxQL-Query-Builder/actions/workflows/ci.yml/badge.svg)](https://github.com/FoolCode/SphinxQL-Query-Builder/actions/workflows/ci.yml) [![Documentation](https://github.com/FoolCode/SphinxQL-Query-Builder/actions/workflows/docs.yml/badge.svg)](https://github.com/FoolCode/SphinxQL-Query-Builder/actions/workflows/docs.yml) [![Latest Stable Version](https://poser.pugx.org/foolz/sphinxql-query-builder/v/stable)](https://packagist.org/packages/foolz/sphinxql-query-builder) -[![Latest Unstable Version](https://poser.pugx.org/foolz/sphinxql-query-builder/v/unstable)](https://packagist.org/packages/foolz/sphinxql-query-builder) [![Total Downloads](https://poser.pugx.org/foolz/sphinxql-query-builder/downloads)](https://packagist.org/packages/foolz/sphinxql-query-builder) -## About +A fluent PHP query builder for SphinxQL and ManticoreQL. -This is a query builder for SphinxQL/ManticoreQL, the SQL dialect used by -Sphinx Search and Manticore Search. It maps most common query-builder use cases -and supports both `mysqli` and `PDO` drivers. +It supports: -This Query Builder has no dependencies except PHP 8.2 or later, `\MySQLi` extension, `PDO`, and [Sphinx](http://sphinxsearch.com)/[Manticore](https://manticoresearch.com). +- `SELECT`, `INSERT`, `REPLACE`, `UPDATE`, `DELETE` +- `MATCH()` building (including `MatchBuilder`) +- `FACET` queries +- batched/multi-queries +- helper commands (`SHOW`, `CALL`, maintenance operations) +- percolate workflows for Manticore +- both `mysqli` and `PDO` drivers -### Missing methods? - -SphinxQL and ManticoreQL evolve fast. This library provides fluent builders for -core query composition and helper wrappers for common operational commands. - -If any feature is still unreachable through this library, open an issue or send -a pull request. - -## Code Quality - -The majority of the methods in the package have been unit tested. - -Helper methods and engine compatibility scenarios are covered by the test suite. - -## Documentation - -The docs are built with modern Sphinx + Furo styling. - -Build locally: +## Installation ```bash -python3 -m pip install -r docs/requirements.txt -sphinx-build --fail-on-warning --keep-going -b html docs docs/_build/html +composer require foolz/sphinxql-query-builder ``` -CI builds docs for pull requests and deploys the rendered site to GitHub Pages -on pushes to `master`. - -## How to Contribute - -### Pull Requests - -1. Fork the SphinxQL Query Builder repository -2. Create a new branch for each feature or improvement -3. Submit a pull request from each branch to the repository default branch - -It is very important to separate new features or improvements into separate feature branches, and to send a pull -request for each branch. This allows me to review and pull in new features or improvements individually. - -### Style Guide +Requirements: -All pull requests should follow PSR-12-compatible formatting. +- PHP 8.2+ +- `mysqli` or `pdo_mysql` +- Running Sphinx Search or Manticore Search server -### Unit Testing - -All pull requests must be accompanied by passing unit tests and complete code coverage. The SphinxQL Query Builder uses -`phpunit` for testing. - -[Learn about PHPUnit](https://github.com/sebastianbergmann/phpunit/) - -## Installation - -This is a Composer package. You can install this package with the following command: `composer require foolz/sphinxql-query-builder` - -## Usage - -The following examples will omit the namespace. +## Quick Start ```php setParams(array('host' => 'domain.tld', 'port' => 9306)); - -$query = (new SphinxQL($conn))->select('column_one', 'colume_two') - ->from('index_ancient', 'index_main', 'index_delta') - ->match('comment', 'my opinion is superior to yours') - ->where('banned', '=', 1); +$conn->setParams([ + 'host' => '127.0.0.1', + 'port' => 9306, +]); -$result = $query->execute(); +$rows = (new SphinxQL($conn)) + ->select('id', 'gid', 'title') + ->from('rt') + ->match('title', 'vacation') + ->where('gid', '>', 300) + ->orderBy('id', 'DESC') + ->limit(5) + ->execute() + ->getStored(); ``` -### Drivers - -We support the following database connection drivers: - -* Foolz\SphinxQL\Drivers\Mysqli\Connection -* Foolz\SphinxQL\Drivers\Pdo\Connection - -### Engine Compatibility Matrix - -| Engine | Query Builder | Helper APIs | Notes | -| --- | --- | --- | --- | -| Sphinx 2.x | Supported | Supported | Full CI lane | -| Sphinx 3.x | Supported | Supported with engine-specific assertions | Full CI lane | -| Manticore | Supported | Supported + Percolate | Full CI lane | - -Detailed feature-level support is tracked in [`docs/feature-matrix.yml`](docs/feature-matrix.yml). - -### Migration to 4.0 - -See [`MIGRATING-4.0.md`](MIGRATING-4.0.md) for the complete migration checklist, -including strict runtime validation behavior introduced in the 4.0 line. - -### Connection - -* __$conn = new Connection()__ - - Create a new Connection instance to be used with the following methods or SphinxQL class. - -* __$conn->setParams($params = array('host' => '127.0.0.1', 'port' => 9306))__ - - Sets the connection parameters used to establish a connection to the server. Supported parameters: 'host', 'port', 'socket', 'username', 'password', 'options'. - -* __$conn->query($query)__ - - Performs the query on the server. Returns a [`ResultSet`](#resultset) object containing the query results. - -_More methods are available in the Connection class, but usually not necessary as these are handled automatically._ - -### SphinxQL - -* __new SphinxQL($conn)__ - - Creates a SphinxQL instance used for generating queries. - -#### Bypass Query Escaping - -Often, you would need to call and run SQL functions that shouldn't be escaped in the query. You can bypass the query escape by wrapping the query in an `\Expression`. - -* __SphinxQL::expr($string)__ - - Returns the string without being escaped. - -#### Query Escaping - -There are cases when an input __must__ be escaped in the SQL statement. SQL value escaping is handled by the active connection object: - -* __$conn->escape($value)__ - - Returns the escaped value. This is processed with the `\MySQLi::real_escape_string()` function. - -* __$conn->quote($value)__ - - Adds quotes to the value and escapes it. For array elements, use `$conn->quoteArr($arr)`. - -`SphinxQL` itself exposes MATCH helpers: +## Connection Setup -* __$sq->escapeMatch($value)__ +### `mysqli` driver - Escapes the string to be used in `MATCH`. - -* __$sq->halfEscapeMatch($value)__ - - Escapes the string to be used in `MATCH`. The following characters are allowed: `-`, `|`, and `"`. - - _Refer to `$sq->match()` for more information._ - -There is no dedicated `quoteIdentifier()` helper; pass only trusted index/column identifiers. - -#### Strict Validation in 4.0 - -4.0 performs fail-fast validation for invalid query-shape input. Examples: - -* invalid `setType()` values -* invalid `ORDER BY` direction values -* negative `limit()`/`offset()` -* invalid value shapes for `IN`/`BETWEEN` - -#### SELECT +```php +select($column1, $column2, ...)->from($index1, $index2, ...)__ +use Foolz\SphinxQL\Drivers\Mysqli\Connection; - Begins a `SELECT` query statement. If no column is specified, the statement defaults to using `*`. Both `$column1` and `$index1` can be arrays. +$conn = new Connection(); +$conn->setParams([ + 'host' => '127.0.0.1', + 'port' => 9306, + 'options' => [ + MYSQLI_OPT_CONNECT_TIMEOUT => 2, + ], +]); +``` -#### INSERT, REPLACE +### `PDO` driver -This will return an `INT` with the number of rows affected. +```php +insert()->into($index)__ +use Foolz\SphinxQL\Drivers\Pdo\Connection; - Begins an `INSERT`. +$conn = new Connection(); +$conn->setParams([ + 'host' => '127.0.0.1', + 'port' => 9306, + 'charset' => 'utf8', +]); +``` -* __$sq = (new SphinxQL($conn))->replace()->into($index)__ +## Query Builder Examples - Begins an `REPLACE`. +### Compile SQL before executing -* __$sq->set($associative_array)__ +```php +value($column1, $value1)->value($column2, $value2)->value($column3, $value3)__ +$sql = (new SphinxQL($conn)) + ->select('a.id') + ->from('rt a') + ->leftJoin('rt b', 'a.id', '=', 'b.id') + ->where('a.id', '>', 1) + ->compile() + ->getCompiled(); - Sets the value of each column individually. +// SELECT a.id FROM rt a LEFT JOIN rt b ON a.id = b.id WHERE a.id > 1 +``` -* __$sq->columns($column1, $column2, $column3)->values($value1, $value2, $value3)->values($value11, $value22, $value33)__ +### Insert rows - Allows the insertion of multiple arrays of values in the specified columns. +```php +insert() + ->into('rt') + ->columns('id', 'gid', 'title', 'content') + ->values(10, 9003, 'modifying the same line again', 'because i am that lazy') + ->values(11, 201, 'replacing value by value', 'i have no idea who would use this directly') + ->execute(); +``` -MVA attributes are inserted/updated by passing arrays as values: +### Replace rows ```php replace() ->into('rt') - ->set(array( - 'id' => 123, - 'title' => 'example', - 'rubrics' => array(10, 20), - 'districts' => array(1, 3, 5), - )) + ->set([ + 'id' => 10, + 'gid' => 9002, + 'title' => 'modified', + 'content' => 'this field was modified with replace', + ]) ->execute(); ``` -#### UPDATE - -This will return an `INT` with the number of rows affected. - -* __$sq = (new SphinxQL($conn))->update($index = null)__ - - Begins an `UPDATE`. You can pass the index immediately or set it later with `->into($index)`. - -* __$sq->value($column1, $value1)->value($column2, $value2)__ - - Updates the selected columns with the respective value. - -* __$sq->set($associative_array)__ - - Inserts the associative array, where the keys are the columns and the respective values are the column values. - -#### DELETE - -Will return an array with an `INT` as first member, the number of rows deleted. - -* __$sq = (new SphinxQL($conn))->delete()->from($index)->where(...)__ - - Begins a `DELETE`. - -#### WHERE - -* __$sq->where($column, $operator, $value)__ - - Standard WHERE, extended to work with Sphinx filters and full-text. - - ```php - where('column', 'value'); - - // WHERE `column` = 'value' - $sq->where('column', '=', 'value'); - - // WHERE `column` >= 'value' - $sq->where('column', '>=', 'value'); - - // WHERE `column` IN ('value1', 'value2', 'value3') - $sq->where('column', 'IN', array('value1', 'value2', 'value3')); - - // WHERE `column` NOT IN ('value1', 'value2', 'value3') - $sq->where('column', 'NOT IN', array('value1', 'value2', 'value3')); - - // WHERE `column` BETWEEN 'value1' AND 'value2' - // WHERE `example` BETWEEN 10 AND 100 - $sq->where('column', 'BETWEEN', array('value1', 'value2')); - ``` - - You can compose grouped boolean filters with: - `orWhere()`, `whereOpen()`, `orWhereOpen()`, and `whereClose()`. - The same grouped API exists for `HAVING` via `having()`, `orHaving()`, - `havingOpen()`, `orHavingOpen()`, and `havingClose()`. - Repeated `having()` calls are additive (`AND`) unless you explicitly use - `orHaving()` or grouped clauses. - -#### MATCH - -* __$sq->match($column, $value, $half = false)__ - - Search in full-text fields. Can be used multiple times in the same query. Column can be an array. Value can be an Expression to bypass escaping (and use your own custom solution). - - ```php - match('title', 'Otoshimono') - ->match('character', 'Nymph') - ->match(array('hates', 'despises'), 'Oregano'); - ``` - - By default, all inputs are escaped. The usage of `SphinxQL::expr($value)` is required to bypass the default escaping and quoting function. - - The `$half` argument, if set to `true`, will not escape and allow the usage of the following characters: `-`, `|`, `"`. If you plan to use this feature and expose it to public interfaces, it is __recommended__ that you wrap the query in a `try catch` block as the character order may `throw` a query error. +### Update rows (including MVA) - ```php - select() - ->from('rt') - ->match('title', 'Sora no || Otoshimono', true) - ->match('title', SphinxQL::expr('"Otoshimono"/3')) - ->match('loves', SphinxQL::expr(custom_escaping_fn('(you | me)'))); - ->execute(); - } - catch (\Foolz\SphinxQL\DatabaseException $e) - { - // an error is thrown because two `|` one after the other aren't allowed - } - ``` - -#### GROUP, WITHIN GROUP, ORDER, OFFSET, LIMIT, OPTION - -* __$sq->groupBy($column)__ - - `GROUP BY $column` - -* __$sq->withinGroupOrderBy($column, $direction = null)__ - - `WITHIN GROUP ORDER BY $column [$direction]` - - Direction can be omitted with `null`, or be `ASC` or `DESC` case insensitive. - -* __$sq->orderBy($column, $direction = null)__ - - `ORDER BY $column [$direction]` - - Direction can be omitted with `null`, or be `ASC` or `DESC` case insensitive. - -* __$sq->orderByKnn($field, $k, array $vector, $direction = 'ASC')__ - - `ORDER BY KNN($field, $k, $vector) [$direction]` - -#### JOIN - -* __$sq->join($table, $left, $operator, $right, $type = 'INNER')__ -* __$sq->innerJoin($table, $left, $operator, $right)__ -* __$sq->leftJoin($table, $left, $operator, $right)__ -* __$sq->rightJoin($table, $left, $operator, $right)__ -* __$sq->crossJoin($table)__ - -* __$sq->offset($offset)__ - - `LIMIT $offset, 9999999999999` - - Set the offset. Since SphinxQL doesn't support the `OFFSET` keyword, `LIMIT` has been set at an extremely high number. - -* __$sq->limit($limit)__ - - `LIMIT $limit` - -* __$sq->limit($offset, $limit)__ - - `LIMIT $offset, $limit` - -* __$sq->option($name, $value)__ - - `OPTION $name = $value` - - Set a SphinxQL option such as `max_matches` or `reverse_scan` for the query. - -#### TRANSACTION - -* __(new SphinxQL($conn))->transactionBegin()__ - - Begins a transaction. - -* __(new SphinxQL($conn))->transactionCommit()__ - - Commits a transaction. - -* __(new SphinxQL($conn))->transactionRollback()__ - - Rollbacks a transaction. - -#### Executing and Compiling - -* __$sq->execute()__ - - Compiles, executes, and __returns__ a [`ResultSet`](#resultset) object containing the query results. - -* __$sq->executeBatch()__ - - Compiles, executes, and __returns__ a [`MultiResultSet`](#multiresultset) object containing the multi-query results. - -* __$sq->compile()__ - - Compiles the query. - -* __$sq->getCompiled()__ - - Returns the last query compiled. - -* __$sq->getResult()__ - - Returns the [`ResultSet`](#resultset) or [` MultiResultSet`](#multiresultset) object, depending on whether single or multi-query have been executed last. +```php +update('rt') + ->where('id', '=', 15) + ->value('tags', [111, 222]) + ->execute(); +``` -* __$sq->enqueue(SphinxQL $next = null)__ +### Delete rows - Queues the query. If a $next is provided, $next is appended and returned, otherwise a new SphinxQL object is returned. +```php +executeBatch()__ +$affected = (new SphinxQL($conn)) + ->delete() + ->from('rt') + ->where('id', 'IN', [11, 12, 13]) + ->match('content', 'content') + ->execute() + ->getStored(); +``` - Returns a [`MultiResultSet`](#multiresultset) object containing the multi-query results. +### Grouped boolean filters ```php conn)) +$sql = (new SphinxQL($conn)) ->select() ->from('rt') - ->match('title', 'sora') - ->enqueue((new SphinxQL($this->conn))->query('SHOW META')) // this returns the object with SHOW META query - ->enqueue() // this returns a new object - ->select() - ->from('rt') - ->match('content', 'nymph') - ->executeBatch(); + ->where('gid', 200) + ->orWhereOpen() + ->where('gid', 304) + ->where('id', '>', 12) + ->whereClose() + ->compile() + ->getCompiled(); + +// SELECT * FROM rt WHERE gid = 200 OR ( gid = 304 AND id > 12 ) ``` -`$result` will contain [`MultiResultSet`](#multiresultset) object. Sequential calls to the `$result->getNext()` method allow you to get a [`ResultSet`](#resultset) object containing the results of the next enqueued query. +### MATCH with builder callback +```php +fetchAllAssoc()__ - - Fetches all result rows as an associative array. - -* __$result->fetchAllNum()__ - - Fetches all result rows as a numeric array. - -* __$result->fetchAssoc()__ +$rows = (new SphinxQL($conn)) + ->select() + ->from('rt') + ->match(function ($m) { + $m->field('content') + ->match('directly') + ->orMatch('lazy'); + }) + ->execute() + ->getStored(); +``` - Fetch a result row as an associative array. +### ORDER BY KNN -* __$result->fetchNum()__ +```php +select('id') + ->from('rt') + ->orderByKnn('embeddings', 5, [0.1, 0.2, 0.3]) + ->compile() + ->getCompiled(); -* __$result->getAffectedRows()__ +// SELECT id FROM rt ORDER BY KNN(embeddings, 5, [0.1,0.2,0.3]) ASC +``` - Returns the number of affected rows in the case of a DML query. +### Subqueries -##### MultiResultSet +```php +select('id') + ->from('rt') + ->orderBy('id', 'DESC'); -* __$result->getNext()__ - - Returns a [`ResultSet`](#resultset) object containing the results of the next query. +$sql = (new SphinxQL($conn)) + ->select() + ->from($subquery) + ->orderBy('id', 'ASC') + ->compile() + ->getCompiled(); +// SELECT * FROM (SELECT id FROM rt ORDER BY id DESC) ORDER BY id ASC +``` -### Helper +## Helper API Example -The `Helper` class contains useful methods that don't need "query building". +```php +execute()` to get a result. +use Foolz\SphinxQL\Helper; -* __Helper::pairsToAssoc($result)__ +$helper = new Helper($conn); - Takes the pairs from a SHOW command and returns an associative array key=>value +$tables = $helper->showTables()->execute()->getStored(); +$variables = Helper::pairsToAssoc($helper->showVariables()->execute()->getStored()); +$keywords = $helper->callKeywords('test case', 'rt', 1)->execute()->getStored(); +``` -* __$helper->getCapabilities()__ +Compile examples from tests: - Returns a `Capabilities` object with detected engine/version and feature flags. +- `$helper->showTables()->compile()->getCompiled()` -> `SHOW TABLES` +- `$helper->showTables('rt')->compile()->getCompiled()` -> `SHOW TABLES LIKE 'rt'` +- `$helper->showTableStatus()->compile()->getCompiled()` -> `SHOW TABLE STATUS` +- `$helper->showTableStatus('rt')->compile()->getCompiled()` -> `SHOW TABLE rt STATUS` +- `$helper->callSuggest('teh', 'rt', ['limit' => 5])->compile()->getCompiled()` -> `CALL SUGGEST('teh', 'rt', 5 AS limit)` -* __$helper->supports($feature)__ +## FACET Example - Checks whether a named feature is supported by the current backend/runtime. +```php +requireSupport($feature, $context = '')__ +use Foolz\SphinxQL\Facet; +use Foolz\SphinxQL\SphinxQL; - Throws `UnsupportedFeatureException` when the requested feature is not available. +$facet = (new Facet($conn)) + ->facet(['gid']) + ->orderBy('gid', 'ASC'); -`SphinxQL` also exposes capability helpers: +$batchRows = (new SphinxQL($conn)) + ->select() + ->from('rt') + ->facet($facet) + ->executeBatch() + ->getStored(); -* __$sphinxql->getCapabilities()__ -* __$sphinxql->supports($feature)__ -* __$sphinxql->requireSupport($feature, $context = '')__ +// $batchRows[0] is SELECT data +// $batchRows[1] is FACET aggregation data +``` -The following methods return a prepared `SphinxQL` object. You can also use `->enqueue($next_object)`: +## Multi Query / Batch Example ```php conn)) +$batch = (new SphinxQL($conn)) ->select() ->from('rt') ->where('gid', 9003) - ->enqueue((new Helper($this->conn))->showMeta()) // this returns the object with SHOW META query prepared - ->enqueue() // this returns a new object + ->enqueue() ->select() ->from('rt') ->where('gid', 201) + ->enqueue((new Helper($conn))->showMeta()) ->executeBatch(); + +$all = $batch->getStored(); ``` -* `(new Helper($conn))->showMeta() => 'SHOW META'` -* `(new Helper($conn))->showWarnings() => 'SHOW WARNINGS'` -* `(new Helper($conn))->showStatus() => 'SHOW STATUS'` -* `(new Helper($conn))->showProfile() => 'SHOW PROFILE'` -* `(new Helper($conn))->showPlan() => 'SHOW PLAN'` -* `(new Helper($conn))->showThreads() => 'SHOW THREADS'` -* `(new Helper($conn))->showVersion() => 'SHOW VERSION'` -* `(new Helper($conn))->showPlugins() => 'SHOW PLUGINS'` -* `(new Helper($conn))->showAgentStatus() => 'SHOW AGENT STATUS'` -* `(new Helper($conn))->showScroll() => 'SHOW SCROLL'` -* `(new Helper($conn))->showDatabases() => 'SHOW DATABASES'` -* `(new Helper($conn))->showCharacterSet() => 'SHOW CHARACTER SET'` -* `(new Helper($conn))->showCollation() => 'SHOW COLLATION'` -* `(new Helper($conn))->showTables($index = null) => 'SHOW TABLES' (null/empty) or 'SHOW TABLES LIKE '` -* `(new Helper($conn))->showVariables() => 'SHOW VARIABLES'` -* `(new Helper($conn))->showCreateTable($table)` -* `(new Helper($conn))->showTableStatus($table = null) => 'SHOW TABLE STATUS' or 'SHOW TABLE STATUS'` -* `(new Helper($conn))->showTableSettings($table)` -* `(new Helper($conn))->showTableIndexes($table)` -* `(new Helper($conn))->showQueries()` -* `(new Helper($conn))->setVariable($name, $value, $global = false)` -* `(new Helper($conn))->callSnippets($data, $index, $query, $options = array())` -* `(new Helper($conn))->callKeywords($text, $index, $hits = null)` -* `(new Helper($conn))->callSuggest($text, $index, $options = array())` -* `(new Helper($conn))->callQSuggest($text, $index, $options = array())` -* `(new Helper($conn))->callAutocomplete($text, $index, $options = array())` -* `(new Helper($conn))->describe($index)` -* `(new Helper($conn))->createFunction($udf_name, $returns, $soname)` -* `(new Helper($conn))->dropFunction($udf_name)` -* `(new Helper($conn))->attachIndex($disk_index, $rt_index)` -* `(new Helper($conn))->flushRtIndex($index)` -* `(new Helper($conn))->optimizeIndex($index)` -* `(new Helper($conn))->showIndexStatus($index)` -* `(new Helper($conn))->flushRamchunk($index)` -* `(new Helper($conn))->flushAttributes()` -* `(new Helper($conn))->flushHostnames()` -* `(new Helper($conn))->flushLogs()` -* `(new Helper($conn))->reloadPlugins()` -* `(new Helper($conn))->kill($queryId)` - -Suggest-family option contract and capability behavior: - -* `callSuggest()`, `callQSuggest()`, and `callAutocomplete()` accept `$options` as an associative array. -* Option keys must be non-empty strings. Each option is compiled as ` AS `. -* Option values are quoted by the active connection (`quote()`/`quoteArr()`), covering scalar values, `null`, `Expression`, and arrays. -* Repository-tested option keys are `limit` (numeric) and `fuzzy` (numeric, `callAutocomplete()`). -* `callQSuggest()` and `callAutocomplete()` are feature-gated and throw `UnsupportedFeatureException` when unavailable. -* `callSuggest()` is runtime-conditional by backend support; use `$helper->supports('call_suggest')` for portable flows. - -### Percolate - The `Percolate` class provides methods for the "Percolate query" feature of Manticore Search. - For more information about percolate queries refer the [Percolate Query](https://docs.manticoresearch.com/latest/html/searching/percolate_query.html) documentation. - -#### INSERT - -The Percolate class provide a dedicated helper for inserting queries in a `percolate` index. +## Percolate Example (Manticore) ```php insert('full text query terms',false) - ->into('pq') - ->tags(['tag1','tag2']) - ->filter('price>3') - ->execute(); - ``` -* __`$pq = (new Percolate($conn))->insert($query,$noEscape)`__ - - Begins an ``INSERT``. A single query is allowed to be added per insert. By default, the query string is escaped. Optional second parameter `$noEscape` can be set to `true` for not applying the escape. - -* __`$pq->into($index)`__ - - Set the percolate index for insert. - -* __`$pq->tags($tags)`__ - - Set a list of tags per query. Accepts array of strings or string delimited by comma - -* __`$pq->filter($filter)`__ - Sets an attribute filtering string. The string must look the same as string of an WHERE attribute filters clause +use Foolz\SphinxQL\Percolate; -* __`$pq->execute()`__ +(new Percolate($conn)) + ->insert('@subject orange') + ->into('pq') + ->tags(['tag2', 'tag3']) + ->filter('price>3') + ->execute(); - Execute the `INSERT`. +$matches = (new Percolate($conn)) + ->callPQ() + ->from('pq') + ->documents(['{"subject":"document about orange"}']) + ->options([ + Percolate::OPTION_QUERY => 1, + Percolate::OPTION_DOCS => 1, + ]) + ->execute() + ->fetchAllAssoc(); +``` -#### CALLPQ +## Capability Checks - Searches for stored queries that provide matching for input documents. - ```php callPQ() - ->from('pq') - ->documents(['multiple documents', 'go this way']) - ->options([ - Percolate::OPTION_VERBOSE => 1, - Percolate::OPTION_DOCS_JSON => 1 - ]) - ->execute(); - ``` - -* __`$pq = (new Percolate($conn))->callPQ()`__ - Begins a `CALL PQ` +use Foolz\SphinxQL\Helper; -* __`$pq->from($index)`__ +$helper = new Helper($conn); +$caps = $helper->getCapabilities(); - Set percolate index. +if ($helper->supports('call_autocomplete')) { + $rows = $helper->callAutocomplete('te', 'rt', ['fuzzy' => 1])->execute()->getStored(); +} +``` -* __`$pq->documents($docs)`__ +## Result Objects - Set the incoming documents. $docs can be: - - - a single plain string (requires `Percolate::OPTION_DOCS_JSON` set to 0) - - array of plain strings (requires `Percolate::OPTION_DOCS_JSON` set to 0) - - a single JSON document - - an array of JSON documents - - a JSON object containing an array of JSON objects - +`execute()` returns `ResultSetInterface`: -* __`$pq->options($options)`__ +- `getStored()` +- `fetchAllAssoc()` +- `fetchAllNum()` +- `fetchAssoc()` +- `fetchNum()` +- `getAffectedRows()` - Set options of `CALL PQ`. Refer the Manticore docs for more information about the `CALL PQ` parameters. - - - __Percolate::OPTION_DOCS_JSON__ (`as docs_json`) default to 1 (docs are json objects). Needs to be set to 0 for plain string documents. - Documents added as associative arrays will be converted to JSON when sending the query to Manticore. - - __Percolate::OPTION_VERBOSE__ (`as verbose`) more information is printed by following `SHOW META`, default is 0 - - __Percolate::OPTION_QUERY__ (`as query`) returns all stored queries fields , default is 0 - - __Percolate::OPTION_DOCS__ (`as docs`) provide result set as per document matched (instead of per query), default is 0 +`executeBatch()` returns `MultiResultSetInterface`: -* `$pq->execute()` +- `getStored()` +- `getNext()` - Execute the `CALL PQ`. +## Documentation Map -## Laravel +- Main docs index: [`docs/index.rst`](docs/index.rst) +- Builder guide: [`docs/query-builder.rst`](docs/query-builder.rst) +- Helper guide: [`docs/helper.rst`](docs/helper.rst) +- Facets: [`docs/features/facet.rst`](docs/features/facet.rst) +- Multi-query: [`docs/features/multi-query-builder.rst`](docs/features/multi-query-builder.rst) +- Migration guide: [`docs/migrating.rst`](docs/migrating.rst) -Laravel's dependency injection and realtime facades brings more convenience to SphinxQL Query Builder usage. +## Running Tests -```php -// Register connection: -use Foolz\SphinxQL\Drivers\ConnectionInterface; -use Foolz\SphinxQL\Drivers\Mysqli\Connection; -use Illuminate\Support\ServiceProvider; - -class AppServiceProvider extends ServiceProvider -{ - public function register() - { - $this->app->singleton(ConnectionInterface::class, function ($app) { - $conn = new Connection(); - $conn->setParams(['host' => 'domain.tld', 'port' => 9306]); - return $conn; - }); - } -} +```bash +./scripts/run-tests-docker.sh +``` -// In another file: -use Facades\Foolz\SphinxQL\SphinxQL; +This runs the repository test matrix (mysqli + pdo) in Docker. -$result = SphinxQL::select('column_one', 'colume_two') - ->from('index_ancient', 'index_main', 'index_delta') - ->match('comment', 'my opinion is superior to yours') - ->where('banned', '=', 1) - ->execute(); -``` +## Contributing -Facade access also works with `Helper` and `Percolate`. +Pull requests are welcome. Please include tests for behavior changes and keep docs in sync with API updates. diff --git a/docs/conf.py b/docs/conf.py index 8a9f9a33..ea16196d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,7 +5,7 @@ copyright = f"2012-{date.today().year}, {author}" # We track release notes in CHANGELOG.md and do not hardcode package versions here. -version = "4.x" +version = "current" release = version extensions = [ diff --git a/docs/config.rst b/docs/config.rst index 93e7510c..298d6681 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -3,10 +3,10 @@ Configuration ============= -Creating a Connection ---------------------- +Driver Setup +------------ -Use one of the supported drivers: +MySQLi driver: .. code-block:: php @@ -15,12 +15,15 @@ Use one of the supported drivers: use Foolz\SphinxQL\Drivers\Mysqli\Connection; $conn = new Connection(); - $conn->setParams(array( + $conn->setParams([ 'host' => '127.0.0.1', 'port' => 9306, - )); + 'options' => [ + MYSQLI_OPT_CONNECT_TIMEOUT => 2, + ], + ]); -You can also use the PDO driver: +PDO driver: .. code-block:: php @@ -29,25 +32,57 @@ You can also use the PDO driver: use Foolz\SphinxQL\Drivers\Pdo\Connection; $conn = new Connection(); - $conn->setParams(array( + $conn->setParams([ 'host' => '127.0.0.1', 'port' => 9306, - )); + 'charset' => 'utf8', + ]); Connection Parameters --------------------- -``setParams()`` and ``setParam()`` accept: +``setParams()`` and ``setParam()`` support: - ``host`` (string, default ``127.0.0.1``) - ``port`` (int, default ``9306``) -- ``socket`` (string|null, default ``null``) -- ``username`` (string|null, optional) -- ``password`` (string|null, optional) -- ``options`` (array, driver-specific client options) +- ``socket`` (string|null) +- ``username`` (string|null) +- ``password`` (string|null) +- ``charset`` (PDO DSN option) +- ``options`` (array, driver-specific options) + +Notes: + +- Setting ``host`` to ``localhost`` is normalized to ``127.0.0.1``. +- Socket notation like ``unix:/path/to/socket`` is converted to ``socket``. + +Escaping and Quoting +-------------------- + +Value escaping and quoting are connection-driven. + +.. code-block:: php + + quote('hello'); // 'hello' + $quotedInt = $conn->quote(42); // 42 + $quotedNull = $conn->quote(null); // null + $quotedList = $conn->quote([1, 2, 3]); // (1,2,3) + +For raw SQL fragments, use ``SphinxQL::expr()``. + +.. code-block:: php + + select() + ->from('rt') + ->option('field_weights', SphinxQL::expr('(title=80, content=35)')) + ->compile() + ->getCompiled(); -The query builder validates critical inputs at runtime in 4.0. -Prefer explicit values over implicit coercion. + // SELECT * FROM rt OPTION field_weights = (title=80, content=35) diff --git a/docs/contribute.rst b/docs/contribute.rst index bed2e044..142121fa 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -1,31 +1,37 @@ Contribute ========== +Thanks for improving SphinxQL Query Builder. + Pull Requests ------------- -1. Fork `SphinxQL Query Builder `_ -2. Create a new branch for each feature or improvement -3. Submit a pull request with your branch against the default branch +1. Fork `SphinxQL Query Builder `_. +2. Create a branch for your change. +3. Open a pull request against the default branch. -It is very important that you create a new branch for each feature, improvement, or fix so that may review the changes and merge the pull requests in a timely manner. +Please keep each pull request focused on one logical change. -Coding Style ------------- +Development Guidelines +---------------------- -All pull requests should follow modern PHP style conventions (PSR-12 compatible formatting). +- Follow PSR-12 style. +- Add/update tests for behavior changes. +- Update docs when public API behavior changes. Testing ------- -All pull requests must be accompanied with passing tests and code coverage. The SphinxQL Query Builder uses `PHPUnit `_ for testing. +Run the Docker-based matrix used in CI: -Documentation -------------- +.. code-block:: bash + + ./scripts/run-tests-docker.sh -Documentation is built with Sphinx and the Furo theme. +This runs PHPUnit for both mysqli and PDO configurations. -Build locally: +Build Docs Locally +------------------ .. code-block:: bash @@ -35,4 +41,5 @@ Build locally: Issue Tracker ------------- -You can find our issue tracker at our `SphinxQL Query Builder `_ repository. +Use the GitHub issue tracker: +`github.com/FoolCode/SphinxQL-Query-Builder/issues `_. diff --git a/docs/features/facet.rst b/docs/features/facet.rst index a780835a..ac2c5df0 100644 --- a/docs/features/facet.rst +++ b/docs/features/facet.rst @@ -1,2 +1,89 @@ Facets ====== + +``Facet`` builds ``FACET`` clauses for grouped aggregations. + +Building a Facet +---------------- + +.. code-block:: php + + facet(['gid']) + ->orderBy('gid', 'ASC'); + +Common compiled outputs from tests: + +- ``facet(['gid'])`` -> ``FACET gid`` +- ``facet(['gid', 'title', 'content'])`` -> ``FACET gid, title, content`` +- ``facet(['alias' => 'gid'])`` -> ``FACET gid AS alias`` +- ``facetFunction('COUNT', 'gid')`` -> ``FACET COUNT(gid)`` +- ``facetFunction('INTERVAL', ['price', 200, 400, 600, 800])`` -> ``FACET INTERVAL(price,200,400,600,800)`` + +Using FACET with SELECT +----------------------- + +FACET is returned as an extra result set, so use ``executeBatch()``. + +.. code-block:: php + + select() + ->from('rt') + ->facet( + (new Facet($conn)) + ->facet(['gid']) + ->orderBy('gid', 'ASC') + ) + ->executeBatch() + ->getStored(); + + // $batch[0] => SELECT rows + // $batch[1] => FACET rows with gid + count(*) + +Advanced Facet Options +---------------------- + +Add ``BY``: + +.. code-block:: php + + $facet = (new Facet($conn)) + ->facet(['gid', 'title', 'content']) + ->by('gid'); + +Sort by expression: + +.. code-block:: php + + $facet = (new Facet($conn)) + ->facet(['gid', 'title']) + ->orderByFunction('COUNT', '*', 'DESC'); + +Paginate facet rows: + +.. code-block:: php + + $facet = (new Facet($conn)) + ->facet(['gid', 'title']) + ->orderByFunction('COUNT', '*', 'DESC') + ->limit(5, 5); + +Validation Notes +---------------- + +``Facet`` throws ``SphinxQLException`` for invalid inputs such as: + +- empty ``facet()`` columns +- invalid order directions +- invalid ``limit()`` / ``offset()`` +- ``facetFunction()`` without parameters diff --git a/docs/features/match-builder.rst b/docs/features/match-builder.rst new file mode 100644 index 00000000..1b195c80 --- /dev/null +++ b/docs/features/match-builder.rst @@ -0,0 +1,74 @@ +MatchBuilder +============ + +``MatchBuilder`` helps compose advanced ``MATCH()`` expressions. + +Use it directly, or pass a callback to ``SphinxQL::match()``. + +Standalone Build +---------------- + +.. code-block:: php + + field('content') + ->match('directly') + ->orMatch('lazy') + ->compile() + ->getCompiled(); + + // @content directly | lazy + +Inline with Query Builder +------------------------- + +.. code-block:: php + + $rows = (new SphinxQL($conn)) + ->select() + ->from('rt') + ->match(function ($m) { + $m->field('content') + ->match('directly') + ->orMatch('lazy'); + }) + ->execute() + ->getStored(); + +Real Compiled Examples from Tests +--------------------------------- + +- ``match('test case')`` -> ``(test case)`` +- ``match('test')->orMatch('case')`` -> ``test | case`` +- ``phrase('test case')`` -> ``"test case"`` +- ``proximity('test case', 5)`` -> ``"test case"~5`` +- ``quorum('this is a test case', 3)`` -> ``"this is a test case"/3`` +- ``field('body', 50)->match('test')`` -> ``@body[50] test`` +- ``ignoreField('title', 'body')->match('test')`` -> ``@!(title,body) test`` +- ``zone(['h3', 'h4'])`` -> ``ZONE:(h3,h4)`` +- ``zonespan('th', 'test')`` -> ``ZONESPAN:(th) test`` + +Expressions and Escaping +------------------------ + +Raw expression bypass: + +.. code-block:: php + + use Foolz\SphinxQL\SphinxQL; + + $expr = (new MatchBuilder($sq)) + ->match(SphinxQL::expr('test|case')) + ->compile() + ->getCompiled(); + + // test|case + +Without ``Expression``, special characters are escaped. diff --git a/docs/features/multi-query-builder.rst b/docs/features/multi-query-builder.rst index 5e42a246..295115b5 100644 --- a/docs/features/multi-query-builder.rst +++ b/docs/features/multi-query-builder.rst @@ -1,12 +1,77 @@ Multi-Query Builder =================== +Use ``enqueue()`` + ``executeBatch()`` to send multiple statements in one roundtrip. + +Basic Batch +----------- + +.. code-block:: php + + select() + ->from('rt') + ->where('gid', 9003) + ->enqueue() + ->select() + ->from('rt') + ->where('gid', 201) + ->executeBatch(); + + $sets = $batch->getStored(); + // $sets[0] => first SELECT rows + // $sets[1] => second SELECT rows + +Mixing Helper Calls in Batch +---------------------------- + .. code-block:: php - $queryBuilder - ->enqueue(SphinxQL $next = null); + select() + ->from('rt') + ->where('gid', 9003) + ->enqueue() + ->select() + ->from('rt') + ->where('gid', 201) + ->enqueue((new Helper($conn))->showMeta()) + ->executeBatch() + ->getStored(); + + // Tests assert: + // $result[0][0]['id'] == '10' + // $result[1][0]['id'] == '11' + // $result[2][0]['Value'] == '1' + +Queue Behavior +-------------- + +- ``enqueue()`` with no argument returns a new ``SphinxQL`` linked to the current query. +- ``enqueue($next)`` links the current query to the provided ``SphinxQL`` instance. +- ``getQueue()`` returns ordered queued query objects. + +If no query was queued, ``executeBatch()`` throws ``SphinxQLException``. + +Consuming Results with Cursors +------------------------------ + +``MultiResultSetInterface`` supports ``getNext()`` for sequential processing. .. code-block:: php - $queryBuilder - ->executeBatch(); + $multi = $query->executeBatch(); + + while ($set = $multi->getNext()) { + $rows = $set->getStored(); + // process each result set + } diff --git a/docs/features/percolate.rst b/docs/features/percolate.rst new file mode 100644 index 00000000..4e36ec21 --- /dev/null +++ b/docs/features/percolate.rst @@ -0,0 +1,83 @@ +Percolate (Manticore) +===================== + +``Percolate`` supports storing queries and matching incoming documents via ``CALL PQ``. + +Store Queries in a Percolate Index +---------------------------------- + +.. code-block:: php + + insert('@subject orange') + ->into('pq') + ->tags(['tag2', 'tag3']) + ->filter('price>3') + ->execute(); + +Compiled SQL pattern (from tests): + +- ``INSERT INTO pq (query) VALUES ('full text query terms')`` +- ``INSERT INTO pq (query, tags) VALUES ('@subject orange', 'tag2,tag3')`` +- ``INSERT INTO pq (query, filters) VALUES ('catch me', 'price>3')`` +- ``INSERT INTO pq (query, tags, filters) VALUES ('@subject match by field', 'tag2,tag3', 'price>3')`` + +Run ``CALL PQ`` +--------------- + +.. code-block:: php + + callPQ() + ->from('pq') + ->documents(['{"subject":"document about orange"}']) + ->options([ + Percolate::OPTION_QUERY => 1, + Percolate::OPTION_DOCS => 1, + ]) + ->execute() + ->fetchAllAssoc(); + +Document Input Shapes +--------------------- + +Supported ``documents()`` inputs include: + +- plain string +- array of plain strings +- JSON object string +- JSON array of objects string +- array of JSON strings +- associative PHP array (converted to JSON) +- array of associative PHP arrays + +Percolate Options +----------------- + +Constants: + +- ``Percolate::OPTION_DOCS_JSON`` +- ``Percolate::OPTION_DOCS`` +- ``Percolate::OPTION_VERBOSE`` +- ``Percolate::OPTION_QUERY`` + +Each option value must be ``0`` or ``1``. + +Validation Notes +---------------- + +``Percolate`` throws ``SphinxQLException`` for invalid payloads, for example: + +- empty index +- empty query/document +- invalid document shape when JSON mode is enforced +- unknown option names +- option values outside ``0``/``1`` diff --git a/docs/helper.rst b/docs/helper.rst index bbc79342..57916744 100644 --- a/docs/helper.rst +++ b/docs/helper.rst @@ -1,11 +1,7 @@ Helper API ========== -The ``Helper`` class exposes convenience wrappers for SphinxQL statements that -do not need fluent query composition. - -Usage ------ +``Helper`` wraps common statements that are awkward as fluent builders. .. code-block:: php @@ -16,85 +12,209 @@ Usage $helper = new Helper($conn); $rows = $helper->showVariables()->execute()->getStored(); -Available Methods ------------------ - -- ``showMeta()`` -- ``showWarnings()`` -- ``showStatus()`` -- ``showProfile()`` -- ``showPlan()`` -- ``showThreads()`` -- ``showVersion()`` -- ``showPlugins()`` -- ``showAgentStatus()`` -- ``showScroll()`` -- ``showDatabases()`` -- ``showCharacterSet()`` -- ``showCollation()`` -- ``showTables($index = null)`` -- ``showVariables()`` -- ``showCreateTable($table)`` -- ``showTableStatus($table = null)`` -- ``showTableSettings($table)`` -- ``showTableIndexes($table)`` -- ``showQueries()`` -- ``setVariable($name, $value, $global = false)`` -- ``callSnippets($data, $index, $query, array $options = array())`` -- ``callKeywords($text, $index, $hits = null)`` -- ``callSuggest($text, $index, array $options = array())`` -- ``callQSuggest($text, $index, array $options = array())`` -- ``callAutocomplete($text, $index, array $options = array())`` -- ``describe($index)`` -- ``createFunction($udfName, $returns, $soName)`` -- ``dropFunction($udfName)`` -- ``attachIndex($diskIndex, $rtIndex)`` -- ``flushRtIndex($index)`` -- ``truncateRtIndex($index)`` -- ``optimizeIndex($index)`` -- ``showIndexStatus($index)`` -- ``flushRamchunk($index)`` -- ``flushAttributes()`` -- ``flushHostnames()`` -- ``flushLogs()`` -- ``reloadPlugins()`` -- ``kill($queryId)`` -- ``getCapabilities()`` -- ``supports($feature)`` -- ``requireSupport($feature, $context = '')`` - -Filtered SHOW Wrappers +Core Patterns +------------- + +Every helper method returns a ``SphinxQL`` instance, so you can: + +- inspect SQL with ``compile()->getCompiled()`` +- run immediately with ``execute()`` +- enqueue into a batch with ``enqueue()`` + +Example: + +.. code-block:: php + + $sql = $helper->showTables('rt')->compile()->getCompiled(); + // SHOW TABLES LIKE 'rt' + +SHOW Commands +------------- + +Frequently used: + +.. code-block:: php + + $helper->showMeta(); + $helper->showWarnings(); + $helper->showStatus(); + $helper->showVariables(); + $helper->showTables(); + $helper->showCreateTable('rt'); + +Table introspection: + +.. code-block:: php + + $helper->showTableStatus(); + $helper->showTableStatus('rt'); + $helper->showTableSettings('rt'); + $helper->showTableIndexes('rt'); + +Compile outputs from tests: + +- ``showMeta()`` -> ``SHOW META`` +- ``showWarnings()`` -> ``SHOW WARNINGS`` +- ``showStatus()`` -> ``SHOW STATUS`` +- ``showTableStatus()`` -> ``SHOW TABLE STATUS`` +- ``showTableStatus('rt')`` -> ``SHOW TABLE rt STATUS`` +- ``showTableSettings('rt')`` -> ``SHOW TABLE rt SETTINGS`` +- ``showTableIndexes('rt')`` -> ``SHOW TABLE rt INDEXES`` + +Maintenance and Runtime Commands +-------------------------------- + +.. code-block:: php + + $helper->attachIndex('disk', 'rt'); + $helper->flushRtIndex('rt'); + $helper->truncateRtIndex('rt'); + $helper->optimizeIndex('rt'); + $helper->showIndexStatus('rt'); + $helper->flushRamchunk('rt'); + $helper->flushAttributes(); + $helper->flushHostnames(); + $helper->flushLogs(); + $helper->reloadPlugins(); + $helper->kill(123); + +Selected compiled SQL from tests: + +- ``attachIndex('disk', 'rt')`` -> ``ATTACH INDEX disk TO RTINDEX rt`` +- ``flushRtIndex('rt')`` -> ``FLUSH RTINDEX rt`` +- ``optimizeIndex('rt')`` -> ``OPTIMIZE INDEX rt`` +- ``showIndexStatus('rt')`` -> ``SHOW INDEX rt STATUS`` +- ``flushRamchunk('rt')`` -> ``FLUSH RAMCHUNK rt`` +- ``kill(123)`` -> ``KILL 123`` + +CALL Helpers +------------ + +``CALL SNIPPETS``: + +.. code-block:: php + + $snippets = $helper->callSnippets( + 'this is my document text', + 'rt', + 'is', + ['before_match' => '', 'after_match' => ''] + )->execute()->getStored(); + +``CALL KEYWORDS``: + +.. code-block:: php + + $keywords = $helper->callKeywords('test case', 'rt', 1) + ->execute() + ->getStored(); + +Suggest-family methods: + +- ``callSuggest($text, $index, $options = [])`` +- ``callQSuggest($text, $index, $options = [])`` +- ``callAutocomplete($text, $index, $options = [])`` + +Compiled outputs from tests: + +.. code-block:: php + + $helper->callSuggest('teh', 'rt', [ + 'limit' => 5, + 'result_stats' => true, + 'search_mode' => 'WORDS', + ])->compile()->getCompiled(); + + // CALL SUGGEST('teh', 'rt', 5 AS limit, 1 AS result_stats, 'words' AS search_mode) + +.. code-block:: php + + $helper->callQSuggest('teh', 'rt', [ + 'limit' => 3, + 'result_line' => false, + ])->compile()->getCompiled(); + + // CALL QSUGGEST('teh', 'rt', 3 AS limit, 0 AS result_line) + +.. code-block:: php + + $helper->callAutocomplete('te', 'rt', [ + 'fuzzy' => 1, + 'append' => true, + 'preserve' => false, + ])->compile()->getCompiled(); + + // CALL AUTOCOMPLETE('te', 'rt', 1 AS fuzzy, 1 AS append, 0 AS preserve) + +Suggest Option Schemas +---------------------- + +``callSuggest()`` and ``callQSuggest()`` allowed options: + +- ``limit`` (int >= 0) +- ``max_edits`` (int >= 0) +- ``result_stats`` (bool) +- ``delta_len`` (int >= 0) +- ``max_matches`` (int >= 0) +- ``reject`` (bool) +- ``result_line`` (bool) +- ``non_char`` (bool) +- ``sentence`` (bool) +- ``force_bigrams`` (bool) +- ``search_mode`` (``phrase`` or ``words``) + +``callAutocomplete()`` allowed options: + +- ``layouts`` (string) +- ``fuzzy`` (int 0..2) +- ``fuzziness`` (int 0..2) +- ``prepend`` (bool) +- ``append`` (bool) +- ``preserve`` (bool) +- ``expansion_len`` (int >= 0) +- ``force_bigrams`` (bool) + +Capability-Aware Usage ---------------------- -- ``showTables($index = null)`` compiles to: +Use capability checks before engine-dependent calls. - - ``SHOW TABLES`` when ``$index`` is ``null`` or an empty string - - ``SHOW TABLES LIKE `` when ``$index`` is a non-empty string -- ``showTableStatus($table = null)`` compiles to: +.. code-block:: php + + $caps = $helper->getCapabilities(); + + if ($helper->supports('call_autocomplete')) { + $rows = $helper->callAutocomplete('te', 'rt', ['fuzzy' => 1]) + ->execute() + ->getStored(); + } + + // throws UnsupportedFeatureException when unavailable + $helper->requireSupport('call_qsuggest', 'search suggestions'); - - ``SHOW TABLE STATUS`` when ``$table`` is ``null`` - - ``SHOW TABLE
STATUS`` when ``$table`` is a non-empty string +``getCapabilities()`` reports: -Suggest-Family Option Contract ------------------------------- +- engine (``MANTICORE``, ``SPHINX2``, ``SPHINX3``, ``UNKNOWN``) +- version string +- feature map -For ``callSuggest()``, ``callQSuggest()``, and ``callAutocomplete()``: +Pairs Utility +------------- + +Convert key-value rows from SHOW commands into associative arrays. + +.. code-block:: php -- ``$options`` must be an associative array. -- Option keys must be non-empty strings; each option is compiled as - `` AS ``. -- Option values are quoted via the active connection driver - (``quote()``/``quoteArr()``), which supports scalar values, ``null``, - ``Expression``, and arrays. -- Repository-tested option keys are ``limit`` (numeric) and ``fuzzy`` - (numeric, autocomplete). + $pairs = Helper::pairsToAssoc($helper->showVariables()->execute()->getStored()); + $autocommit = (int) ($pairs['autocommit'] ?? 1); -Validation Notes ----------------- +Validation Behavior +------------------- -In 4.0, helper methods validate required identifiers and input shapes and throw -``SphinxQLException`` on invalid arguments. +Helper methods validate required identifiers and option shapes. +Examples that throw ``SphinxQLException``: -``callQSuggest()`` and ``callAutocomplete()`` are feature-gated and may throw -``UnsupportedFeatureException`` when unsupported. ``callSuggest()`` is not -pre-gated; use ``supports('call_suggest')`` when runtime portability is needed. +- empty/invalid index names +- unknown suggest option keys +- invalid option types/ranges +- non-positive ``kill()`` query ID diff --git a/docs/index.rst b/docs/index.rst index d8dcc5b6..f3c01e86 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,17 +1,30 @@ SphinxQL Query Builder ====================== -Modern PHP query builder documentation for SphinxQL and ManticoreQL. +Practical documentation for building and executing SphinxQL/ManticoreQL queries in PHP. .. toctree:: - :caption: Contents - :maxdepth: 2 + :caption: Getting Started + :maxdepth: 2 - intro - changelog/index - config - query-builder - helper - features/multi-query-builder - features/facet - contribute + intro + config + +.. toctree:: + :caption: Guides + :maxdepth: 2 + + query-builder + helper + features/multi-query-builder + features/facet + features/match-builder + features/percolate + +.. toctree:: + :caption: Project + :maxdepth: 2 + + migrating + changelog/index + contribute diff --git a/docs/intro.rst b/docs/intro.rst index c6d53951..43832b74 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -3,36 +3,71 @@ Introduction ============ -SphinxQL Query Builder is a lightweight query builder for SphinxQL and ManticoreQL. -It supports both ``mysqli`` and ``PDO`` connection drivers and focuses on -predictable SQL generation with explicit runtime validation. +SphinxQL Query Builder provides a fluent PHP API for SphinxQL and ManticoreQL. -Compatibility -------------- - -The 4.0 line targets: +It is designed for teams that want: -- PHP 8.2+ -- Sphinx 2.x -- Sphinx 3.x -- Manticore Search +- readable query composition +- safe value quoting/escaping via connection drivers +- testable SQL compilation (`compile()` + `getCompiled()`) +- helper wrappers for common engine commands -Driver support: +Supported drivers: - ``Foolz\\SphinxQL\\Drivers\\Mysqli\\Connection`` - ``Foolz\\SphinxQL\\Drivers\\Pdo\\Connection`` -Runtime Contract ----------------- +Quick Example +------------- + +.. code-block:: php + + setParams([ + 'host' => '127.0.0.1', + 'port' => 9306, + ]); -Starting with 4.0 pre-release hardening, invalid builder input fails fast with -``SphinxQLException`` rather than being silently coerced. + $rows = (new SphinxQL($conn)) + ->select('id', 'gid', 'title') + ->from('rt') + ->match('title', 'vacation') + ->where('gid', '>', 300) + ->orderBy('id', 'DESC') + ->limit(5) + ->execute() + ->getStored(); -Examples: +Compile-First Workflow +---------------------- -- invalid query type in ``setType()`` -- invalid order direction (must be ``ASC`` or ``DESC``) -- negative ``limit()`` / ``offset()`` -- invalid ``WHERE/HAVING`` payload shapes for ``IN`` / ``BETWEEN`` +Compiling queries before execution is useful for debugging and tests. + +.. code-block:: php + + select('a.id') + ->from('rt a') + ->leftJoin('rt b', 'a.id', '=', 'b.id') + ->where('a.id', '>', 1) + ->compile() + ->getCompiled(); + + // SELECT a.id FROM rt a LEFT JOIN rt b ON a.id = b.id WHERE a.id > 1 + +Where To Next +------------- -See the migration guide for complete details. +- :doc:`config` for connection parameters and driver notes +- :doc:`query-builder` for full query API with examples +- :doc:`helper` for SHOW/CALL/maintenance wrappers +- :doc:`features/percolate` for Manticore percolate workflows diff --git a/docs/migrating.rst b/docs/migrating.rst new file mode 100644 index 00000000..6c863214 --- /dev/null +++ b/docs/migrating.rst @@ -0,0 +1,69 @@ +Migrating to 4.0 +================ + +This page summarizes migration from the 3.x line to 4.0. + +Baseline Requirements +--------------------- + +- PHP 8.2+ +- ``mysqli`` or ``pdo_mysql`` extension + +Key Behavioral Changes +---------------------- + +4.0 introduces stricter runtime validation. Invalid query-shape input now throws +``Foolz\\SphinxQL\\Exception\\SphinxQLException`` instead of being coerced. + +Builder validation highlights: + +- unknown ``setType()`` values +- ``compile()`` without selecting a query type +- invalid ``from()`` input +- invalid ``facet()`` payload type +- invalid ``orderBy()`` / ``withinGroupOrderBy()`` direction +- invalid ``limit()`` / ``offset()`` / ``groupNBy()`` values +- invalid ``IN``/``NOT IN``/``BETWEEN`` value shapes +- missing ``into($index)`` for ``update()`` before compile/execute + +Facet validation highlights: + +- empty ``facet()`` +- empty function/params in ``facetFunction()`` and ``orderByFunction()`` +- invalid direction +- invalid ``limit()`` / ``offset()`` values + +Helper validation highlights: + +- required identifiers must be non-empty strings +- stricter ``setVariable()`` and CALL option validation +- helper feature-gated methods can raise ``UnsupportedFeatureException`` + +Percolate validation highlights: + +- stricter payload checks for ``documents()`` and options +- earlier failure for unsupported/invalid document shapes + +Exception Message Prefixes +-------------------------- + +Driver exceptions now include explicit source prefixes, for example: + +- ``[mysqli][connect]...`` +- ``[mysqli][query]...`` +- ``[pdo][connect]...`` +- ``[pdo][query]...`` + +Migration Checklist +------------------- + +1. Validate user input before passing values to builder/helper methods. +2. Replace implicit coercion assumptions with explicit casting in your app layer. +3. Prefer exception class checks over full-message string equality checks. +4. Run integration tests against your target backend (Sphinx 2, Sphinx 3, or Manticore). +5. Add capability checks (``supports()``) for backend-specific helper calls. + +Repository Source +----------------- + +The canonical migration checklist also exists in ``MIGRATING-4.0.md`` at repository root. diff --git a/docs/query-builder.rst b/docs/query-builder.rst index 06c3991c..c989dced 100644 --- a/docs/query-builder.rst +++ b/docs/query-builder.rst @@ -12,111 +12,415 @@ Creating a Builder use Foolz\SphinxQL\SphinxQL; $conn = new Connection(); - $conn->setParams(array('host' => '127.0.0.1', 'port' => 9306)); + $conn->setParams(['host' => '127.0.0.1', 'port' => 9306]); - $queryBuilder = new SphinxQL($conn); + $sq = new SphinxQL($conn); Supported Query Types --------------------- -- ``SELECT`` -- ``INSERT`` -- ``REPLACE`` -- ``UPDATE`` -- ``DELETE`` -- raw query via ``query($sql)`` +- ``select()`` +- ``insert()`` +- ``replace()`` +- ``update()`` +- ``delete()`` +- ``query($sql)`` for raw statements -Compilation and Execution -------------------------- +SELECT +------ + +Basic select: + +.. code-block:: php + + $rows = (new SphinxQL($conn)) + ->select('id', 'gid') + ->from('rt') + ->execute() + ->getStored(); + +No explicit columns defaults to ``*``. + +.. code-block:: php + + $sql = (new SphinxQL($conn)) + ->select() + ->from('rt') + ->compile() + ->getCompiled(); + + // SELECT * FROM rt + +FROM Variants +------------- + +Multiple indexes: + +.. code-block:: php + + $sql = (new SphinxQL($conn)) + ->select('id') + ->from('rt_main', 'rt_delta') + ->compile() + ->getCompiled(); + +Array input: .. code-block:: php - $sql = $queryBuilder + $sql = (new SphinxQL($conn)) ->select('id') + ->from(['rt_main', 'rt_delta']) + ->compile() + ->getCompiled(); + +Subquery as closure: + +.. code-block:: php + + $sql = (new SphinxQL($conn)) + ->select() + ->from(function ($q) { + $q->select('id') + ->from('rt') + ->orderBy('id', 'DESC'); + }) + ->orderBy('id', 'ASC') + ->compile() + ->getCompiled(); + + // SELECT * FROM (SELECT id FROM rt ORDER BY id DESC) ORDER BY id ASC + +MATCH +----- + +Simple full-text match: + +.. code-block:: php + + $rows = (new SphinxQL($conn)) + ->select() + ->from('rt') + ->match('content', 'content') + ->execute() + ->getStored(); + +Multiple ``match()`` calls are combined: + +.. code-block:: php + + $rows = (new SphinxQL($conn)) + ->select() + ->from('rt') + ->match('title', 'value') + ->match('content', 'directly') + ->execute() + ->getStored(); + +Array field list: + +.. code-block:: php + + $rows = (new SphinxQL($conn)) + ->select() + ->from('rt') + ->match(['title', 'content'], 'to') + ->execute() + ->getStored(); + +Half-escape mode (lets operators like ``|`` pass through): + +.. code-block:: php + + $rows = (new SphinxQL($conn)) + ->select() + ->from('rt') + ->match('content', 'directly | lazy', true) + ->execute() + ->getStored(); + +Use ``MatchBuilder`` callback for advanced expressions: + +.. code-block:: php + + $rows = (new SphinxQL($conn)) + ->select() + ->from('rt') + ->match(function ($m) { + $m->field('content') + ->match('directly') + ->orMatch('lazy'); + }) + ->execute() + ->getStored(); + +WHERE +----- + +Supported styles: + +.. code-block:: php + + $sq->where('gid', 304); // gid = 304 + $sq->where('gid', '>', 300); // gid > 300 + $sq->where('id', 'IN', [11, 12, 13]); // id IN (...) + $sq->where('gid', 'BETWEEN', [300, 400]); // gid BETWEEN ... + +Grouped boolean clauses: + +.. code-block:: php + + $sql = (new SphinxQL($conn)) + ->select() + ->from('rt') + ->where('gid', 200) + ->orWhereOpen() + ->where('gid', 304) + ->where('id', '>', 12) + ->whereClose() + ->compile() + ->getCompiled(); + + // SELECT * FROM rt WHERE gid = 200 OR ( gid = 304 AND id > 12 ) + +HAVING +------ + +``HAVING`` mirrors the ``WHERE`` API, including grouping. + +.. code-block:: php + + $sql = (new SphinxQL($conn)) + ->select('gid') + ->from('rt') + ->groupBy('gid') + ->having('gid', '>', 100) + ->orHavingOpen() + ->having('gid', '<', 10) + ->having('gid', '>', 9000) + ->havingClose() + ->compile() + ->getCompiled(); + + // SELECT gid FROM rt GROUP BY gid HAVING gid > 100 OR ( gid < 10 AND gid > 9000 ) + +JOIN +---- + +.. code-block:: php + + $sql = (new SphinxQL($conn)) + ->select('a.id') + ->from('rt a') + ->leftJoin('rt b', 'a.id', '=', 'b.id') + ->where('a.id', '>', 1) + ->compile() + ->getCompiled(); + + // SELECT a.id FROM rt a LEFT JOIN rt b ON a.id = b.id WHERE a.id > 1 + +Cross join: + +.. code-block:: php + + $sql = (new SphinxQL($conn)) + ->select('a.id') + ->from('rt a') + ->crossJoin('rt b') + ->compile() + ->getCompiled(); + + // SELECT a.id FROM rt a CROSS JOIN rt b + +GROUP / ORDER / LIMIT / OPTION +------------------------------ + +.. code-block:: php + + $sql = (new SphinxQL($conn)) + ->select() ->from('rt') + ->groupBy('gid') + ->groupNBy(3) + ->withinGroupOrderBy('id', 'DESC') + ->orderBy('id', 'ASC') + ->limit(0, 20) ->compile() ->getCompiled(); +``orderByKnn()``: + .. code-block:: php - $result = $queryBuilder + $sql = (new SphinxQL($conn)) ->select('id') ->from('rt') + ->orderByKnn('embeddings', 5, [0.1, 0.2, 0.3]) + ->compile() + ->getCompiled(); + + // SELECT id FROM rt ORDER BY KNN(embeddings, 5, [0.1,0.2,0.3]) ASC + +Options: + +.. code-block:: php + + $sql = (new SphinxQL($conn)) + ->select() + ->from('rt') + ->option('comment', 'this should be quoted') + ->compile() + ->getCompiled(); + + // SELECT * FROM rt OPTION comment = 'this should be quoted' + +Array option values are compiled as ``(key=value, ...)``: + +.. code-block:: php + + $sql = (new SphinxQL($conn)) + ->select() + ->from('rt') + ->option('field_weights', [ + 'title' => 80, + 'content' => 35, + 'tags' => 92, + ]) + ->compile() + ->getCompiled(); + + // SELECT * FROM rt OPTION field_weights = (title=80, content=35, tags=92) + +INSERT / REPLACE +---------------- + +``set()`` style: + +.. code-block:: php + + (new SphinxQL($conn)) + ->insert() + ->into('rt') + ->set([ + 'id' => 10, + 'gid' => 9001, + 'title' => 'the story of a long test unit', + 'content' => 'once upon a time there was a foo in the bar', + ]) ->execute(); -Escaping --------- +``columns()`` + ``values()`` style: -- ``SphinxQL::expr()`` bypasses escaping for trusted SQL fragments. -- ``quote()`` and ``quoteArr()`` are provided by the connection. -- ``escapeMatch()`` and ``halfEscapeMatch()`` are available on ``SphinxQL``. +.. code-block:: php -Strict Validation in 4.0 ------------------------- + (new SphinxQL($conn)) + ->replace() + ->into('rt') + ->columns('id', 'title', 'content', 'gid') + ->values(10, 'modifying the same line again', 'because i am that lazy', 9003) + ->values(11, 'i am getting really creative with these strings', "i'll need them to test MATCH!", 300) + ->execute(); -The builder now validates critical query-shape input and throws -``SphinxQLException`` on invalid values: +UPDATE +------ -- invalid ``setType()`` values -- invalid order direction values -- negative ``limit()`` / ``offset()`` -- invalid shapes for ``IN`` and ``BETWEEN`` filters -- invalid ``facet()`` object type +Standard update: -Boolean Grouping and OR Filters -------------------------------- +.. code-block:: php -The builder supports grouped boolean filters for ``WHERE`` and ``HAVING``: + $affected = (new SphinxQL($conn)) + ->update('rt') + ->where('id', '=', 11) + ->value('gid', 201) + ->execute() + ->getStored(); -- ``orWhere()`` -- ``whereOpen()`` / ``orWhereOpen()`` / ``whereClose()`` -- ``orHaving()`` -- ``havingOpen()`` / ``orHavingOpen()`` / ``havingClose()`` +Late ``into()`` (from tests): -Repeated ``having()`` calls are additive and compile as ``AND`` conditions unless -you explicitly use ``orHaving()`` / grouped clauses. +.. code-block:: php -JOIN and KNN Ordering ---------------------- + $sql = (new SphinxQL($conn)) + ->update() + ->into('rt') + ->set(['gid' => 777]) + ->where('id', '=', 11) + ->compile() + ->getCompiled(); -``SELECT`` queries support fluent joins: + // UPDATE rt SET gid = 777 WHERE id = 11 -- ``join()``, ``innerJoin()``, ``leftJoin()``, ``rightJoin()``, ``crossJoin()`` +MVA update: -Vector-oriented ordering is available through: +.. code-block:: php -- ``orderByKnn($field, $k, array $vector, $direction = 'ASC')`` + (new SphinxQL($conn)) + ->update('rt') + ->where('id', '=', 15) + ->value('tags', [111, 222]) + ->execute(); + +DELETE +------ + +.. code-block:: php -Capability Introspection ------------------------- + $affected = (new SphinxQL($conn)) + ->delete() + ->from('rt') + ->where('id', 'IN', [11, 12, 13]) + ->match('content', 'content') + ->execute() + ->getStored(); + +Raw Query +--------- + +.. code-block:: php + + $rows = (new SphinxQL($conn)) + ->query('DESCRIBE rt') + ->execute() + ->getStored(); + +Transactions +------------ + +.. code-block:: php -``SphinxQL`` exposes runtime capability helpers for connection-aware behavior: + $sq = new SphinxQL($conn); -- ``getCapabilities()`` -- ``supports($feature)`` -- ``requireSupport($feature, $context = '')`` + $sq->transactionBegin(); + // write operations + $sq->transactionCommit(); -Helper Capability-Aware Calls ------------------------------ + // or + $sq->transactionRollback(); -The same capability model is used by ``Helper`` wrappers: +Reset Methods +------------- -- filtered ``SHOW`` wrappers: +You can reuse a builder and selectively clear parts of the query: - - ``showTables($index = null)`` => ``SHOW TABLES`` (when ``null`` or empty) or ``SHOW TABLES LIKE `` - - ``showTableStatus($table = null)`` => ``SHOW TABLE STATUS`` or ``SHOW TABLE
STATUS`` +- ``resetWhere()`` +- ``resetJoins()`` +- ``resetMatch()`` +- ``resetGroupBy()`` +- ``resetWithinGroupOrderBy()`` +- ``resetHaving()`` +- ``resetOrderBy()`` +- ``resetOptions()`` +- ``resetFacets()`` -- suggest-family option contract (``callSuggest()``, ``callQSuggest()``, - ``callAutocomplete()``): +Result Objects +-------------- - - options must be an associative array - - option keys must be non-empty strings - - values are quoted by the active connection (``quote()``/``quoteArr()``) - - tested keys in this repository are ``limit`` (numeric) and ``fuzzy`` (numeric, autocomplete) +``execute()`` returns ``ResultSetInterface`` with methods such as: -- capability behavior: +- ``getStored()`` +- ``fetchAllAssoc()`` +- ``fetchAllNum()`` +- ``fetchAssoc()`` +- ``fetchNum()`` +- ``getAffectedRows()`` - - ``callQSuggest()`` and ``callAutocomplete()`` are feature-gated and may throw - ``UnsupportedFeatureException`` when unsupported - - ``callSuggest()`` is runtime-conditional; use ``supports('call_suggest')`` for - portable code paths +For batch execution see :doc:`features/multi-query-builder`.