avadim / manticore-query-builder-php
Manticore Search Query Builder for PHP (unofficial PHP client)
Package info
github.com/aVadim483/manticore-query-builder-php
pkg:composer/avadim/manticore-query-builder-php
Requires
- php: ^7.4|^8.1
- ext-json: *
- ext-mbstring: *
- ext-pdo: *
- psr/log: >=1.1
Requires (Dev)
- phpunit/phpunit: ^9.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
English | Русский
Manticore Search Query Builder for PHP (unofficial PHP client)
Query Builder for Manticore Search in PHP with Laravel-like syntax
Features
- MySQL multiple connections via PDO (because Manticore is SQL-first)
- Placeholders as prefix in table names
- Named parameters in expressions
- Clear Laravel-like syntax
- Multiple INSERT and REPLACE
- Support MATCH() and multi-level WHERE for SELECT
- Support faceted search, JOIN of two tables and KNN vector search
- MVA ANY/ALL filters and geographic radius searches, distance columns and sorting
- Column types are applied in both directions: PHP values on write, PHP types on read
- The helpers of the Laravel query builder: aggregates, chunked walks, conditional building, conditions on dates, upsert and the rest
- CALL helpers:
callAutocomplete(),callSuggest(),callQsuggest(),callKeywords(),callSnippets()andcallPq()(percolate queries) - A rejected read throws, a rejected write answers with false or zero and keeps its reason
- PSR-3 logging of queries and EXPLAIN of full-text expressions
- Rows without metadata via
getRows(), an immutableMetaobject, and optional lazy retrieval of inserted IDs
Manticore Search server documentation: https://manual.manticoresearch.com/
Related packages
This builder works without a framework. Composer installs its PHP dependencies; the two packages below build on it.
avadim/manticore-query-builder-laravel— the integration with Laravel and Lumen: a service provider,config/manticore.php, named connections, a facade, and reads answering with aCollectionof row objects.avadim/manticore-laravel-scout— the ManticoreSearch driver for Laravel Scout: full-text search of Eloquent models asPost::search('manticore')->get().
Requirements
- PHP 7.4 or PHP 8.1 and above (PHP 8.0 is excluded by the Composer constraint)
- PHP extensions: JSON, mbstring, PDO and the PDO MySQL driver (
pdo_mysql) - Manticore Search reachable over its SQL interface (port 9306 by default). The HTTP/JSON API is not used.
What needs more than the bare server
Most of the builder talks to the server and nothing else. A few methods depend on a library loaded by the server, or on Manticore Buddy running next to it:
| Method | Needs | Without it |
|---|---|---|
| everything else | the server alone | — |
floatVector(), whereKnn() |
KNN library | the table cannot be created: knn library not loaded |
columnar(), columnEngine('columnar'), engine => 'columnar' |
Columnar library | the table cannot be created: columnar library not loaded |
rename() |
Manticore Buddy | the server answers with a syntax error |
callAutocomplete() |
Manticore Buddy and table infixes | the server rejects the call |
The KNN and columnar libraries both come from the Manticore Columnar Library; the secondary-index library ships with them and speeds filtering up without changing anything in the API.
The libraries have to match the daemon: it checks their ABI version on load and refuses one
that is too new, saying so only when started with --console. Which ones did load is written
in the version string:
SHOW STATUS LIKE 'version'; -- 28.6.6 … (columnar 13.8.3 …) (secondary 13.8.3 …) (knn 13.8.3 …)
A library that is missing from that line is not loaded, whatever the files on disk say.
Installation
composer require avadim/manticore-query-builder-php
Quick start guide
use avadim\Manticore\QueryBuilder\Builder as ManticoreDb; use avadim\Manticore\QueryBuilder\Schema\SchemaTable; // Define config $config = [ 'defaultConnection' => 'default', 'connections' => [ // Default connection which will be used with environment variables 'default' => [ 'host' => 'localhost', 'port' => 9306, 'username' => null, 'password' => null, 'timeout' => 5, 'prefix' => 'test_', // prefix that will replace the placeholder "?<table_name>" 'force_prefix' => false, ], ], ]; // Init query builder ManticoreDb::init($config); // Create table ManticoreDb::create('?products', function (SchemaTable $table) { $table->timestamp('created_at'); $table->string('manufacturer'); $table->text('title'); $table->json('info'); $table->float('price'); $table->multi('categories'); $table->bool('on_sale'); }); // Insert single row $singleRow = [ 'created_at' => time(), 'manufacturer' => 'Samsung', 'title' => 'Galaxy S23 Ultra', 'info' => ['color' => 'Red', 'storage' => 512], 'price' => 1199.00, 'categories' => [5, 7, 11], 'on_sale' => true, ]; $id = ManticoreDb::table('?products')->insertGetId($singleRow); // insert() returns true/false, insertGetId() - the <id> of the new record // Insert multiple rows $multipleRows = [ [ 'created_at' => time(), 'manufacturer' => '...', 'title' => '...', 'info' => [], // ... ], [ 'created_at' => time(), 'manufacturer' => '...', 'title' => '...', 'info' => [], // ... ], ]; $res = ManticoreDb::table('?products')->insertResultSet($multipleRows); // $res->result() => array of <id> of new records // Update and delete answer with the number of affected rows $updated = ManticoreDb::table('?products')->where('price', '<', 100)->update(['on_sale' => true]); $deleted = ManticoreDb::table('?products')->where('price', '<', 1)->delete(); // A failed write is not thrown - insert() gives false, update()/delete() give 0, // and the reason is in the ResultSet left behind if (!ManticoreDb::table('?products')->insert($singleRow)) { $error = ManticoreDb::lastResultSet()->error(); } // A failed read is thrown instead: null in place of the rows would look like an empty table try { $rows = ManticoreDb::table('?products')->where('nosuchcolumn', 1)->get(); } catch (\avadim\Manticore\QueryBuilder\QueryErrorException $e) { $error = $e->getMessage(); } // Search: get() returns rows keyed by document id, with values cast back to PHP types // ('info' as an array, 'categories' as int[], 'on_sale' as bool) $rows = ManticoreDb::table('?products')->match('galaxy')->where('price', '>', 1100)->get();
2.6: rows, metadata and insert IDs
Available since 2.6.0. Existing method signatures and default execution behavior are preserved.
$query = ManticoreDb::table('?products')->limit(20); $rows = $query->getRows(); // no SHOW META $returned = count($rows); // returned rows, not total matches $meta = $query->getMeta(); // optional explicit SHOW META, before another search $total = $meta->totalFound(); // also available as $meta['total_found'] $result = $query->search(); // rows and metadata together $array = $result->meta(); // existing array API $object = $result->metaObject(); // object over loaded metadata, no SQL
Meta is read-only. Its accessors return null for absent values; count($meta) counts
metadata fields. Enable lazy_insert_ids in the connection configuration to defer ID
retrieval for boolean insert()/replace() until lastResultSet()->result(). Read pending
IDs before any subsequent command on that connection. The setting defaults to false;
*GetId() and *ResultSet() still retrieve IDs immediately.
See searching, ResultSet and writing documents for the complete contracts and examples.
Differences from Laravel Query Builder
The API uses familiar Laravel syntax, with these differences:
- Use
whereRegex()for string attributes andmatch()/whereMatch()for full-text fields instead ofwhereLike(). match()accepts a query-language expression, unlikewhereFullText(); process ordinary user input withescapeMatch().whereColumn()anddistinct()are not provided.- The
join()/leftJoin()API builds equality joins without table aliases orjoinSub(). - The builder has no dedicated UNION, subquery or row-locking methods. The server supports limited sub-selects; a missing builder method does not mean the server lacks the feature.
The WHERE helpers, aggregates, chunked walks, conditional building, upsert, date conditions and transactions use Laravel-style syntax. Their server constraints and error behavior are documented separately; the API does not promise identical Laravel behavior.
Documentation
More detailed documentation is available in the /docs folder: configuration, searching, inserting and replacing documents, tables, result set, testing and Buddy, logging.
Документация на русском языке: README.ru.md и папка /docs/ru.
Tests
vendor/bin/phpunit --testsuite unit # SQL building and parsing, no server needed vendor/bin/phpunit # adds the integration tests vendor/bin/phpunit --exclude-group buddy # omit autocomplete tests; rename may still skip without Buddy
The integration tests create and drop their own tables on a live server. They are skipped,
not failed, when nothing listens on MANTICORE_HOST:MANTICORE_PORT (127.0.0.1:9306
by default).
GitHub Actions runs the full suite against Manticore 29.9.0 on PHP 7.4 and 8.4,
plus a separate latest run on PHP 8.4. Before PHPUnit, tests/ci/check-manticore.php
waits for working SQL, Columnar filtering, KNN search and a Buddy table rename,
using temporary tables that it removes afterwards. Run this probe only against a test server.
The readiness timeout defaults to 90 seconds (MANTICORE_READY_TIMEOUT, 1–120 seconds);
the probe uses the same host and port environment variables as the integration tests.
CI uses --fail-on-skipped --fail-on-incomplete: missing components cannot produce
a successful full run. Local test skipping remains unchanged. JUnit reports, readiness
output and container diagnostics are uploaded as artifacts, including on failure,
and retained for 14 days. The workflow also supports manual runs from the Actions tab
once the workflow is on the default branch. Both pinned-version and latest runs must pass.
The workflow runs on pushes to main, on pull requests, and manually. The unit matrix covers
PHP 7.4 and 8.1–8.4. All Buddy-dependent tests are included in full CI runs. See the
testing guide for Docker setup, readiness checks and targeted Buddy tests.
Want to support?
If you find this package useful, just give me a star on GitHub :)