<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sergey Nikolaev</title>
    <description>The latest articles on DEV Community by Sergey Nikolaev (@sanikolaev).</description>
    <link>https://dev.to/sanikolaev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F363352%2F6f7a2da7-fa00-47f5-aaca-a007b1d43350.jpeg</url>
      <title>DEV Community: Sergey Nikolaev</title>
      <link>https://dev.to/sanikolaev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sanikolaev"/>
    <language>en</language>
    <item>
      <title>UUID in Manticore: a single ID for the primary database and Manticore</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Wed, 05 Aug 2026 04:21:12 +0000</pubDate>
      <link>https://dev.to/sanikolaev/uuid-in-manticore-a-single-id-for-the-primary-database-and-manticore-39bl</link>
      <guid>https://dev.to/sanikolaev/uuid-in-manticore-a-single-id-for-the-primary-database-and-manticore-39bl</guid>
      <description>&lt;p&gt;Suppose your product already has the ID &lt;code&gt;550e8400-e29b-41d4-a716-446655440000&lt;/code&gt; in the primary database. It shows up in events, logs, and API responses. But when you load that same product into Manticore, the application still has to assign it another, numeric ID.&lt;/p&gt;

&lt;p&gt;Before Manticore Search 28.5.0, the document ID was an unsigned 64-bit number. A UUID could be stored in a separate string attribute, but that did not make it the document ID. &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;REPLACE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; still required a numeric &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As a result, you had to keep a mapping between the UUID from the primary database and the numeric ID in the Manticore table. Now you can do without it: an RT table in Manticore can use a UUID as the document ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a second ID is a problem
&lt;/h2&gt;

&lt;p&gt;A mapping table between IDs is not exactly complicated, but only as long as the application is loading data and searching. The problems start when documents change.&lt;/p&gt;

&lt;p&gt;A worker receives an event with a UUID, finds the matching numeric ID, and only then sends the update to Manticore. Deletion follows the same path. If the mapping record is missing or stale, the wrong document may be updated, or the changes may never reach Manticore at all.&lt;/p&gt;

&lt;p&gt;Another option is to turn the UUID into a 64-bit hash. Then the application itself has to account for possible collisions. You can also introduce a separate sequence counter, but then it has to be coordinated across all processes that create documents.&lt;/p&gt;

&lt;p&gt;There is another subtle point with numeric IDs, and it appears at the API level. Inside Manticore it is a &lt;code&gt;uint64&lt;/code&gt;, while SQL shows it as a signed &lt;code&gt;BIGINT&lt;/code&gt;. Because of that, SQL can return values greater than &lt;code&gt;2^63-1&lt;/code&gt; as negative numbers, and the client has to convert them carefully. A UUID is passed and returned as a string, so there is no need to worry about signed range or overflow.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;id uuid&lt;/code&gt; solves the problem at the root: the object identifier no longer needs conversion. The same UUID is used in the primary DB, the queue, Manticore, logs, and the external API.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a UUID is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9562.html" rel="noopener noreferrer"&gt;UUID&lt;/a&gt; is essentially a 128-bit identifier that does not require a central registry to create. In text form, it usually consists of 36 characters: 32 hexadecimal digits and four hyphens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;550e8400-e29b-41d4-a716-446655440000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A UUID contains a version and a &lt;code&gt;variant&lt;/code&gt;. The version defines how the remaining bits are formed. UUIDv4 is based on random or pseudorandom data. UUIDv7 includes a time component and preserves the chronological order of identifiers. In UUIDv8, the placement of the remaining bits is defined by a specific implementation.&lt;/p&gt;

&lt;p&gt;The lack of a central registry makes it possible to assign an ID before writing to a shared database. For example, two independent services can create objects in parallel, and a mobile client with no LTE signal can prepare data without a server connection. After synchronization, the object keeps the same identifier.&lt;/p&gt;

&lt;p&gt;However, a UUID should not be treated as an absolute guarantee of uniqueness. In practice, that depends on the correctness of the chosen generator. Also keep in mind that a UUID is not a secret and does not replace a password, token, or permission check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in Manticore 28.5.0
&lt;/h2&gt;

&lt;p&gt;For an RT table, you can now explicitly set the document ID type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;products_uuid&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;products_uuid&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'550e8400-e29b-41d4-a716-446655440000'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'Mechanical keyboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'KB-001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;149&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After insertion, the same UUID can be used in equality and &lt;code&gt;IN&lt;/code&gt; filters, as well as in &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;REPLACE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt;. SQL returns the ID as a string. You can also pass an explicit UUID or ask Manticore to generate an ID through the JSON API; we will cover detailed requests and responses in the next article.&lt;/p&gt;

&lt;p&gt;If you run &lt;code&gt;INSERT&lt;/code&gt; again with a UUID that already exists, Manticore rejects it, just as it used to with a numeric ID. Use &lt;code&gt;REPLACE&lt;/code&gt; to overwrite a document by ID.&lt;/p&gt;

&lt;p&gt;Note that the &lt;code&gt;uuid&lt;/code&gt; type applies only to the document ID. You cannot declare a regular user attribute with the &lt;code&gt;uuid&lt;/code&gt; type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who creates the UUID
&lt;/h2&gt;

&lt;p&gt;There are two options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;How the document is written&lt;/th&gt;
&lt;th&gt;Who creates the ID&lt;/th&gt;
&lt;th&gt;What Manticore does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The &lt;code&gt;id&lt;/code&gt; field is provided&lt;/td&gt;
&lt;td&gt;The primary DB, a client library, or another system component&lt;/td&gt;
&lt;td&gt;Validates the format, version, and &lt;code&gt;variant&lt;/code&gt;, then stores the UUID in lowercase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The &lt;code&gt;id&lt;/code&gt; field is missing&lt;/td&gt;
&lt;td&gt;Manticore&lt;/td&gt;
&lt;td&gt;Creates a UUIDv8 and encodes its internal numeric auto-ID into it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your application already creates the UUID, you do not need to adapt it to Manticore. The format is standard: five groups of &lt;code&gt;8-4-4-4-12&lt;/code&gt;. The version can be any from v1 to v8, and the &lt;code&gt;variant&lt;/code&gt; position must contain &lt;code&gt;8&lt;/code&gt;, &lt;code&gt;9&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, or &lt;code&gt;b&lt;/code&gt;. Case does not matter: you can send the UUID in uppercase, but Manticore will store it in lowercase.&lt;/p&gt;

&lt;p&gt;That is where validation ends: Manticore checks the UUID format, but not the quality of the generator. The client library is responsible for the randomness of UUIDv4 and the correct time component in UUIDv7.&lt;/p&gt;

&lt;p&gt;Server-side generation works differently. If the &lt;code&gt;id&lt;/code&gt; field is not provided, Manticore creates a UUIDv8 with its own structure and encodes the built-in numeric auto-ID into it. This is not a random UUIDv4 and it is not a secret value.&lt;/p&gt;

&lt;p&gt;A UUIDv8 created by the application will be validated and stored by Manticore as is; the server will not reshape it according to its own scheme.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Technical note. The full UUID remains the external identifier: Manticore does not replace it with a 64-bit hash and does not require the application to store a mapping table. How the UUID is laid out inside the engine is an implementation detail and does not affect the external contract.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where it works
&lt;/h2&gt;

&lt;p&gt;You can use a UUID as the document ID in regular RT tables, RT tables with &lt;code&gt;engine='columnar'&lt;/code&gt;, and tables in a replication cluster. With such IDs, exact match search, &lt;code&gt;IN&lt;/code&gt; filters, and the usual document operations are available: &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;REPLACE&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Keep these limitations in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;uuid&lt;/code&gt; type is not suitable for regular attributes: Manticore will reject a declaration such as &lt;code&gt;guid uuid&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;in plain, percolate/PQ, and shard tables, UUIDs cannot be used as the document ID;&lt;/li&gt;
&lt;li&gt;an existing table cannot be switched from a numeric ID to a UUID, or back, with &lt;code&gt;ALTER TABLE&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;range conditions &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;lt;=&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;gt;=&lt;/code&gt;, as well as arithmetic operations on IDs of this type, are not supported;&lt;/li&gt;
&lt;li&gt;the document ID cannot be changed through &lt;code&gt;UPDATE&lt;/code&gt; - this applies to both UUIDs and numeric IDs;&lt;/li&gt;
&lt;li&gt;for automatic generation, the &lt;code&gt;id&lt;/code&gt; field must be omitted: the value &lt;code&gt;0&lt;/code&gt; does not work here as a special marker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is easy to forget when porting legacy code. For a numeric RT table, &lt;code&gt;0&lt;/code&gt; can mean “create the ID automatically.” In a UUID table, you simply need to leave out the &lt;code&gt;id&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;UUIDv7 also does not make &lt;code&gt;id&lt;/code&gt; suitable for time-based filters. You can use it as an external identifier, but Manticore does not yet support queries like &lt;code&gt;id &amp;gt; ...&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;If the UUID already exists in the primary DB, you can now pass it to Manticore together with the document. If the document appears in Manticore first, do not specify &lt;code&gt;id&lt;/code&gt; and take the generated UUID from the response.&lt;/p&gt;

&lt;p&gt;From there, use the same UUID in &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;REPLACE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt;. There is no longer any need to give the document a separate numeric ID and keep a mapping between the two identifiers.&lt;/p&gt;

&lt;p&gt;The full contract and current limitations are documented here: &lt;a href="https://manual.manticoresearch.com/dev/Creating_a_table/Data_types#UUID-document-IDs" rel="noopener noreferrer"&gt;UUID document IDs&lt;/a&gt;. Support for UUID as a document ID appeared in &lt;a href="https://manticoresearch.com/blog/manticore-search-28-5-0/" rel="noopener noreferrer"&gt;Manticore Search 28.5.0&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the next article, &lt;a href="https://manticoresearch.com/blog/uuid-document-ids-cookbook/" rel="noopener noreferrer"&gt;a practical guide to using UUIDs as document IDs&lt;/a&gt;, we will step through how to set and generate IDs with SQL and JSON, search, update, replace, and delete documents, and handle errors.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
    </item>
    <item>
      <title>Manticore Search 28.6.6: UUID document IDs, ordered GROUP_CONCAT(), and 16 fixes</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Fri, 31 Jul 2026 09:33:57 +0000</pubDate>
      <link>https://dev.to/sanikolaev/manticore-search-2866-uuid-document-ids-ordered-groupconcat-and-16-fixes-12ch</link>
      <guid>https://dev.to/sanikolaev/manticore-search-2866-uuid-document-ids-ordered-groupconcat-and-16-fixes-12ch</guid>
      <description>&lt;p&gt;Manticore Search 28.6.6 has been released. The headline additions are UUID document IDs for real-time tables and ordered, limited &lt;code&gt;GROUP_CONCAT()&lt;/code&gt; for grouped queries. The release also includes 16 fixes for backups, replication, query processing, SQL compatibility, and secondary indexes.&lt;/p&gt;

&lt;p&gt;This post covers everything shipped from &lt;strong&gt;28.4.5 through 28.6.6&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Upgrade notes
&lt;/h2&gt;

&lt;p&gt;There are no new mandatory data migrations in this release. UUID IDs are an opt-in table definition: existing numeric-ID tables keep working as they are. If you want UUID identifiers, create a real-time table with &lt;code&gt;id uuid&lt;/code&gt;; &lt;code&gt;ALTER TABLE&lt;/code&gt; cannot convert an existing table between numeric and UUID IDs.&lt;/p&gt;

&lt;p&gt;Two fixes are particularly useful for production installations. Successful backups now always unfreeze real-time tables when they finish (previously in rare cases they didn't), rather than leaving writes blocked. And authenticated replication can again add an existing populated RT table with &lt;code&gt;ALTER CLUSTER ... ADD&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  UUID document IDs for real-time tables
&lt;/h2&gt;

&lt;p&gt;Applications often already have UUID identifiers from the system of record. Until now, using them with Manticore Search meant maintaining a separate numeric ID mapping. Real-time tables can now use &lt;a href="https://manual.manticoresearch.com/Creating_a_table/Data_types#UUID-document-IDs" rel="noopener noreferrer"&gt;UUID document IDs&lt;/a&gt; directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;products_uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Manticore accepts an explicit UUID string, or generates one when &lt;code&gt;id&lt;/code&gt; is omitted from an insert or replace. UUID equality and &lt;code&gt;IN&lt;/code&gt; filters work in queries, and UUID IDs can be used with &lt;code&gt;REPLACE&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is currently a real-time-table capability, including columnar and replicated RT tables. Plain, percolate, and sharded tables continue to use their existing ID models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ordered and limited GROUP_CONCAT()
&lt;/h2&gt;

&lt;p&gt;Grouped results often need a compact preview of the most relevant values in each group. &lt;code&gt;GROUP_CONCAT()&lt;/code&gt; can now sort values and retain only the requested number of them in explicit SQL &lt;code&gt;GROUP BY&lt;/code&gt; queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;GROUP_CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="n"&gt;SEPARATOR&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new &lt;code&gt;ORDER BY&lt;/code&gt;, &lt;code&gt;SEPARATOR&lt;/code&gt;, and &lt;code&gt;LIMIT&lt;/code&gt; options let the query return the top values in the requested order instead of concatenating an arbitrary-sized set and sorting it again in application code. See the &lt;a href="https://manual.manticoresearch.com/Searching/Grouping#GROUP_CONCAT%28field%29" rel="noopener noreferrer"&gt;GROUP_CONCAT() reference&lt;/a&gt; for the full syntax.&lt;/p&gt;




&lt;h2&gt;
  
  
  Safer table, backup, and cluster maintenance
&lt;/h2&gt;

&lt;p&gt;A few changes remove failures that tend to appear during routine operations rather than ordinary searches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/manticoresoftware/manticoresearch-backup" rel="noopener noreferrer"&gt;Manticore Backup&lt;/a&gt; 1.10.2 fixes a path where a successful backup could leave an RT table frozen and future writes blocked.&lt;/li&gt;
&lt;li&gt;Authenticated replication state transfer now works when &lt;code&gt;ALTER CLUSTER ... ADD&lt;/code&gt; is used to add a populated RT table.&lt;/li&gt;
&lt;li&gt;RT lifecycle operations now remove obsolete external files after &lt;code&gt;ALTER&lt;/code&gt;, &lt;code&gt;TRUNCATE&lt;/code&gt;, chunk removal, and optimization. The same release preserves Jieba settings during unrelated &lt;code&gt;ALTER TABLE&lt;/code&gt; operations and rejects unsupported effective setting changes explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manual.manticoresearch.com/Securing_and_compacting_a_table/Compacting_a_table#OPTIMIZE-TABLE" rel="noopener noreferrer"&gt;OPTIMIZE TABLE&lt;/a&gt; accepts qualified &lt;code&gt;system.&amp;lt;table&amp;gt;&lt;/code&gt; names, so physical sharded tables can be optimized.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Query processing and client compatibility
&lt;/h2&gt;

&lt;p&gt;The fixes are broad, but several stand out for busy search services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://manual.manticoresearch.com/Searching/Highlighting#CALL-SNIPPETS" rel="noopener noreferrer"&gt;CALL SNIPPETS&lt;/a&gt; no longer spends seconds simplifying some complex boolean queries before highlighting them when &lt;code&gt;boolean_simplify&lt;/code&gt; is enabled.&lt;/li&gt;
&lt;li&gt;Long exact and phrase-like queries no longer risk a query-stack underestimate that could corrupt memory or crash &lt;code&gt;searchd&lt;/code&gt;; repeated keyword statistics are also deduplicated for &lt;code&gt;local_df&lt;/code&gt; work on multi-chunk RT and distributed tables.&lt;/li&gt;
&lt;li&gt;The updated columnar library fixes a StreamVByte decoder over-read that could crash secondary-index queries, including &lt;code&gt;GROUP BY&lt;/code&gt; on JSON fields. &lt;code&gt;indextool --check&lt;/code&gt; now reports corrupted secondary-index files instead of crashing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;USER()&lt;/code&gt; remains usable after &lt;code&gt;USER&lt;/code&gt; became an authentication keyword, restoring MySQL client &lt;code&gt;status&lt;/code&gt; command compatibility. &lt;code&gt;SHOW INDEX ... STATUS&lt;/code&gt; is also protected from a concurrent percentile-calculation race.&lt;/li&gt;
&lt;li&gt;Document-ID &lt;code&gt;IN(...)&lt;/code&gt; filters correctly accept valid unsigned 64-bit IDs above &lt;code&gt;Long.MAX_VALUE&lt;/code&gt;, and a &lt;code&gt;LEFT JOIN&lt;/code&gt; with multiple &lt;code&gt;FACET&lt;/code&gt; clauses no longer crashes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The release also fixes a SphinxQL SSL connection-handling race that could consume workers at 100% CPU, and fixes PHP API response parsing and the Ruby API fixture so valid response chunks ending in &lt;code&gt;"0"&lt;/code&gt; are preserved.&lt;/p&gt;

&lt;p&gt;For the complete list, see the &lt;a href="https://manual.manticoresearch.com/Changelog#Version-28.6.6" rel="noopener noreferrer"&gt;Version 28.6.6 changelog&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Get Manticore Search 28.6.6
&lt;/h2&gt;

&lt;p&gt;Install or upgrade Manticore Search with the &lt;a href="https://manticoresearch.com/install/" rel="noopener noreferrer"&gt;installation guide&lt;/a&gt;. If you are upgrading an authenticated or replicated deployment, review the existing &lt;a href="https://manticoresearch.com/blog/manticore-auth-migration-hardening-checklist/" rel="noopener noreferrer"&gt;authentication rollout checklist&lt;/a&gt; before changing production traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Need help or want to connect?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Join our &lt;a href="https://slack.manticoresearch.com" rel="noopener noreferrer"&gt;Slack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Visit the &lt;a href="https://forum.manticoresearch.com" rel="noopener noreferrer"&gt;Forum&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Report issues or suggest features on &lt;a href="https://github.com/manticoresoftware/manticoresearch/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email us at &lt;code&gt;contact@manticoresearch.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>search</category>
      <category>sql</category>
    </item>
    <item>
      <title>Faceted search under active filters</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Thu, 30 Jul 2026 04:54:21 +0000</pubDate>
      <link>https://dev.to/sanikolaev/faceted-search-under-active-filters-3bn7</link>
      <guid>https://dev.to/sanikolaev/faceted-search-under-active-filters-3bn7</guid>
      <description>&lt;p&gt;Facets in an online store seem simple until the first filter is selected.&lt;/p&gt;

&lt;p&gt;In a catalog, they are part of the navigation. A selected color should not disappear from the list. Other colors should remain available: the user may want to switch to one or broaden the selection. Options with no matching products are more useful when shown as unavailable. Within a single facet, &lt;code&gt;OR&lt;/code&gt; usually applies: red or blue. Across different facets, it is &lt;code&gt;AND&lt;/code&gt;: brand, color, and size at the same time.&lt;/p&gt;

&lt;p&gt;The difficult part begins after the first selection. Imagine a product catalog where the user has selected a brand, color, and size. The main result set must remain narrow and show only products matching all three conditions. But the filter panel follows different rules. It needs to preserve the selected color while also showing the colors the user can switch to under the same brand and size.&lt;/p&gt;

&lt;p&gt;If every facet inherits all filters from the main query, it quickly collapses to the values already selected. If the filters for every facet have to be rebuilt manually, you end up with separate query branches for color, size, brand, availability, seller, and every other attribute.&lt;/p&gt;

&lt;p&gt;Manticore Search 25.12.0 introduced &lt;code&gt;facet_filter_mode&lt;/code&gt;, which moves this behavior into the facet API. A query can now describe how the store's filter panel should behave, without the application manually building nearly identical queries for every facet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What e-commerce facets need
&lt;/h2&gt;

&lt;p&gt;For a filter panel, answering "how many products have this value?" is not enough. It needs statuses that the interface can use directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;selected values remain visible and can be cleared quickly;&lt;/li&gt;
&lt;li&gt;other values in the same facet remain available because they broaden the filter through &lt;code&gt;IN (...)&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;values in other facets indicate whether selecting them would return at least one product;&lt;/li&gt;
&lt;li&gt;counts remain predictable: the interface knows whether a number refers to the current result set or a broader selection;&lt;/li&gt;
&lt;li&gt;search within long brand lists, categories, price ranges, SEO rules, and query analytics still matter; they are a separate part of search design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;facet_filter_mode&lt;/code&gt; handles the most common part of this problem: recalculating facets after the user has already selected several filters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;Facets now have three filter inheritance modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applies all filters from the main query to the facet. This is the previous behavior and remains the default.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applies every filter except filters on the facet itself and adds &lt;code&gt;status&lt;/code&gt;: selected values get &lt;code&gt;selected&lt;/code&gt;, while other values get &lt;code&gt;available&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Calculates buckets over a broad base result set and adds &lt;code&gt;status&lt;/code&gt; to distinguish selected, available, and unavailable values.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can also control this manually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ALL FILTERS&lt;/code&gt; - apply all filters to the facet;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FILTERS color_id, size_id&lt;/code&gt; - apply only the listed filters;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EXCLUDE FILTERS color_id&lt;/code&gt; - apply every filter except those listed;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ZEROES&lt;/code&gt; - starting with Manticore Search 27.3.0, preserve buckets from the broad &lt;code&gt;max&lt;/code&gt; scope in SQL &lt;code&gt;max&lt;/code&gt; mode even when their &lt;code&gt;count(*)&lt;/code&gt; in the current facet is &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same options are available in the JSON API through &lt;code&gt;facet_filter_mode&lt;/code&gt;, &lt;code&gt;mode&lt;/code&gt;, &lt;code&gt;filters&lt;/code&gt;, &lt;code&gt;exclude_filters&lt;/code&gt;, and &lt;code&gt;zeroes: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In short, &lt;code&gt;strict&lt;/code&gt; answers "what is in the current result set?", &lt;code&gt;auto&lt;/code&gt; answers "what happens if this particular filter changes?", and &lt;code&gt;max&lt;/code&gt; shows a broad list of buckets with a &lt;code&gt;status&lt;/code&gt; field: &lt;code&gt;selected&lt;/code&gt;, &lt;code&gt;available&lt;/code&gt;, or &lt;code&gt;unavailable&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal example
&lt;/h2&gt;

&lt;p&gt;Consider a small catalog with a brand, color, size, and SKU:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;brand_id&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;color_id&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;color_name&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;size_id&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;size_name&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;brand_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;color_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;color_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;size_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;size_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'p1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'red'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'small'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'sku1'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'p2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'red'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'large'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'sku2'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'p3'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'blue'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'small'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'sku3'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'p4'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'green'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'xlarge'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'sku4'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'p5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'red'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'small'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'sku5'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'p6'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'black'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'large'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'sku6'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'p7'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'white'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'small'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'sku7'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user selected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;brand_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under these conditions, the result set contains one product: &lt;code&gt;p1&lt;/code&gt;. Let's see what happens to the facets.&lt;/p&gt;

&lt;h2&gt;
  
  
  strict: the previous behavior
&lt;/h2&gt;

&lt;p&gt;Without additional options, Manticore Search uses &lt;code&gt;strict&lt;/code&gt;: every facet receives all filters from the main query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;brand_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;FACET&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="n"&gt;FACET&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+----------+----------+
| color_id | count(*) |
+----------+----------+
|        1 |        1 |
+----------+----------+
+---------+----------+
| size_id | count(*) |
+---------+----------+
|      10 |        1 |
+---------+----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an accurate SQL response, but it is often too narrow for an online store. The user sees only the already selected &lt;code&gt;color_id=1&lt;/code&gt; and &lt;code&gt;size_id=10&lt;/code&gt;. It looks as if there are no other options, even though the data contains a product with the same brand and size but a different color (&lt;code&gt;color_id=2&lt;/code&gt;), and another with the same brand and color but a different size (&lt;code&gt;size_id=20&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  auto: a facet ignores its own filter
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;auto&lt;/code&gt; mode keeps every filter except the filter on the facet currently being calculated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;brand_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;OPTION&lt;/span&gt; &lt;span class="n"&gt;facet_filter_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'auto'&lt;/span&gt;
&lt;span class="n"&gt;FACET&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="n"&gt;FACET&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+----------+----------+-----------+
| color_id | count(*) | status    |
+----------+----------+-----------+
|        1 |        1 | selected  |
|        2 |        1 | available |
+----------+----------+-----------+
+---------+----------+-----------+
| size_id | count(*) | status    |
+---------+----------+-----------+
|      10 |        1 | selected  |
|      20 |        1 | available |
+---------+----------+-----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FACET color_id&lt;/code&gt; applied &lt;code&gt;brand_id=7 AND size_id=10&lt;/code&gt;, but not &lt;code&gt;color_id=1&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FACET size_id&lt;/code&gt; applied &lt;code&gt;brand_id=7 AND color_id=1&lt;/code&gt;, but not &lt;code&gt;size_id=10&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives the interface alternative values without a separate query for every facet. Selected buckets are marked &lt;code&gt;selected&lt;/code&gt;, while other values in the same facet are marked &lt;code&gt;available&lt;/code&gt;: they can be added to the current filter as a broader selection through &lt;code&gt;IN (...)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  max: a broad bucket list with statuses
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;auto&lt;/code&gt; shows only values from the current filter scope for a particular facet. &lt;code&gt;max&lt;/code&gt; goes further: it calculates buckets over the base result set and marks their status separately for the interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;brand_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;OPTION&lt;/span&gt; &lt;span class="n"&gt;facet_filter_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'max'&lt;/span&gt;
&lt;span class="n"&gt;FACET&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="n"&gt;FACET&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+----------+----------+-----------+
| color_id | count(*) | status    |
+----------+----------+-----------+
|        1 |        3 | selected  |
|        2 |        1 | available |
|        3 |        1 | available |
|        4 |        1 | available |
|        5 |        1 | available |
+----------+----------+-----------+
+---------+----------+-----------+
| size_id | count(*) | status    |
+---------+----------+-----------+
|      10 |        4 | selected  |
|      20 |        2 | available |
|      30 |        1 | available |
+---------+----------+-----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;status&lt;/code&gt; comes directly from Manticore Search:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;selected&lt;/code&gt; - the value is already present in the filter on this facet;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;available&lt;/code&gt; - the value can be selected; for another value in the same facet, this broadens the filter through &lt;code&gt;IN (...)&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unavailable&lt;/code&gt; - the value exists in the facet's broad result set, but selecting it would return no documents under the current filters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this example, &lt;code&gt;color_id=1&lt;/code&gt; occurs in three products across the catalog, so its count in &lt;code&gt;max&lt;/code&gt; is &lt;code&gt;3&lt;/code&gt;. It is marked &lt;code&gt;selected&lt;/code&gt; because it already participates in the filter. The other colors are marked &lt;code&gt;available&lt;/code&gt;: if the user selects one, the color filter becomes broader, for example &lt;code&gt;color_id IN (1,2)&lt;/code&gt;. Unavailable values appear when a bucket in the broad result set cannot return documents under the current filters; the &lt;code&gt;sku&lt;/code&gt; example below demonstrates this case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manually setting the filter scope
&lt;/h2&gt;

&lt;p&gt;The global &lt;code&gt;facet_filter_mode&lt;/code&gt; covers most common cases, but sometimes different facets need different behavior. For example, color can remain strictly constrained by all filters, size can use &lt;code&gt;max&lt;/code&gt;, SKU can be calculated using only color and size, and brand can be calculated without the color filter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;brand_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;OPTION&lt;/span&gt; &lt;span class="n"&gt;facet_filter_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'max'&lt;/span&gt;
&lt;span class="n"&gt;FACET&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;FILTERS&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="n"&gt;FACET&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="n"&gt;FACET&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="n"&gt;FILTERS&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="n"&gt;FACET&lt;/span&gt; &lt;span class="n"&gt;brand_id&lt;/span&gt; &lt;span class="n"&gt;EXCLUDE&lt;/span&gt; &lt;span class="n"&gt;FILTERS&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;brand_id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+----------+----------+-----------+
| color_id | count(*) | status    |
+----------+----------+-----------+
|        1 |        1 | selected  |
+----------+----------+-----------+
+---------+----------+-----------+
| size_id | count(*) | status    |
+---------+----------+-----------+
|      10 |        4 | selected  |
|      20 |        2 | available |
|      30 |        1 | available |
+---------+----------+-----------+
+------+----------+-------------+
| sku  | count(*) | status      |
+------+----------+-------------+
| sku1 |        1 | available   |
| sku5 |        1 | unavailable |
+------+----------+-------------+
+----------+----------+----------+
| brand_id | count(*) | status   |
+----------+----------+----------+
|        7 |        2 | selected |
+----------+----------+----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to read this query:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FACET color_id ALL FILTERS&lt;/code&gt; applies every filter and returns only the selected color;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FACET size_id&lt;/code&gt; inherits the query-level &lt;code&gt;max&lt;/code&gt; mode;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FACET sku FILTERS color_id, size_id&lt;/code&gt; applies only the color and size filters;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FACET brand_id EXCLUDE FILTERS color_id&lt;/code&gt; applies every filter except color.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mode is useful when a filter panel contains both regular and technical facets, and some values need to be calculated according to special rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  ZEROES: zero-count buckets in max
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ZEROES&lt;/code&gt; is useful when counts need to remain strict but the list of values needs to stay broad. It works with &lt;code&gt;max&lt;/code&gt;, either through &lt;code&gt;OPTION facet_filter_mode='max'&lt;/code&gt; or through &lt;code&gt;MODE max&lt;/code&gt; on an individual facet.&lt;/p&gt;

&lt;p&gt;Without &lt;code&gt;ZEROES&lt;/code&gt;, a facet with &lt;code&gt;ALL FILTERS&lt;/code&gt; shows only the bucket that passes every filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;brand_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;OPTION&lt;/span&gt; &lt;span class="n"&gt;facet_filter_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'max'&lt;/span&gt;
&lt;span class="n"&gt;FACET&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;FILTERS&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+---------+----------+----------+
| size_id | count(*) | status   |
+---------+----------+----------+
|      10 |        1 | selected |
+---------+----------+----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;ZEROES&lt;/code&gt;, Manticore Search keeps the same visible counts but returns the remaining buckets from the broad &lt;code&gt;max&lt;/code&gt; scope with a count of zero:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;brand_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;color_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;OPTION&lt;/span&gt; &lt;span class="n"&gt;facet_filter_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'max'&lt;/span&gt;
&lt;span class="n"&gt;FACET&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;FILTERS&lt;/span&gt; &lt;span class="n"&gt;ZEROES&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;size_id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+---------+----------+-----------+
| size_id | count(*) | status    |
+---------+----------+-----------+
|      10 |        1 | selected  |
|      20 |        0 | available |
|      30 |        0 | available |
+---------+----------+-----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interface can then show &lt;code&gt;large&lt;/code&gt; and &lt;code&gt;xlarge&lt;/code&gt; alongside the selected &lt;code&gt;small&lt;/code&gt;: the number refers to the current strict result set, while &lt;code&gt;status&lt;/code&gt; shows that these values can still be selected to broaden the filter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same query through the JSON API
&lt;/h2&gt;

&lt;p&gt;SQL is shorter here and shows the mechanics more clearly, but the JSON API supports the same approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/search&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"products"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"must"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"equals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"brand_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"equals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"color_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"equals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"size_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"facet_filter_mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"max"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aggs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"colors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"terms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"color_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"color_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"asc"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"terms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"size_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"size_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"asc"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"took"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timed_out"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_relation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aggregations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"colors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"buckets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"doc_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"selected"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"doc_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"available"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"doc_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"available"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"doc_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"available"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"doc_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"available"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"buckets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"doc_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"selected"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"doc_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"available"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"doc_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"available"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In JSON aggregations, &lt;code&gt;mode&lt;/code&gt;, &lt;code&gt;filters&lt;/code&gt;, &lt;code&gt;exclude_filters&lt;/code&gt;, and &lt;code&gt;zeroes&lt;/code&gt; can also be set for each individual aggregation.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this differs from Meilisearch, Elasticsearch, and OpenSearch
&lt;/h2&gt;

&lt;p&gt;We tested the same scenario in Manticore Search, Meilisearch, Elasticsearch, and OpenSearch: the &lt;code&gt;brand_id=7&lt;/code&gt;, &lt;code&gt;color_id=1&lt;/code&gt;, and &lt;code&gt;size_id=10&lt;/code&gt; filters are active; the result set remains strict, while the facets need to show options that appear when their own filter is excluded.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Excludes its own filter in one query&lt;/th&gt;
&lt;th&gt;Native bucket status&lt;/th&gt;
&lt;th&gt;What remains for the application&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manticore Search&lt;/td&gt;
&lt;td&gt;Yes, through &lt;code&gt;auto&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes, in &lt;code&gt;auto&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt;; &lt;code&gt;unavailable&lt;/code&gt; only in &lt;code&gt;max&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Rendering the interface, SEO, analytics, and custom rules.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meilisearch&lt;/td&gt;
&lt;td&gt;Not in this form&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Run additional queries and assemble facets in the application.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenSearch&lt;/td&gt;
&lt;td&gt;Yes, but manually through &lt;code&gt;global&lt;/code&gt; + &lt;code&gt;filter&lt;/code&gt; aggregations&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Duplicate filter branches and calculate &lt;code&gt;status&lt;/code&gt; in the application.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elasticsearch&lt;/td&gt;
&lt;td&gt;Yes, but manually through &lt;code&gt;global&lt;/code&gt; + &lt;code&gt;filter&lt;/code&gt; aggregations&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;The same: maintain separate aggregation branches and client-side logic.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Basic facets are easy to use in Meilisearch, but a query such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"brand_id = 7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"color_id = 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"size_id = 10"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"facets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"color_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"size_id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;returns counts for the already filtered result set. In our dataset, that means only &lt;code&gt;color_id=1&lt;/code&gt; and &lt;code&gt;size_id=10&lt;/code&gt;. The alternative &lt;code&gt;color_id=2&lt;/code&gt; and &lt;code&gt;size_id=20&lt;/code&gt; values do not appear in this response. If the interface needs them, the application must make additional queries and merge the results.&lt;/p&gt;

&lt;p&gt;In Elasticsearch and OpenSearch, similar behavior can be built in one query, but each facet needs its own explicit aggregation branch. For &lt;code&gt;color&lt;/code&gt;, you keep &lt;code&gt;brand_id&lt;/code&gt; and &lt;code&gt;size_id&lt;/code&gt;; for &lt;code&gt;size&lt;/code&gt;, you keep &lt;code&gt;brand_id&lt;/code&gt; and &lt;code&gt;color_id&lt;/code&gt;; and so on. This works, but the query grows quickly, and bucket statuses still have to be calculated in the application.&lt;/p&gt;

&lt;p&gt;The main difference is where this logic lives. In Manticore Search, it is configured directly in the facet API; in other systems, it is usually assembled from several similar filter trees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance and limitations
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;strict&lt;/code&gt; remains the default and preserves the previous behavior. If you only need facets within the current result set, nothing needs to change.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;auto&lt;/code&gt; is usually the better fit for e-commerce filters: it shows alternative values within each facet, marks selected values as &lt;code&gt;selected&lt;/code&gt;, and marks other values as &lt;code&gt;available&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;max&lt;/code&gt; when the interface needs value lists broader than the current result set and needs to show unavailable options. This mode costs more: Manticore Search calculates buckets over a broad scope and then determines their &lt;code&gt;status&lt;/code&gt; separately. This is worth considering with large datasets and many facets.&lt;/p&gt;

&lt;p&gt;There are also some limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local filter rewriting for a facet supports only attribute filters combined with &lt;code&gt;AND&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;complex &lt;code&gt;AND&lt;/code&gt;/&lt;code&gt;OR&lt;/code&gt; trees are not rewritten automatically for individual facets;&lt;/li&gt;
&lt;li&gt;selected values are currently matched reliably only for explicit value filters such as &lt;code&gt;=&lt;/code&gt; and &lt;code&gt;IN&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;statuses are not yet calculated automatically for ranges such as prices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For prices, discounts, and ratings, it is better to define separate ranges explicitly. For example, keep a numeric field for sorting and sliders, and add &lt;code&gt;price_band&lt;/code&gt; or &lt;code&gt;discount_band&lt;/code&gt; for the facet. This lets the interface show clear statuses for predefined ranges.&lt;/p&gt;

&lt;p&gt;SEO for faceted URLs, A/B tests, merchandising rules, and query analytics remain the responsibility of the application or platform around search.&lt;/p&gt;

&lt;p&gt;In practice, start with &lt;code&gt;auto&lt;/code&gt; when you need alternative values, and move to &lt;code&gt;max&lt;/code&gt; when the interface needs a broad bucket list with &lt;code&gt;selected&lt;/code&gt;, &lt;code&gt;available&lt;/code&gt;, and &lt;code&gt;unavailable&lt;/code&gt; statuses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;For an introduction to faceted search in Manticore Search, start with the earlier &lt;a href="https://manticoresearch.com/blog/faceted-search/" rel="noopener noreferrer"&gt;Faceted search&lt;/a&gt; article. It covers basic &lt;code&gt;FACET&lt;/code&gt; queries, sorting, limits, and facets over expressions.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://manual.manticoresearch.com/Searching/Faceted_search" rel="noopener noreferrer"&gt;&lt;code&gt;FACET&lt;/code&gt; documentation&lt;/a&gt; for details.&lt;/p&gt;

&lt;p&gt;For an interactive introduction to facets, take the &lt;a href="https://play.manticoresearch.com/faceting/" rel="noopener noreferrer"&gt;Manticore Faceting&lt;/a&gt; course.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Install Manticore Search with one command</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Wed, 29 Jul 2026 03:24:47 +0000</pubDate>
      <link>https://dev.to/sanikolaev/install-manticore-search-with-one-command-4cj5</link>
      <guid>https://dev.to/sanikolaev/install-manticore-search-with-one-command-4cj5</guid>
      <description>&lt;p&gt;There is now a one-line installer for Manticore Search:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://manticoresearch.com | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is still a normal package-manager install. The script checks whether the machine uses APT, YUM/DNF, or Homebrew, configures the Manticore repository when needed, installs the &lt;code&gt;manticore&lt;/code&gt; package, and starts the service unless you tell it not to.&lt;/p&gt;

&lt;p&gt;Use it when you want to bring up Manticore Search quickly and do not need to walk through the package-manager steps by hand. If you prefer to control the repository setup, package install, service start, or deployment method yourself, the manual installation pages are still the right place to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  After it runs
&lt;/h2&gt;

&lt;p&gt;A successful run leaves Manticore installed through the system package manager. Upgrades, service management, uninstall, and host inspection use the usual system tools.&lt;/p&gt;

&lt;p&gt;By default, the installer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;installs from the stable Manticore package repository&lt;/li&gt;
&lt;li&gt;installs the &lt;code&gt;manticore&lt;/code&gt; package&lt;/li&gt;
&lt;li&gt;starts the &lt;code&gt;manticore&lt;/code&gt; service&lt;/li&gt;
&lt;li&gt;keeps the default listeners on &lt;code&gt;9306&lt;/code&gt;, &lt;code&gt;9308&lt;/code&gt;, and &lt;code&gt;9312&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;writes an install log for troubleshooting&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check the service
&lt;/h2&gt;

&lt;p&gt;Once the service is started, a quick SQL-over-HTTP check is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="s2"&gt;"http://127.0.0.1:9308/sql?mode=raw"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"SELECT VERSION()"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use the MySQL client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-P9306&lt;/span&gt; &lt;span class="nt"&gt;-h0&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT VERSION()"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns a version, the package is installed and the service is answering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why add another install path?
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://manticoresearch.com/install/" rel="noopener noreferrer"&gt;manual instructions&lt;/a&gt; are not going away. They are still better when you need Docker, Kubernetes, Windows, a pinned package-manager flow, or a deployment policy that does not allow remote shell scripts.&lt;/p&gt;

&lt;p&gt;The one-line installer covers the shorter path. Instead of asking the reader to pick between APT, YUM/DNF, and Homebrew first, it detects the package manager and runs the same standard setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debian or Ubuntu: add the APT repo, update metadata, install the package, start the service.&lt;/li&gt;
&lt;li&gt;RHEL-compatible distribution: install the repository RPM, install the package, start the service.&lt;/li&gt;
&lt;li&gt;macOS: use Homebrew, then start the service through &lt;code&gt;brew services&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Other environments: stop and point to the manual options.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small difference, but useful when you just want a running local or test instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the script does
&lt;/h2&gt;

&lt;p&gt;The installer is a shell script. If you want to read it first, the source is &lt;a href="https://github.com/manticoresoftware/manticoresearch/blob/main/installer/bootstrap-standalone.sh" rel="noopener noreferrer"&gt;&lt;code&gt;installer/bootstrap-standalone.sh&lt;/code&gt;&lt;/a&gt; in the Manticore Search GitHub repository.&lt;/p&gt;

&lt;p&gt;On a system it supports, it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;detects APT, YUM/DNF, or Homebrew&lt;/li&gt;
&lt;li&gt;checks that the CPU architecture is x86-64 or ARM64&lt;/li&gt;
&lt;li&gt;installs the Manticore repository package when needed&lt;/li&gt;
&lt;li&gt;installs the &lt;code&gt;manticore&lt;/code&gt; package&lt;/li&gt;
&lt;li&gt;starts the &lt;code&gt;manticore&lt;/code&gt; service by default&lt;/li&gt;
&lt;li&gt;checks the default listener ports before startup when &lt;code&gt;ss&lt;/code&gt; or &lt;code&gt;netstat&lt;/code&gt; is available&lt;/li&gt;
&lt;li&gt;writes an install log to &lt;code&gt;/var/log/manticore_install.log&lt;/code&gt; when possible, otherwise to &lt;code&gt;/tmp/manticore_installer.log&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The default listener ports are &lt;code&gt;9306&lt;/code&gt;, &lt;code&gt;9308&lt;/code&gt;, and &lt;code&gt;9312&lt;/code&gt;. If one of them is already occupied, Manticore is not already running, and the system has &lt;code&gt;ss&lt;/code&gt; or &lt;code&gt;netstat&lt;/code&gt;, the installer fails instead of editing the configuration for you. If neither utility is available, the pre-check cannot run; service startup may still fail if a port is already taken.&lt;/p&gt;

&lt;p&gt;Port changes are left to you because changing them can affect clients, service files, and monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it applies
&lt;/h2&gt;

&lt;p&gt;The installer currently supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debian, Ubuntu, Linux Mint, and Debian-like systems using APT&lt;/li&gt;
&lt;li&gt;RHEL, CentOS, Fedora, Rocky Linux, AlmaLinux, Amazon Linux, and RHEL/Fedora-like systems using YUM or DNF&lt;/li&gt;
&lt;li&gt;macOS when Homebrew is installed&lt;/li&gt;
&lt;li&gt;x86-64 and ARM64 CPUs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The command shown above uses &lt;code&gt;curl&lt;/code&gt;. You can also fetch the script with &lt;code&gt;wget&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget &lt;span class="nt"&gt;-O-&lt;/span&gt; https://manticoresearch.com | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bash must be installed. The downloaded script starts under &lt;code&gt;/bin/sh&lt;/code&gt;, checks for Bash, and then runs the main installer payload with Bash. You also need the usual permissions to install packages and start services, either as root or through &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the operating system, package manager, or CPU architecture is not supported, the installer stops and prints a message pointing to the manual installation guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Default behavior
&lt;/h2&gt;

&lt;p&gt;With no extra options, the installer uses the stable release channel, installs the package, starts the service, and keeps the standard listener ports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stable packages are used unless you pass &lt;code&gt;dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the service starts unless you pass &lt;code&gt;no-start&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;listener ports stay at &lt;code&gt;9306&lt;/code&gt;, &lt;code&gt;9308&lt;/code&gt;, and &lt;code&gt;9312&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;existing data is not removed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other checks apply only in specific cases. Before an upgrade, the installer backs up configuration. It copies data directories only if you pass &lt;code&gt;backup-data&lt;/code&gt;. During &lt;code&gt;uninstall&lt;/code&gt; and &lt;code&gt;purge&lt;/code&gt;, it keeps data in place. If it finds an existing &lt;code&gt;searchd&lt;/code&gt; binary outside the package-managed install, it prints a warning.&lt;/p&gt;

&lt;p&gt;The destructive option is still separate: &lt;code&gt;purge-all&lt;/code&gt; removes packages, repository state, configuration, and data. Without non-interactive mode, it asks you to type &lt;code&gt;DELETE&lt;/code&gt; before removing &lt;code&gt;/etc/manticoresearch&lt;/code&gt; and &lt;code&gt;/var/lib/manticore&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common commands
&lt;/h2&gt;

&lt;p&gt;Most installer behavior is controlled by passing options after &lt;code&gt;sh -s&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the latest stable package and start the service&lt;/span&gt;
curl https://manticoresearch.com | sh

&lt;span class="c"&gt;# Install but do not start the service&lt;/span&gt;
curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; no-start

&lt;span class="c"&gt;# Upgrade an existing package-managed installation&lt;/span&gt;
curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; upgrade

&lt;span class="c"&gt;# Upgrade and include the data directory in the backup&lt;/span&gt;
curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; upgrade backup-data

&lt;span class="c"&gt;# Choose where upgrade backups are written&lt;/span&gt;
curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; upgrade backup-data backup-dir /path/to/backups

&lt;span class="c"&gt;# List available stable versions&lt;/span&gt;
curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; list-versions

&lt;span class="c"&gt;# List versions from the development repository&lt;/span&gt;
curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; dev list-versions

&lt;span class="c"&gt;# Install or switch to a specific version without starting the service&lt;/span&gt;
curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; version 25.0.0 no-start

&lt;span class="c"&gt;# Show installer help&lt;/span&gt;
curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Specific version selection is for Linux package-manager installs. Homebrew installs and upgrades follow the Homebrew formula and do not support &lt;code&gt;version&lt;/code&gt; through this installer.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;dev&lt;/code&gt; option switches to the development repository. The default is the release channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI and non-interactive use
&lt;/h2&gt;

&lt;p&gt;For automation, use &lt;code&gt;silent&lt;/code&gt; or &lt;code&gt;yes&lt;/code&gt; only when you want the installer to run without prompts and accept the script's defaults:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; silent no-start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;silent&lt;/code&gt; and &lt;code&gt;yes&lt;/code&gt; are aliases for the same non-interactive mode. They do not mean "no output": the installer still writes progress and errors, but without interactive prompts and color. They also do not request an upgrade by themselves. If an existing installation should be upgraded, pass &lt;code&gt;upgrade&lt;/code&gt; explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; upgrade silent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be careful with cleanup commands in non-interactive mode. In the current installer, &lt;code&gt;silent&lt;/code&gt; also skips the "DELETE" prompt for &lt;code&gt;purge-all&lt;/code&gt;, so it can delete configuration and data without asking again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup options
&lt;/h2&gt;

&lt;p&gt;Uninstalling is split into levels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove packages, keep configuration, data, and repository state&lt;/span&gt;
curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; uninstall

&lt;span class="c"&gt;# Remove packages and the repository bootstrap package&lt;/span&gt;
curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; purge

&lt;span class="c"&gt;# Remove packages, repository state, configuration, and data&lt;/span&gt;
curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; purge-all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;purge-all&lt;/code&gt; is the destructive option. Do not combine it with &lt;code&gt;silent&lt;/code&gt; or &lt;code&gt;yes&lt;/code&gt; unless you intentionally want to remove &lt;code&gt;/etc/manticoresearch&lt;/code&gt; and &lt;code&gt;/var/lib/manticore&lt;/code&gt; without a confirmation prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; purge-all silent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Removing software and deleting data are not the same operation, so the installer keeps those paths separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  If your policy forbids pipe-to-shell
&lt;/h2&gt;

&lt;p&gt;Some environments do not allow &lt;code&gt;curl | sh&lt;/code&gt;. That is fine. Use the &lt;a href="https://manticoresearch.com/install/" rel="noopener noreferrer"&gt;manual package-manager instructions&lt;/a&gt;, or review the script source first and run it only after inspection.&lt;/p&gt;

&lt;p&gt;For manual package-manager commands, Docker, Kubernetes, Windows, and separate packages, see the &lt;a href="https://manticoresearch.com/install/" rel="noopener noreferrer"&gt;Manticore Search installation guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>database</category>
      <category>devops</category>
      <category>search</category>
    </item>
    <item>
      <title>Manticore Search authentication rollout checklist for production</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Tue, 28 Jul 2026 06:15:49 +0000</pubDate>
      <link>https://dev.to/sanikolaev/manticore-search-authentication-rollout-checklist-for-production-5427</link>
      <guid>https://dev.to/sanikolaev/manticore-search-authentication-rollout-checklist-for-production-5427</guid>
      <description>&lt;p&gt;In production, "turn it on and done" almost never works. On a standalone node, the technical sequence is short: enable &lt;code&gt;auth&lt;/code&gt;, restart Manticore, create an admin user, and update clients. A topology with distributed tables or replication clusters needs extra preparation because nodes must authenticate to each other as well.&lt;/p&gt;

&lt;p&gt;Handle the rollout like a small release. Inventory clients and nodes, prepare the auth data, rehearse the procedure for your topology, and then cut over. A rehearsal exposes failures before the maintenance window.&lt;/p&gt;

&lt;p&gt;This checklist is for users who plan to enable authentication and want to roll it out as safely as possible in an existing system. Remember that authentication is disabled until you configure &lt;code&gt;auth&lt;/code&gt;; after cutover, clients that still omit credentials will fail.&lt;/p&gt;

&lt;p&gt;Choose the procedure by topology:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topology&lt;/th&gt;
&lt;th&gt;Migration requirement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;standalone node&lt;/td&gt;
&lt;td&gt;bootstrap the first administrator after enabling &lt;code&gt;auth&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;distributed tables and remote agents&lt;/td&gt;
&lt;td&gt;distribute one canonical auth store before authenticated remote queries begin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;replication cluster&lt;/td&gt;
&lt;td&gt;prepare the persisted cluster user and auth store before startup, then recover the cluster in a controlled order&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Phase 1: inventory before changing anything
&lt;/h2&gt;

&lt;p&gt;Start by listing every client that connects to Manticore Search, including each application and independently deployed application component. Do this before editing the config.&lt;/p&gt;

&lt;p&gt;Common things to check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;application search frontends&lt;/li&gt;
&lt;li&gt;ingest workers&lt;/li&gt;
&lt;li&gt;cron jobs&lt;/li&gt;
&lt;li&gt;dashboards and BI tools&lt;/li&gt;
&lt;li&gt;support or admin tools&lt;/li&gt;
&lt;li&gt;schema change/update scripts&lt;/li&gt;
&lt;li&gt;backup and maintenance scripts&lt;/li&gt;
&lt;li&gt;local scripts run manually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each client, record something like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Tables or clusters&lt;/th&gt;
&lt;th&gt;Needed actions&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;product search app&lt;/td&gt;
&lt;td&gt;HTTP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;products&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;read&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;will use Bearer token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;catalog ingest worker&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;products&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;write&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;uses password auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;schema migration job&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;products&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;schema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;run only during deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;access administrator&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;admin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;manages users and permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then confirm the deployment basics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this a standalone node, a distributed-table topology, a replication cluster, or a combination of them?&lt;/li&gt;
&lt;li&gt;Are you running RT mode or plain mode?&lt;/li&gt;
&lt;li&gt;In plain mode, where should the auth file live?&lt;/li&gt;
&lt;li&gt;Is &lt;code&gt;pid_file&lt;/code&gt; configured in the config? Bootstrap needs it.&lt;/li&gt;
&lt;li&gt;Are all participating nodes running a version that supports the same authentication protocol?&lt;/li&gt;
&lt;li&gt;Will SQL clients use SSL, and will HTTP clients use HTTPS when credentials cross a network?&lt;/li&gt;
&lt;li&gt;Where will credentials, including temporary Bearer tokens, be stored safely?&lt;/li&gt;
&lt;li&gt;Who is allowed to see the first admin password?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If replication is present, also record every cluster name and its persisted &lt;code&gt;user&lt;/code&gt; in each node's &lt;code&gt;&amp;lt;data_dir&amp;gt;/manticore.json&lt;/code&gt;. Back up the complete data directory, configuration, and any existing auth store before the rehearsal and again before production cutover.&lt;/p&gt;

&lt;p&gt;Do not skip the credential-handling questions. &lt;code&gt;CREATE USER&lt;/code&gt; returns a raw Bearer token; &lt;code&gt;TOKEN&lt;/code&gt; generates a new token for the specified user. &lt;code&gt;SHOW TOKEN&lt;/code&gt; later shows the stored token hash, not the raw token. If the raw token is lost, rotate it with &lt;code&gt;TOKEN&lt;/code&gt; and update the token in your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: set up least-privilege users
&lt;/h2&gt;

&lt;p&gt;Create users based on specific workloads, rather than granting permissions based on what someone might need later.&lt;/p&gt;

&lt;p&gt;Use a small matrix like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;User&lt;/th&gt;
&lt;th&gt;Permissions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;search frontend&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app_read&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GRANT read ON 'products' TO 'app_read'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ingest worker&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app_ingest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GRANT write ON 'products' TO 'app_ingest'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;schema migration job&lt;/td&gt;
&lt;td&gt;&lt;code&gt;schema_job&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GRANT schema ON 'products' TO 'schema_job'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;auth operator&lt;/td&gt;
&lt;td&gt;&lt;code&gt;security_admin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GRANT admin ON * TO 'security_admin'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;replication operator&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cluster_repl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GRANT replication ON 'posts' TO 'cluster_repl'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keep in mind that &lt;code&gt;admin&lt;/code&gt; is a narrow permission. It only allows managing authentication and authorization state; it does not imply &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;schema&lt;/code&gt;, or &lt;code&gt;replication&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That matters because a person who manages credentials does not automatically need to read business data. A service that searches products does not need to write documents. A migration job does not need to manage users.&lt;/p&gt;

&lt;p&gt;Plan negative permission tests as well. For every user you create, choose at least one thing it should be able to do and one thing it should not be able to do.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;app_read&lt;/code&gt; can search &lt;code&gt;products&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app_read&lt;/code&gt; cannot insert into &lt;code&gt;products&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app_ingest&lt;/code&gt; can write to &lt;code&gt;products&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app_ingest&lt;/code&gt; cannot manage auth.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;schema_job&lt;/code&gt; can change schema for &lt;code&gt;products&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;schema_job&lt;/code&gt; cannot read tables unless you grant &lt;code&gt;read&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Phase 3: test in staging
&lt;/h2&gt;

&lt;p&gt;Use a staging environment to rehearse the sequence of steps you plan to follow in production.&lt;/p&gt;

&lt;p&gt;In RT mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;searchd&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/lib/manticore&lt;/span&gt;
    &lt;span class="py"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
    &lt;span class="py"&gt;auth_log_level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;
    &lt;span class="err"&gt;...&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To turn auth off explicitly in RT mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;searchd&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/lib/manticore&lt;/span&gt;
    &lt;span class="py"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
    &lt;span class="err"&gt;...&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In plain mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;searchd&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/lib/manticore/auth.json&lt;/span&gt;
    &lt;span class="py"&gt;auth_log_level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;
    &lt;span class="err"&gt;...&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the auth file private. Before the first bootstrap, Manticore may create an empty auth file. After bootstrap, that file stores auth data and credential hashes.&lt;/p&gt;

&lt;p&gt;This bootstrap sequence works on a standalone node or on an isolated temporary daemon that prepares auth data for several nodes. Never start an existing replication data directory with an empty auth store. Its persisted cluster user will be missing, and Manticore may skip the cluster descriptor.&lt;/p&gt;

&lt;p&gt;Start &lt;code&gt;searchd&lt;/code&gt;, then bootstrap the first administrator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;searchd &lt;span class="nt"&gt;--config&lt;/span&gt; /etc/manticoresearch/manticore.conf &lt;span class="nt"&gt;--auth&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For scripted setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'admin\nStrongPass#2026\nStrongPass#2026\n'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  searchd &lt;span class="nt"&gt;--config&lt;/span&gt; /etc/manticoresearch/manticore.conf &lt;span class="nt"&gt;--auth-non-interactive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Warning: The command above includes the password in plain text. In production automation, feed the three input lines to standard input from your secret-management system.&lt;/p&gt;

&lt;p&gt;Bootstrap creates the first administrator with all actions, including &lt;code&gt;replication&lt;/code&gt;; no additional grant is needed. The command does not return a bearer token. If the administrator needs HTTP Bearer access, connect as that user and run &lt;code&gt;TOKEN&lt;/code&gt;, or use the HTTP &lt;code&gt;POST /token&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;For a multi-node deployment, create the auth store once. Use a temporary daemon with its own empty data directory, PID file, and listeners. Bootstrap the administrator and shared service users, then stop the daemon cleanly. Copy the resulting auth store to every participating node before enabling authenticated node-to-node traffic. Do not recreate the same users independently: matching names and passwords can still produce different stored authentication material.&lt;/p&gt;

&lt;p&gt;Next, create staging users based on the notes from the previous phases. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'app_read'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'ReadPass#2026'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="s1"&gt;'products'&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'app_read'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'app_ingest'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'IngestPass#2026'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;write&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="s1"&gt;'products'&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'app_ingest'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'schema_job'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'SchemaPass#2026'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="s1"&gt;'products'&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'schema_job'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'security_admin'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'AdminPass#2026'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;admin&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'security_admin'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store the returned Bearer tokens in secure storage. Remember: raw tokens must not be left in logs, shell history, or unprotected files. If you need to rotate one, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="s1"&gt;'app_read'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SHOW TOKEN&lt;/code&gt; is not a way to recover the raw token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="s1"&gt;'app_read'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review users and permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;USERS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;PERMISSIONS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;PERMISSIONS&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="s1"&gt;'app_read'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run one allow and one deny test for every user. For the read-only user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;app_read_token&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://127.0.0.1:9308/sql?mode&lt;span class="o"&gt;=&lt;/span&gt;raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM products LIMIT 10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then verify that an unauthorized operation is denied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;app_read_token&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://127.0.0.1:9308/sql?mode&lt;span class="o"&gt;=&lt;/span&gt;raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"INSERT INTO products(id,title) VALUES(1,'test')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this HTTP request, expect a &lt;code&gt;403 Forbidden&lt;/code&gt; response. Over SQL/MySQL, a permission denial returns &lt;code&gt;ERROR 1045&lt;/code&gt; with a permission-denied message.&lt;/p&gt;

&lt;p&gt;SQL clients should connect with a Manticore user name and password. The SQL/MySQL protocol in Manticore supports &lt;code&gt;mysql_native_password&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MYSQL_PWD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ReadPass#2026 &lt;span class="se"&gt;\&lt;/span&gt;
  mysql &lt;span class="nt"&gt;-h127&lt;/span&gt;.0.0.1 &lt;span class="nt"&gt;-P9306&lt;/span&gt; &lt;span class="nt"&gt;-uapp_read&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM products LIMIT 10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTTP clients can use Basic authentication or Bearer tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-u&lt;/span&gt; app_read:ReadPass#2026 &lt;span class="se"&gt;\&lt;/span&gt;
  http://127.0.0.1:9308/sql?mode&lt;span class="o"&gt;=&lt;/span&gt;raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM products LIMIT 10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTTP authentication schemes (&lt;code&gt;Basic&lt;/code&gt;, &lt;code&gt;Bearer&lt;/code&gt;) are case-insensitive; user names are case-sensitive.&lt;/p&gt;

&lt;p&gt;If you edit the auth file outside the daemon during maintenance, reload it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;RELOAD&lt;/span&gt; &lt;span class="n"&gt;AUTH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Phase 4: production rollout checklist
&lt;/h2&gt;

&lt;p&gt;Use this checklist for every deployment, then follow the procedure for your topology.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Confirm you have a current configuration and data-directory backup.&lt;/li&gt;
&lt;li&gt;[ ] Back up &lt;code&gt;manticore.json&lt;/code&gt; and the existing auth store separately.&lt;/li&gt;
&lt;li&gt;[ ] Confirm existing network protections stay in place.&lt;/li&gt;
&lt;li&gt;[ ] Confirm &lt;code&gt;pid_file&lt;/code&gt; is set in the config.&lt;/li&gt;
&lt;li&gt;[ ] Confirm where the auth file will be created or loaded from.&lt;/li&gt;
&lt;li&gt;[ ] Confirm password and token storage is ready.&lt;/li&gt;
&lt;li&gt;[ ] Confirm every participating node runs a compatible Manticore version.&lt;/li&gt;
&lt;li&gt;[ ] Rehearse the same topology and restart order in staging.&lt;/li&gt;
&lt;li&gt;[ ] Prepare one canonical auth store for users shared across nodes.&lt;/li&gt;
&lt;li&gt;[ ] Choose the standalone, distributed, or replication procedure below.&lt;/li&gt;
&lt;li&gt;[ ] Enable auth during a planned maintenance window.&lt;/li&gt;
&lt;li&gt;[ ] Expect unauthenticated clients to fail after auth is enabled.&lt;/li&gt;
&lt;li&gt;[ ] Store tokens in a protected secrets store immediately after they are issued. Do not store raw tokens in files, shell history, or logs.&lt;/li&gt;
&lt;li&gt;[ ] Update SQL connection code to send user names and passwords.&lt;/li&gt;
&lt;li&gt;[ ] Update HTTP connection code to use Basic authentication or Bearer tokens.&lt;/li&gt;
&lt;li&gt;[ ] Run the allow and deny tests from staging.&lt;/li&gt;
&lt;li&gt;[ ] Verify internal node-to-node operations when the deployment has remote agents or replication.&lt;/li&gt;
&lt;li&gt;[ ] Check the auth log.&lt;/li&gt;
&lt;li&gt;[ ] Run application stress tests that cover search, ingest, dashboards, and maintenance scripts.&lt;/li&gt;
&lt;li&gt;[ ] Rotate any temporary rollout credentials.&lt;/li&gt;
&lt;li&gt;[ ] Keep the first admin credential out of normal application use.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Standalone node
&lt;/h3&gt;

&lt;p&gt;For a standalone node, the direct bootstrap sequence is sufficient:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stop Manticore cleanly and take the final backup.&lt;/li&gt;
&lt;li&gt;Configure &lt;code&gt;auth&lt;/code&gt; and start &lt;code&gt;searchd&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Bootstrap the first administrator with &lt;code&gt;searchd --config &amp;lt;path&amp;gt; --auth&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create the production users and permissions.&lt;/li&gt;
&lt;li&gt;Update clients and run the planned allow and deny tests.&lt;/li&gt;
&lt;li&gt;Confirm that existing tables and known rows are still available.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Distributed tables and remote agents
&lt;/h3&gt;

&lt;p&gt;Distributed queries send remote-agent requests as the current session user. Each remote node must have the same stored authentication material for that user and grant the required permission on the remote table.&lt;/p&gt;

&lt;p&gt;For a new rollout across distributed nodes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the shared users once in the isolated bootstrap daemon from Phase 3.&lt;/li&gt;
&lt;li&gt;Stop the affected agents and masters for the coordinated cutover.&lt;/li&gt;
&lt;li&gt;Configure &lt;code&gt;auth&lt;/code&gt; and place the same canonical auth store on every participating node. Preserve restrictive ownership and permissions, and compare checksums.&lt;/li&gt;
&lt;li&gt;Start remote agents before the masters that query them.&lt;/li&gt;
&lt;li&gt;Test a direct authenticated query on each agent, followed by the equivalent distributed query through the master.&lt;/li&gt;
&lt;li&gt;Create node-local users only after shared traffic works, and keep shared user records synchronized whenever their passwords, tokens, or permissions change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Create shared users only once. Independently created accounts can have different stored authentication material even when their names and passwords match.&lt;/p&gt;

&lt;h3&gt;
  
  
  Existing replication cluster
&lt;/h3&gt;

&lt;p&gt;Moving an existing unauthenticated replication cluster to auth requires a coordinated restart. Do not enable &lt;code&gt;auth&lt;/code&gt; and bootstrap the first user against the existing cluster data. The empty store does not contain the persisted cluster user, so Manticore may skip the cluster descriptor.&lt;/p&gt;

&lt;p&gt;Use this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;While the unauthenticated cluster is healthy, choose its future replication identity and persist it:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;   &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt; &lt;span class="s1"&gt;'cluster_repl'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UPDATE user&lt;/code&gt; writes the name to the cluster metadata. Authentication is still disabled, so Manticore neither creates nor checks the account at this point. Create it in step 3, before restarting any real cluster node with authentication enabled.&lt;/p&gt;

&lt;p&gt;Verify that every node's &lt;code&gt;&amp;lt;data_dir&amp;gt;/manticore.json&lt;/code&gt; now stores &lt;code&gt;"user": "cluster_repl"&lt;/code&gt; for the cluster. If a node hosts several clusters, update each cluster to a user that will exist in the new auth store, or create and grant every persisted user before cutover.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Stop all cluster nodes cleanly and use the replication state to select the safe primary. After a clean shutdown, this is normally the node stopped last, with &lt;code&gt;safe_to_bootstrap: 1&lt;/code&gt; in its &lt;code&gt;&amp;lt;data_dir&amp;gt;/grastate.dat&lt;/code&gt;. See &lt;a href="https://manual.manticoresearch.com/Creating_a_cluster/Setting_up_replication/Restarting_a_cluster" rel="noopener noreferrer"&gt;Restarting a cluster&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bootstrap &lt;code&gt;cluster_repl&lt;/code&gt; as the first administrator in the isolated temporary daemon. The first administrator already has every action, including &lt;code&gt;replication&lt;/code&gt;, so it needs no additional grant. If the cluster will use a separate least-privilege identity, create it once and grant &lt;code&gt;replication&lt;/code&gt; before distributing the store.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stop the temporary daemon. Configure &lt;code&gt;auth&lt;/code&gt; on every real cluster node and copy the exact generated auth store to each one. Keep the files private and byte-identical. Do not start a real cluster node before this store is in place.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Restart a two-node cluster in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start the non-primary peer normally and wait only for its daemon listener. Its cluster can report &lt;code&gt;closed&lt;/code&gt; at this point; do not write to it.&lt;/li&gt;
&lt;li&gt;Start the recorded safe primary with &lt;code&gt;--new-cluster&lt;/code&gt;, or use the corresponding &lt;code&gt;manticore_new_cluster&lt;/code&gt; service action.&lt;/li&gt;
&lt;li&gt;Wait for the safe primary to report &lt;code&gt;cluster_products_status=primary&lt;/code&gt; and &lt;code&gt;cluster_products_node_state=synced&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Stop the first peer cleanly and start it normally again. Wait for the same &lt;code&gt;primary&lt;/code&gt; and &lt;code&gt;synced&lt;/code&gt; values on that peer. Never use &lt;code&gt;--new-cluster&lt;/code&gt; on it.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The safe primary reads the persisted cluster user from a descriptor peer during startup, so the initial peer must already be listening. Starting the safe primary alone can fail with &lt;code&gt;failed to fetch donor user from any node&lt;/code&gt; even when the auth store is correct. For a larger cluster, rehearse the sequence in staging. Use one non-primary node as the initial metadata peer, establish the safe primary, and then start or restart the remaining peers normally.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On each node, check that the cluster component is primary, the local node is synchronized, and the pre-migration data is present:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;   &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'cluster_products_status'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'cluster_products_node_state'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;existing_table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait until the two status values are &lt;code&gt;primary&lt;/code&gt; and &lt;code&gt;synced&lt;/code&gt;, respectively, before treating the node as writable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Before returning traffic, make a disposable one-row table and add it to the recovered cluster:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;   &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;migration_control&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;migration_control&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'post-auth control'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="n"&gt;migration_control&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm that the peer returns one row from &lt;code&gt;products:migration_control&lt;/code&gt;. If this check fails after the pre-migration data checks passed, investigate table transfer rather than the migration itself.&lt;/p&gt;

&lt;p&gt;After the cluster is healthy, create the remaining administrative users and, if needed, a dedicated least-privilege replication user. Change the stored cluster identity only after the new user and its auth data are visible on every node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'repluser'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;strong-secret&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="n"&gt;replication&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'repluser'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt; &lt;span class="s1"&gt;'repluser'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not remove the bootstrap administrator until another administrator and the final replication identity have both been verified.&lt;/p&gt;

&lt;p&gt;When a node joins an authenticated cluster, the donor's auth data replaces the joining node's local auth data. At &lt;code&gt;info&lt;/code&gt; or a more verbose auth log level, Manticore writes the previous data to &lt;code&gt;searchd.log.auth&lt;/code&gt; as a backup. The log can contain salts and credential hashes, so restrict access and redact it before sharing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication logging during cutover
&lt;/h3&gt;

&lt;p&gt;Authentication events are written to a separate auth log when authentication is enabled. If the daemon log is &lt;code&gt;/var/log/manticore/searchd.log&lt;/code&gt;, the auth log is &lt;code&gt;/var/log/manticore/searchd.log.auth&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;auth_log_level&lt;/code&gt; values are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;disabled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;error&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;warning&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;info&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;all&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;trace&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The default is &lt;code&gt;info&lt;/code&gt;. Start there unless you have a reason to reduce or increase the logging detail. Use &lt;code&gt;trace&lt;/code&gt; only for diagnostics; it also includes all successful internal auth traffic.&lt;/p&gt;

&lt;p&gt;Useful cleanup and maintenance commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'NewReadPass#2026'&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="s1"&gt;'app_read'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;REVOKE&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="s1"&gt;'products'&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="s1"&gt;'app_read'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'app_read'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SET PASSWORD&lt;/code&gt; changes the password used by SQL/MySQL and HTTP Basic auth. It does not revoke existing bearer tokens. To rotate Bearer access, create a new token with &lt;code&gt;TOKEN&lt;/code&gt; or &lt;code&gt;POST /token&lt;/code&gt; and update the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 5: rollback and troubleshooting
&lt;/h2&gt;

&lt;p&gt;For a standalone node, restore the previous configuration and network restrictions, restart Manticore, and revert the client configuration if necessary.&lt;/p&gt;

&lt;p&gt;Roll back all communicating nodes together. A mix of authenticated and unauthenticated nodes will not work. Restore the same configuration and auth state on each node, then restart remote agents before their masters.&lt;/p&gt;

&lt;p&gt;Keep the pre-cutover &lt;code&gt;manticore.json&lt;/code&gt; backup for every replication cluster. If a node started with auth before the persisted cluster user existed, stop it and compare the current descriptor with the backup. A clean stop may have saved the skipped state without the cluster descriptor; restore the backed-up descriptor before retrying. Do not recreate clustered tables or delete their data.&lt;/p&gt;

&lt;p&gt;Do not delete the rollout notes. They are usually the fastest way to see which client was updated, which token was stored where, and which permissions were created.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQL access denied&lt;/td&gt;
&lt;td&gt;Wrong user, wrong password, or client auth mismatch&lt;/td&gt;
&lt;td&gt;Check the configured user and confirm the client can use &lt;code&gt;mysql_native_password&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 401&lt;/td&gt;
&lt;td&gt;Missing or invalid credentials&lt;/td&gt;
&lt;td&gt;Check the &lt;code&gt;Authorization&lt;/code&gt; header and whether the client uses Basic auth or Bearer token auth.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 403&lt;/td&gt;
&lt;td&gt;User authenticated but lacks permission&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;SHOW PERMISSIONS FOR '&amp;lt;user&amp;gt;'&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bearer token does not work&lt;/td&gt;
&lt;td&gt;Token was lost, copied incorrectly, or already revoked&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;TOKEN '&amp;lt;user&amp;gt;'&lt;/code&gt;, store the returned token, and update the client.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User has fewer permissions than expected&lt;/td&gt;
&lt;td&gt;Missing action grant&lt;/td&gt;
&lt;td&gt;Check whether the operation needs &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;schema&lt;/code&gt;, &lt;code&gt;replication&lt;/code&gt;, or &lt;code&gt;admin&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User has more permissions than expected&lt;/td&gt;
&lt;td&gt;Broad target or missing explicit deny&lt;/td&gt;
&lt;td&gt;Check wildcard grants, exact target grants, and any &lt;code&gt;WITH ALLOW 0&lt;/code&gt; rules.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distributed query is denied remotely&lt;/td&gt;
&lt;td&gt;Shared user is missing, differs, or lacks permission on the agent&lt;/td&gt;
&lt;td&gt;Compare the auth stores and permissions on the master and agent.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing cluster is skipped at startup&lt;/td&gt;
&lt;td&gt;Persisted cluster user is missing from the auth store or lacks &lt;code&gt;replication&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;manticore.json&lt;/code&gt;, &lt;code&gt;SHOW PERMISSIONS&lt;/code&gt;, and the pre-cutover backup before restarting.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failed to fetch donor user from any node&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No descriptor peer is available, or daemon-to-daemon authentication failed&lt;/td&gt;
&lt;td&gt;Check the restart order, peer availability, and &lt;code&gt;searchd.log.auth&lt;/code&gt; on both nodes.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Permission rules are determined by action type. When rules conflict, an explicit deny always takes precedence over an allow, even if the allow is more specific. If no matching allow exists, access is denied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final check
&lt;/h2&gt;

&lt;p&gt;Before calling the rollout done, confirm that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] You used the procedure for the deployment's topology.&lt;/li&gt;
&lt;li&gt;[ ] Every isolated part of the system that uses Manticore Search has its own user.&lt;/li&gt;
&lt;li&gt;[ ] Every user has only the actions it needs.&lt;/li&gt;
&lt;li&gt;[ ] Users shared between nodes have the same stored authentication material.&lt;/li&gt;
&lt;li&gt;[ ] Bearer tokens are stored in a protected secrets store; raw tokens are not saved outside a controlled environment.&lt;/li&gt;
&lt;li&gt;[ ] The operations team knows that &lt;code&gt;SHOW TOKEN&lt;/code&gt; does not return the raw token, but shows its hash; use &lt;code&gt;TOKEN&lt;/code&gt; or the HTTP endpoint to get a new token.&lt;/li&gt;
&lt;li&gt;[ ] SQL and HTTP clients have been updated.&lt;/li&gt;
&lt;li&gt;[ ] Expected denials were tested.&lt;/li&gt;
&lt;li&gt;[ ] You tested distributed queries or replication operations when applicable.&lt;/li&gt;
&lt;li&gt;[ ] Every migrated replication cluster is &lt;code&gt;synced&lt;/code&gt;, and its existing data is present on every node.&lt;/li&gt;
&lt;li&gt;[ ] Auth logs are visible.&lt;/li&gt;
&lt;li&gt;[ ] The rollback procedure is clear and documented.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We wish you a smooth authentication and authorization rollout!&lt;/p&gt;

</description>
      <category>database</category>
      <category>infrastructure</category>
      <category>production</category>
      <category>security</category>
    </item>
    <item>
      <title>How to secure Manticore Search with built-in authentication and authorization</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Mon, 27 Jul 2026 05:54:33 +0000</pubDate>
      <link>https://dev.to/sanikolaev/how-to-secure-manticore-search-with-built-in-authentication-and-authorization-5f8d</link>
      <guid>https://dev.to/sanikolaev/how-to-secure-manticore-search-with-built-in-authentication-and-authorization-5f8d</guid>
      <description>&lt;p&gt;Search is often treated as infrastructure. In production, though, it behaves much more like an application API. It accepts user traffic, exposes business data, powers dashboards, and often sits close to records that shouldn't be open to every client on the network.&lt;/p&gt;

&lt;p&gt;Manticore Search now (since release &lt;a href=""&gt;27.1.5&lt;/a&gt; has built-in authentication and authorization for SQL over the MySQL protocol, HTTP/HTTPS endpoints, and replication-related operations.&lt;/p&gt;

&lt;p&gt;Authentication answers "who is calling?" Authorization answers "what is this user allowed to do?"&lt;/p&gt;

&lt;p&gt;Users who already work with Manticore can access the new functionality while keeping their familiar Manticore workflows. Existing SQL and HTTP clients retain their usual connection patterns. Applications require only minimal changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Manticore Adds
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SQL/MySQL password authentication with &lt;code&gt;mysql_native_password&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;HTTP Basic authentication with the same user name and password.&lt;/li&gt;
&lt;li&gt;HTTP Bearer tokens for clients that shouldn't send a password on every request.&lt;/li&gt;
&lt;li&gt;Permissions for five clear actions: &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;schema&lt;/code&gt;, &lt;code&gt;replication&lt;/code&gt;, and &lt;code&gt;admin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Targets such as &lt;code&gt;products&lt;/code&gt;, &lt;code&gt;logs_*&lt;/code&gt;, &lt;code&gt;posts&lt;/code&gt;, and &lt;code&gt;*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;SQL commands for managing users, tokens, and permissions.&lt;/li&gt;
&lt;li&gt;Auth logging with &lt;code&gt;disabled&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, &lt;code&gt;info&lt;/code&gt;, &lt;code&gt;all&lt;/code&gt;, and &lt;code&gt;trace&lt;/code&gt; levels. The default is &lt;code&gt;info&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The access model is deliberately designed small. You just create users, grant the actions they need, update your app to send credentials, and test that denied operations are actually denied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable Authentication
&lt;/h2&gt;

&lt;p&gt;Authentication is controlled by the &lt;code&gt;auth&lt;/code&gt; setting in the &lt;code&gt;searchd&lt;/code&gt; section.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://manual.manticoresearch.com/Read_this_first#Real-time-mode-vs-plain-mode" rel="noopener noreferrer"&gt;RT mode&lt;/a&gt;, use &lt;code&gt;auth = 1&lt;/code&gt;. Manticore stores auth data in &lt;code&gt;auth.json&lt;/code&gt; under &lt;code&gt;data_dir&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;searchd&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/lib/manticore&lt;/span&gt;
    &lt;span class="py"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
    &lt;span class="py"&gt;auth_log_level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To disable authentication explicitly in RT mode, use &lt;code&gt;auth = 0&lt;/code&gt; or remove the setting.&lt;/p&gt;

&lt;p&gt;In plain mode, set &lt;code&gt;auth&lt;/code&gt; to the auth file path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;searchd&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/lib/manticore/auth.json&lt;/span&gt;
    &lt;span class="py"&gt;auth_log_level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use SSL for SQL connections or HTTPS for HTTP clients when passwords or bearer tokens cross a network.&lt;/p&gt;

&lt;p&gt;Keep the auth file private. Before the first bootstrap, Manticore may create an empty auth file. After bootstrap, that file stores auth data and credential hashes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bootstrap the First Administrator
&lt;/h2&gt;

&lt;p&gt;After enabling &lt;code&gt;auth&lt;/code&gt;, start &lt;code&gt;searchd&lt;/code&gt;. Then create the first administrator with the same configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;searchd &lt;span class="nt"&gt;--config&lt;/span&gt; /etc/manticoresearch/manticore.conf &lt;span class="nt"&gt;--auth&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For automation, use the non-interactive bootstrap mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'admin\nStrongPass#2026\nStrongPass#2026\n'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  searchd &lt;span class="nt"&gt;--config&lt;/span&gt; /etc/manticoresearch/manticore.conf &lt;span class="nt"&gt;--auth-non-interactive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bootstrap requires a running daemon. It creates the first administrator and grants that user all actions.&lt;/p&gt;

&lt;p&gt;It doesn't return a bearer token. If the admin user needs HTTP Bearer access, connect as that admin and run &lt;code&gt;TOKEN&lt;/code&gt;, or call the HTTP &lt;code&gt;POST /token&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create Users
&lt;/h2&gt;

&lt;p&gt;After initializing the administrator, you can use the account to manage users and permissions through SQL commands. However, do not use it in the application. Instead, create separate users for specific tasks.&lt;/p&gt;

&lt;p&gt;For example, a search frontend that only reads from &lt;code&gt;products&lt;/code&gt; needs &lt;code&gt;read&lt;/code&gt;, not &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;schema&lt;/code&gt;, &lt;code&gt;replication&lt;/code&gt;, or &lt;code&gt;admin&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'app_read'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'ReadPass#2026'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="s1"&gt;'products'&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'app_read'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CREATE USER&lt;/code&gt; returns a raw bearer token for the new user. Store it immediately. Manticore won't show the raw token again.&lt;/p&gt;

&lt;p&gt;To create or rotate a bearer token later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="s1"&gt;'app_read'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TOKEN 'app_read'&lt;/code&gt; returns a new raw token that is ready to use. &lt;code&gt;SHOW TOKEN&lt;/code&gt; shows the stored token hash, not the token to send in an HTTP request. This is useful for verification and audit: you can confirm that a token exists or changed after rotation without exposing the raw secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="s1"&gt;'app_read'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SET PASSWORD&lt;/code&gt; changes the password used by SQL/MySQL and HTTP Basic auth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'NewReadPass#2026'&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="s1"&gt;'app_read'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It doesn't revoke existing bearer tokens. To rotate Bearer access, create a new token with &lt;code&gt;TOKEN&lt;/code&gt; or &lt;code&gt;POST /token&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="s1"&gt;'app_read'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An ingest pipeline can get &lt;code&gt;write&lt;/code&gt; without broader access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'app_ingest'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'IngestPass#2026'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;write&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="s1"&gt;'products'&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'app_ingest'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A table schema migration job can get &lt;code&gt;schema&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'schema_job'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'SchemaPass#2026'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="s1"&gt;'products'&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'schema_job'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An auth administrator can get &lt;code&gt;admin&lt;/code&gt; privileges:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'security_admin'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'AdminPass#2026'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;admin&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'security_admin'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;admin&lt;/code&gt; privileges only manage authentication and authorization state. They don't imply &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;schema&lt;/code&gt;, or &lt;code&gt;replication&lt;/code&gt;. Grant those separately when the same user really needs them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect with SQL and HTTP
&lt;/h2&gt;

&lt;p&gt;mysql clients authenticate with a Manticore user name and password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MYSQL_PWD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ReadPass#2026 &lt;span class="se"&gt;\&lt;/span&gt;
  mysql &lt;span class="nt"&gt;-h127&lt;/span&gt;.0.0.1 &lt;span class="nt"&gt;-P9306&lt;/span&gt; &lt;span class="nt"&gt;-uapp_read&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM products LIMIT 10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTTP/HTTPS clients can use Basic authentication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-u&lt;/span&gt; app_read:ReadPass#2026 &lt;span class="se"&gt;\&lt;/span&gt;
  http://127.0.0.1:9308/sql?mode&lt;span class="o"&gt;=&lt;/span&gt;raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM products LIMIT 10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They can also use a bearer token returned by the &lt;code&gt;CREATE USER&lt;/code&gt;, &lt;code&gt;TOKEN&lt;/code&gt;, or &lt;code&gt;POST /token&lt;/code&gt; commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;app_read_token&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://127.0.0.1:9308/sql?mode&lt;span class="o"&gt;=&lt;/span&gt;raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM products LIMIT 10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Basic&lt;/code&gt; and &lt;code&gt;Bearer&lt;/code&gt; authentication schemes are case-insensitive. User names are case-sensitive.&lt;/p&gt;

&lt;p&gt;The permission check is the same either way. The client authenticates, Manticore identifies the user, and checks the requested action against that user's permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing After Enabling Authentication
&lt;/h2&gt;

&lt;p&gt;Tip: when enabling access to Manticore, don't stop at confirming that the client can connect after authentication is enabled. Also test denied access and how your application reacts to it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;app_read&lt;/code&gt; user should be able to read from &lt;code&gt;products&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;app_read_token&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://127.0.0.1:9308/sql?mode&lt;span class="o"&gt;=&lt;/span&gt;raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM products LIMIT 10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same user shouldn't be able to write to &lt;code&gt;products&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;app_read_token&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://127.0.0.1:9308/sql?mode&lt;span class="o"&gt;=&lt;/span&gt;raw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"INSERT INTO products(id,title) VALUES(1,'test')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That error is expected. Over HTTP, a valid user without permission gets &lt;code&gt;403 Forbidden&lt;/code&gt;. Over SQL/MySQL, Manticore reports &lt;code&gt;ERROR 1045&lt;/code&gt; with a permission-denied message.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Manticore Checks What Is Allowed and Denied
&lt;/h2&gt;

&lt;p&gt;Manticore permissions are action and target rules. If no rule allows the requested action on the requested target, access is denied.&lt;/p&gt;

&lt;p&gt;The short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rules are matched for the requested action only. &lt;code&gt;admin&lt;/code&gt; doesn't satisfy &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;schema&lt;/code&gt;, or &lt;code&gt;replication&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WITH ALLOW 0&lt;/code&gt; creates an explicit deny.&lt;/li&gt;
&lt;li&gt;Any matching explicit deny wins, including over more specific allow rules.&lt;/li&gt;
&lt;li&gt;If no matching allow exists, access is denied.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'analyst'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'AnalystPass#2026'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'analyst'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="s1"&gt;'private_logs'&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'analyst'&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;ALLOW&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;analyst&lt;/code&gt; user can read other tables, but can't read &lt;code&gt;private_logs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A wildcard deny plus an exact allow can't be used as an exception pattern. If the deny matches the request, it still wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll Out Access on an Existing Deployment
&lt;/h2&gt;

&lt;p&gt;For an existing Manticore deployment, handle auth as a staged rollout rather than switching everything at once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inventory SQL and HTTP clients that connect to Manticore.&lt;/li&gt;
&lt;li&gt;Choose the auth file path: &lt;code&gt;auth.json&lt;/code&gt; under &lt;code&gt;data_dir&lt;/code&gt; in RT mode, or an explicit path in plain mode.&lt;/li&gt;
&lt;li&gt;Enable auth in staging.&lt;/li&gt;
&lt;li&gt;Bootstrap the first administrator.&lt;/li&gt;
&lt;li&gt;Create least-privilege users for search, ingest, schema changes, replication, and auth administration.&lt;/li&gt;
&lt;li&gt;Update your application's SQL connection code to send user names and passwords.&lt;/li&gt;
&lt;li&gt;Update your HTTP/HTTPS connection code to use Basic authentication or Bearer tokens.&lt;/li&gt;
&lt;li&gt;Verify expected success and expected denial for each application user.&lt;/li&gt;
&lt;li&gt;Configure an appropriate auth logging level and make sure the log is stored safely with the rest of your operational logs.&lt;/li&gt;
&lt;li&gt;Roll out gradually, and after cutover rotate credentials for extra safety and an additional test and training pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Distributed tables need one extra check. Remote-agent queries authenticate as the current session user, so the remote daemon needs matching auth material for that user and the required permission on the remote target table.&lt;/p&gt;

&lt;p&gt;Replication clusters use the &lt;code&gt;replication&lt;/code&gt; action. When auth is enabled, cluster joins can replace local auth data with auth data from the donor cluster, so keep auth material consistent across nodes before joining. During cluster work, handle the auth log (by default, the &lt;code&gt;searchd.log.auth&lt;/code&gt; file) carefully because it can contain salts and credential hashes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Reference
&lt;/h2&gt;

&lt;p&gt;The manual page covers the full command set and operational details for the new functionality, including password policy, auth logging levels, &lt;code&gt;SHOW USERS&lt;/code&gt;, &lt;code&gt;SHOW PERMISSIONS&lt;/code&gt;, &lt;code&gt;RELOAD AUTH&lt;/code&gt;, distributed remote agents, and replication clusters:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://manual.manticoresearch.com/Security/Authentication_and_authorization" rel="noopener noreferrer"&gt;Authentication and authorization manual&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But for most applications, the path is direct: enable auth, bootstrap an admin, create least-privilege users, update clients, and verify that the right requests are allowed while the wrong ones are denied.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>search</category>
      <category>security</category>
    </item>
    <item>
      <title>Meilisearch vs Manticore: Setting the Record Straight</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Fri, 24 Jul 2026 13:41:44 +0000</pubDate>
      <link>https://dev.to/sanikolaev/meilisearch-vs-manticore-setting-the-record-straight-289p</link>
      <guid>https://dev.to/sanikolaev/meilisearch-vs-manticore-setting-the-record-straight-289p</guid>
      <description>&lt;p&gt;A few days ago, Meilisearch published &lt;a href="https://www.meilisearch.com/blog/meilisearch-vs-manticore" rel="noopener noreferrer"&gt;a comparison of Meilisearch and Manticore Search&lt;/a&gt;. We like comparisons — we &lt;a href="https://manticoresearch.com/blog/manticoresearch-vs-meilisearch/" rel="noopener noreferrer"&gt;published our own&lt;/a&gt; back in 2023, with benchmarks you can reproduce. And to be fair, parts of the Meilisearch's comparison are accurate and even generous toward Manticore.&lt;/p&gt;

&lt;p&gt;But several claims about Manticore are outdated or simply not what you observe when you run the current version. So instead of arguing, we did what we always do: spun up the latest versions of both engines (Manticore Search 28.4.4 and Meilisearch 1.41/1.48) and tested the claims. Every command and response below is real — feel free to copy-paste and check us.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Full-text search and real-time analytics" — that's maybe 1% of it
&lt;/h2&gt;

&lt;p&gt;The article introduces Manticore like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Manticore Search is an open-source search engine built as a continuation of Sphinx, improving its functionality with full-text search and real-time analytics.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sphinx already &lt;em&gt;was&lt;/em&gt; full-text search, and real-time analytics is a small slice of what's been added since. Here's what "improving its functionality" actually looks like over the years:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://manticoresearch.com/blog/rt_vs_plain_mode/" rel="noopener noreferrer"&gt;RT mode&lt;/a&gt;&lt;/strong&gt; — Sphinx had RT indexes, but now the whole engine runs without config files: &lt;code&gt;CREATE TABLE&lt;/code&gt;, insert, and search on the fly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://manticoresearch.com/blog/mike-replication/" rel="noopener noreferrer"&gt;Replication&lt;/a&gt;&lt;/strong&gt; — Galera-based synchronous cluster replication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A full HTTP JSON API&lt;/strong&gt; — nearly everything SQL can do, plus Elasticsearch-compatible bulk endpoints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://manticoresearch.com/blog/create_table/" rel="noopener noreferrer"&gt;Auto-schema&lt;/a&gt;&lt;/strong&gt; — insert data without creating a table first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://manticoresearch.com/blog/vector-search-deep-dive/" rel="noopener noreferrer"&gt;Vector search&lt;/a&gt;&lt;/strong&gt; with &lt;a href="https://manticoresearch.com/blog/auto-embeddings/" rel="noopener noreferrer"&gt;auto-embeddings&lt;/a&gt;, &lt;a href="https://manticoresearch.com/blog/quantization/" rel="noopener noreferrer"&gt;quantization&lt;/a&gt;, and &lt;a href="https://manticoresearch.com/blog/knn-prefiltering/" rel="noopener noreferrer"&gt;filtered KNN&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://manticoresearch.com/blog/hybrid-search/" rel="noopener noreferrer"&gt;Hybrid search&lt;/a&gt;&lt;/strong&gt; — full-text + semantic fused with Reciprocal Rank Fusion in one query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://manticoresearch.com/blog/new-fuzzy-search-and-autocomplete/" rel="noopener noreferrer"&gt;Fuzzy search&lt;/a&gt; and &lt;a href="https://manticoresearch.com/blog/autocomplete-the-predictive-search/" rel="noopener noreferrer"&gt;autocomplete&lt;/a&gt;&lt;/strong&gt; — typo tolerance, keyboard-layout awareness, suggestions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://manticoresearch.com/blog/mcl/" rel="noopener noreferrer"&gt;Columnar storage&lt;/a&gt;&lt;/strong&gt; and secondary indexes — for datasets larger than RAM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://manticoresearch.com/blog/manticore-search-27-1-5/" rel="noopener noreferrer"&gt;Sharded tables, authentication, and conversational search&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://manticoresearch.com/blog/integration-with-kafka/" rel="noopener noreferrer"&gt;Kafka integration&lt;/a&gt;&lt;/strong&gt;, &lt;a href="https://manticoresearch.com/blog/kibana-demo/" rel="noopener noreferrer"&gt;Kibana&lt;/a&gt; and &lt;a href="https://manticoresearch.com/blog/grafana-dashboard-docker/" rel="noopener noreferrer"&gt;Grafana&lt;/a&gt; support, an &lt;a href="https://manticoresearch.com/blog/mcp-manticore-server/" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; for AI assistants&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Sphinx plus real-time analytics" undersells that by a wide margin.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Complex setup" and "advanced customization"? Let's count commands
&lt;/h2&gt;

&lt;p&gt;The article's verdict on Manticore:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Best for: Large-scale projects and teams that need advanced customization, SQL familiarity, and high-performance querying at scale.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And among the cons: "Complex setup: Initial configuration and tuning can be time-consuming."&lt;/p&gt;

&lt;p&gt;Here is the entire "complex setup" of Manticore, from zero to searching — three commands, no config files, no schema:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Install (and start — &lt;a href="https://manticoresearch.com/blog/one-line-installer/" rel="noopener noreferrer"&gt;the one-line installer&lt;/a&gt; launches the service too):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://manticoresearch.com | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Insert a document. Note there's no table yet — &lt;a href="https://manticoresearch.com/blog/create_table/" rel="noopener noreferrer"&gt;auto-schema&lt;/a&gt; creates it from the data:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; 0:9308/insert &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "table": "products",
  "doc": {"title": "yellow travel backpack", "price": 4990, "color": "yellow"}
}'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"products"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8938969113712132097&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Search — full-text match plus a filter on a numeric field, immediately:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; 0:9308/search &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "table": "products",
  "query": {"bool": {"must": [
    {"match": {"*": "backpack"}},
    {"range": {"price": {"lt": 6000}}}
  ]}}
}'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"took"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timed_out"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_relation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8938969113712132097&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1356&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yellow travel backpack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yellow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4990&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is queryable the moment it lands in the table: text fields are full-text searchable — per-field too, e.g. &lt;code&gt;{"match": {"color": "yellow"}}&lt;/code&gt; — and attribute fields like &lt;code&gt;price&lt;/code&gt; are filterable, sortable, groupable, and facetable, with no declarations and no re-indexing. Here's the schema Manticore inferred on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------+--------+----------------+
| Field | Type   | Properties     |
+-------+--------+----------------+
| id    | bigint |                |
| title | text   | indexed stored |
| color | text   | indexed stored |
| price | uint   |                |
+-------+--------+----------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the same task in Meilisearch (we tested both the &lt;code&gt;latest&lt;/code&gt; Docker tag, v1.41.0, and the newest release, v1.48.3). Meilisearch's installer downloads a binary into the current directory, which you then run yourself — and in production mode you'll also need to configure a master key before it starts. Adding documents and searching is just as easy as in Manticore — credit where due. But then you try to filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST localhost:7700/indexes/products/search &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"q": "backpack", "filter": "price &amp;lt; 6000"}'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Index `products`: Attribute `price` is not filterable. This index does not have configured filterable attributes."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invalid_search_filter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invalid_request"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since we were re-checking everything for this post anyway, here's precisely what does and doesn't work out of the box in Meilisearch — each point verified against the docs and a live instance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Searching works immediately.&lt;/strong&gt; By default &lt;code&gt;searchableAttributes&lt;/code&gt; is &lt;code&gt;["*"]&lt;/code&gt; — every field of every document is searchable, no declarations needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filtering doesn't.&lt;/strong&gt; Fields must first be declared in &lt;code&gt;filterableAttributes&lt;/code&gt;. &lt;a href="https://www.meilisearch.com/docs/learn/filtering_and_sorting/filter_search_results" rel="noopener noreferrer"&gt;Meilisearch's documentation&lt;/a&gt; is upfront about the cost: &lt;em&gt;"This step is mandatory and cannot be done at search time. … Updating &lt;code&gt;filterableAttributes&lt;/code&gt; requires Meilisearch to re-index all your data, which will take an amount of time proportionate to your dataset size and complexity."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faceting doesn't either.&lt;/strong&gt; Facets are built on the same &lt;code&gt;filterableAttributes&lt;/code&gt; list, so the same declare-and-reindex step applies. In Manticore, &lt;a href="https://manticoresearch.com/blog/faceted-search-without-manual-filter-building/" rel="noopener noreferrer"&gt;faceted search needs no manual filter building&lt;/a&gt; at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sorting doesn't.&lt;/strong&gt; &lt;a href="https://www.meilisearch.com/docs/learn/filtering_and_sorting/sort_search_results" rel="noopener noreferrer"&gt;Per Meilisearch's docs&lt;/a&gt;: &lt;em&gt;"To allow your users to sort results at search time you must … add those attributes to the &lt;code&gt;sortableAttributes&lt;/code&gt; index setting."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tuning search fields re-indexes too.&lt;/strong&gt; The order of &lt;code&gt;searchableAttributes&lt;/code&gt; determines relevancy, so restricting or re-ordering it is a step most real projects take — and &lt;em&gt;"updating &lt;code&gt;searchableAttributes&lt;/code&gt; triggers a re-indexing of all documents in the index."&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a bug — it's a design trade-off, and each step is one small API call. But it adds up to a model where you declare capabilities attribute by attribute and pay a full, dataset-proportional re-index every time you change your mind. Manticore's model is different: fields have &lt;em&gt;types&lt;/em&gt; — text fields are full-text searchable, attributes (numbers, strings, JSON) are filterable, sortable, groupable, and facetable, vectors are KNN-searchable — and every capability of a type is available from the moment of insert. There's no per-attribute capability registry to maintain, and no re-indexing to change how you query. Deep customization — ranking formulas, tokenization, morphology, per-field weights — is absolutely there (the article's pros list is right about that), but it's opt-in, not a prerequisite. Which is why we're not sure "needs advanced customization" is pointing at the right engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  "SQL familiarity" — optional, not required
&lt;/h2&gt;

&lt;p&gt;The article frames Manticore as the choice for "teams with SQL familiarity." SQL support &lt;em&gt;is&lt;/em&gt; a distinctive Manticore strength — you can talk to it with any MySQL client, &lt;code&gt;mysqldump&lt;/code&gt;, or a BI tool (a capability the comparison's own integrations table marks as "Not supported" in Meilisearch).&lt;/p&gt;

&lt;p&gt;But notice what you just read above: the entire quick-start demo was JSON over HTTP. Practically everything in Manticore is available both ways — SQL for those who like it, a JSON API very similar to what Meilisearch and Elasticsearch users are used to. You don't need to write a single line of SQL to use Manticore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vector search: Meilisearch's own table says it best
&lt;/h2&gt;

&lt;p&gt;This is our favorite part. The use-cases table in Meilisearch's article rates the two engines on vector search:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Vector search.&lt;/strong&gt; Meilisearch: Basic vector search. Manticore Search: Better suited for hybrid and advanced setups.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We agree with both columns. Here's what "hybrid and advanced setups" looks like in practice with &lt;a href="https://manticoresearch.com/blog/auto-embeddings/" rel="noopener noreferrer"&gt;auto-embeddings&lt;/a&gt; — no external embedding pipeline, no API keys, the model downloads automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;products_ai&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="n"&gt;FLOAT_VECTOR&lt;/span&gt; &lt;span class="n"&gt;KNN_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'hnsw'&lt;/span&gt; &lt;span class="n"&gt;HNSW_SIMILARITY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'l2'&lt;/span&gt;
    &lt;span class="n"&gt;MODEL_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'sentence-transformers/all-MiniLM-L6-v2'&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'title,description'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Insert documents as plain JSON — embeddings are generated for you (there's also a &lt;code&gt;/bulk&lt;/code&gt; endpoint for batches):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; 0:9308/insert &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "table": "products_ai",
  "id": 1,
  "doc": {
    "title": "green hiking backpack",
    "description": "Lightweight backpack suitable for hiking trails",
    "price": 5999
  }
}'&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; 0:9308/insert &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "table": "products_ai",
  "id": 3,
  "doc": {
    "title": "trail running shoes",
    "description": "Lightweight shoes with great grip for trails",
    "price": 7500
  }
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And run a &lt;a href="https://manticoresearch.com/blog/hybrid-search/" rel="noopener noreferrer"&gt;hybrid search&lt;/a&gt; — full-text and semantic search fused with RRF — in a single request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; 0:9308/search &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "table": "products_ai",
  "hybrid": {"query": "gear for a mountain hike"},
  "_source": ["title", "price"],
  "size": 2
}'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"_knn_dist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.88308269&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"_hybrid_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.01639344&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"green hiking backpack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5999&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"_knn_dist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.09160841&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"_hybrid_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.01612903&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"trail running shoes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7500&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No document mentions the word "gear" or "mountain" — that's semantic understanding out of the box, two requests total. Under the hood there's &lt;a href="https://manticoresearch.com/blog/quantization/" rel="noopener noreferrer"&gt;vector quantization&lt;/a&gt; to cut RAM usage, &lt;a href="https://manticoresearch.com/blog/knn-prefiltering/" rel="noopener noreferrer"&gt;KNN prefiltering&lt;/a&gt; so filters work &lt;em&gt;inside&lt;/em&gt; the HNSW traversal, and &lt;a href="https://manticoresearch.com/blog/knn-hnsw-performance/" rel="noopener noreferrer"&gt;continuous KNN performance work&lt;/a&gt;. "Better suited for hybrid and advanced setups" — yes, we'll take that.&lt;/p&gt;

&lt;p&gt;And you don't have to take our word for it — these live demos all run on Manticore: &lt;a href="https://catalog.manticoresearch.com/" rel="noopener noreferrer"&gt;catalog search&lt;/a&gt; with filters, facets, typo tolerance, and semantic search; &lt;a href="https://image.manticoresearch.com/" rel="noopener noreferrer"&gt;reverse image search&lt;/a&gt;; and &lt;a href="https://chat.manticoresearch.com/" rel="noopener noreferrer"&gt;conversational search&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Relevance: measured, not argued
&lt;/h2&gt;

&lt;p&gt;Search-quality claims can be tested empirically. We benchmarked Manticore Search and Meilisearch in full-text, vector, and hybrid modes across 14 public IR dataset/corpus-size groups: seven BEIR datasets, ACORD, MS MARCO Passage at 250K and 8.8M documents, TREC DL 2019/2020, Wayfair's WANDS product-search dataset, and the typo-focused DL-Typo. For each engine/mode pair, we select its best completed run in each group and compute an unweighted average across the groups. The first row then selects each engine's best mode per group before averaging. Vector and hybrid runs use the same Qwen3-Embedding-8B model on both engines. Quality is scored with each dataset's canonical metric: NDCG@10, or MRR@10 for MS MARCO.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Manticore&lt;/th&gt;
&lt;th&gt;Meilisearch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best mode per dataset (average)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.543&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.527&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.530&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.527&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.515&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.475&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-text&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.420&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.237&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Comparing each engine's best result per group, Manticore leads on 11 of the 14. Vector quality is nearly tied at 0.530 and 0.527; both engines use the same embedding model, while their indexing and retrieval implementations still differ. The hybrid results show a wider difference, with Manticore's RRF averaging 0.515 against 0.475.&lt;/p&gt;

&lt;p&gt;Full-text has the largest gap: 0.420 against 0.237. The wider benchmark helps explain why. The engines using BM25 or a BM25-family ranker cluster well above Meilisearch in full-text relevance. &lt;a href="https://www.meilisearch.com/docs/learn/relevancy/ranking_rules" rel="noopener noreferrer"&gt;Meilisearch instead applies an ordered set of rules&lt;/a&gt; based on matching words, typos, proximity, attributes, and exactness. It therefore lacks BM25's combination of inverse document frequency, term-frequency saturation, and document-length normalization, which is the main reason it performs poorly on these standard IR datasets. Typesense, another engine in the wider comparison that does not use BM25, shows a similar pattern. Meilisearch's ten-word query limit can hurt longer queries further because the remaining words are ignored.&lt;/p&gt;

&lt;p&gt;Manticore also leads on DL-Typo, which focuses specifically on typo robustness.&lt;/p&gt;

&lt;p&gt;The same harness also tests Elasticsearch, OpenSearch, Qdrant, Typesense, Vespa, and Weaviate. Some engines could not complete the largest tests with the resources available on the benchmark server, so the report provides two coverage-controlled leaderboards. Recomputed over the ten-group subset covered by all eight engines, Manticore's best-mode average places second overall, behind Qdrant. Across all 14 groups, it also places second among the five engines with complete coverage. Meilisearch records a lower average in both comparisons.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://gist.github.com/manticoresearch/7c9b5d85ca8a62ce0066db6f0e17a11a" rel="noopener noreferrer"&gt;full comparison&lt;/a&gt; already includes coverage, per-dataset tables, and exact per-run configurations. This work is still in progress though. We'll publish the complete methodology and remaining details soon. We'll also release our new benchmarking tool as open source, making benchmarks like this much easier to run and extend.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Primarily used for complex, large-scale workloads" — the data says otherwise
&lt;/h2&gt;

&lt;p&gt;The article claims:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Manticore is primarily used for complex, large-scale search and analytics workloads that require high performance and deep customization.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And in the use-cases table, Manticore "may be overkill for simple CMS needs."&lt;/p&gt;

&lt;p&gt;We checked our &lt;a href="https://manual.manticoresearch.com/Telemetry" rel="noopener noreferrer"&gt;anonymized telemetry&lt;/a&gt;. Among Manticore instances that report data-size metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;29%&lt;/strong&gt; hold less than &lt;strong&gt;1 MB&lt;/strong&gt; of data&lt;/li&gt;
&lt;li&gt;about &lt;strong&gt;half&lt;/strong&gt; hold less than &lt;strong&gt;100 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;only about &lt;strong&gt;one in eight&lt;/strong&gt; exceeds &lt;strong&gt;10 GB&lt;/strong&gt;, and fewer than &lt;strong&gt;1%&lt;/strong&gt; exceed &lt;strong&gt;1 TB&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So no — Manticore is &lt;em&gt;not&lt;/em&gt; primarily used for large-scale workloads. Most real-world Manticore installations are small projects: sites, catalogs, blogs, internal tools. Exactly the "simple CMS needs" the table warns you about. A Manticore container idles at under 200 MB of RAM, and as you saw above, a working search takes three commands — it's hard to call that overkill for anything.&lt;/p&gt;

&lt;p&gt;The nice part is that the &lt;em&gt;same&lt;/em&gt; engine also holds up at the other end: &lt;a href="https://manticoresearch.com/blog/manticore-search-at-scale-on-google-cloud/" rel="noopener noreferrer"&gt;Locally serves tens of thousands of queries per second on Manticore&lt;/a&gt; after migrating from Elasticsearch. Scaling down and scaling up aren't mutually exclusive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-time analytics: we'll be honest here too
&lt;/h2&gt;

&lt;p&gt;Curiously, the same table gives Manticore a compliment we'd soften ourselves: "Designed for real-time data and analytics." Manticore has solid analytics capabilities — &lt;a href="https://manticoresearch.com/blog/mcl/" rel="noopener noreferrer"&gt;columnar storage&lt;/a&gt;, aggregations, &lt;a href="https://manticoresearch.com/blog/kibana-demo/" rel="noopener noreferrer"&gt;Kibana&lt;/a&gt; and &lt;a href="https://manticoresearch.com/blog/grafana-dashboard-docker/" rel="noopener noreferrer"&gt;Grafana&lt;/a&gt; integrations, &lt;a href="https://manticoresearch.com/blog/integration-of-manticore-with-logstash-filebeat/" rel="noopener noreferrer"&gt;log-pipeline integrations&lt;/a&gt; — and it's &lt;a href="https://manticoresearch.com/blog/kibana-demo/" rel="noopener noreferrer"&gt;well-suited for log search&lt;/a&gt;. But it is first and foremost a search engine. Analytics is one of its use cases, not what it's "primarily designed" for, and not what most of our users run it for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Meilisearch's article has a point
&lt;/h2&gt;

&lt;p&gt;We promised facts, and facts cut both ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Typo tolerance by default.&lt;/strong&gt; True: Meilisearch ships with typo tolerance on. In Manticore, &lt;a href="https://manticoresearch.com/blog/new-fuzzy-search-and-autocomplete/" rel="noopener noreferrer"&gt;fuzzy search&lt;/a&gt; is opt-in — &lt;code&gt;min_infix_len='2'&lt;/code&gt; in the table definition and &lt;code&gt;fuzzy=1&lt;/code&gt; at query time. Two small settings, but Meilisearch's default is the friendlier one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Querying straight from the frontend.&lt;/strong&gt; Meilisearch's search API keys make backend-less search a first-class pattern. Manticore has traditionally lived behind a backend; &lt;a href="https://manticoresearch.com/blog/manticore-search-27-1-5/" rel="noopener noreferrer"&gt;authentication arrived in 27.1.5&lt;/a&gt;, so that gap is closing, but today the point goes to Meilisearch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation gaps.&lt;/strong&gt; Every project has them, including us — and including Meilisearch. We'd just push back on "steep learning curve": if you know a bit of SQL &lt;em&gt;or&lt;/em&gt; a bit of JSON, you know enough to start, and there are &lt;a href="https://play.manticoresearch.com/" rel="noopener noreferrer"&gt;free interactive courses&lt;/a&gt; that walk you through everything from first table to vector search.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Client libraries: the table, completed
&lt;/h2&gt;

&lt;p&gt;Since this post is a response to &lt;a href="https://www.meilisearch.com/blog/meilisearch-vs-manticore" rel="noopener noreferrer"&gt;Meilisearch's comparison&lt;/a&gt;, let's fix its integrations table too. The original lists Go twice with contradictory answers ("supported via community" and "not supported"), gets several official/community statuses wrong — on both sides, in both directions — and skips two languages entirely. Here it is again, checked against &lt;a href="https://www.meilisearch.com/docs/learn/resources/sdks" rel="noopener noreferrer"&gt;Meilisearch's own SDK docs&lt;/a&gt; and &lt;a href="https://github.com/manticoresoftware" rel="noopener noreferrer"&gt;Manticore's GitHub org&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Integration&lt;/th&gt;
&lt;th&gt;Meilisearch&lt;/th&gt;
&lt;th&gt;Manticore Search&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript (Node.js)&lt;/td&gt;
&lt;td&gt;Official client (TypeScript-based)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/manticoresoftware/manticoresearch-javascript" rel="noopener noreferrer"&gt;Official JavaScript client&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript †&lt;/td&gt;
&lt;td&gt;Same official TypeScript client&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/manticoresoftware/manticoresearch-typescript" rel="noopener noreferrer"&gt;Official TypeScript client&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Official client (sync + async)&lt;/td&gt;
&lt;td&gt;Official &lt;a href="https://github.com/manticoresoftware/manticoresearch-python" rel="noopener noreferrer"&gt;sync&lt;/a&gt; and &lt;a href="https://github.com/manticoresoftware/manticoresearch-python-asyncio" rel="noopener noreferrer"&gt;asyncio&lt;/a&gt; clients ✱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP&lt;/td&gt;
&lt;td&gt;Official client&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/manticoresoftware/manticoresearch-php" rel="noopener noreferrer"&gt;Official PHP client&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;Official client&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/manticoresoftware/manticoresearch-java" rel="noopener noreferrer"&gt;Official Java client&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Official client ✱&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/manticoresoftware/manticoresearch-go" rel="noopener noreferrer"&gt;Official Go client&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;Community-maintained client ✱&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/manticoresoftware/manticoresearch-rust" rel="noopener noreferrer"&gt;Official Rust client&lt;/a&gt; ✱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elixir †&lt;/td&gt;
&lt;td&gt;Not listed in Meilisearch's SDK docs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/manticoresoftware/manticoresearch-elixir" rel="noopener noreferrer"&gt;Official Elixir client&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dart/Flutter&lt;/td&gt;
&lt;td&gt;Official client&lt;/td&gt;
&lt;td&gt;No official client (HTTP JSON API)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET/C#&lt;/td&gt;
&lt;td&gt;Official client ✱&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/manticoresoftware/manticoresearch-net" rel="noopener noreferrer"&gt;Official .NET client&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REST API&lt;/td&gt;
&lt;td&gt;Core interface&lt;/td&gt;
&lt;td&gt;Full HTTP/JSON API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Native SQL (MySQL protocol)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;† — language missing from the original table • ✱ — status corrected vs the original table (for the Meilisearch column, per Meilisearch's current SDK docs)&lt;/p&gt;

&lt;p&gt;Adding it up across the ten languages above: &lt;strong&gt;Manticore ships official clients for nine&lt;/strong&gt; — everything except Dart/Flutter. &lt;strong&gt;Meilisearch ships official clients for eight&lt;/strong&gt; — everything except Rust (community-maintained) and Elixir (not listed). Both cover REST in full, and Manticore additionally speaks SQL over the MySQL protocol. So "a wide range of clients" — the one thing the original article does say about Manticore's integrations — is accurate; the details just needed refreshing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Meilisearch is a good search engine, and the article gets a real thing right: it's polished for the instant-search, frontend-first use case. If you're choosing for the long haul, though, weigh the cons Meilisearch's own article lists too — for instance, that a search query "must involve no more than ten words; any more will be ignored." Limits like that tend to matter more in month six than on day one.&lt;/p&gt;

&lt;p&gt;But the bigger issue is that the Manticore the article describes — a complex, SQL-only system for large-scale analytics teams — isn't the Manticore that exists in 2026, and most of it you can disprove in about five minutes of terminal time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://manticoresearch.com | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then insert a JSON document and search it (&lt;a href="https://manticoresearch.com/blog/one-line-installer/" rel="noopener noreferrer"&gt;more on the one-line installer&lt;/a&gt;). No schema, no config, no "advanced customization." If you want numbers rather than words, the relevance results above, our &lt;a href="https://manticoresearch.com/blog/manticoresearch-vs-meilisearch/" rel="noopener noreferrer"&gt;benchmark-backed comparison&lt;/a&gt;, and &lt;a href="https://db-benchmarks.com/" rel="noopener noreferrer"&gt;db-benchmarks.com&lt;/a&gt; are good places to start — and if we got anything wrong about Meilisearch here, we'll happily correct it. That's how we'd want to be treated, too.&lt;/p&gt;

&lt;p&gt;Questions or disagreements? Come tell us in &lt;a href="https://forum.manticoresearch.com/" rel="noopener noreferrer"&gt;the forum&lt;/a&gt; or &lt;a href="https://slack.manticoresearch.com/" rel="noopener noreferrer"&gt;Slack&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>database</category>
      <category>opensource</category>
      <category>performance</category>
      <category>search</category>
    </item>
    <item>
      <title>Built-in authentication and authorization for Manticore Search</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Thu, 23 Jul 2026 04:57:42 +0000</pubDate>
      <link>https://dev.to/sanikolaev/built-in-authentication-and-authorization-for-manticore-search-4ec3</link>
      <guid>https://dev.to/sanikolaev/built-in-authentication-and-authorization-for-manticore-search-4ec3</guid>
      <description>&lt;p&gt;Search is no longer just a box on a website. In many deployments, Manticore Search behaves more like an internal data service: applications query it, ingest workers update its tables, dashboards read from it, and operators change schemas. When all of that relies only on network-level trust, the access-control story gets weak quickly.&lt;/p&gt;

&lt;p&gt;That is why Manticore Search now has built-in authentication and authorization. Introduced in the 27.x release line and published in the &lt;a href="https://manticoresearch.com/blog/manticore-search-27-1-5/" rel="noopener noreferrer"&gt;27.1.5 release post&lt;/a&gt;, it lets Manticore check who is connecting and what that user is allowed to do.&lt;/p&gt;

&lt;p&gt;The feature works across SQL, HTTP/HTTPS clients, distributed remote agents, and replication-related operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Manticore adds
&lt;/h2&gt;

&lt;p&gt;Manticore now supports authentication and authorization for the core access paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL/MySQL clients authenticate with a user name and password.&lt;/li&gt;
&lt;li&gt;HTTP clients can use Basic authentication or Bearer tokens.&lt;/li&gt;
&lt;li&gt;Permissions are granted by action and target.&lt;/li&gt;
&lt;li&gt;Auth events can be logged at configurable levels.&lt;/li&gt;
&lt;li&gt;Users and permissions are managed through dedicated SQL commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The authorization model uses five actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;read&lt;/code&gt; for search and read-only access.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;write&lt;/code&gt; for data changes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;schema&lt;/code&gt; for table and schema management.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;replication&lt;/code&gt; for cluster operations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;admin&lt;/code&gt; for authentication and authorization management.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Targets are plain names and wildcards, for example &lt;code&gt;products&lt;/code&gt;, &lt;code&gt;logs_*&lt;/code&gt;, or &lt;code&gt;*&lt;/code&gt;. That keeps the model close to how people already think about Manticore tables and clusters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes for operators
&lt;/h2&gt;

&lt;p&gt;It is common for a client application to be able to search a table without having permission to write to it. A data ingestion process may update documents without changing the schema. A schema migration task may run without being granted security administrator privileges. An operator can manage authentication and authorization without automatically gaining read access to business data.&lt;/p&gt;

&lt;p&gt;Those permission boundaries matter once search becomes a part of shared infrastructure. The point is not just to keep outsiders out; it is to keep each trusted client limited to what it actually needs.&lt;/p&gt;

&lt;p&gt;Manticore now gives those controls a native home. Users authenticate through the protocols they already use. Manticore checks permissions against actions and targets. Operators manage access with SQL commands. Applications keep their normal SQL or HTTP integration pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for SaaS and shared search
&lt;/h2&gt;

&lt;p&gt;SaaS and shared-service teams often need one search layer to serve several products, teams, or tenants. Some integrations are customer-facing, some are internal tools, and some run in the background. They should not all inherit the same level of trust.&lt;/p&gt;

&lt;p&gt;Before built-in auth, teams had to solve much of that outside Manticore. A common workaround was to put nginx in front of Manticore and rely on HTTP Basic authentication there. That could protect one entry point, but it did not give Manticore its own user and permission model. Now teams can model more of it directly where the data is served.&lt;/p&gt;

&lt;p&gt;That makes Manticore easier to use for customer-facing search over sensitive data, internal tools with different permission levels, and shared search clusters used by multiple services.&lt;/p&gt;

&lt;p&gt;Authentication and authorization are not a compliance program by themselves. Still, they are the kind of control reviewers expect to see. For GDPR, SOC 2, and internal security reviews, it helps to show named users, limited permissions, and authentication logs instead of a shared credential that can do everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollout notes
&lt;/h2&gt;

&lt;p&gt;Operators can enable authentication, bootstrap the first administrator, and then manage users and permissions with familiar SQL commands. Applications do not need a new Manticore-specific gateway just to start sending credentials. SQL clients use usernames and passwords. HTTP clients use Basic authentication or Bearer tokens.&lt;/p&gt;

&lt;p&gt;For existing deployments, treat the switch as a rollout, not just a config change. By default authentication is disabled until configured. Once it is enabled, clients that do not send credentials should fail, so update your application first and test expected denials before switching production traffic.&lt;/p&gt;

&lt;p&gt;For distributed or replicated topologies, plan the rollout carefully. Upgrade remote agents and replication peers first, then upgrade the masters that query or manage them, and enable auth only after the whole topology is on a compatible version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learn more
&lt;/h2&gt;

&lt;p&gt;For a hands-on walkthrough, see the practical launch post:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://manticoresearch.com/blog/how-to-secure-manticore-search-with-authentication-authorization/" rel="noopener noreferrer"&gt;How to secure Manticore Search with built-in authentication and authorization&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For existing deployments, see the rollout checklist:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://manticoresearch.com/blog/manticore-auth-migration-hardening-checklist/" rel="noopener noreferrer"&gt;Manticore Search authentication rollout checklist for existing deployments&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the full reference, see the manual page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://manual.manticoresearch.com/Security/Authentication_and_authorization" rel="noopener noreferrer"&gt;Authentication and authorization manual&lt;/a&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>search</category>
      <category>security</category>
    </item>
    <item>
      <title>Turbopuffer vs. Manticore Search on a couple of cheap VPS</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Thu, 16 Jul 2026 05:42:27 +0000</pubDate>
      <link>https://dev.to/sanikolaev/turbopuffer-vs-manticore-search-on-a-couple-of-cheap-vps-1c9</link>
      <guid>https://dev.to/sanikolaev/turbopuffer-vs-manticore-search-on-a-couple-of-cheap-vps-1c9</guid>
      <description>&lt;p&gt;Serverless vector databases are marketed on a simple promise: you don't run anything, you don't tune anything, and someone else worries about storage, scaling and uptime. &lt;a href="https://turbopuffer.com/" rel="noopener noreferrer"&gt;turbopuffer&lt;/a&gt; is one of the better examples of the category: a fast search engine built on object storage, used by Cursor, Notion, Linear and others.&lt;/p&gt;

&lt;p&gt;The promise is real, but it isn't free. So the obvious question is: for a small, well-defined workload, how much of that do you actually need, and what does the same workload cost and perform like on two cheap VPS running Manticore Search?&lt;/p&gt;

&lt;p&gt;This article answers that with numbers from an apples-to-apples benchmark on the same dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workload
&lt;/h2&gt;

&lt;p&gt;We took a concrete, modest workload, the kind a lot of real applications actually have, and sized turbopuffer for it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dataset&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://storage.googleapis.com/ann-filtered-benchmark/datasets/dbpedia_openai_1M.tgz" rel="noopener noreferrer"&gt;DBpedia&lt;/a&gt; (OpenAI embeddings)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stored documents&lt;/td&gt;
&lt;td&gt;975k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector dimensions&lt;/td&gt;
&lt;td&gt;1536 (cosine similarity)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;turbopuffer latency P50 / P90 / P99 (advertised, warm namespace)&lt;/td&gt;
&lt;td&gt;14 / 17 / 27 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;turbopuffer estimated cost&lt;/td&gt;
&lt;td&gt;$75 / month (July 2026)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two turbopuffer numbers in that table are its advertised latency and its calculator-estimated cost, not measurements. The cost comes from turbopuffer's &lt;a href="https://turbopuffer.com/pricing" rel="noopener noreferrer"&gt;cost calculator&lt;/a&gt;, and the latency is turbopuffer's published target: specifically the warm-namespace P50/P90/P99 from its own benchmark:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0rjw5449igj4gi4wwitm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0rjw5449igj4gi4wwitm.png" alt="turbopuffer's advertised vector-search latency: warm namespace 14/17/27 ms vs cold namespace 874/1214/1686 ms" width="669" height="595"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two caveats come straight from that screenshot. First, those 14/17/27 ms are for a warm namespace; the same benchmark reports 874 / 1214 / 1686 ms for a cold one. A namespace that has aged out of cache is ~50–60× slower on its first queries, a direct consequence of turbopuffer's object-storage tiering. Second, turbopuffer's benchmark runs a different workload (1024 dimensions, 10M docs) than ours (1536 dimensions, 975k docs). turbopuffer's actually measured latency and throughput on our dataset are reported further down, and they differ from these advertised figures. Our benchmark dataset is 975k docs (DBpedia); both systems were loaded with the same data.&lt;/p&gt;

&lt;p&gt;That $75/month comes straight from turbopuffer's own cost calculator for 1536-dim vectors with no attributes (the calculator's "Attributes: None"). This matches our benchmark, which was pure vector search: no stored attributes and no metadata filtering on either side. Both engines support attribute filtering and full-text search (Manticore also adds &lt;a href="https://manual.manticoresearch.com/Searching/Hybrid_search" rel="noopener noreferrer"&gt;hybrid search&lt;/a&gt;); we simply didn't exercise it here, so the comparison is like-for-like on plain KNN. The pricing is usage-based, and the breakdown is the interesting part:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;th&gt;This workload&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;≤ $0.33 / GB&lt;/td&gt;
&lt;td&gt;1M docs (~3 GB)&lt;/td&gt;
&lt;td&gt;$1.01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writes&lt;/td&gt;
&lt;td&gt;≤ $2.00 / GB&lt;/td&gt;
&lt;td&gt;10M writes, ~4 WPS (~61 GB)&lt;/td&gt;
&lt;td&gt;$61.44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries&lt;/td&gt;
&lt;td&gt;≤ $1.00 / PB&lt;/td&gt;
&lt;td&gt;10M queries, ~4 QPS (~13 PB)&lt;/td&gt;
&lt;td&gt;$12.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Namespaces&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;100 × 10K docs&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;$75 / month (min $16 Launch plan)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Falam26mv6kv5ds64ksba.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Falam26mv6kv5ds64ksba.png" alt="turbopuffer cost calculator for 1536-dim vectors with no attributes: $75/month, dominated by writes" width="800" height="634"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The headline number is almost entirely writes: $61.44 of the $75 is the cost of ingesting ~61 GB of vector data, while storage (3 GB) is a rounding error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkgbczbpxv4qcr50ai088.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkgbczbpxv4qcr50ai088.png" alt="Monthly cost breakdown: turbopuffer $75 (mostly writes) vs Manticore's flat $16.50" width="799" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The "~13 PB" against the query line looks alarming but is a billing abstraction, not bandwidth. turbopuffer meters queries by "GB queried" with a minimum billable of 1.28 GB per query, so the calculator simply computes &lt;code&gt;10M queries * 1.28 GB ≈ 13 PB&lt;/code&gt;. The data actually moving per query is tiny (a 1536-dim float32 query vector is ~6 KB in, a top-10 result is a few KB out), yet every query on a namespace this small just bills at the 1.28 GB floor. At ≤$1/PB that whole 13 PB costs only $13, so query pricing is trivial here.&lt;/p&gt;

&lt;p&gt;This matters for the comparison because on a self-hosted engine none of these meters exist: writes are just CPU time, queries have no per-query floor, and there is no per-GB charge at all. (&lt;a href="https://turbopuffer.com/pricing" rel="noopener noreferrer"&gt;turbopuffer pricing&lt;/a&gt;, &lt;a href="https://turbopuffer.com/docs/pricing-log" rel="noopener noreferrer"&gt;pricing changelog&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  The contenders
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;turbopuffer&lt;/strong&gt; is serverless for the user: storage lives on S3/GCS/Azure object storage (~$0.02/GB), with hot data tiered through NVMe and RAM caches. You never provision or patch a server, never manage replication, never do capacity planning. You pay per GB stored / written / queried, and query compute runs on turbopuffer's managed infrastructure rather than on hardware you provision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manticore Search&lt;/strong&gt; is software you run yourself. For this comparison we used the cheapest realistic setup that still gives you redundancy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nodes&lt;/td&gt;
&lt;td&gt;2 × Hetzner CX23 (region "eu-central") in a cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per VPS&lt;/td&gt;
&lt;td&gt;$8.25 / month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU per VPS&lt;/td&gt;
&lt;td&gt;2 cores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk per VPS&lt;/td&gt;
&lt;td&gt;40 GB (table data uses ~6 GB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Role of 2nd node&lt;/td&gt;
&lt;td&gt;Replica for redundancy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manticore total cost&lt;/td&gt;
&lt;td&gt;$16.50 / month (or $8.25 for a single node if you don't need redundancy)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Manticore stores vectors in an HNSW index (via the &lt;a href="https://github.com/manticoresoftware/columnar" rel="noopener noreferrer"&gt;Manticore Columnar Library&lt;/a&gt;) and supports scalar quantization (8-bit and 1-bit) with oversampling and rescoring to trade memory and speed against recall. For this test we used 1-bit (binary) quantization with &lt;code&gt;oversampling=2.0&lt;/code&gt; and rescoring, settings chosen specifically to land at roughly the same recall as turbopuffer, so &lt;strong&gt;the latency and throughput numbers are compared at equal quality&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line first
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;turbopuffer&lt;/th&gt;
&lt;th&gt;Manticore (2× cheap VPS)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost&lt;/td&gt;
&lt;td&gt;$75&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$16.50&lt;/strong&gt; (high-availability mode) / $8.25 (single node)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@10 (5k queries)&lt;/td&gt;
&lt;td&gt;0.9622&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.9663&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query latency P50 / P90 / P99 (end-to-end)&lt;/td&gt;
&lt;td&gt;21 / 24 / 30 ms (incl. network round-trip)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;10 / 13 / 16 ms&lt;/strong&gt; (local, no network hop, fully compacted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read (query) throughput ceiling&lt;/td&gt;
&lt;td&gt;~300 QPS unbatched (concurrency=8); &lt;strong&gt;~800–1,100 QPS&lt;/strong&gt; with query batching&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~330 QPS&lt;/strong&gt; (concurrency=2, unbatched)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sustained write throughput&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~1150 docs/s&lt;/strong&gt; (bulk load)&lt;/td&gt;
&lt;td&gt;490-1100 docs/s (bulk load)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ops burden&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;None (fully managed)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You run &amp;amp; maintain it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On this workload, Manticore on two $8.25 VPS is ~4.5× cheaper, matches turbopuffer on recall, and, once the table is compacted (its steady state), is actually faster end-to-end (~10–13 ms vs turbopuffer's ~21 ms in-region), because a local query skips the network round-trip turbopuffer always pays. turbopuffer, in turn, reaches a higher query-throughput ceiling, but only with query batching (~800–1,100 QPS vs ~330 on two 2-core VPS); unbatched from a single machine the two are comparable (~300 QPS each). For a 4 QPS workload both have enormous headroom regardless. What you give up with Manticore is the "zero operations" part, which is the whole point of the rest of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quality: matched recall
&lt;/h2&gt;

&lt;p&gt;There's no point comparing latency or cost unless both systems return results of comparable quality. Using the &lt;a href="https://storage.googleapis.com/ann-filtered-benchmark/datasets/dbpedia_openai_1M.tgz" rel="noopener noreferrer"&gt;DBpedia&lt;/a&gt; dataset (1536-dim OpenAI embeddings, cosine similarity, 975k docs) and 5,000 queries:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;recall@10&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;turbopuffer&lt;/td&gt;
&lt;td&gt;0.9622&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manticore&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.9663&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two are within ~0.4 points of each other, close enough that the rest of the comparison is fair. Manticore reached this with &lt;a href="https://dev.to/blog/quantization/"&gt;binary quantization&lt;/a&gt; + &lt;code&gt;oversampling=2.0&lt;/code&gt; + rescoring: the binary vector representation is 32× smaller than float32, while rescoring recomputes final distances on the full-precision vectors for the top candidates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Latency
&lt;/h2&gt;

&lt;p&gt;The number that matters is the one you actually observe: end-to-end, from issuing the query to getting the answer. turbopuffer is a remote service, so that includes the network round-trip to the namespace's region. Measured from a machine in the same region as the namespace (&lt;code&gt;gcp-europe-west3&lt;/code&gt;, same country as the query host):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;turbopuffer (in-region, end-to-end):   P50=21ms   P90=24ms   P99=30ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your application farther from the region and this rises with the distance (easily 100 ms+ across continents), so co-locating the two matters, and you always pay at least the in-region round-trip.&lt;/p&gt;

&lt;p&gt;turbopuffer also reports two of its own internal timers: the time it spent running the search (~12 ms, and stable even under heavy load), and the total time on its side including requests waiting in line for an execution slot. We lean on those only to diagnose the throughput results below (the engine's own run-time stays fast under load), not as a latency figure, because any time a request waits is real latency you pay. And one point keeps the comparison honest: that ~12 ms is turbopuffer's own self-reported, engine-only time, excluding its internal queuing. Manticore has query profiling too, but we didn't isolate a single engine-only number equivalent to it; every Manticore figure here is end-to-end and already includes any internal queuing it does. So throughout this section we compare end-to-end against end-to-end, never turbopuffer's engine-only figure against Manticore's total.&lt;/p&gt;

&lt;p&gt;Manticore's latency (queried locally, so end-to-end with negligible network) under a mixed 4 QPS + 4 WPS load (10-minute run) depends heavily on how many disk chunks the table has been compacted into. A freshly bulk-loaded real-time table is split into many chunks; Manticore's auto-optimize merges them over time, and each KNN query has to consult every chunk, so fewer chunks means faster queries:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1sye9pkmc815qy4qoqv5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1sye9pkmc815qy4qoqv5.png" alt="Manticore query latency falls as the table compacts, crossing below turbopuffer's in-region band at ~2 chunks" width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The 32 / 16 / 8 / 4-chunk states above were produced deliberately, by optimizing to a fixed chunk count, to map out the latency curve. A real bulk load lands at far fewer: at our benchmark's batch=1000 the table settled at 7 chunks (1 insert thread) or 8 chunks (2 threads), so a freshly loaded table sits around the 8-chunk mark (~28 ms). The number you end up with is really a race between two processes: inserts flush RAM chunks to disk, and auto-optimize merges those disk chunks in the background. The faster you ingest, the faster new disk chunks pile up, so a high-throughput load (batch=1000) out-runs the background merger and leaves more chunks behind (7–8), while slow single-document inserts (batch=1) produce chunks slowly enough that auto-optimize keeps pace and the table ends at just 3 chunks, at a far lower ingest rate. So it's ingest rate, not concurrency per se, that drives the final chunk count. From whatever the load leaves behind, Manticore's auto-optimize then continues compacting toward 2 chunks (~13 ms avg).&lt;/p&gt;

&lt;p&gt;The takeaway: chunk count is the single biggest latency lever. At 32 chunks Manticore is several times slower than turbopuffer's ~21 ms end-to-end. But auto-optimized to its default floor of 2 chunks (~12–13 ms) it's already faster end-to-end than turbopuffer, and fully compacted to a single chunk (~10 ms) faster still, all on $8.25 hardware. The reason is mostly structural rather than a difference in engine speed: both do the core search in roughly the same ~10–12 ms, but Manticore runs on your own VPS and can be queried over localhost/LAN, so it pays no network round-trip, while turbopuffer always pays at least the ~9 ms in-region hop. The end-to-end gap is essentially that network hop.&lt;/p&gt;

&lt;p&gt;Read (query) latency, not write latency, tracks how compacted the table is, and keeping it compacted is Manticore's job, not yours. Auto-optimize runs in the background and merges chunks down to a target it maintains automatically (our table settled at 2 chunks). That target is a setting, not a chore: the &lt;a href="https://manual.manticoresearch.com/Creating_a_table/Local_tables/Plain_and_real-time_table_settings#optimize_cutoff" rel="noopener noreferrer"&gt;&lt;code&gt;optimize_cutoff&lt;/code&gt;&lt;/a&gt; option (per-table via &lt;code&gt;CREATE&lt;/code&gt;/&lt;code&gt;ALTER&lt;/code&gt;, or globally in &lt;code&gt;searchd&lt;/code&gt;) controls it, so if you want the very best latency you can set &lt;code&gt;optimize_cutoff=1&lt;/code&gt; and the background optimizer will keep the table at a single chunk for you, with no manual step in steady state. The only real catch is timing: right after a large bulk load the table is still at many chunks and queries are slower until auto-optimize catches up. In our run, with 8 chunks left after the concurrency=2 load, the background optimizer took about 15 minutes to compact down to 2 chunks, during which query latency steadily improved. turbopuffer has no equivalent step at all, which is part of what you're paying the premium for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Throughput
&lt;/h2&gt;

&lt;p&gt;Because each query costs the ~12 ms of engine time plus a network round-trip, a single connection is limited to roughly &lt;code&gt;1 / (engine + round-trip)&lt;/code&gt; queries per second, so throughput is recovered by issuing queries concurrently, and the in-region numbers below reflect that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;turbopuffer, in-region, plain single queries&lt;/strong&gt; (no batching). Adding concurrent connections lifts throughput, but only up to a point:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1kxy3yd5k94wlfx5bdao.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1kxy3yd5k94wlfx5bdao.png" alt="Unbatched throughput vs concurrency: both engines peak then decline (turbopuffer ~296 QPS at 8 connections, Manticore 329 at 2 connections)" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unbatched throughput peaks around 8 connections (~296 QPS) and then falls (233 QPS at 16, 153 at 32) while end-to-end latency climbs steeply (P90 33 → 108 → 423 ms at 8 → 16 → 32 connections). Through all of it, turbopuffer's own reported server-side time stays flat at ~12 ms. Because that reported time doesn't move while end-to-end latency rises, the extra delay isn't captured by turbopuffer's own timers: the bottleneck could be on our machine, the transport path, or some portion of request handling outside those timers, and this benchmark can't distinguish them. What it does show is that simply raising unbatched concurrency past 8 didn't improve throughput from this one machine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz31e0wpqjgn1x28ib8gg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz31e0wpqjgn1x28ib8gg.png" alt="turbopuffer latency vs concurrency: end-to-end latency climbs past ~8 connections while turbopuffer's self-reported engine time stays flat at ~12 ms" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We therefore treat ~300 QPS as the observed unbatched ceiling from this single machine, not a documented turbopuffer service limit. (Whether more machines would push higher depends on what's actually saturating, a per-machine resource versus the shared path, which we didn't test.) The lever that took the same single machine well past ~300 was query batching.&lt;/p&gt;

&lt;p&gt;Query batching, packing several queries into one request, amortizes the network round-trip and lifts throughput much higher, with a clear quality-of-service gradient:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~800 QPS cleanly&lt;/strong&gt;: concurrency=4 / batch=8 held 821 QPS with total server-side time P50=15 / P90=20 / P99=29 ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~1,000–1,100 QPS with higher tail latency&lt;/strong&gt;: concurrency=8 / batch=8 reached 1,015 QPS but P99 rose to 58 ms; conc 4 / batch 16 hit 1,105 QPS at P99=61 ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~1,170 QPS oversaturated&lt;/strong&gt;: concurrency=8 / batch=16 served 1,169 QPS, but the total time per request ballooned (P99=455 ms, avg ~70 ms) even though turbopuffer's reported server search time was still only ~12 ms, i.e. requests spent hundreds of milliseconds waiting in line on the server. Not a sustainable operating point.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the practical batched ceiling we observed on this dataset is roughly ~800 QPS cleanly, rising to ~1,100 QPS with elevated server-side tails, comfortably above the two VPS either way.&lt;/p&gt;

&lt;p&gt;Three caveats on batching, though:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It only helps if you have many independent queries arriving at once&lt;/strong&gt; to bundle into a single request: a high-traffic search backend can, but a typical "one user, one query" path can't, so for many real apps the relevant number is the non-batched ceiling above (~300 QPS), not the ~1,000+ headline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The per-query latencies in batched mode are amortized, not wall-clock.&lt;/strong&gt; A query that has to wait for a batch to fill experiences the batch-formation delay plus the request's own latency, so end-user latency under batching is higher than the amortized figure suggests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It pushes complexity onto your application.&lt;/strong&gt; You have to collect queries, hold them briefly to form a batch, fan the combined response back out to the right callers, and reason about partial failures, often inconvenient enough that teams simply don't do it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Manticore, on the two 2-core VPS&lt;/strong&gt;, after the initial load, serving queries &lt;strong&gt;while also taking 4 WPS of writes&lt;/strong&gt; (queried locally, so no network round-trip):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concurrent connections&lt;/th&gt;
&lt;th&gt;Queries per second&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;154&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;329&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;287&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Manticore tops out around 329 QPS at 2 connections; beyond that you're oversubscribing the 2 cores, so 4 connections is slightly slower. Unlike turbopuffer's unbatched decline, this is a hard ceiling: it's the engine on 2 cores, and the only way past it is more cores or more nodes, which costs more money.&lt;/p&gt;

&lt;p&gt;These Manticore figures are unbatched, plain one-query-per-request, the same shape as turbopuffer's unbatched sweep above. Batching wouldn't lift Manticore's ceiling the way it does turbopuffer's: batching helps turbopuffer mainly by amortizing its ~9 ms network round-trip across many queries, but a local Manticore query has no such round-trip to amortize, and the real limit here is CPU on 2 cores, which batching doesn't relieve. So the fair like-for-like is unbatched-vs-unbatched, and there the two land close: &lt;strong&gt;turbopuffer ~296 QPS vs Manticore ~329&lt;/strong&gt;. What differs is what each number means: Manticore's ~329 is a hard 2-core ceiling, while turbopuffer's ~296 is simply where our single machine topped out. Adding concurrency only made it worse, and we couldn't localize the cause (turbopuffer's self-reported engine time stayed flat, but that's its own number, not something this benchmark can verify). So ~300 was the practical unbatched limit in our setup, and the proven path beyond it on turbopuffer was query batching (~800–1,100 QPS from the same machine); on Manticore it's more hardware.&lt;/p&gt;

&lt;p&gt;So on query throughput, &lt;strong&gt;turbopuffer's batched ceiling (~800–1,100 QPS, on its managed compute) is higher than two 2-core VPS (~330 QPS)&lt;/strong&gt;, &lt;strong&gt;while unbatched the two are comparable&lt;/strong&gt;, and turbopuffer's query pricing is cheap (the whole workload's query bill was ~$13). Manticore scales by buying hardware; turbopuffer scales on infrastructure you don't manage. The one nuance in Manticore's favor: at low concurrency &lt;strong&gt;Manticore is actually faster per connection (154 QPS at 1 connection vs turbopuffer's 40)&lt;/strong&gt;, because a local query has no ~9 ms network round-trip to pay.&lt;/p&gt;

&lt;p&gt;But for this workload the throughput axis is close to irrelevant. The target is 4 QPS, and both systems clear it by a huge margin: ~200× headroom for turbopuffer at clean latency, ~80× for Manticore. Throughput only becomes a deciding factor if you expect sustained query volume in the hundreds of QPS, at which point turbopuffer's higher batched ceiling is the safer bet and Manticore needs a bigger (pricier) cluster.&lt;/p&gt;

&lt;p&gt;On the write side the two land closer together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;turbopuffer&lt;/strong&gt; bulk load: 975k docs at ~1150 docs/s. This is the end-to-end rate we observed; what bounds it (its ingestion path, the network, or its own indexing) we can't tell from the outside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manticore&lt;/strong&gt; bulk load (batch=1000): 750–1100 docs/s until the first disk chunk is flushed. Here the bottleneck is visible: HNSW graph construction on 2 cores, since adding insert workers barely helps (451 → 490 docs/s from 1 → 2 threads).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ingestion in practice
&lt;/h2&gt;

&lt;p&gt;Loading the full DBpedia set into Manticore (975k docs, 1536 dims, building the HNSW graph) took:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;Batch&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;th&gt;Wall time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;365 docs/s&lt;/td&gt;
&lt;td&gt;~45 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;451 docs/s&lt;/td&gt;
&lt;td&gt;~36 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;490 docs/s&lt;/td&gt;
&lt;td&gt;~33 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For comparison, turbopuffer loaded the same data at ~1150 docs/s, roughly 2.3x faster than Manticore on these two cheap VPS. That's expected: turbopuffer builds the index on its own (larger, managed) compute, while Manticore is doing the HNSW graph construction on the 2 inexpensive CPU cores. So turbopuffer wins clearly on raw ingest speed, though for a one-time initial load, the difference is ~14 minutes vs ~33–36 minutes, a one-off cost either way.&lt;/p&gt;

&lt;p&gt;After the initial load at batch=1000 the Manticore table sat at 7 disk chunks (1 insert thread) or 8 chunks (2 threads); adding insert workers doesn't help on a 2-core VPS. The final chunk count is driven by ingest rate rather than concurrency: inserts flush disk chunks while auto-optimize merges them in the background, so the faster you load, the more chunks the background merger is left behind by. A fast batch=1000 load leaves 7–8 chunks; slow single-document inserts (batch=1) produce chunks gently enough that auto-optimize keeps up and the table ends at only 3 chunks, but at a lower 365 docs/s instead of ~450–490. From there, Manticore's auto-optimize keeps compacting toward 2 chunks (its default target); lowering &lt;code&gt;optimize_cutoff&lt;/code&gt; to &lt;code&gt;1&lt;/code&gt; makes it maintain a single chunk, the lowest-latency state, automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage and headroom
&lt;/h2&gt;

&lt;p&gt;Manticore's table data for this workload is ~6 GB, against 40 GB of disk on each cheap VPS, so &lt;strong&gt;a single node has room for several times this dataset before you'd need a bigger plan&lt;/strong&gt;. On turbopuffer, storage is billed at ≤$0.33/GB and came to just $1.01 for this dataset, genuinely negligible. As the cost breakdown above shows, the difference between $75 and $16.50 is not about storage; it's the per-GB write charge ($61.44) that Manticore simply doesn't have.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually trade
&lt;/h2&gt;

&lt;p&gt;The numbers favor Manticore on cost and on latency when the table is compacted. But the comparison isn't only numbers: turbopuffer's price buys things that don't show up in a latency table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You give up (by self-hosting Manticore):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero operations.&lt;/strong&gt; turbopuffer has no servers to patch, no replica to fail over, no disk to outgrow. With Manticore, that's your job (though compaction itself is automatic, see below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elastic scale and a higher throughput ceiling.&lt;/strong&gt; turbopuffer separates storage and compute and scales each independently; you can go from 1M to 1B docs without re-architecting, and its managed compute reached ~800–1,100 QPS with batching where two 2-core VPS cap at ~330. Two fixed VPS have a fixed ceiling: raising it means buying more hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale-to-zero economics.&lt;/strong&gt; For spiky or tiny workloads, usage-based billing can undercut even a $16.50/month fixed cost: you pay for what you query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A managed SLA&lt;/strong&gt; (uptime guarantees, support) on the higher tiers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You gain (with Manticore):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~4.5× lower cost&lt;/strong&gt; on this workload: a fixed, predictable bill with no per-GB write charge and no per-query metering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No network round-trip.&lt;/strong&gt; Querying over localhost/LAN avoids the round-trip a remote service always costs you (a ~9 ms floor even when co-located, more across regions), so a compacted table is faster end-to-end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full control and data locality&lt;/strong&gt;: your data on your servers, plus full-text search, filtering, &lt;a href="https://manual.manticoresearch.com/Searching/Hybrid_search" rel="noopener noreferrer"&gt;hybrid search&lt;/a&gt; and SQL in the same engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security and data sovereignty&lt;/strong&gt;: your vectors and source documents never leave infrastructure you control, so nothing is sent to or stored by a third party. For teams with data-residency, GDPR, or confidentiality requirements that's often decisive: you own the encryption, network isolation, and access policy, and there's no external service to vet, trust, or breach. With a managed service the same data lives in the provider's cloud by design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated compute on your own VPS&lt;/strong&gt;: fixed, predictable resources that aren't shared with other tenants.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;For a large, elastic, or bursty workload, or for a team that simply doesn't want to operate a database, turbopuffer's $75/month (and the managed, serverless model behind it) is easy to justify.&lt;/p&gt;

&lt;p&gt;But for the very common case of a bounded workload (around 1M vectors, a few QPS/WPS) where someone is willing to run a couple of servers, two $8.25 VPS running Manticore deliver the same recall, comparable latency after auto-optimize (and better latency when compacted to one chunk, with no network round-trip), ample throughput headroom for the workload, and a ~4.5× lower bill. The catches are that you run and keep the cluster healthy, and that if you ever need many hundreds of QPS, turbopuffer's managed compute scales past two fixed cores.&lt;/p&gt;

&lt;p&gt;The serverless premium is real value, not a markup. The question this benchmark answers is just how much of that value a small, well-understood workload actually consumes, and the answer is: often, not $75 worth.&lt;/p&gt;

</description>
      <category>benchmarks</category>
      <category>vectordatabase</category>
    </item>
    <item>
      <title>Manticore Search 28.4.4: Faster KNN, better conversational search, easier installs and more faceting controls</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Tue, 14 Jul 2026 11:24:55 +0000</pubDate>
      <link>https://dev.to/sanikolaev/manticore-search-2844-faster-knn-better-conversational-search-easier-installs-and-more-32pf</link>
      <guid>https://dev.to/sanikolaev/manticore-search-2844-faster-knn-better-conversational-search-easier-installs-and-more-32pf</guid>
      <description>&lt;p&gt;&lt;a href="https://manticoresearch.com/install/" rel="noopener noreferrer"&gt;Manticore Search 28.4.4&lt;/a&gt; has been released. This release brings faster KNN rescoring, more flexible conversational search, a simpler install and upgrade path, better faceting controls, per-table relevance defaults, and fixes across authentication, replication, SQL compatibility, distributed queries, and columnar/KNN internals.&lt;/p&gt;

&lt;p&gt;This post is a catch-up for everything shipped from &lt;strong&gt;27.2.0 through 28.4.4&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Upgrade Notes
&lt;/h2&gt;

&lt;p&gt;Please review these before upgrading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;28.0.0 bumps the plugin ABI version &lt;code&gt;SPH_UDF_VERSION&lt;/code&gt; to 12.&lt;/strong&gt; External UDF, ranker, and token-filter plugin binaries must be rebuilt before loading them into this version. The change lets token-filter plugins receive long &lt;a href="https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Low-level_tokenization#dict" rel="noopener noreferrer"&gt;&lt;code&gt;dict=keywords_32k&lt;/code&gt;&lt;/a&gt; tokens instead of silently bypassing values above the old 126-byte plugin limit. Existing table data and configuration remain compatible, and no index migration is required. Downgrade is possible if you also restore plugin binaries built for the older ABI and do not rely on the new long-token plugin behavior. (&lt;a href="https://github.com/manticoresoftware/manticoresearch/issues/4667" rel="noopener noreferrer"&gt;Issue #4667&lt;/a&gt;, &lt;a href="https://github.com/manticoresoftware/manticoresearch/pull/4668" rel="noopener noreferrer"&gt;PR #4668&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If authentication is enabled, prioritize the 28.4.x update.&lt;/strong&gt; 28.4.2 fixes an authentication/authorization permission-check bypass in MySQL multi-statement execution. 28.3.4 also improves authenticated MySQL startup compatibility for Connector/J and PyMySQL, and 28.4.1 fixes authenticated replication-cluster restart recovery by persisting the stored replication user in cluster metadata. (&lt;a href="https://github.com/manticoresoftware/manticoresearch/pull/4713" rel="noopener noreferrer"&gt;PR #4713&lt;/a&gt;, &lt;a href="https://github.com/manticoresoftware/manticoresearch/issues/4691" rel="noopener noreferrer"&gt;Issue #4691&lt;/a&gt;, &lt;a href="https://github.com/manticoresoftware/manticoresearch/issues/4705" rel="noopener noreferrer"&gt;Issue #4705&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you manage MCL separately from the daemon, upgrade it together with Manticore.&lt;/strong&gt; This release includes &lt;a href="https://github.com/manticoresoftware/columnar" rel="noopener noreferrer"&gt;MCL&lt;/a&gt; 13.7.0, the new &lt;a href="https://manual.manticoresearch.com/Server_settings/Searchd#embeddings_threads" rel="noopener noreferrer"&gt;&lt;code&gt;embeddings_threads&lt;/code&gt;&lt;/a&gt; setting, and several columnar/KNN build, packaging, and cleanup fixes. Mixing an older library with a newer daemon is not recommended. (&lt;a href="https://github.com/manticoresoftware/columnar/pull/169" rel="noopener noreferrer"&gt;PR #169&lt;/a&gt;, &lt;a href="https://github.com/manticoresoftware/columnar/pull/186" rel="noopener noreferrer"&gt;PR #186&lt;/a&gt;, &lt;a href="https://github.com/manticoresoftware/manticoresearch/pull/4676" rel="noopener noreferrer"&gt;PR #4676&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Highlights
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Faster KNN rescoring
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://manual.manticoresearch.com/Searching/KNN#KNN-vector-search" rel="noopener noreferrer"&gt;KNN search&lt;/a&gt; now batches distance calculations during the &lt;code&gt;rescore&lt;/code&gt; pass. After HNSW returns the candidate set, Manticore recomputes final full-precision distances and re-sorts the results. Batching that work reduces per-candidate overhead in the final stage of vector search.&lt;/p&gt;

&lt;p&gt;For vector-heavy workloads, this takes work out of the part of the query that runs after candidate selection. Results do not change; the final ranking pass just has less overhead when many candidates are rescored.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conversational search through SQL and HTTP
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://manual.manticoresearch.com/Searching/Conversational_search" rel="noopener noreferrer"&gt;Conversational search&lt;/a&gt; is now available through the &lt;code&gt;/search&lt;/code&gt; JSON API as well as SQL &lt;code&gt;CALL CHAT&lt;/code&gt;. That makes it easier to use Manticore's chat flow from applications that already talk to the HTTP API and do not want to add a separate SQL path just for chat requests.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CREATE CHAT MODEL&lt;/code&gt; also gained &lt;code&gt;custom_prompt&lt;/code&gt; support, so answers can follow application-specific instructions such as citation rules, tone, response length, or formatting. The feature is still built on the same Manticore Search flow: retrieve relevant documents from an existing vectorized table, build context, keep conversation history, and return an answer with supporting sources.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-line installation
&lt;/h3&gt;

&lt;p&gt;The quick-start install path is now simpler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://manticoresearch.com | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same installer can also upgrade an existing installation, list available versions, switch between stable and development repositories, and install a selected version. Package managers still remain the source of truth for installed files, repositories, services, and dependencies; the new path just removes the manual setup steps around them.&lt;/p&gt;

&lt;p&gt;For all options, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://manticoresearch.com | sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Facets can keep zero-count buckets visible
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://manual.manticoresearch.com/Searching/Faceted_search" rel="noopener noreferrer"&gt;Faceted search&lt;/a&gt; now supports zero-count facet buckets through SQL &lt;code&gt;ZEROES&lt;/code&gt; and JSON &lt;code&gt;"zeroes": true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a small but important UI feature. In e-commerce-style filtering, you often want to keep an option visible even when the current filter combination gives it a count of &lt;code&gt;0&lt;/code&gt;. Combined with &lt;code&gt;max&lt;/code&gt;-mode facet behavior, zero-count buckets make it easier to show selected, available, and currently unavailable choices without hiding part of the filter vocabulary from the user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better defaults for search relevance
&lt;/h3&gt;

&lt;p&gt;Manticore now supports &lt;a href="https://manual.manticoresearch.com/Creating_a_table/Local_tables/Plain_and_real-time_table_settings#profile" rel="noopener noreferrer"&gt;&lt;code&gt;CREATE TABLE ... profile='relevance'&lt;/code&gt;&lt;/a&gt;, plus stored per-table defaults for &lt;a href="https://manual.manticoresearch.com/Searching/Options#ranker" rel="noopener noreferrer"&gt;&lt;code&gt;ranker&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://manual.manticoresearch.com/Searching/Options#boolean_mode" rel="noopener noreferrer"&gt;&lt;code&gt;boolean_mode&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Based on our search quality tests, &lt;code&gt;profile='relevance'&lt;/code&gt; and the ranking settings it enables improve relevance in many cases. The application also no longer needs to repeat the same ranking parameters in every request.&lt;/p&gt;

&lt;h3&gt;
  
  
  More control over embedding CPU usage
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://manual.manticoresearch.com/Server_settings/Searchd#embeddings_threads" rel="noopener noreferrer"&gt;&lt;code&gt;embeddings_threads&lt;/code&gt;&lt;/a&gt; caps the CPU threads used for auto-embedding inserts, &lt;code&gt;ALTER TABLE ... REBUILD KNN&lt;/code&gt;, and text-to-vector KNN queries.&lt;/p&gt;

&lt;p&gt;This matters on shared hosts and mixed workloads. Embedding generation and KNN rebuilds can be CPU-heavy; a server-level cap makes those jobs easier to schedule without letting them take over the whole machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug Fixes
&lt;/h2&gt;

&lt;p&gt;This release includes &lt;strong&gt;17 bug fixes&lt;/strong&gt;. The most important ones are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;28.4.3 fixed incompatible multi-statement handling around &lt;a href="https://manual.manticoresearch.com/Searching/Grouping#COUNT%28DISTINCT-field%29" rel="noopener noreferrer"&gt;&lt;code&gt;COUNT(DISTINCT ...)&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;28.4.2 fixed an authentication/authorization permission-check bypass in MySQL multi-statement execution, so each statement in a multi-statement request is validated correctly under auth.&lt;/li&gt;
&lt;li&gt;28.4.1 and 28.3.1 fixed replication-cluster recovery edge cases: authenticated cluster restart recovery and startup when a node was left alone in the cluster.&lt;/li&gt;
&lt;li&gt;Distributed queries with remote stored fields now fail on remote &lt;code&gt;GETFIELD&lt;/code&gt; fetch errors or malformed replies instead of returning apparently successful rows with empty or untrusted stored-field values.&lt;/li&gt;
&lt;li&gt;Interrupted columnar/KNN merge cleanup now removes temporary component files, preventing orphaned &lt;code&gt;.tmp.spc.*&lt;/code&gt; files from breaking later table rename, attach, or drop operations.&lt;/li&gt;
&lt;li&gt;DBeaver compatibility improved by accepting simple single-table aliases in &lt;code&gt;SELECT&lt;/code&gt; queries.&lt;/li&gt;
&lt;li&gt;Connector/J and PyMySQL clients can now complete authenticated native-password login flows, and harmless session &lt;code&gt;SET&lt;/code&gt; statements no longer fail under auth.&lt;/li&gt;
&lt;li&gt;The built-in Ukrainian lemmatizer now normalizes apostrophe words correctly, so forms such as &lt;code&gt;здоров'ям&lt;/code&gt; match &lt;code&gt;здоров'я&lt;/code&gt; under &lt;code&gt;lemmatize_uk_all&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Blended-keyword handling now honors the configured &lt;code&gt;blend_mode&lt;/code&gt;, restoring separator-stripped variants consistently for indexing and keyword extraction.&lt;/li&gt;
&lt;li&gt;RT auto-optimization no longer compacts a table below two disk chunks unless that lower cutoff is explicitly requested.&lt;/li&gt;
&lt;li&gt;A crash involving &lt;code&gt;percentiles&lt;/code&gt; aggregations together with terms aggregations in the same &lt;code&gt;/search&lt;/code&gt; request on multi-chunk RT tables was fixed.&lt;/li&gt;
&lt;li&gt;Long SQL parse errors now preserve UTF-8 character boundaries, so invalid queries containing Cyrillic and other multibyte text no longer produce truncated or corrupted error messages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the complete list, see the &lt;a href="https://manual.manticoresearch.com/Changelog#Version-28.4.4" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Need help or want to connect?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Join our &lt;a href="https://slack.manticoresearch.com" rel="noopener noreferrer"&gt;Slack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Visit the &lt;a href="https://forum.manticoresearch.com" rel="noopener noreferrer"&gt;Forum&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Report issues or suggest features on &lt;a href="https://github.com/manticoresoftware/manticoresearch/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email us at &lt;code&gt;contact@manticoresearch.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>machinelearning</category>
      <category>news</category>
    </item>
    <item>
      <title>Sharding in Manticore Search: automatic distribution and replication</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Fri, 03 Jul 2026 03:32:41 +0000</pubDate>
      <link>https://dev.to/sanikolaev/sharding-in-manticore-search-automatic-distribution-and-replication-12ha</link>
      <guid>https://dev.to/sanikolaev/sharding-in-manticore-search-automatic-distribution-and-replication-12ha</guid>
      <description>&lt;p&gt;Search systems often start simple: one table on one server. That works until one of two things happens. Either a single query stops being able to use all the CPU you paid for, or a single server stops being enough — for capacity, for throughput, or for the simple fact that a server can fail and take your data with it.&lt;/p&gt;

&lt;p&gt;The automatic sharding built into Manticore Search, available since release &lt;a href="https://dev.to/blog/manticore-search-27-1-5/"&gt;27.1.5&lt;/a&gt;, addresses both issues by &lt;strong&gt;splitting a table into several smaller physical pieces (shards), that can be searched in parallel and placed on different nodes&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On a single node&lt;/strong&gt;, sharding spreads concurrent writes across independent pieces and keeps each one small enough to stay fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Across a cluster&lt;/strong&gt;, sharding distributes data over multiple nodes and — this is the main point — &lt;strong&gt;automatically replicates each shard and keeps that replication factor intact&lt;/strong&gt; as nodes fail and recover.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second part is the real reason most people reach for sharding: high availability. You declare how many shards you want and how many copies of each should exist, and Manticore handles placement, replication, and rebalancing. You don't script failover.&lt;/p&gt;

&lt;p&gt;Below: both use cases, the machinery without drowning in internals, the commands you'll run, and the current limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Short glossary
&lt;/h2&gt;

&lt;p&gt;Key terms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shard&lt;/td&gt;
&lt;td&gt;One physical piece of a table — a real table that Manticore creates and manages for you. A table with &lt;code&gt;shards='4'&lt;/code&gt; has four of them.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replica&lt;/td&gt;
&lt;td&gt;A copy of a shard on another node. Replicas are how data survives a node failure.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replication factor (RF)&lt;/td&gt;
&lt;td&gt;How many nodes hold a copy of each shard. &lt;code&gt;rf='2'&lt;/code&gt; means every shard exists on two nodes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distributed table&lt;/td&gt;
&lt;td&gt;The table you actually query. It has the name you gave it and transparently fans queries out to all shards.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster&lt;/td&gt;
&lt;td&gt;A Manticore replication cluster — the group of nodes between which data is replicated.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Master&lt;/td&gt;
&lt;td&gt;The node that currently coordinates sharding operations (placement, rebalancing). Elected automatically.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rebalancing&lt;/td&gt;
&lt;td&gt;The automatic process that moves or copies shards when the set of nodes changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to create a sharded table
&lt;/h2&gt;

&lt;p&gt;Sharding in Manticore is driven entirely by two simple options on &lt;code&gt;CREATE TABLE&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;shards&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'4'&lt;/span&gt; &lt;span class="n"&gt;rf&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'2'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;shards='N'&lt;/code&gt; — split the table into &lt;code&gt;N&lt;/code&gt; physical pieces.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rf='M'&lt;/code&gt; — keep &lt;code&gt;M&lt;/code&gt; copies of each piece across the cluster (the replication factor).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the common case, that one &lt;code&gt;CREATE TABLE&lt;/code&gt; is all you write. There is no separate "make this distributed" step, no manual &lt;code&gt;agent=&lt;/code&gt; lists as in older manual sharding setups, and no per-node table creation. Manticore creates the physical shards, places them, sets up replication, and creates a distributed table named &lt;code&gt;products&lt;/code&gt; on every node, so the application can use the same table name from any cluster node.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case A: sharding on a single node
&lt;/h2&gt;

&lt;p&gt;Start with the case where you have a single server — perhaps a big, many-core one — and no cluster yet. In this setup, sharding is not about storage durability; it helps use that one machine more effectively. If all writes go into one real-time table, concurrent INSERTs contend on the same internal table locks. As the table grows, &lt;a href="https://manual.manticoresearch.com/Securing_and_compacting_a_table/Compacting_a_table#Compacting-a-Table" rel="noopener noreferrer"&gt;RAM-chunk merges&lt;/a&gt; get heavier and can slow down ingestion. Splitting that table into several independent shards helps on both fronts: writes spread across the shards, and each piece stays small. High availability isn't part of the picture yet — that needs more than one node — so this is purely a performance play.&lt;/p&gt;

&lt;p&gt;The simplest form has no cluster and &lt;code&gt;rf='1'&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;logs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;shards&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'8'&lt;/span&gt; &lt;span class="n"&gt;rf&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'1'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates eight physical shards on the one node and a distributed table &lt;code&gt;logs&lt;/code&gt; that points at all of them. How does that help on a single machine?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;More concurrent ingestion.&lt;/strong&gt; Each shard is an independent real-time table, so concurrent writes spread across them instead of serializing on one table's locks — the win the benchmarks below measure directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smaller pieces stay fast.&lt;/strong&gt; Real-time tables periodically merge their internal RAM chunks. A table split into shards keeps each shard's chunks smaller, so those merges use fewer resources and are less likely to slow inserts down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query parallelism (usually a small gain on one node).&lt;/strong&gt; A distributed table searches its shards in parallel across the server's worker thread pool, so a single query can use several cores instead of one — bounded by &lt;code&gt;searchd.threads&lt;/code&gt; and the number of physical cores. On a single node, though, this overlaps with &lt;a href="https://manual.manticoresearch.com/Server_settings/Searchd#pseudo_sharding" rel="noopener noreferrer"&gt;pseudo-sharding&lt;/a&gt;, and the gain is usually small (~5–12%) — see the read benchmarks below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've used Manticore's &lt;a href="https://manual.manticoresearch.com/Server_settings/Searchd#pseudo_sharding" rel="noopener noreferrer"&gt;pseudo-sharding&lt;/a&gt; before, the goal may be familiar — use all the cores for one query — but the mechanism is different. Pseudo-sharding parallelizes a &lt;em&gt;single&lt;/em&gt; physical table automatically at query time. Explicit sharding creates &lt;em&gt;real&lt;/em&gt; shards you control: you decide how many, they're separate tables you can reason about, and — crucially — the &lt;em&gt;same&lt;/em&gt; sharded table can later be spread across nodes without changing how your application talks to it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The two are complementary, but they don't stack for free. Physical sharding — a distributed table over several local tables — already keeps the worker threads busy, so if you've explicitly sharded a table, enabling &lt;code&gt;pseudo_sharding&lt;/code&gt; on top usually adds little and can even cost a bit of throughput. Test it both ways with &lt;a href="https://github.com/manticoresoftware/manticore-load" rel="noopener noreferrer"&gt;&lt;code&gt;manticore-load&lt;/code&gt;&lt;/a&gt;: run your workload with and without &lt;code&gt;pseudo_sharding&lt;/code&gt;, and if it adds nothing on top of explicit shards, turn it off.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On a single node the replication factor must be &lt;code&gt;1&lt;/code&gt;: there's only one node, so there's nowhere to put a second copy. That's also the catch — single-node sharding gives you parallelism, not durability. For durability you need more than one node.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case B: multi-node sharding and automatic replication
&lt;/h2&gt;

&lt;p&gt;This is what sharding is really for. Start from a replication cluster of several nodes (see &lt;a href="https://manual.manticoresearch.com/Creating_a_cluster/Setting_up_replication/Setting_up_replication" rel="noopener noreferrer"&gt;Setting up replication&lt;/a&gt; for how to create one), then create the table inside that cluster with the &lt;code&gt;cluster:&lt;/code&gt; prefix and an RF greater than 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;mycluster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;shards&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'4'&lt;/span&gt; &lt;span class="n"&gt;rf&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'2'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what Manticore does for you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creates four shards.&lt;/li&gt;
&lt;li&gt;Places them across the cluster's nodes in a balanced way.&lt;/li&gt;
&lt;li&gt;Creates a &lt;strong&gt;second copy of every shard on a different node&lt;/strong&gt;, because &lt;code&gt;rf='2'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Wires up replication between each shard and its replica.&lt;/li&gt;
&lt;li&gt;Creates a distributed table &lt;code&gt;products&lt;/code&gt; on every node, so any node can serve reads and accept writes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From the application's point of view, nothing changed — you still &lt;code&gt;INSERT INTO products …&lt;/code&gt; and &lt;code&gt;SELECT … FROM products&lt;/code&gt;. Reads fan out across the shards and the results are merged; writes are routed to a shard. But now every shard lives on two nodes, and that's the property you care about: &lt;strong&gt;any single node can fail and the table stays fully available with no data loss.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The replication factor scales with your durability needs and your node count:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RF&lt;/th&gt;
&lt;th&gt;Copies per shard&lt;/th&gt;
&lt;th&gt;Survives&lt;/th&gt;
&lt;th&gt;Typical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;nothing — a lost node loses its shards&lt;/td&gt;
&lt;td&gt;single-node parallelism, dev/test, data you can rebuild&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;one node failure&lt;/td&gt;
&lt;td&gt;the common production choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3+&lt;/td&gt;
&lt;td&gt;3 or more&lt;/td&gt;
&lt;td&gt;multiple simultaneous failures&lt;/td&gt;
&lt;td&gt;mission-critical, frequent-failure environments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The constraint is simple: &lt;strong&gt;you can't ask for more copies than you have nodes.&lt;/strong&gt; &lt;code&gt;rf='3'&lt;/code&gt; needs at least three nodes in the cluster. Manticore checks this when you create the table and tells you if the cluster is too small.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 6 shards, 3 copies each, across a 3+ node cluster&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;mycluster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;shards&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'6'&lt;/span&gt; &lt;span class="n"&gt;rf&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'3'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Putting it together: a multi-node walkthrough
&lt;/h2&gt;

&lt;p&gt;Say you have a three-node replication cluster called &lt;code&gt;mycluster&lt;/code&gt; (if you don't yet, &lt;a href="https://manual.manticoresearch.com/Creating_a_cluster/Setting_up_replication/Setting_up_replication" rel="noopener noreferrer"&gt;Setting up replication&lt;/a&gt; walks through &lt;code&gt;CREATE CLUSTER&lt;/code&gt; and &lt;code&gt;JOIN CLUSTER&lt;/code&gt;). Create a sharded, replicated table from any node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;mycluster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;shards&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'4'&lt;/span&gt; &lt;span class="n"&gt;rf&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'2'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Manticore creates four shards, puts two copies of each across the three nodes, and a distributed table &lt;code&gt;products&lt;/code&gt; on every node. Check the placement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;SHARDING&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- illustrative output (abbreviated columns)
+-------+-------+--------+----+-----------+
| shard | node  | status | rf | rf_status |
+-------+-------+--------+----+-----------+
|     0 | node1 | active |  2 | ok        |
|     0 | node2 | active |  2 | ok        |
|     1 | node2 | active |  2 | ok        |
|     1 | node3 | active |  2 | ok        |
|     2 | node1 | active |  2 | ok        |
|     2 | node3 | active |  2 | ok        |
|     3 | node1 | active |  2 | ok        |
|     3 | node2 | active |  2 | ok        |
+-------+-------+--------+----+-----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every shard appears on two distinct nodes — that's &lt;code&gt;rf=2&lt;/code&gt; — and every &lt;code&gt;rf_status&lt;/code&gt; is &lt;code&gt;ok&lt;/code&gt;. (The full result also includes &lt;code&gt;table&lt;/code&gt;, &lt;code&gt;cluster&lt;/code&gt;, and &lt;code&gt;replication_cluster&lt;/code&gt; columns.) Now use it like any other table, from any node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Wireless mouse'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mouse'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The write is routed to a shard and replicated to that shard's other copy; the read fans out across all four shards and merges the results. Your application never names a shard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintaining the replication factor
&lt;/h2&gt;

&lt;p&gt;Setting &lt;code&gt;rf='2'&lt;/code&gt; is easy. The hard part in any distributed system is honoring that condition &lt;em&gt;over time&lt;/em&gt;, as machines fail and come back and as you add capacity. But you no longer have to worry about that. Manticore Search automates this work.&lt;/p&gt;

&lt;p&gt;How it works in Manticore is that the cluster elects a &lt;strong&gt;master&lt;/strong&gt; node that runs a coordination loop. It monitors the cluster's topology — which nodes are alive — and reacts to changes:&lt;/p&gt;

&lt;h4&gt;
  
  
  A node fails
&lt;/h4&gt;

&lt;p&gt;Its shards now have fewer copies than RF requires. The master detects the missing node and tries to rebuild the missing replicas. If the cluster still has at least &lt;code&gt;rf&lt;/code&gt; active nodes after the failure, it places new copies on active nodes that don't already hold them, restoring the replication factor. Queries keep working as long as at least one copy of each shard is still available.&lt;/p&gt;

&lt;p&gt;Continuing the walkthrough above — if &lt;code&gt;node3&lt;/code&gt; goes down, &lt;code&gt;SHOW SHARDING STATUS products&lt;/code&gt; shows the affected shards as &lt;code&gt;degraded&lt;/code&gt; (one copy down, one still up):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- illustrative: node3 is down
+-------+-------+----------+----+-----------+
| shard | node  | status   | rf | rf_status |
+-------+-------+----------+----+-----------+
|     1 | node2 | active   |  2 | degraded  |
|     1 | node3 | inactive |  2 | degraded  |
|     2 | node1 | active   |  2 | degraded  |
|     2 | node3 | inactive |  2 | degraded  |
|   ... | ...   | ...      |    | ...       |
+-------+-------+----------+----+-----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are still two active nodes (&lt;code&gt;node1&lt;/code&gt;, &lt;code&gt;node2&lt;/code&gt;) and &lt;code&gt;rf=2&lt;/code&gt;, so the master creates the missing copies of shards 1 and 2 on the active node that lacks them. While a new copy is being built it shows up as &lt;code&gt;pending&lt;/code&gt;; once replication catches up it becomes &lt;code&gt;active&lt;/code&gt; and &lt;code&gt;rf_status&lt;/code&gt; returns to &lt;code&gt;ok&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The important caveat: &lt;strong&gt;Manticore can only restore RF if there is somewhere to put the new copy.&lt;/strong&gt; If the active node count drops below &lt;code&gt;rf&lt;/code&gt;, the requested RF cannot be met yet: affected shards stay &lt;code&gt;degraded&lt;/code&gt; with their surviving copy until a node returns or you add one. Manticore won't create another copy of the same shard on the same node, and it won't silently pretend RF is met. If no live copy of a shard remains, its status becomes &lt;code&gt;broken&lt;/code&gt;; that case is covered below. For &lt;code&gt;rf='1'&lt;/code&gt;, a failed node's shards are simply gone — there was never a second copy.&lt;/p&gt;

&lt;h4&gt;
  
  
  A node joins
&lt;/h4&gt;

&lt;p&gt;New capacity should be used. The master rebalances so the new node takes its share of the load. How it does this depends on the RF:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RF = 1:&lt;/strong&gt; shards must be &lt;em&gt;moved&lt;/em&gt; (there's only one copy, so it can't just be duplicated). Manticore moves them safely using a temporary internal cluster: it copies the data to the new node first and removes it from the old one only after that, so the shard always has an available copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RF ≥ 2:&lt;/strong&gt; shards are &lt;em&gt;replicated&lt;/em&gt; to the new node using the cluster's existing replication, then the distribution is rebalanced. No risky data movement, because another copy always exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Every copy of a shard is down
&lt;/h4&gt;

&lt;p&gt;If all nodes holding a given shard are lost at once, that shard's &lt;code&gt;rf_status&lt;/code&gt; becomes &lt;code&gt;broken&lt;/code&gt; — there's no surviving copy to serve or to replicate from. The rest of the table keeps working; the broken shard recovers when one of its nodes returns. RF reduces the chance of this case: with &lt;code&gt;rf='2'&lt;/code&gt; it takes two simultaneous failures of the &lt;em&gt;right&lt;/em&gt; nodes, with &lt;code&gt;rf='3'&lt;/code&gt; three.&lt;/p&gt;

&lt;p&gt;All of this happens through an internal, ordered, rollback-aware operation queue, so a rebalancing operation either completes or is cleanly rolled back — even if the master node itself dies mid-operation, the next master cleans up the half-finished work. The point for you as an operator: &lt;strong&gt;you set RF once, and the cluster works to keep it true.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood (the short version)
&lt;/h2&gt;

&lt;p&gt;You don't need this to use sharding, but it helps to know what's happening.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Physical shards are real tables.&lt;/strong&gt; A table with four shards is backed by four real tables that Manticore creates and manages for you. You normally never touch them directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your application talks to a &lt;a href="https://manual.manticoresearch.com/Creating_a_table/Creating_a_distributed_table/Creating_a_distributed_table" rel="noopener noreferrer"&gt;distributed table&lt;/a&gt;.&lt;/strong&gt; Manticore creates one named &lt;code&gt;products&lt;/code&gt; on every node. In its internal definition, local shards are listed directly, and shards on other nodes are connected through &lt;code&gt;agent&lt;/code&gt;. That's what makes &lt;code&gt;SELECT … FROM products&lt;/code&gt; transparently hit everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coordination state lives in the cluster.&lt;/strong&gt; Manticore tracks its own internal metadata — shard placement, coordination state, and the pending-operation queue — so it always knows who holds what and what work is still outstanding. In a multi-node setup this state is replicated across the cluster, so every node shares the same view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The master drives changes.&lt;/strong&gt; Placement, replication setup, and rebalancing are computed by the master and pushed onto the queue as ordered commands with rollback instructions, then executed across nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replication reuses Manticore's clustering.&lt;/strong&gt; The same proven replication mechanism Manticore already uses for clusters keeps shard replicas in sync.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Architecturally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;            CREATE TABLE ... shards='4' rf='2'
                          │
                          ▼
                  ┌────────────────┐
                  │ Manticore      │  computes placement,
                  │ elected master │  enqueues ordered ops
                  └────────┬───────┘
                          │
        ┌─────────────────┼─────────────────┐
        ▼                 ▼                 ▼
    ┌──────────┐     ┌──────────┐     ┌──────────┐
    │  node1   │     │  node2   │     │  node3   │
    │ s0 s2 s3 │◄───►│ s0 s1 s3 │◄───►│  s1 s2   │   (each shard on 2 nodes = rf 2)
    └──────────┘     └──────────┘     └──────────┘
    distributed table "products" exists on every node
    (same placement as the SHOW SHARDING STATUS output above)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Operating a sharded table
&lt;/h2&gt;

&lt;p&gt;Everything you'd expect to work, works — and there are a couple of sharding-specific commands for visibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inspect the schema.&lt;/strong&gt; &lt;code&gt;DESC&lt;/code&gt; and &lt;code&gt;SHOW CREATE TABLE&lt;/code&gt; work on the logical table; Manticore resolves them through the underlying shards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;See where every shard lives and whether the RF is healthy.&lt;/strong&gt; This is the command you'll watch during failures and rebalancing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;SHARDING&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------+-------+--------+----+-----------+
| shard | node  | status | rf | rf_status |
+-------+-------+--------+----+-----------+
|     0 | node1 | active |  2 | ok        |
|     0 | node2 | active |  2 | ok        |
|   ... | ...   | ...    |    | ...       |
+-------+-------+--------+----+-----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reports one row per shard copy, with these columns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the logical table name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;shard number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the node holding this copy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;active&lt;/code&gt;, &lt;code&gt;inactive&lt;/code&gt; (node down), or &lt;code&gt;pending&lt;/code&gt; (being created)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cluster&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the replication cluster the table belongs to&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;replication_cluster&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the internal cluster that keeps this shard's copies in sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;how many copies this shard currently has&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rf_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ok&lt;/code&gt; (RF satisfied), &lt;code&gt;degraded&lt;/code&gt; (some copies down but at least one up), or &lt;code&gt;broken&lt;/code&gt; (no copies up)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;rf_status&lt;/code&gt; is the at-a-glance health signal: all &lt;code&gt;ok&lt;/code&gt; means the cluster is meeting the replication factor you asked for; &lt;code&gt;degraded&lt;/code&gt; means it's working but exposed; &lt;code&gt;broken&lt;/code&gt; means a shard is down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find the coordinator:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;SHARDING&lt;/span&gt; &lt;span class="n"&gt;MASTER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Drop it cleanly.&lt;/strong&gt; Dropping a sharded table works exactly like dropping a regular table — &lt;code&gt;DROP TABLE&lt;/code&gt; removes the table and all its shards across the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Scale by changing the cluster.&lt;/strong&gt; Because rebalancing is automatic, the way you scale a sharded table out is by adding nodes to the cluster (&lt;a href="https://manual.manticoresearch.com/Creating_a_cluster/Adding_a_new_node" rel="noopener noreferrer"&gt;Adding a new node&lt;/a&gt;). The master notices the new node and rebalances onto it without any action on the table itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the shard count and replication factor
&lt;/h2&gt;

&lt;p&gt;A few rules of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shards for faster writes:&lt;/strong&gt; a handful of shards is usually enough, well below your core count — in the benchmarks below a 16-core / 32-thread box peaked at &lt;strong&gt;4–8 shards&lt;/strong&gt; and by 32 shards was &lt;em&gt;slower than no sharding at all&lt;/em&gt;. Start small (4–8), measure, and only add more if your own numbers say so. More shards than that rarely helps and adds per-shard overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shards for distribution:&lt;/strong&gt; with multiple nodes, you want enough shards that they divide evenly across nodes and leave room to grow — a multiple of your node count is a good default. Don't go wild: each shard is a real table with its own overhead. (Manticore caps the shard count at &lt;strong&gt;3000&lt;/strong&gt;.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RF for durability:&lt;/strong&gt; &lt;code&gt;rf='2'&lt;/code&gt; is the standard production choice — it survives any single node failure at 2× storage. Use &lt;code&gt;rf='3'&lt;/code&gt; only when you genuinely need to survive simultaneous failures or have strict availability requirements, and remember it costs 3× the storage and more replication traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RF=1 is for performance or throwaway data only.&lt;/strong&gt; It has no fault tolerance. Use it on a single node for parallelism, or in a cluster only when you have an external way to rebuild lost data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Benchmarks: does sharding actually speed up inserts?
&lt;/h2&gt;

&lt;p&gt;A feature is only worth using if it earns its keep, so we measured. The question we wanted answered honestly: &lt;strong&gt;for the same workload, does sharding make ingestion faster — and if so, when?&lt;/strong&gt; The methodology was simple: compare every sharded run against the same baseline — a regular table without sharding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt; on a 16-core box with 32 concurrent writers, sharding raised insert throughput by about &lt;strong&gt;1.5×&lt;/strong&gt; at its best — from ~163k to ~253k docs/s — but only when the shard count stayed small. The best results came at &lt;strong&gt;4–8 shards&lt;/strong&gt;; by 32 shards, throughput had fallen &lt;em&gt;below&lt;/em&gt; the unsharded baseline. The binary log cost roughly &lt;strong&gt;25%&lt;/strong&gt; of write performance, and &lt;code&gt;rf=2&lt;/code&gt; replication across two real machines cost about &lt;strong&gt;30%&lt;/strong&gt; more — fair prices for durability, but not free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup.&lt;/strong&gt; A dedicated server with no other significant CPU load — AMD Ryzen 9 5950X (16 cores / 32 threads), 128 GB RAM. Everything ran inside a single Docker container running a recent dev build with the sharding feature. Manticore ran with &lt;strong&gt;stock settings — no performance tuning&lt;/strong&gt;: only listener ports, the data directory, and the binary log path were set; the thread pool, RT memory limits, and binary log behaviour were left at their defaults. Load came from &lt;a href="https://github.com/manticoresoftware/manticore-load" rel="noopener noreferrer"&gt;&lt;code&gt;manticore-load&lt;/code&gt;&lt;/a&gt;. Each run inserts the same documents — &lt;code&gt;(id bigint, name text, type int)&lt;/code&gt; where &lt;code&gt;name&lt;/code&gt; is 10–100 random words — in batches of 1000 into a real-time table. Only the &lt;strong&gt;shard count&lt;/strong&gt;, the &lt;strong&gt;replication factor&lt;/strong&gt;, and the &lt;strong&gt;binary log&lt;/strong&gt; change between runs; the "no sharding" baseline is a plain RT table. We run the full single-node shard sweep &lt;strong&gt;twice — once with the binary log on (the default), once with it off&lt;/strong&gt; — inserting &lt;strong&gt;20,000,000&lt;/strong&gt; docs per run with &lt;strong&gt;32 concurrent writers&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# the exact shape of every insert run (shards/rf vary)&lt;/span&gt;
manticore-load &lt;span class="nt"&gt;--batch-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000 &lt;span class="nt"&gt;--threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;32 &lt;span class="nt"&gt;--total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20000000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--init&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"create table test(id bigint, name text, type int) shards='8' rf='1'"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"insert into test(id,name,type) values(&amp;lt;increment&amp;gt;,'&amp;lt;text/10/100&amp;gt;',&amp;lt;int/1/100&amp;gt;)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Single node: throughput vs shard count
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn9o2x51594mvacc1z4y3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn9o2x51594mvacc1z4y3.png" alt=" " width="800" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docs inserted per second, 20M docs, 32 writers — both binary log modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shards&lt;/th&gt;
&lt;th&gt;binary log on (default)&lt;/th&gt;
&lt;th&gt;binary log off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;none (baseline)&lt;/td&gt;
&lt;td&gt;162,920&lt;/td&gt;
&lt;td&gt;218,079&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;191,976&lt;/td&gt;
&lt;td&gt;246,288&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;252,807&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;290,665&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;251,008&lt;/td&gt;
&lt;td&gt;265,015&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;175,848&lt;/td&gt;
&lt;td&gt;182,288&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;108,006&lt;/td&gt;
&lt;td&gt;111,381&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The chart plots the full sweep twice — binary log &lt;strong&gt;on&lt;/strong&gt; (blue, the default) and &lt;strong&gt;off&lt;/strong&gt; (orange). Three things stand out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent inserts get a real boost.&lt;/strong&gt; With 32 writers, splitting the table lifts throughput by &lt;strong&gt;~1.5× at the peak&lt;/strong&gt; — from &lt;strong&gt;163k to 253k docs/s&lt;/strong&gt; on the default (binary-log-on) line. Each shard is an independent real-time table, so spreading writes across several sharply cuts the lock contention a single RT table hits under concurrency, and lets the inserts use more cores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The best range is small — 4–8 shards.&lt;/strong&gt; The gain peaks there and then falls off fast. By 16 shards it's barely above baseline, and &lt;strong&gt;at 32 shards throughput is &lt;em&gt;below&lt;/em&gt; the unsharded table (0.66×)&lt;/strong&gt; — past that range, per-shard and coordination overhead outweighs the extra parallelism. More shards is emphatically &lt;em&gt;not&lt;/em&gt; better. Both lines have the &lt;strong&gt;same shape and the same best range&lt;/strong&gt; — the binary log doesn't move it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durability costs most where you're fastest.&lt;/strong&gt; Turning the binary log off (orange) lifts the whole curve, but the gap is widest in the high-throughput region — &lt;strong&gt;253k → 291k at 4 shards&lt;/strong&gt; — and nearly vanishes once the shard count is too high (&lt;strong&gt;108k vs 111k at 32 shards&lt;/strong&gt;), where coordination overhead, not durability, is the bottleneck. We measure the binary-log impact separately below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caveat worth stating plainly: this speedup comes from &lt;strong&gt;concurrent writes&lt;/strong&gt;. A single, strictly-sequential writer can't exploit parallel shards and will see no speedup (and a touch of distributed-layer overhead). Sharding pays off when many clients or consumers write at once — which is what real ingestion pipelines do.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost of durability: the binary log
&lt;/h3&gt;

&lt;p&gt;Manticore's binary log makes inserts crash-safe (it can replay un-flushed transactions after an unclean shutdown). It's on by default. You can already see its cost in the chart above — the orange (binary-log-off) line runs above the blue one. Isolating it on the plain unsharded baseline, with only &lt;code&gt;binlog_path&lt;/code&gt; changing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fob30uyrezdrhz383aoy5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fob30uyrezdrhz383aoy5.png" alt=" " width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disabling the binary log raised baseline throughput from &lt;strong&gt;162k to 218k docs/s&lt;/strong&gt; — so &lt;strong&gt;crash-safe inserts cost roughly 25%&lt;/strong&gt;. That's the price of not losing un-flushed writes on a hard crash; leave it on in production unless you can rebuild the data from source and want the extra speed during bulk loads.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost of replication on writes
&lt;/h3&gt;

&lt;p&gt;Durability across machines isn't free either — and this is the one test that's only honest on &lt;strong&gt;real, separate hardware&lt;/strong&gt;. So unlike the single-node benchmarks above, we ran the two replication tests on &lt;strong&gt;two distinct physical machines&lt;/strong&gt;: two 4-core / 7 GB cloud VMs joined as a Manticore cluster. That way &lt;code&gt;rf=1&lt;/code&gt; and &lt;code&gt;rf=2&lt;/code&gt; never fight over the same cores or memory — the fair comparison a single box simply can't give.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rf=1&lt;/code&gt; keeps a single copy on one machine; &lt;code&gt;rf=2&lt;/code&gt; keeps a full copy on &lt;strong&gt;both&lt;/strong&gt;, so every insert is synchronously replicated across the network before it's acknowledged. Same 1M-doc load, 4 writer threads:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0qi4w2jx16cpyk2e5tv3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0qi4w2jx16cpyk2e5tv3.png" alt=" " width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Replication cost about &lt;strong&gt;30% of insert throughput&lt;/strong&gt; (112k → 78k docs/s) — the price of copying every write to the second machine before acking. That's the &lt;code&gt;rf&lt;/code&gt; trade-off in one number: write a bit slower, survive losing an entire machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do sharding and replication speed up reads?
&lt;/h3&gt;

&lt;p&gt;Reads are easy to measure wrong, so first the method. A &lt;strong&gt;trivial&lt;/strong&gt; query — fetch 20 rows, no ranking — runs in well under a millisecond, so its cost is &lt;em&gt;all fixed overhead&lt;/em&gt;; a distributed table that hops to an agent then looks &lt;strong&gt;~3× slower&lt;/strong&gt; (≈7,700 vs 2,400 q/s here) purely because the network round-trip dwarfs the near-zero work. That's not a real query. A &lt;strong&gt;realistic&lt;/strong&gt; full-text query (a few terms to match and rank, ~10–30 ms) tells the truth: the distribution overhead shrinks to a &lt;strong&gt;few percent&lt;/strong&gt;, because now real work dominates the fixed cost. The numbers below all use realistic queries on the 2-node cluster, read from a single entry node.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# realistic read — what the numbers below use: full-text match-and-rank, ~10–30 ms each&lt;/span&gt;
manticore-load &lt;span class="nt"&gt;--threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="nt"&gt;--total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"select id from &amp;lt;table&amp;gt; where match('&amp;lt;text/1/5&amp;gt; &amp;lt;text/1/5&amp;gt;')"&lt;/span&gt;

&lt;span class="c"&gt;# trivial read — sub-ms, all fixed overhead, exaggerates distributed cost ~3x&lt;/span&gt;
manticore-load &lt;span class="nt"&gt;--threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="nt"&gt;--total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"select id from &amp;lt;table&amp;gt; limit 20 option ranker=none"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; is one of: a plain RT table (&lt;strong&gt;no sharding&lt;/strong&gt;), a &lt;code&gt;type='distributed'&lt;/code&gt; table over local shards (&lt;strong&gt;single-node sharding&lt;/strong&gt; — &lt;code&gt;local='s0' local='s1' …&lt;/code&gt;), or a sharded table with &lt;code&gt;shards='4' rf='1'&lt;/code&gt;/&lt;code&gt;rf='2'&lt;/code&gt;. Every read is issued to &lt;strong&gt;one&lt;/strong&gt; node — we never query both nodes as separate clients.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdbwfp0uov8njojm2wcp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdbwfp0uov8njojm2wcp.png" alt=" " width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sharding across nodes speeds reads up.&lt;/strong&gt; A 4-shard &lt;code&gt;rf=1&lt;/code&gt; table spread over both machines does &lt;strong&gt;~516 q/s vs ~315&lt;/strong&gt; for a single unsharded node — about &lt;strong&gt;1.6×&lt;/strong&gt; — because each query runs across both nodes' cores at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replication keeps reads fast.&lt;/strong&gt; &lt;code&gt;rf=2&lt;/code&gt; (every shard on both nodes) does &lt;strong&gt;~410 q/s&lt;/strong&gt; — a touch below &lt;code&gt;rf=1&lt;/code&gt;. The reason is &lt;em&gt;not&lt;/em&gt; that the load sticks to one node (it spreads across replicas); it's that with &lt;code&gt;rf=2&lt;/code&gt; every shard is reached through the agent/mirror path — even the copies that happen to be local — and that path costs the few percent noted above, whereas &lt;code&gt;rf=1&lt;/code&gt; reads its on-node shards in-process. Either way reads stay well above a single node, and the data now survives a machine loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On one box, sharding's read win is small.&lt;/strong&gt; Splitting a table into 2–4 shards on a &lt;em&gt;single&lt;/em&gt; node adds only ~5–12% (query parallelism), and only when there are spare cores — heavier queries help (≈12% vs ≈3% for light ones), but a fully-loaded box is a wash. The real read scaling comes from adding &lt;strong&gt;machines&lt;/strong&gt;, not shards on one box.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Method note: the read and replication tests run on a &lt;strong&gt;separate pair of cloud VMs — 4 vCPU / 7 GB RAM each, two distinct physical machines&lt;/strong&gt; — joined as a 2-node Manticore cluster, same recent dev build, stock settings, &lt;strong&gt;1.5M docs&lt;/strong&gt;. They are a different, much less powerful setup than the 16-core / 32-thread server used for the insert sweeps above, so &lt;strong&gt;draw conclusions within each test, not across them&lt;/strong&gt;. The small core count is also why single-node sharding's read gain is modest here — there are few spare cores for a query to parallelize across.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Bottom line
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;On one box, sharding gives a &lt;strong&gt;~1.5× concurrent insert speedup&lt;/strong&gt; (163k → 253k docs/s here at 20M).&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;best range here is 4–8 shards&lt;/strong&gt; on this 16-core / 32-thread CPU. Too many shards &lt;em&gt;hurts&lt;/em&gt;: at 32 shards throughput fell &lt;em&gt;below&lt;/em&gt; the unsharded baseline. Match shard count to cores and write concurrency, not to a big round number.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;binary log costs ~25%&lt;/strong&gt; of write throughput (and the gap shrinks to near-zero once the shard count is too high), and &lt;strong&gt;&lt;code&gt;rf=2&lt;/code&gt; replication ~30%&lt;/strong&gt; on writes (measured on two separate machines) — both fair prices for crash safety and node-failure survival, respectively.&lt;/li&gt;
&lt;li&gt;With &lt;strong&gt;realistic&lt;/strong&gt; full-text queries, sharding across a 2-node cluster reads &lt;strong&gt;~1.6×&lt;/strong&gt; a single node, and &lt;code&gt;rf=2&lt;/code&gt; keeps reads fast while surviving a node loss. Trivial id-lookup queries exaggerate distributed overhead ~3× — always benchmark reads with realistic queries.&lt;/li&gt;
&lt;li&gt;Absolute numbers depend on hardware and document shape; what travels between setups is the &lt;em&gt;shape&lt;/em&gt; of these curves — so benchmark your own workload before committing to a shard count.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations and things to know
&lt;/h2&gt;

&lt;p&gt;There are sharp edges worth knowing up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rf&lt;/code&gt; is required.&lt;/strong&gt; A sharded &lt;code&gt;CREATE TABLE&lt;/code&gt; must specify &lt;code&gt;rf=&lt;/code&gt;. If you omit it, &lt;code&gt;CREATE TABLE&lt;/code&gt; fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rf&lt;/code&gt; greater than 1 requires a cluster.&lt;/strong&gt; You can't create a multi-copy sharded table on a standalone node — there's nowhere to put the copies. Multi-copy tables must use the &lt;code&gt;cluster:name&lt;/code&gt; form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local sharded tables can't be created on a node that's already in a cluster.&lt;/strong&gt; If a node belongs to a replication cluster, create the table &lt;em&gt;in&lt;/em&gt; that cluster (&lt;code&gt;CREATE TABLE cluster:name …&lt;/code&gt;) rather than as a local table, or the sharding metadata won't be tracked correctly. Manticore detects this and tells you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maximum 3000 shards&lt;/strong&gt; per table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RF=1 means no fault tolerance.&lt;/strong&gt; A lost node's shards are gone. This is inherent, not a bug — it's the trade-off you accept for &lt;code&gt;rf='1'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need at least RF nodes.&lt;/strong&gt; &lt;code&gt;rf='M'&lt;/code&gt; requires a cluster of at least &lt;code&gt;M&lt;/code&gt; nodes; creation fails otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creation is synchronous up to a timeout.&lt;/strong&gt; &lt;code&gt;CREATE TABLE&lt;/code&gt; waits for the distribution to complete (default 30s). For very large shard counts, raise it with &lt;code&gt;timeout='N'&lt;/code&gt; (seconds), e.g. &lt;code&gt;shards='3000' rf='3' timeout='60'&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;Sharding in Manticore covers a wide span with a deliberately small interface. With &lt;code&gt;shards='N' rf='1'&lt;/code&gt; on a single box, sharding spreads concurrent writes across independent pieces and keeps each one small. With &lt;code&gt;shards='N' rf='M'&lt;/code&gt; inside a cluster, it gives you a distributed, replicated table that survives node failures and rebalances itself when the cluster changes — without you writing a line of failover logic. The same table definition grows from one node to many, and your application keeps talking to it the same way throughout. In practice, this means you can start by improving write throughput on one node and later move to a fault-tolerant cluster without changing the application.&lt;/p&gt;

&lt;p&gt;To go deeper into the building blocks sharding stands on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://manual.manticoresearch.com/Creating_a_table/Creating_a_sharded_table/Creating_a_sharded_table" rel="noopener noreferrer"&gt;Creating a sharded table&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manual.manticoresearch.com/Creating_a_cluster/Setting_up_replication/Setting_up_replication" rel="noopener noreferrer"&gt;Setting up replication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manual.manticoresearch.com/Server_settings/Searchd#pseudo_sharding" rel="noopener noreferrer"&gt;Pseudo-sharding&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have a sharding question or a workload you'd like us to benchmark? &lt;a href="https://manticoresearch.com/contact-us/" rel="noopener noreferrer"&gt;Let us know&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Faster KNN index builds in Manticore</title>
      <dc:creator>Sergey Nikolaev</dc:creator>
      <pubDate>Thu, 02 Jul 2026 08:48:19 +0000</pubDate>
      <link>https://dev.to/sanikolaev/faster-knn-index-builds-in-manticore-390d</link>
      <guid>https://dev.to/sanikolaev/faster-knn-index-builds-in-manticore-390d</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;Building a KNN index used to be the slow part of saving and merging tables with vector attributes. As of release &lt;a href="https://manticoresearch.com/blog/manticore-search-27-1-5/" rel="noopener noreferrer"&gt;v27.1.5&lt;/a&gt;, Manticore can use several CPU cores for this work during chunk saves, &lt;code&gt;OPTIMIZE&lt;/code&gt; merges, auto-optimize, and &lt;code&gt;ALTER TABLE ... REBUILD KNN&lt;/code&gt;. On a 16-core Ryzen 9 5950X, building a KNN index for 1 million 1536-dimensional vectors dropped from 8 minutes to 39 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why HNSW build speed matters
&lt;/h2&gt;

&lt;p&gt;Manticore uses HNSW graphs to power KNN search over &lt;code&gt;float_vector&lt;/code&gt; attributes. You can think of an HNSW graph as a map that helps Manticore quickly find vectors that are close to the query vector.&lt;/p&gt;

&lt;p&gt;Building that map can take a long time. For tables without KNN, saving or merging data is mostly about writing ordinary table data. For tables with KNN, Manticore also has to insert every vector into an HNSW graph, and that extra work can dominate the total time.&lt;/p&gt;

&lt;p&gt;This matters most after large inserts and during maintenance. Fresh data is saved from memory to disk chunks. Existing chunks are later merged by &lt;code&gt;OPTIMIZE&lt;/code&gt; or auto-optimize. Each saved or merged chunk needs its own HNSW graph, so faster graph building means shorter waits after bulk loading, faster background optimization, and less time spent on maintenance operations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://manual.manticoresearch.com/Creating_a_table/Local_tables/Plain_and_real-time_table_settings#Important-notes-about-RAM-chunks" rel="noopener noreferrer"&gt;Disk chunk&lt;/a&gt; count matters for search too. Manticore stores RT tables as disk chunks, and each chunk has its own HNSW graph. A KNN query searches every chunk and merges the results, so fewer chunks usually mean faster KNN queries. The fastest layout is often one chunk with one graph.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://manual.manticoresearch.com/Server_settings/Searchd#auto_optimize" rel="noopener noreferrer"&gt;Auto-optimize&lt;/a&gt; does not go all the way to one chunk by default. It merges chunks in the background, but stops when the table reaches a target chunk count. For ordinary tables, the target is &lt;code&gt;2 * num_logical_cpus&lt;/code&gt;; for tables with a KNN attribute, it is lower: &lt;code&gt;num_physical_cpus / 2&lt;/code&gt;. On a 32-thread / 16-core host, that means 8 chunks for a KNN table instead of 64 for an ordinary table. The KNN target is lower because extra chunks hurt KNN search latency more, but the default still leaves more than one graph. To make auto-optimize converge to a single chunk, set &lt;a href="https://manual.manticoresearch.com/Server_settings/Searchd#optimize_cutoff" rel="noopener noreferrer"&gt;optimize_cutoff&lt;/a&gt; to &lt;code&gt;1&lt;/code&gt; server-wide, per table, or at runtime with &lt;code&gt;SET GLOBAL optimize_cutoff = 1&lt;/code&gt;. You can also do it manually with &lt;a href="https://manual.manticoresearch.com/Securing_and_compacting_a_table/Compacting_a_table" rel="noopener noreferrer"&gt;OPTIMIZE TABLE ... OPTION cutoff = 1&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What used to happen
&lt;/h2&gt;

&lt;p&gt;Freshly inserted documents first accumulate in a RAM chunk. When that RAM chunk reaches &lt;a href="https://manual.manticoresearch.com/Creating_a_table/Local_tables/Plain_and_real-time_table_settings#rt_mem_limit" rel="noopener noreferrer"&gt;rt_mem_limit&lt;/a&gt;, which defaults to 128MB, Manticore saves it as a new disk chunk. For a table with a KNN attribute, that save includes building a fresh HNSW graph from the vectors in the RAM chunk.&lt;/p&gt;

&lt;p&gt;The same kind of HNSW build happens when disk chunks are merged. &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; and auto-optimize read live rows from existing chunks, write a new merged chunk, and build a new HNSW graph for that merged result. &lt;code&gt;ALTER TABLE ... REBUILD KNN&lt;/code&gt;, and ALTER operations that add or drop a &lt;code&gt;float_vector&lt;/code&gt; column, also rebuild the graph.&lt;/p&gt;

&lt;p&gt;Before this change, the HNSW part of each individual save, merge, or rebuild used &lt;strong&gt;one worker&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A RAM-to-disk chunk save walked all live rows from the RAM chunk's segments one by one and inserted their vectors into one HNSW graph.&lt;/li&gt;
&lt;li&gt;A chunk merge walked all live rows from the input disk chunks one by one and inserted their vectors into the new graph.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ALTER TABLE ... REBUILD KNN&lt;/code&gt; rebuilt each graph in one worker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Manticore already had some parallelism around these operations. Up to 2 RAM-chunk saves can run at the same time. The optimizer can also run several chunk merges at once, controlled by &lt;a href="https://manual.manticoresearch.com/Server_settings/Searchd#parallel_chunk_merges" rel="noopener noreferrer"&gt;parallel_chunk_merges&lt;/a&gt;. The default is 2 when the host has enough CPU cores. But inside each individual save or merge, the KNN graph build was still single-worker. On KNN-heavy tables, that single worker often determined how long the whole operation took.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;Manticore now splits one KNN graph build across several workers. Each worker gets part of the rows, inserts its vectors into the same destination graph, and finishes independently. The graph-building library coordinates those concurrent inserts so the graph remains valid.&lt;/p&gt;

&lt;p&gt;The exact split depends on the operation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;During RAM-to-disk saves, workers take RAM segments from a shared queue until all segments are processed.&lt;/li&gt;
&lt;li&gt;During chunk merges and &lt;code&gt;ALTER TABLE ... REBUILD KNN&lt;/code&gt;, Manticore divides the live rows into similarly sized ranges so the work is spread evenly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Single-thread improvements
&lt;/h3&gt;

&lt;p&gt;The same release also improves the single-worker path. Even when &lt;code&gt;knn_parallel_build&lt;/code&gt; is set to &lt;code&gt;1&lt;/code&gt;, the benchmark below shows a 10% improvement before adding parallelism. That comes from three changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Two-pass neighbor processing.&lt;/strong&gt; When inserting a vector, the algorithm walks through candidate neighbors and computes distances to them. The new code splits that into two passes: the first pass walks the neighbor list and prefetches the vector data, and the second pass computes the distances. This gives the CPU time to bring the vectors into cache before they are used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two comparisons at a time.&lt;/strong&gt; Some distance calculations now process two candidate vectors together. This reduces repeated work in the inner loop where most of the build time is spent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compile-time distance dispatch in build mode.&lt;/strong&gt; The builder now picks the right distance function once for the build, such as inner product vs. L2 and raw float vs. binary-quantized vectors. That avoids a function-pointer lookup on every distance call and lets the compiler optimize the inner loop more aggressively.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The default and the config
&lt;/h3&gt;

&lt;p&gt;A new searchd setting, &lt;a href="https://manual.manticoresearch.com/Server_settings/Searchd#knn_parallel_build" rel="noopener noreferrer"&gt;knn_parallel_build&lt;/a&gt;, controls how many workers one KNN build may use. The default is &lt;code&gt;min(4, threads / 4)&lt;/code&gt;, where &lt;code&gt;threads&lt;/code&gt; is Manticore's &lt;a href="https://manual.manticoresearch.com/Server_settings/Searchd#threads" rel="noopener noreferrer"&gt;threads&lt;/a&gt; setting - the size of the worker pool that runs queries and background tasks, which defaults to the number of logical CPU cores on the host.&lt;/p&gt;

&lt;p&gt;In practical terms, that means one worker on small hosts and up to four workers by default on larger hosts: a 4-thread host gets one worker, an 8-thread host gets two, a 16-thread host gets four, and anything above that is also capped at four. The default is conservative because production machines often need to handle searches, inserts, and background work at the same time.&lt;/p&gt;

&lt;p&gt;You usually do not need to change it. Consider raising it when you are rebuilding or optimizing a KNN-heavy table on a host that is not serving live traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;knn_parallel_build&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set it to &lt;code&gt;1&lt;/code&gt; if you need the old single-worker behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;knn_parallel_build&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value can also be set in the searchd config and checked with &lt;code&gt;SHOW VARIABLES&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  CPU usage
&lt;/h3&gt;

&lt;p&gt;Multiple saves and merges can be active at the same time, and each one can use up to &lt;code&gt;knn_parallel_build&lt;/code&gt; workers. These workers use Manticore's existing &lt;code&gt;threads&lt;/code&gt; pool. They do not create an unlimited number of extra operating-system threads; if all pool threads are busy, extra work waits in the queue.&lt;/p&gt;

&lt;p&gt;This is why the default leaves headroom. On a 32-thread host, the default is 4 workers per KNN build. If two chunk saves overlap, the KNN build work can use up to 8 workers, leaving the rest of the thread pool available for other work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark
&lt;/h2&gt;

&lt;p&gt;Setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AMD Ryzen 9 5950X (16 physical / 32 logical cores)&lt;/li&gt;
&lt;li&gt;Dataset: &lt;a href="https://storage.googleapis.com/ann-filtered-benchmark/datasets/dbpedia_openai_1M.tgz" rel="noopener noreferrer"&gt;dbpedia-openai-1M&lt;/a&gt; - 1M vectors, 1536 dimensions, cosine distance&lt;/li&gt;
&lt;li&gt;Quantization: 1-bit (binary) quantization&lt;/li&gt;
&lt;li&gt;Data inserted into an RT table, then &lt;code&gt;OPTIMIZE&lt;/code&gt; to a single disk chunk&lt;/li&gt;
&lt;li&gt;Measurement: &lt;code&gt;ALTER TABLE knn_data REBUILD KNN&lt;/code&gt;, three runs per setting, single-chunk so timings are stable&lt;/li&gt;
&lt;li&gt;HNSW settings: defaults&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;ALTER TABLE ... REBUILD KNN&lt;/code&gt; was used because it exercises the same parallel build path as chunk saves and chunk merges, while giving stable timings that are easy to reproduce.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy0c3w95q1k69aqbjgd3b.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy0c3w95q1k69aqbjgd3b.jpg" alt=" " width="800" height="457"&gt;&lt;/a&gt;&lt;br&gt;
Results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With one worker, the new code is already 10% faster than the old code: 492 seconds dropped to 442 seconds.&lt;/li&gt;
&lt;li&gt;With 16 workers, rebuild time dropped to 39 seconds, about 11x faster than the new one-worker result.&lt;/li&gt;
&lt;li&gt;Going from 16 to 32 workers helped only a little: 39 seconds became 36 seconds. On this machine, the useful limit is close to the 16 physical CPU cores.&lt;/li&gt;
&lt;li&gt;The default is meant for shared production hosts. For maintenance work on a dedicated host, raising &lt;code&gt;knn_parallel_build&lt;/code&gt; can be worth it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Migration
&lt;/h2&gt;

&lt;p&gt;No action is required. Existing tables keep working, and KNN graphs built by the parallel path are functionally equivalent to graphs built by the old single-worker path.&lt;/p&gt;

&lt;p&gt;One detail can matter for strict reproducibility: parallel workers may insert vectors in a different order, so Manticore's on-disk KNN graph file, stored with the &lt;code&gt;.spknn&lt;/code&gt; extension, is not guaranteed to be byte-for-byte identical to a single-worker build. Search quality and query speed are expected to be the same. If byte-for-byte reproducibility matters, set &lt;code&gt;knn_parallel_build = 1&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This change speeds up one of the slowest maintenance steps for KNN tables. Parallel graph building reduces the time needed to save chunks, merge chunks, and rebuild KNN data, while the improved single-worker path also speeds up builds on smaller systems. Existing tables continue to work without changes. When CPU resources are available during maintenance, &lt;code&gt;knn_parallel_build&lt;/code&gt; can be raised to build KNN graphs faster.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
