Search by

avadim / manticore-query-builder-php

avadim

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

Statistics

Installs: 7 040

Dependents: 0

Suggesters: 0

Stars: 8

Open Issues: 0

v2.6.0 2026-10-20 15:37 UTC

README

English | Русский

GitHub Release Packagist Downloads GitHub License PHP

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() and callPq() (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 immutable Meta object, 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.

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 and match() / whereMatch() for full-text fields instead of whereLike().
  • match() accepts a query-language expression, unlike whereFullText(); process ordinary user input with escapeMatch().
  • whereColumn() and distinct() are not provided.
  • The join() / leftJoin() API builds equality joins without table aliases or joinSub().
  • 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 :)